amplifier
Version:
Use the awesome AmplifyJs v1.1.2 library as a Node.js module.
834 lines (722 loc) • 28.8 kB
JavaScript
var jQuery = require('jquery');
/*!
* 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 || {}));
(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;
}
};
}(this.amplify = this.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);
};
});
}(this.amplify = this.amplify || {}, jQuery));
module.exports = this.amplify;