/*! * Amplify Request 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 */ /*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 ) );