Created
March 20, 2012 16:34
-
-
Save ifandelse/2137980 to your computer and use it in GitHub Desktop.
amplify.js in an AMD Wrapper
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! | |
* AmplifyJS 1.0.0 - Core, Store, Request | |
* | |
* 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 | |
*/ | |
(function(root, doc, factory) { | |
if (typeof define === "function" && define.amd) { | |
// AMD. Register as an anonymous module. | |
define(["jquery"], function($) { | |
return factory($, root, doc); | |
}); | |
} else { | |
// Browser globals | |
factory(root.jQuery, root, doc); | |
} | |
}(this, document, function($, global, document, undefined) {/*! | |
* Amplify Core 1.0.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 | |
*/ | |
(function( global, undefined ) { | |
var slice = [].slice, | |
subscriptions = {}; | |
var amplify = global.amplify = { | |
publish: function( topic ) { | |
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 ( 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, callback ) { | |
if ( !subscriptions[ topic ] ) { | |
return; | |
} | |
var length = subscriptions[ topic ].length, | |
i = 0; | |
for ( ; i < length; i++ ) { | |
if ( subscriptions[ topic ][ i ].callback === callback ) { | |
subscriptions[ topic ].splice( i, 1 ); | |
break; | |
} | |
} | |
} | |
}; | |
}( this ) ); | |
/*! | |
* Amplify Store - Persistent Client-Side Storage 1.0.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 | |
*/ | |
(function( amplify, undefined ) { | |
var store = amplify.store = function( key, value, options, type ) { | |
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 | |
try { | |
if ( window[ webStorageType ].getItem ) { | |
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\u37f-\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 || {} ) ); | |
/*! | |
* Amplify Request 1.0.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 | |
*/ | |
(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 ) ); | |
return amplify; | |
})); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment