amplifyjsify
Version:
AmplifyJS lib for browsers - AmplifyJS v1.1.0
390 lines (333 loc) • 9.85 kB
JavaScript
/*!
* Amplify Request 1.1.0
*
* Copyright 2011 appendTo LLC. (http://appendto.com/team)
* Dual licensed under the MIT or GPL licenses.
* http://appendto.com/open-source-licenses
*
* http://amplifyjs.com
*/
var amplify = require('amplifyjsify/lib/core');
var jQuery = require('jqueryify');
(function( amplify, undefined ) {
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 ) {
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,
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.overrideMideType( 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 );
}
};
amplify.publish( "request.ajax.preprocess",
defnSettings, settings, ajaxSettings, ampXHR );
$.extend( ajaxSettings, {
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 );
}
});
$.ajax( ajaxSettings );
function handleResponse( 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;
}
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,
checksum = chunk();
while ( i < length ) {
checksum ^= chunk();
}
function chunk() {
return data.charCodeAt( i++ ) << 24 |
data.charCodeAt( i++ ) << 16 |
data.charCodeAt( i++ ) << 8 |
data.charCodeAt( i++ ) << 0;
}
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.url, 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.url, 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" );
}
}
};
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 ) );
module.exports = amplify.request;