meteor/meteor

View on GitHub
packages/deprecated/amplify/amplify.js

Summary

Maintainability
F
3 days
Test Coverage
/*!
 * Amplify 1.1.2
 *
 * Copyright 2011 - 2013 appendTo LLC. (http://appendto.com/team)
 * Dual licensed under the MIT or GPL licenses.
 * http://appendto.com/open-source-licenses
 *
 * http://amplifyjs.com
 */
(function( global, undefined ) {

var slice = [].slice,
    subscriptions = {};

var amplify = global.amplify = {
    publish: function( topic ) {
        if ( typeof topic !== "string" ) {
            throw new Error( "You must provide a valid topic to publish." );
        }

        var args = slice.call( arguments, 1 ),
            topicSubscriptions,
            subscription,
            length,
            i = 0,
            ret;

        if ( !subscriptions[ topic ] ) {
            return true;
        }

        topicSubscriptions = subscriptions[ topic ].slice();
        for ( length = topicSubscriptions.length; i < length; i++ ) {
            subscription = topicSubscriptions[ i ];
            ret = subscription.callback.apply( subscription.context, args );
            if ( ret === false ) {
                break;
            }
        }
        return ret !== false;
    },

    subscribe: function( topic, context, callback, priority ) {
        if ( typeof topic !== "string" ) {
            throw new Error( "You must provide a valid topic to create a subscription." );
        }

        if ( arguments.length === 3 && typeof callback === "number" ) {
            priority = callback;
            callback = context;
            context = null;
        }
        if ( arguments.length === 2 ) {
            callback = context;
            context = null;
        }
        priority = priority || 10;

        var topicIndex = 0,
            topics = topic.split( /\s/ ),
            topicLength = topics.length,
            added;
        for ( ; topicIndex < topicLength; topicIndex++ ) {
            topic = topics[ topicIndex ];
            added = false;
            if ( !subscriptions[ topic ] ) {
                subscriptions[ topic ] = [];
            }

            var i = subscriptions[ topic ].length - 1,
                subscriptionInfo = {
                    callback: callback,
                    context: context,
                    priority: priority
                };

            for ( ; i >= 0; i-- ) {
                if ( subscriptions[ topic ][ i ].priority <= priority ) {
                    subscriptions[ topic ].splice( i + 1, 0, subscriptionInfo );
                    added = true;
                    break;
                }
            }

            if ( !added ) {
                subscriptions[ topic ].unshift( subscriptionInfo );
            }
        }

        return callback;
    },

    unsubscribe: function( topic, context, callback ) {
        if ( typeof topic !== "string" ) {
            throw new Error( "You must provide a valid topic to remove a subscription." );
        }

        if ( arguments.length === 2 ) {
            callback = context;
            context = null;
        }

        if ( !subscriptions[ topic ] ) {
            return;
        }

        var length = subscriptions[ topic ].length,
            i = 0;

        for ( ; i < length; i++ ) {
            if ( subscriptions[ topic ][ i ].callback === callback ) {
                if ( !context || subscriptions[ topic ][ i ].context === context ) {
                    subscriptions[ topic ].splice( i, 1 );
                    
                    // Adjust counter and length for removed item
                    i--;
                    length--;
                }
            }
        }
    }
};

}( this ) );

(function( amplify, undefined ) {

var store = amplify.store = function( key, value, options ) {
    var type = store.type;
    if ( options && options.type && options.type in store.types ) {
        type = options.type;
    }
    return store.types[ type ]( key, value, options || {} );
};

store.types = {};
store.type = null;
store.addType = function( type, storage ) {
    if ( !store.type ) {
        store.type = type;
    }

    store.types[ type ] = storage;
    store[ type ] = function( key, value, options ) {
        options = options || {};
        options.type = type;
        return store( key, value, options );
    };
};
store.error = function() {
    return "amplify.store quota exceeded";
};

var rprefix = /^__amplify__/;
function createFromStorageInterface( storageType, storage ) {
    store.addType( storageType, function( key, value, options ) {
        var storedValue, parsed, i, remove,
            ret = value,
            now = (new Date()).getTime();

        if ( !key ) {
            ret = {};
            remove = [];
            i = 0;
            try {
                // accessing the length property works around a localStorage bug
                // in Firefox 4.0 where the keys don't update cross-page
                // we assign to key just to avoid Closure Compiler from removing
                // the access as "useless code"
                // https://bugzilla.mozilla.org/show_bug.cgi?id=662511
                key = storage.length;

                while ( key = storage.key( i++ ) ) {
                    if ( rprefix.test( key ) ) {
                        parsed = JSON.parse( storage.getItem( key ) );
                        if ( parsed.expires && parsed.expires <= now ) {
                            remove.push( key );
                        } else {
                            ret[ key.replace( rprefix, "" ) ] = parsed.data;
                        }
                    }
                }
                while ( key = remove.pop() ) {
                    storage.removeItem( key );
                }
            } catch ( error ) {}
            return ret;
        }

        // protect against name collisions with direct storage
        key = "__amplify__" + key;

        if ( value === undefined ) {
            storedValue = storage.getItem( key );
            parsed = storedValue ? JSON.parse( storedValue ) : { expires: -1 };
            if ( parsed.expires && parsed.expires <= now ) {
                storage.removeItem( key );
            } else {
                return parsed.data;
            }
        } else {
            if ( value === null ) {
                storage.removeItem( key );
            } else {
                parsed = JSON.stringify({
                    data: value,
                    expires: options.expires ? now + options.expires : null
                });
                try {
                    storage.setItem( key, parsed );
                // quota exceeded
                } catch( error ) {
                    // expire old data and try again
                    store[ storageType ]();
                    try {
                        storage.setItem( key, parsed );
                    } catch( error ) {
                        throw store.error();
                    }
                }
            }
        }

        return ret;
    });
}

// localStorage + sessionStorage
// IE 8+, Firefox 3.5+, Safari 4+, Chrome 4+, Opera 10.5+, iPhone 2+, Android 2+
for ( var webStorageType in { localStorage: 1, sessionStorage: 1 } ) {
    // try/catch for file protocol in Firefox and Private Browsing in Safari 5
    try {
        // Safari 5 in Private Browsing mode exposes localStorage
        // but doesn't allow storing data, so we attempt to store and remove an item.
        // This will unfortunately give us a false negative if we're at the limit.
        window[ webStorageType ].setItem( "__amplify__", "x" );
        window[ webStorageType ].removeItem( "__amplify__" );
        createFromStorageInterface( webStorageType, window[ webStorageType ] );
    } catch( e ) {}
}

// globalStorage
// non-standard: Firefox 2+
// https://developer.mozilla.org/en/dom/storage#globalStorage
if ( !store.types.localStorage && window.globalStorage ) {
    // try/catch for file protocol in Firefox
    try {
        createFromStorageInterface( "globalStorage",
            window.globalStorage[ window.location.hostname ] );
        // Firefox 2.0 and 3.0 have sessionStorage and globalStorage
        // make sure we default to globalStorage
        // but don't default to globalStorage in 3.5+ which also has localStorage
        if ( store.type === "sessionStorage" ) {
            store.type = "globalStorage";
        }
    } catch( e ) {}
}

// userData
// non-standard: IE 5+
// http://msdn.microsoft.com/en-us/library/ms531424(v=vs.85).aspx
(function() {
    // IE 9 has quirks in userData that are a huge pain
    // rather than finding a way to detect these quirks
    // we just don't register userData if we have localStorage
    if ( store.types.localStorage ) {
        return;
    }

    // append to html instead of body so we can do this from the head
    var div = document.createElement( "div" ),
        attrKey = "amplify";
    div.style.display = "none";
    document.getElementsByTagName( "head" )[ 0 ].appendChild( div );

    // we can't feature detect userData support
    // so just try and see if it fails
    // surprisingly, even just adding the behavior isn't enough for a failure
    // so we need to load the data as well
    try {
        div.addBehavior( "#default#userdata" );
        div.load( attrKey );
    } catch( e ) {
        div.parentNode.removeChild( div );
        return;
    }

    store.addType( "userData", function( key, value, options ) {
        div.load( attrKey );
        var attr, parsed, prevValue, i, remove,
            ret = value,
            now = (new Date()).getTime();

        if ( !key ) {
            ret = {};
            remove = [];
            i = 0;
            while ( attr = div.XMLDocument.documentElement.attributes[ i++ ] ) {
                parsed = JSON.parse( attr.value );
                if ( parsed.expires && parsed.expires <= now ) {
                    remove.push( attr.name );
                } else {
                    ret[ attr.name ] = parsed.data;
                }
            }
            while ( key = remove.pop() ) {
                div.removeAttribute( key );
            }
            div.save( attrKey );
            return ret;
        }

        // convert invalid characters to dashes
        // http://www.w3.org/TR/REC-xml/#NT-Name
        // simplified to assume the starting character is valid
        // also removed colon as it is invalid in HTML attribute names
        key = key.replace( /[^\-._0-9A-Za-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c-\u200d\u203f\u2040\u2070-\u218f]/g, "-" );
        // adjust invalid starting character to deal with our simplified sanitization
        key = key.replace( /^-/, "_-" );

        if ( value === undefined ) {
            attr = div.getAttribute( key );
            parsed = attr ? JSON.parse( attr ) : { expires: -1 };
            if ( parsed.expires && parsed.expires <= now ) {
                div.removeAttribute( key );
            } else {
                return parsed.data;
            }
        } else {
            if ( value === null ) {
                div.removeAttribute( key );
            } else {
                // we need to get the previous value in case we need to rollback
                prevValue = div.getAttribute( key );
                parsed = JSON.stringify({
                    data: value,
                    expires: (options.expires ? (now + options.expires) : null)
                });
                div.setAttribute( key, parsed );
            }
        }

        try {
            div.save( attrKey );
        // quota exceeded
        } catch ( error ) {
            // roll the value back to the previous value
            if ( prevValue === null ) {
                div.removeAttribute( key );
            } else {
                div.setAttribute( key, prevValue );
            }

            // expire old data and try again
            store.userData();
            try {
                div.setAttribute( key, parsed );
                div.save( attrKey );
            } catch ( error ) {
                // roll the value back to the previous value
                if ( prevValue === null ) {
                    div.removeAttribute( key );
                } else {
                    div.setAttribute( key, prevValue );
                }
                throw store.error();
            }
        }
        return ret;
    });
}() );

// in-memory storage
// fallback for all browsers to enable the API even if we can't persist data
(function() {
    var memory = {},
        timeout = {};

    function copy( obj ) {
        return obj === undefined ? undefined : JSON.parse( JSON.stringify( obj ) );
    }

    store.addType( "memory", function( key, value, options ) {
        if ( !key ) {
            return copy( memory );
        }

        if ( value === undefined ) {
            return copy( memory[ key ] );
        }

        if ( timeout[ key ] ) {
            clearTimeout( timeout[ key ] );
            delete timeout[ key ];
        }

        if ( value === null ) {
            delete memory[ key ];
            return null;
        }

        memory[ key ] = value;
        if ( options.expires ) {
            timeout[ key ] = setTimeout(function() {
                delete memory[ key ];
                delete timeout[ key ];
            }, options.expires );
        }

        return value;
    });
}() );

}( this.amplify = this.amplify || {} ) );

/*global amplify*/

(function( amplify, undefined ) {
'use strict';

function noop() {}
function isFunction( obj ) {
    return ({}).toString.call( obj ) === "[object Function]";
}

function async( fn ) {
    var isAsync = false;
    setTimeout(function() {
        isAsync = true;
    }, 1 );
    return function() {
        var that = this,
            args = arguments;
        if ( isAsync ) {
            fn.apply( that, args );
        } else {
            setTimeout(function() {
                fn.apply( that, args );
            }, 1 );
        }
    };
}

amplify.request = function( resourceId, data, callback ) {
    // default to an empty hash just so we can handle a missing resourceId
    // in one place
    var settings = resourceId || {};

    if ( typeof settings === "string" ) {
        if ( isFunction( data ) ) {
            callback = data;
            data = {};
        }
        settings = {
            resourceId: resourceId,
            data: data || {},
            success: callback
        };
    }

    var request = { abort: noop },
        resource = amplify.request.resources[ settings.resourceId ],
        success = settings.success || noop,
        error = settings.error || noop;

    settings.success = async( function( data, status ) {
        status = status || "success";
        amplify.publish( "request.success", settings, data, status );
        amplify.publish( "request.complete", settings, data, status );
        success( data, status );
    });

    settings.error = async( function( data, status ) {
        status = status || "error";
        amplify.publish( "request.error", settings, data, status );
        amplify.publish( "request.complete", settings, data, status );
        error( data, status );
    });

    if ( !resource ) {
        if ( !settings.resourceId ) {
            throw "amplify.request: no resourceId provided";
        }
        throw "amplify.request: unknown resourceId: " + settings.resourceId;
    }

    if ( !amplify.publish( "request.before", settings ) ) {
        settings.error( null, "abort" );
        return;
    }

    amplify.request.resources[ settings.resourceId ]( settings, request );
    return request;
};

amplify.request.types = {};
amplify.request.resources = {};
amplify.request.define = function( resourceId, type, settings ) {
    if ( typeof type === "string" ) {
        if ( !( type in amplify.request.types ) ) {
            throw "amplify.request.define: unknown type: " + type;
        }

        settings.resourceId = resourceId;
        amplify.request.resources[ resourceId ] =
            amplify.request.types[ type ]( settings );
    } else {
        // no pre-processor or settings for one-off types (don't invoke)
        amplify.request.resources[ resourceId ] = type;
    }
};

}( amplify ) );


(function( amplify, $, undefined ) {
'use strict';

var xhrProps = [ "status", "statusText", "responseText", "responseXML", "readyState" ],
        rurlData = /\{([^\}]+)\}/g;

amplify.request.types.ajax = function( defnSettings ) {
    defnSettings = $.extend({
        type: "GET"
    }, defnSettings );

    return function( settings, request ) {
        var xhr, handleResponse,
            url = defnSettings.url,
            abort = request.abort,
            ajaxSettings = $.extend( true, {}, defnSettings, { data: settings.data } ),
            aborted = false,
            ampXHR = {
                readyState: 0,
                setRequestHeader: function( name, value ) {
                    return xhr.setRequestHeader( name, value );
                },
                getAllResponseHeaders: function() {
                    return xhr.getAllResponseHeaders();
                },
                getResponseHeader: function( key ) {
                    return xhr.getResponseHeader( key );
                },
                overrideMimeType: function( type ) {
                    return xhr.overrideMimeType( type );
                },
                abort: function() {
                    aborted = true;
                    try {
                        xhr.abort();
                    // IE 7 throws an error when trying to abort
                    } catch( e ) {}
                    handleResponse( null, "abort" );
                },
                success: function( data, status ) {
                    settings.success( data, status );
                },
                error: function( data, status ) {
                    settings.error( data, status );
                }
            };

        handleResponse = function( data, status ) {
            $.each( xhrProps, function( i, key ) {
                try {
                    ampXHR[ key ] = xhr[ key ];
                } catch( e ) {}
            });
            // Playbook returns "HTTP/1.1 200 OK"
            // TODO: something also returns "OK", what?
            if ( /OK$/.test( ampXHR.statusText ) ) {
                ampXHR.statusText = "success";
            }
            if ( data === undefined ) {
                // TODO: add support for ajax errors with data
                data = null;
            }
            if ( aborted ) {
                status = "abort";
            }
            if ( /timeout|error|abort/.test( status ) ) {
                ampXHR.error( data, status );
            } else {
                ampXHR.success( data, status );
            }
            // avoid handling a response multiple times
            // this can happen if a request is aborted
            // TODO: figure out if this breaks polling or multi-part responses
            handleResponse = $.noop;
        };

        amplify.publish( "request.ajax.preprocess",
            defnSettings, settings, ajaxSettings, ampXHR );

        $.extend( ajaxSettings, {
            isJSONP: function () {
                return (/jsonp/gi).test(this.dataType);
            },
            cacheURL: function () {
                if (!this.isJSONP()) {
                    return this.url;
                }

                var callbackName = 'callback';

                // possible for the callback function name to be overridden
                if (this.hasOwnProperty('jsonp')) {
                    if (this.jsonp !== false) {
                        callbackName = this.jsonp;
                    } else {
                        if (this.hasOwnProperty('jsonpCallback')) {
                            callbackName = this.jsonpCallback;
                        }
                    }
                }

                // search and replace callback parameter in query string with empty string
                var callbackRegex = new RegExp('&?' + callbackName + '=[^&]*&?', 'gi');
                return this.url.replace(callbackRegex, '');
            },
            success: function( data, status ) {
                handleResponse( data, status );
            },
            error: function( _xhr, status ) {
                handleResponse( null, status );
            },
            beforeSend: function( _xhr, _ajaxSettings ) {
                xhr = _xhr;
                ajaxSettings = _ajaxSettings;
                var ret = defnSettings.beforeSend ?
                    defnSettings.beforeSend.call( this, ampXHR, ajaxSettings ) : true;
                return ret && amplify.publish( "request.before.ajax",
                    defnSettings, settings, ajaxSettings, ampXHR );
            }
        });

        // cache all JSONP requests
        if (ajaxSettings.cache && ajaxSettings.isJSONP()) {
            $.extend(ajaxSettings, {
                cache: true
            });
        }

        $.ajax( ajaxSettings );

        request.abort = function() {
            ampXHR.abort();
            abort.call( this );
        };
    };
};



amplify.subscribe( "request.ajax.preprocess", function( defnSettings, settings, ajaxSettings ) {
    var mappedKeys = [],
        data = ajaxSettings.data;

    if ( typeof data === "string" ) {
        return;
    }

    data = $.extend( true, {}, defnSettings.data, data );

    ajaxSettings.url = ajaxSettings.url.replace( rurlData, function ( m, key ) {
        if ( key in data ) {
            mappedKeys.push( key );
            return data[ key ];
        }
    });

    // We delete the keys later so duplicates are still replaced
    $.each( mappedKeys, function ( i, key ) {
        delete data[ key ];
    });

    ajaxSettings.data = data;
});



amplify.subscribe( "request.ajax.preprocess", function( defnSettings, settings, ajaxSettings ) {
    var data = ajaxSettings.data,
        dataMap = defnSettings.dataMap;

    if ( !dataMap || typeof data === "string" ) {
        return;
    }

    if ( $.isFunction( dataMap ) ) {
        ajaxSettings.data = dataMap( data );
    } else {
        $.each( defnSettings.dataMap, function( orig, replace ) {
            if ( orig in data ) {
                data[ replace ] = data[ orig ];
                delete data[ orig ];
            }
        });
        ajaxSettings.data = data;
    }
});



var cache = amplify.request.cache = {
    _key: function( resourceId, url, data ) {
        data = url + data;
        var length = data.length,
            i = 0;

        /*jshint bitwise:false*/
        function chunk() {
            return data.charCodeAt( i++ ) << 24 |
                data.charCodeAt( i++ ) << 16 |
                data.charCodeAt( i++ ) << 8 |
                data.charCodeAt( i++ ) << 0;
        }

        var checksum = chunk();
        while ( i < length ) {
            checksum ^= chunk();
        }
        /*jshint bitwise:true*/

        return "request-" + resourceId + "-" + checksum;
    },

    _default: (function() {
        var memoryStore = {};
        return function( resource, settings, ajaxSettings, ampXHR ) {
            // data is already converted to a string by the time we get here
            var cacheKey = cache._key( settings.resourceId,
                    ajaxSettings.cacheURL(), ajaxSettings.data ),
                duration = resource.cache;

            if ( cacheKey in memoryStore ) {
                ampXHR.success( memoryStore[ cacheKey ] );
                return false;
            }
            var success = ampXHR.success;
            ampXHR.success = function( data ) {
                memoryStore[ cacheKey ] = data;
                if ( typeof duration === "number" ) {
                    setTimeout(function() {
                        delete memoryStore[ cacheKey ];
                    }, duration );
                }
                success.apply( this, arguments );
            };
        };
    }())
};

if ( amplify.store ) {
    $.each( amplify.store.types, function( type ) {
        cache[ type ] = function( resource, settings, ajaxSettings, ampXHR ) {
            var cacheKey = cache._key( settings.resourceId,
                    ajaxSettings.cacheURL(), ajaxSettings.data ),
                cached = amplify.store[ type ]( cacheKey );

            if ( cached ) {
                ajaxSettings.success( cached );
                return false;
            }
            var success = ampXHR.success;
            ampXHR.success = function( data ) {
                amplify.store[ type ]( cacheKey, data, { expires: resource.cache.expires } );
                success.apply( this, arguments );
            };
        };
    });
    cache.persist = cache[ amplify.store.type ];
}

amplify.subscribe( "request.before.ajax", function( resource ) {
    var cacheType = resource.cache;
    if ( cacheType ) {
        // normalize between objects and strings/booleans/numbers
        cacheType = cacheType.type || cacheType;
        return cache[ cacheType in cache ? cacheType : "_default" ]
            .apply( this, arguments );
    }
});



amplify.request.decoders = {
    // http://labs.omniti.com/labs/jsend
    jsend: function( data, status, ampXHR, success, error ) {
        if ( data.status === "success" ) {
            success( data.data );
        } else if ( data.status === "fail" ) {
            error( data.data, "fail" );
        } else if ( data.status === "error" ) {
            delete data.status;
            error( data, "error" );
        } else {
            error( null, "error" );
        }
    }
};

amplify.subscribe( "request.before.ajax", function( resource, settings, ajaxSettings, ampXHR ) {
    var _success = ampXHR.success,
        _error = ampXHR.error,
        decoder = $.isFunction( resource.decoder ) ?
            resource.decoder :
            resource.decoder in amplify.request.decoders ?
                amplify.request.decoders[ resource.decoder ] :
                amplify.request.decoders._default;

    if ( !decoder ) {
        return;
    }

    function success( data, status ) {
        _success( data, status );
    }
    function error( data, status ) {
        _error( data, status );
    }
    ampXHR.success = function( data, status ) {
        decoder( data, status, ampXHR, success, error );
    };
    ampXHR.error = function( data, status ) {
        decoder( data, status, ampXHR, success, error );
    };
});

}( amplify, jQuery ) );