aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJustin Karneges <justin@affinix.com>2013-01-31 23:07:39 -0800
committerJustin Karneges <justin@affinix.com>2013-01-31 23:07:39 -0800
commit771ca115d8ea03d194e8100e7107e88a14ed0d97 (patch)
tree63e609206c305bad895bdec22a8dc4b554a7dca5
parent223a60bcb8a57b093b62364a600c2dde1fced46b (diff)
downloadpollymer-771ca115d8ea03d194e8100e7107e88a14ed0d97.tar
pollymer-771ca115d8ea03d194e8100e7107e88a14ed0d97.tar.gz
recurring flag. cleanup
-rw-r--r--README111
-rw-r--r--polldance.js331
2 files changed, 290 insertions, 152 deletions
diff --git a/README b/README
index 33ff3e5..e2dd87d 100644
--- a/README
+++ b/README
@@ -39,21 +39,102 @@ Usage:
req.maxTries = 2; // try twice
req.start('POST', 'http://example.com/path', headers, body);
-Non-JSON Results:
-
- By default, this library will parse response body data as JSON and return an
- object to the application. Set the rawResponse property to true to disable
- this behavior and have a string returned instead.
-
-Polling and Retries:
-
- Set the maxTries property to enable multiple request attempts, with
- exponential backoff. Set maxTries to -1 to indicate infinite attempts.
- Transport errors or HTTP responses in the 5xx range will cause a retry to
- occur. Any other error will be returned to the application. Request objects
- may be reused. If reused, a random delay is inserted before starting the
- next request. By default this is a value between 0-1000ms. Set the maxDelay
- property to change the upper bound.
+Methods of Request Object:
+
+ on(event_name, callback) - Add callback for event:
+ event_name: name of event
+ callback: method to call when event occurs
+
+ available events:
+ 'finished': function(code, result, headers)
+ code: HTTP status code
+ result: JSON object or string
+ headers: hash of key/value strings
+ 'error': function(reason)
+ reason: PollDance.errorType
+
+ off(event_name) - Remove callback for event
+
+ start(method, url, headers, body) - start request
+ method: name of method (e.g. 'GET')
+ url: string url, or function that returns a string url if called
+ headers: hash of key/value strings (optional)
+ body: string body data (optional)
+
+ Sometime after the request has been started, a finished or error event
+ will be raised and the object will return to inactive state (unless the
+ recurring flag is set, see below).
+
+ The start method may be called again once the request has completed
+ (unless the recurring flag is set, see below). If called again on the same
+ object, a short random delay will be added before performing the request.
+
+ retry() - Attempt the exact same request again. Normally, PollDance will
+ automatically retry a request that it considers to be a failure, but this
+ method may be used if the application needs to retry the request for any
+ another reason. Retries have an exponentially increasing delay between
+ them. Do not use retry() if the previous request attempt was considered to
+ be successful, as it will add penalizing delays that you probably don't
+ want in that case.
+
+ abort() - Stop any current request and return the object to inactive state.
+
+Properties of Request Object:
+
+ Properties are simple members of the object. They can be set directly:
+ req.rawResponse = true;
+ or passed in a hash during construction:
+ var req = new PollDance.Request({rawResponse: true});
+
+ rawResponse: boolean
+ By default, this library will parse response body data as JSON and return
+ an object to the application. Set the rawResponse property to true to
+ disable this behavior and have a string returned instead.
+
+ maxTries: int
+ The number of tries a request should be attempted with temporary failure
+ before raising an error event. Set to -1 to indicate infinite attempts.
+ Default is 1.
+
+ maxDelay: int
+ The maximum amount of random milliseconds to delay between requests.
+ Default 1000.
+
+ recurring: boolean
+ If set to true, then after a request finishes with a code between 200 and
+ 299, and the finished event has been raised, the same request will be
+ started again. This allows PollDance to automatically poll a resource
+ endlessly. Pass a function as the url argument in order to be able to
+ change the url between polls.
+
+ transport: PollDance.transportType
+ Explicitly set the transport to use. Default is transportType.Auto, which
+ automatically chooses the best transport when the request is started.
+
+Retries:
+
+ When a request fails at the transport level, or the request succeeds with an
+ error code between 500 and 599, and maxTries has not been reached, then
+ PollDance will retry the request silently, with an exponentially increasing
+ delay between attempts. In any other case, the request will succeed and
+ the finished event will be raised. If the application determines that the
+ response indicates a temporary error and should be retried with the same
+ backoff delay that PollDance normally uses, the retry() method may be used.
+
+Request Reuse:
+
+ If start() is called on an object that has completed a request and is now
+ inactive, then a random delay will be added before performing the next
+ request. This is ideal behavior when repeatedly polling a resource, so do
+ try to reuse the request object rather than throwing it away and creating a
+ new one every time. When the recurring flag is used, the effect is the same
+ as if you had called start() again yourself after a request finished.
+
+ Be sure to recognize the difference between a retry and a reuse of the
+ object. A retry implies that the previous request attempt was a failure,
+ whereas reusing the object means the previous request attempt was
+ successful. This distinction is important because it changes the way the
+ delaying works.
JSON-P Protocol:
diff --git a/polldance.js b/polldance.js
index c633da7..4e08d3d 100644
--- a/polldance.js
+++ b/polldance.js
@@ -12,15 +12,18 @@ var DEBUG = true;
var TIMEOUT = 60000;
var emptyMethod = function(){};
- var log;
+ var consoleinfo;
+ var consoleerror;
if (DEBUG) {
// don't break if there's no console
if (typeof (window.console) === "undefined") {
- window.console = { log: emptyMethod, dir: emptyMethod };
+ window.console = { log: emptyMethod, error: emptyMethod };
}
- log = function(output) { window.console.log(output); };
+ consoleinfo = function(output) { window.console.info(output); };
+ consoleerror = function (output) { window.console.error(output); };
} else {
- log = emptyMethod;
+ consoleinfo = emptyMethod;
+ consoleerror = emptyMethod;
}
var copyArray = function(array) {
@@ -65,6 +68,20 @@ var DEBUG = true;
}
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,
@@ -75,7 +92,7 @@ var DEBUG = true;
if (id in this.requests) {
cb = function (result) { requests[id]._jsonp_callback(result); };
} else {
- log("no callback with id " + id);
+ consoleinfo("no callback with id " + id);
cb = emptyMethod;
}
return cb;
@@ -105,6 +122,20 @@ var DEBUG = true;
return !a.hostname || (a.hostname == loc.hostname && a.port == loc.port && a.protocol == loc.protocol);
};
+ var chooseTransport = function (transportType) {
+ 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 = {};
};
@@ -161,18 +192,20 @@ var DEBUG = true;
this._delayNext = false;
this._retryTime = 0;
this._timer = null;
- this._callbackInfo = 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];
@@ -188,95 +221,94 @@ var DEBUG = true;
if ("maxDelay" in config) {
this.maxDelay = config.maxDelay;
}
+ if ("recurring" in config) {
+ this.recurring = config.recurring;
+ }
}
};
Request.prototype.start = function (method, url, headers, body) {
- var self = this;
-
- self._tries = 0;
-
- var delaytime;
- if (self._delayNext) {
- self._delayNext = false;
- delaytime = Math.floor(Math.random() * self.maxDelay);
- log("PD: polling again in " + delaytime + "ms");
- } else {
- delaytime = 0; // always queue the call, to prevent browser "busy"
+ if (this._timer != null) {
+ consoleerror("PD: start() called on a Request object that is currently running.");
+ return;
}
self._method = method;
self._url = url;
self._headers = headers;
self._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("PD: polling again in " + delayTime + "ms");
+ } else {
+ delayTime = 0; // always queue the call, to prevent browser "busy"
+ }
- self._timer = window.setTimeout(function () { self._connect(); }, delaytime);
+ this._initiate(delayTime);
};
Request.prototype.retry = function () {
+ if (this._tries == 0) {
+ consoleerror("PD: retry() called on a Request object that has never been started.");
+ return;
+ }
+ if (this._timer != null) {
+ consoleerror("PD: retry() called on a Request object that is currently running.");
+ return;
+ }
this._retry();
};
- Request.prototype._connect = function () {
- var self = this;
- self._timer = window.setTimeout(function () { self._timeout(); }, TIMEOUT);
-
- self._tries++;
-
- if (self.transport == transportTypes.Auto) {
- if (corsAvailable || sameOrigin(self._url)) {
- self.transport = transportTypes.Xhr;
- } else {
- self.transport = transportTypes.Jsonp;
- }
+ Request.prototype._retry = function () {
+ if (this._tries === 1) {
+ this._retryTime = 1;
+ } else if (this._tries < 8) {
+ this._retryTime = this._retryTime * 2;
}
- if (self.transport == transportTypes.Xhr) {
-
- self._xhr = new window.XMLHttpRequest();
- self._xhr.onreadystatechange = function () { self._xhr_readystatechange(); };
- self._xhr.open(self._method, self._url, true);
-
- for (var key in self._headers) {
- if (self._headers.hasOwnProperty(key)) {
- self._xhr.setRequestHeader(key, self._headers[key]);
- }
- }
+ var delayTime = this._retryTime * 1000;
+ delayTime += Math.floor(Math.random() * this.maxDelay);
+ consoleinfo("PD: trying again in " + delayTime + "ms");
- self._xhr.send(self._body);
+ this._initiate(delayTime);
+ };
+ Request.prototype._initiate = function (delayMsecs) {
+ var self = this;
+ self._timer = window.setTimeout(function () { self._connect(); }, delayMsecs);
+ };
+ Request.prototype._connect = function () {
+ var self = this;
+ this._timer = window.setTimeout(function () { self._timeout(); }, TIMEOUT);
- log("PD: xhr " + self._url + " " + self._body);
+ this._tries++;
- } else { // Jsonp
+ var method = this._method;
+ var url = (typeof (this._url) == "function") ? this._url() : this._url;
+ var headers = this._headers;
+ var body = this._body;
- this._callbackInfo = jsonCallbacks.newCallbackInfo();
+ // Create a copy of the transport because we don't want
+ // to give public access to it (changing it between now and
+ // cleanout would be a no-no)
+ this._transport = chooseTransport(this.transport);
- var paramList = [];
+ if (this._transport == transportTypes.Xhr) {
- paramList.push("_callback=" + encodeURIComponent("window['" + NAMESPACE + "']._getJsonpCallback(\"" + this._callbackInfo.id + "\")"));
- if (self._method != "GET") {
- paramList.push("_method=" + encodeURIComponent(self._method));
- }
- if (self._headers) {
- paramList.push("_headers=" + encodeURIComponent(JSON.stringify(self._headers)));
- }
- if (self._body) {
- paramList.push("_body=" + encodeURIComponent(self._body));
- }
- var params = paramList.join("&");
+ this._xhr = this._xhr_start(method, url, headers, body);
+ consoleinfo("PD: xhr start");
- var src;
- if (self._url.indexOf("?") != -1) {
- src = self._url + "&" + params;
- } else {
- src = self._url + "?" + params;
- }
+ } else { // Jsonp
- log("PD: json-p " + this._callbackInfo.id + " " + src);
- this._addJsonpCallback(this._callbackInfo, src);
+ this._jsonp = this._jsonp_start(method, url, headers, body);
+ consoleinfo("PD: json-p start " + this._jsonp.id + " " + src);
}
};
Request.prototype.abort = function () {
- window.clearTimeout(this._timer);
- this._timer = null;
- this._cancelreq();
+ this._cleanup(true);
};
Request.prototype.on = function (type, handler) {
this._events.on(type, handler);
@@ -285,84 +317,109 @@ var DEBUG = true;
var args = copyArray(arguments, 1).unshift(type);
this._events.off.apply(this._events, args);
};
- Request.prototype._addJsonpCallback = function (callbackInfo, src) {
- jsonCallbacks.addJsonpCallback(callbackInfo.id, this);
+ Request.prototype._xhr_start = function(method, url, headers, body) {
+ var xhr = new window.XMLHttpRequest();
+ var self = this;
+ xhr.onreadystatechange = function () { self._xhr_callback(); };
+ xhr.open(method, url, true);
- var script = window.document.createElement("script");
- script.type = "text/javascript";
- script.id = callbackInfo.scriptId;
- script.src = src;
+ for (var key in headers) {
+ if (headers.hasOwnProperty(key)) {
+ xhr.setRequestHeader(key, headers[key]);
+ }
+ }
- var head = window.document.getElementsByTagName("head")[0];
- head.appendChild(script);
- };
- Request.prototype._removeJsonpCallback = function (callbackInfo) {
- jsonCallbacks.removeJsonpCallback(callbackInfo.id, this);
+ xhr.send(body);
- var script = window.document.getElementById(callbackInfo.scriptId);
- script.parentNode.removeChild(script);
+ return xhr;
};
- Request.prototype._cancelreq = function () {
- if (this.transport == transportTypes.Xhr) {
- this._xhr.onreadystatechange = emptyMethod;
- this._xhr.abort();
- this._xhr = null;
- } else { // Jsonp
- log("PD: json-p " + this._callbackInfo.id + " cancel");
- this._removeJsonpCallback(this._callbackInfo);
- this._callbackInfo = null;
+ Request.prototype._xhr_cleanup = function(xhr, abort) {
+ xhr.onreadystatechange = emptyMethod;
+ if (abort) {
+ xhr.abort();
}
};
- Request.prototype._xhr_readystatechange = function () {
+ Request.prototype._xhr_callback = function () {
var xhr = this._xhr;
if (xhr != null && xhr.readyState === 4) {
- this._xhr = null;
+ consoleinfo("PD: XHR finished");
- window.clearTimeout(this._timer);
- this._timer = null;
+ var code = xhr.status;
+ var status = xhr.statusText;
+ var headers = parseResponseHeaders(xhr.getAllResponseHeaders());
+ var body = xhr.responseText;
- if ((!xhr.status || (xhr.status >= 500 && xhr.status < 600)) &&
- (this.maxTries == -1 || this._tries < this.maxTries)) {
- this._retry();
- } else {
- if (xhr.status) {
- var headersStr = xhr.getAllResponseHeaders();
- var headers = parseResponseHeaders(headersStr);
- this._handle_response(xhr.status, xhr.statusText, headers, xhr.responseText);
- } else {
- this._error(errorTypes.TransportError);
- }
- }
+ this._handle_response(code, status, headers, body);
}
};
- Request.prototype._jsonp_callback = function (result) {
- log("PD: json-p " + this._callbackInfo.id + " finished");
+ Request.prototype._jsonp_start = function (method, url, headers, body) {
+ var jsonp = jsonCallbacks.newCallbackInfo();
- window.clearTimeout(this._timer);
- this._timer = null;
+ var paramList = [
+ "_callback=" + encodeURIComponent("window['" + NAMESPACE + "']._getJsonpCallback(\"" + jsonp.id + "\")")
+ ];
- this._removeJsonpCallback(this._callbackInfo);
- this._callbackInfo = null;
+ 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);
+
+ return jsonp;
+ };
+ Request.prototype._jsonp_cleanup = function (jsonp, abort) {
+ jsonCallbacks.removeJsonpCallback(jsonp.id, this);
+ removeJsonpScriptFromDom(jsonp.scriptId);
+ };
+ Request.prototype._jsonp_callback = function (result) {
+ consoleinfo("PD: json-p " + this._jsonp.id + " finished");
+
+ var code = ("code" in result) ? result.code : 0;
+ var status = ("status" in result) ? result.status : null;
var headers = ("headers" in result) ? result.headers : {};
- this._handle_response(result.code, result.status, headers, result.body);
+ var body = ("body" in result) ? result.body : null;
+
+ this._handle_response(code, status, headers, body);
};
Request.prototype._handle_response = function (code, status, headers, body) {
- var result;
- if (this.rawResponse) {
- result = body;
+ this._cleanup();
+
+ if ((code == 0 || (code >= 500 && code < 600)) &&
+ (this.maxTries == -1 || this._tries < this.maxTries)) {
+ this._retry();
} else {
- try {
- result = JSON.parse(body);
- } catch (e) {
- result = body;
+ 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);
}
}
- this._finished(code, result, headers);
};
Request.prototype._timeout = function () {
- this._timer = null;
- this._cancelreq();
+ this._cleanup(true);
if (this.maxTries == -1 || this._tries < this.maxTries) {
this._retry();
@@ -370,20 +427,6 @@ var DEBUG = true;
this._error(errorTypes.TimeoutError);
}
};
- 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);
- log("PD: trying again in " + delaytime + "ms");
-
- var self = this;
- self._timer = window.setTimeout(function () { self._connect(); }, delaytime);
- };
Request.prototype._finished = function (code, result, headers) {
this._delayNext = true;
this._events.trigger('finished', this, code, result, headers);
@@ -392,7 +435,21 @@ var DEBUG = true;
this._delayNext = true;
this._events.trigger('error', this, reason);
};
+ Request.prototype._cleanup = function (abort) {
+ window.clearTimeout(this._timer);
+ this._timer = null;
+ if (this._transport == transportTypes.Xhr) {
+ consoleinfo("PD: XHR canceled");
+ this._xhr_cleanup(this._xhr, abort);
+ this._xhr = null;
+ } else { // Jsonp
+ consoleinfo("PD: json-p " + this._jsonp.id + " canceled");
+ this._jsonp_cleanup(this._jsonp, abort);
+ this._jsonp = null;
+ }
+ };
+
var exports = {
Request: Request,
TransportTypes: transportTypes,