From 771ca115d8ea03d194e8100e7107e88a14ed0d97 Mon Sep 17 00:00:00 2001 From: Justin Karneges Date: Thu, 31 Jan 2013 23:07:39 -0800 Subject: recurring flag. cleanup --- README | 111 +++++++++++++++++--- polldance.js | 331 ++++++++++++++++++++++++++++++++++------------------------- 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, -- cgit v1.2.3