aboutsummaryrefslogtreecommitdiff
path: root/pollymer.js
diff options
context:
space:
mode:
Diffstat (limited to 'pollymer.js')
-rw-r--r--pollymer.js467
1 files changed, 467 insertions, 0 deletions
diff --git a/pollymer.js b/pollymer.js
new file mode 100644
index 0000000..d4cd078
--- /dev/null
+++ b/pollymer.js
@@ -0,0 +1,467 @@
+/**
+ * Pollymer JavaScript Library v1.0.0
+ * Copyright 2013 Fan Out Networks, Inc.
+ * Released under the MIT license (see COPYING file in source distribution)
+ */
+(function () {
+"use strict";
+var DEBUG = true;
+(function (window, undefined) {
+
+ var NAMESPACE = "Pollymer";
+ var TIMEOUT = 60000;
+ var emptyMethod = function () { };
+
+ var consoleInfo;
+ var consoleError;
+ if (DEBUG) {
+ // don't break if there's no console
+ if (typeof (window.console) === "undefined") {
+ window.console = { info: emptyMethod, error: emptyMethod };
+ }
+ consoleInfo = function (output) { window.console.info(output); };
+ consoleError = function (output) { window.console.error(output); };
+ } else {
+ consoleInfo = emptyMethod;
+ consoleError = emptyMethod;
+ }
+
+ var copyArray = function (array) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ return Array.prototype.slice.apply(array, args);
+ };
+
+ var indexOfItemInArray = function (array, item) {
+ for (var i = 0, length = array.length; i < length; i++) {
+ if (array[i] === item) {
+ return i;
+ }
+ }
+ return -1;
+ };
+ var removeFromArray = function (array, item) {
+ var again = true;
+ while (again) {
+ var index = indexOfItemInArray(array, item);
+ if (index != -1) {
+ array.splice(index, 1);
+ } else {
+ again = false;
+ }
+ }
+ };
+
+ var parseResponseHeaders = function (headerStr) {
+ var headers = {};
+ if (!headerStr) {
+ return headers;
+ }
+ var headerPairs = headerStr.split('\u000d\u000a');
+ for (var i = 0; i < headerPairs.length; i++) {
+ var headerPair = headerPairs[i];
+ var index = headerPair.indexOf('\u003a\u0020');
+ if (index > 0) {
+ var key = headerPair.substring(0, index);
+ headers[key] = headerPair.substring(index + 2);
+ }
+ }
+ return headers;
+ };
+
+ var addJsonpScriptToDom = function (src, scriptId) {
+ var script = window.document.createElement("script");
+ script.type = "text/javascript";
+ script.id = scriptId;
+ script.src = src;
+
+ var head = window.document.getElementsByTagName("head")[0];
+ head.appendChild(script);
+ };
+ var removeJsonpScriptFromDom = function (scriptId) {
+ var script = window.document.getElementById(scriptId);
+ script.parentNode.removeChild(script);
+ };
+
+ var jsonCallbacks = {
+ id: 0,
+ requests: {},
+ getJsonpCallback: function (id) {
+ var cb;
+ var requests = this.requests;
+ if (id in this.requests) {
+ cb = function (result) { requests[id]._jsonpCallback(result); };
+ } else {
+ consoleInfo("no callback with id " + id);
+ cb = emptyMethod;
+ }
+ return cb;
+ },
+ addJsonpCallback: function (id, obj) {
+ this.requests[id] = obj;
+ },
+ removeJsonpCallback: function (id) {
+ delete this.requests[id];
+ },
+ newCallbackInfo: function () {
+ var callbackInfo = {
+ id: "cb-" + this.id,
+ scriptId: "pd-jsonp-script-" + this.id
+ };
+ this.id++;
+ return callbackInfo;
+ }
+ };
+
+ var corsAvailable = "withCredentials" in new window.XMLHttpRequest();
+
+ var sameOrigin = function (url) {
+ var loc = window.location;
+ var a = window.document.createElement('a');
+ a.href = url;
+ return !a.hostname || (a.hostname == loc.hostname && a.port == loc.port && a.protocol == loc.protocol);
+ };
+
+ var chooseTransport = function (transportType, url) {
+ var transport;
+ if (transportType == transportTypes.Auto) {
+ if (corsAvailable || sameOrigin(url)) {
+ transport = transportTypes.Xhr;
+ } else {
+ transport = transportTypes.Jsonp;
+ }
+ } else {
+ transport = transportType;
+ }
+ return transport;
+ };
+
+ var Events = function () {
+ this._events = {};
+ };
+ Events.prototype._getHandlersForType = function (type) {
+ if (!(type in this._events)) {
+ this._events[type] = [];
+ }
+ return this._events[type];
+ };
+ Events.prototype.on = function (type, handler) {
+ var handlers = this._getHandlersForType(type);
+ handlers.push(handler);
+ };
+ Events.prototype.off = function (type) {
+ if (arguments.length > 1) {
+ var handler = arguments[1];
+ var handlers = this._getHandlersForType(type);
+ removeFromArray(handlers, handler);
+ } else {
+ delete this._events[type];
+ }
+ };
+ Events.prototype.trigger = function (type, obj) {
+ var args = copyArray(arguments, 2);
+ var handlers = copyArray(this._getHandlersForType(type));
+ for (var i = 0, n = handlers.length; i < n; i++) {
+ var handler = handlers[i];
+ handler.apply(obj, args);
+ }
+ };
+
+ var errorTypes = {
+ "TransportError": 0,
+ "TimeoutError": 1
+ };
+
+ var transportTypes = {
+ "Auto": 0,
+ "Xhr": 1,
+ "Jsonp": 2
+ };
+
+ // Pollymer.Request has callback members:
+ // on('finished', int code, object result, object headers)
+ // on('error', int reason)
+
+ var Request = function () {
+ if (!(this instanceof Request)) {
+ throw new window.Error("Constructor called as a function");
+ }
+
+ this._events = new Events();
+ this._tries = 0;
+ this._delayNext = false;
+ this._retryTime = 0;
+ this._timer = null;
+ this._jsonp = null;
+
+ this._xhr = null;
+ this._method = null;
+ this._url = null;
+ this._headers = null;
+ this._body = null;
+ this._transport = null;
+
+ this.transport = transportTypes.Auto;
+ this.rawResponse = false;
+ this.maxTries = 1;
+ this.maxDelay = 1000;
+ this.recurring = false;
+
+ if (arguments.length > 0) {
+ var config = arguments[0];
+ if ("transport" in config) {
+ this.transport = config.transport;
+ }
+ if ("rawResponse" in config) {
+ this.rawResponse = config.rawResponse;
+ }
+ if ("maxTries" in config) {
+ this.maxTries = config.maxTries;
+ }
+ if ("maxDelay" in config) {
+ this.maxDelay = config.maxDelay;
+ }
+ if ("recurring" in config) {
+ this.recurring = config.recurring;
+ }
+ }
+ };
+ Request.prototype.start = function (method, url, headers, body) {
+ if (this._timer != null) {
+ consoleError("pollymer: start() called on a Request object that is currently running.");
+ return;
+ }
+
+ this._method = method;
+ this._url = url;
+ this._headers = headers;
+ this._body = body;
+ this._start();
+ };
+ Request.prototype._start = function () {
+ this._tries = 0;
+
+ var delayTime;
+ if (this._delayNext) {
+ this._delayNext = false;
+ delayTime = Math.floor(Math.random() * this.maxDelay);
+ consoleInfo("pollymer: polling again in " + delayTime + "ms");
+ } else {
+ delayTime = 0; // always queue the call, to prevent browser "busy"
+ }
+
+ this._initiate(delayTime);
+ };
+ Request.prototype.retry = function () {
+ if (this._tries == 0) {
+ consoleError("pollymer: retry() called on a Request object that has never been started.");
+ return;
+ }
+ if (this._timer != null) {
+ consoleError("pollymer: retry() called on a Request object that is currently running.");
+ return;
+ }
+ this._retry();
+ };
+ Request.prototype._retry = function () {
+ if (this._tries === 1) {
+ this._retryTime = 1;
+ } else if (this._tries < 8) {
+ this._retryTime = this._retryTime * 2;
+ }
+
+ var delayTime = this._retryTime * 1000;
+ delayTime += Math.floor(Math.random() * this.maxDelay);
+ consoleInfo("pollymer: trying again in " + delayTime + "ms");
+
+ this._initiate(delayTime);
+ };
+ Request.prototype._initiate = function (delayMsecs) {
+ var self = this;
+ self._timer = window.setTimeout(function () { self._startConnect(); }, delayMsecs);
+ };
+ Request.prototype._startConnect = function () {
+ var self = this;
+ this._timer = window.setTimeout(function () { self._timeout(); }, TIMEOUT);
+
+ this._tries++;
+
+ var method = this._method;
+ var url = (typeof (this._url) == "function") ? this._url() : this._url;
+ var headers = this._headers;
+ var body = this._body;
+
+ // Create a copy of the transport because we don't want
+ // to give public access to it (changing it between now and
+ // cleanup would be a no-no)
+ this._transport = chooseTransport(this.transport, url);
+
+ if (this._transport == transportTypes.Xhr) {
+ this._xhr = this._startXhr(method, url, headers, body);
+ } else { // Jsonp
+ this._jsonp = this._startJsonp(method, url, headers, body);
+ }
+ };
+ Request.prototype._cleanupConnect = function (abort) {
+ window.clearTimeout(this._timer);
+ this._timer = null;
+
+ if (this._transport == transportTypes.Xhr) {
+ consoleInfo("pollymer: XHR cleanup");
+ this._cleanupXhr(this._xhr, abort);
+ this._xhr = null;
+ } else { // Jsonp
+ consoleInfo("pollymer: json-p " + this._jsonp.id + " cleanup");
+ this._cleanupJsonp(this._jsonp, abort);
+ this._jsonp = null;
+ }
+ };
+ Request.prototype.abort = function () {
+ this._cleanupConnect(true);
+ };
+ Request.prototype.on = function (type, handler) {
+ this._events.on(type, handler);
+ };
+ Request.prototype.off = function (type) {
+ var args = copyArray(arguments, 1);
+ args.unshift(type);
+ this._events.off.apply(this._events, args);
+ };
+ Request.prototype._startXhr = function (method, url, headers, body) {
+ var xhr = new window.XMLHttpRequest();
+ var self = this;
+ xhr.onreadystatechange = function () { self._xhrCallback(); };
+ xhr.open(method, url, true);
+
+ for (var key in headers) {
+ if (headers.hasOwnProperty(key)) {
+ xhr.setRequestHeader(key, headers[key]);
+ }
+ }
+
+ xhr.send(body);
+
+ consoleInfo("pollymer: XHR start " + url);
+
+ return xhr;
+ };
+ Request.prototype._cleanupXhr = function (xhr, abort) {
+ if (xhr != null) {
+ xhr.onreadystatechange = emptyMethod;
+ if (abort) {
+ xhr.abort();
+ }
+ }
+ };
+ Request.prototype._xhrCallback = function () {
+ var xhr = this._xhr;
+ if (xhr != null && xhr.readyState === 4) {
+ consoleInfo("pollymer: XHR finished");
+
+ var code = xhr.status;
+ var reason = xhr.statusText;
+ var headers = parseResponseHeaders(xhr.getAllResponseHeaders());
+ var body = xhr.responseText;
+
+ this._handleResponse(code, reason, headers, body);
+ }
+ };
+ Request.prototype._startJsonp = function (method, url, headers, body) {
+ var jsonp = jsonCallbacks.newCallbackInfo();
+
+ var paramList = [
+ "callback=" + encodeURIComponent("window['" + NAMESPACE + "']._getJsonpCallback(\"" + jsonp.id + "\")")
+ ];
+
+ if (method != "GET") {
+ paramList.push("_method=" + encodeURIComponent(method));
+ }
+ if (headers) {
+ paramList.push("_headers=" + encodeURIComponent(JSON.stringify(headers)));
+ }
+ if (body) {
+ paramList.push("_body=" + encodeURIComponent(body));
+ }
+ var params = paramList.join("&");
+
+ var src = (url.indexOf("?") != -1) ? url + "&" + params : url + "?" + params;
+
+ jsonCallbacks.addJsonpCallback(jsonp.id, this);
+ addJsonpScriptToDom(src, jsonp.scriptId);
+
+ consoleInfo("pollymer: json-p start " + jsonp.id + " " + src);
+
+ return jsonp;
+ };
+ Request.prototype._cleanupJsonp = function (jsonp, abort) {
+ if (jsonp != null) {
+ jsonCallbacks.removeJsonpCallback(jsonp.id);
+ removeJsonpScriptFromDom(jsonp.scriptId);
+ }
+ };
+ Request.prototype._jsonpCallback = function (result) {
+ consoleInfo("pollymer: json-p " + this._jsonp.id + " finished");
+
+ var code = ("code" in result) ? result.code : 0;
+ var reason = ("reason" in result) ? result.reason : null;
+ var headers = ("headers" in result) ? result.headers : {};
+ var body = ("body" in result) ? result.body : null;
+
+ this._handleResponse(code, reason, headers, body);
+ };
+ Request.prototype._handleResponse = function (code, reason, headers, body) {
+ this._cleanupConnect();
+
+ if ((code == 0 || (code >= 500 && code < 600)) &&
+ (this.maxTries == -1 || this._tries < this.maxTries)) {
+ this._retry();
+ } else {
+ if (code > 0) {
+ var result;
+ if (this.rawResponse) {
+ result = body;
+ } else {
+ try {
+ result = JSON.parse(body);
+ } catch (e) {
+ result = body;
+ }
+ }
+ this._finished(code, result, headers);
+ if (this.recurring && code >= 200 && code < 300) {
+ this._start();
+ }
+ } else {
+ this._error(errorTypes.TransportError);
+ }
+ }
+ };
+ Request.prototype._timeout = function () {
+ this._cleanupConnect(true);
+
+ if (this.maxTries == -1 || this._tries < this.maxTries) {
+ this._retry();
+ } else {
+ this._error(errorTypes.TimeoutError);
+ }
+ };
+ Request.prototype._finished = function (code, result, headers) {
+ this._delayNext = true;
+ this._events.trigger('finished', this, code, result, headers);
+ };
+ Request.prototype._error = function (reason) {
+ this._delayNext = true;
+ this._events.trigger('error', this, reason);
+ };
+
+ var exports = {
+ Request: Request,
+ TransportTypes: transportTypes,
+ ErrorTypes: errorTypes,
+ _getJsonpCallback: function (id) {
+ return jsonCallbacks.getJsonpCallback(id);
+ }
+ };
+ window[NAMESPACE] = window[NAMESPACE] || exports;
+
+})(window);
+})();