diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 0d039e8ec5ec..8afcd39f9e0a 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -20,6 +20,7 @@ * Contributor(s): * Dan Mills * Anant Narayanan + * Philipp von Weitershausen * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or @@ -35,7 +36,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ["Resource"]; +const EXPORTED_SYMBOLS = ["Resource", "AsyncResource"]; const Cc = Components.classes; const Ci = Components.interfaces; @@ -50,24 +51,39 @@ Cu.import("resource://services-sync/ext/Sync.js"); Cu.import("resource://services-sync/log4moz.js"); Cu.import("resource://services-sync/util.js"); -// = Resource = -// -// Represents a remote network resource, identified by a URI. -function Resource(uri) { +/* + * AsyncResource represents a remote network resource, identified by a URI. + * Create an instance like so: + * + * let resource = new AsyncResource("http://foobar.com/path/to/resource"); + * + * The 'resource' object has the following methods to issue HTTP requests + * of the corresponding HTTP methods: + * + * get(callback) + * put(data, callback) + * post(data, callback) + * delete(callback) + * + * 'callback' is a function with the following signature: + * + * function callback(error, result) {...} + * + * 'error' will be null on successful requests. Likewise, result will not be + * passes (=undefined) when an error occurs. Note that this is independent of + * the status of the HTTP response. + */ +function AsyncResource(uri) { this._log = Log4Moz.repository.getLogger(this._logName); this._log.level = Log4Moz.Level[Utils.prefs.getCharPref("log.logger.network.resources")]; this.uri = uri; this._headers = {}; + this._onComplete = Utils.bind2(this, this._onComplete); } -Resource.prototype = { +AsyncResource.prototype = { _logName: "Net.Resource", - // ** {{{ Resource.serverTime }}} ** - // - // Caches the latest server timestamp (X-Weave-Timestamp header). - serverTime: null, - // ** {{{ Resource.authenticator }}} ** // // Getter and setter for the authenticator module @@ -137,7 +153,7 @@ Resource.prototype = { // ** {{{ Resource._createRequest }}} ** // // This method returns a new IO Channel for requests to be made - // through. It is never called directly, only {{{_request}}} uses it + // through. It is never called directly, only {{{_doRequest}}} uses it // to obtain a request channel. // _createRequest: function Res__createRequest() { @@ -165,14 +181,9 @@ Resource.prototype = { _onProgress: function Res__onProgress(channel) {}, - // ** {{{ Resource._request }}} ** - // - // Perform a particular HTTP request on the resource. This method - // is never called directly, but is used by the high-level - // {{{get}}}, {{{put}}}, {{{post}}} and {{delete}} methods. - _request: function Res__request(action, data) { - let iter = 0; - let channel = this._createRequest(); + _doRequest: function _doRequest(action, data, callback) { + this._callback = callback; + let channel = this._channel = this._createRequest(); if ("undefined" != typeof(data)) this._data = data; @@ -200,29 +211,21 @@ Resource.prototype = { // Setup a channel listener so that the actual network operation // is performed asynchronously. - let [chanOpen, chanCb] = Sync.withCb(channel.asyncOpen, channel); - let listener = new ChannelListener(chanCb, this._onProgress, this._log); + let listener = new ChannelListener(this._onComplete, this._onProgress, + this._log); channel.requestMethod = action; + channel.asyncOpen(listener, null); + }, - // The channel listener might get a failure code - try { - this._data = chanOpen(listener, null); + _onComplete: function _onComplete(error, data) { + if (error) { + this._callback(error); + return; } - catch(ex) { - // Combine the channel stack with this request stack - let error = Error(ex.message); - let chanStack = []; - if (ex.stack) - chanStack = ex.stack.trim().split(/\n/).slice(1); - let requestStack = error.stack.split(/\n/).slice(1); - // Strip out the args for the last 2 frames because they're usually HUGE! - for (let i = 0; i <= 1; i++) - requestStack[i] = requestStack[i].replace(/\(".*"\)@/, "(...)@"); - - error.stack = chanStack.concat(requestStack).join("\n"); - throw error; - } + this._data = data; + let channel = this._channel; + let action = channel.requestMethod; // Set some default values in-case there's no response header let headers = {}; @@ -240,13 +243,13 @@ Resource.prototype = { // Log the status of the request let mesg = [action, success ? "success" : "fail", status, - channel.URI.spec].join(" "); + channel.URI.spec].join(" "); if (mesg.length > 200) mesg = mesg.substr(0, 200) + "…"; this._log.debug(mesg); // Additionally give the full response body when Trace logging if (this._log.level <= Log4Moz.Level.Trace) - this._log.trace(action + " body: " + this._data); + this._log.trace(action + " body: " + data); // This is a server-side safety valve to allow slowing down // clients without hurting performance. @@ -263,7 +266,7 @@ Resource.prototype = { this._log.debug(action + " cached: " + status); } - let ret = new String(this._data); + let ret = new String(data); ret.headers = headers; ret.status = status; ret.success = success; @@ -284,22 +287,96 @@ Resource.prototype = { // Do the same type of request but with the new uri if (subject.newUri != "") { this.uri = subject.newUri; - return this._request.apply(this, arguments); + this._doRequest(action, this._data, this._callback); + return; } } - return ret; + this._callback(null, ret); + }, + + get: function get(callback) { + this._doRequest("GET", undefined, callback); + }, + + put: function put(data, callback) { + if (typeof data == "function") + [data, callback] = [undefined, data]; + this._doRequest("PUT", data, callback); + }, + + post: function post(data, callback) { + if (typeof data == "function") + [data, callback] = [undefined, data]; + this._doRequest("POST", data, callback); + }, + + delete: function delete(callback) { + this._doRequest("DELETE", undefined, callback); + } +}; + + +/* + * Represent a remote network resource, identified by a URI, with a + * synchronous API. + * + * 'Resource' is not recommended for new code. Use the asynchronous API of + * 'AsyncResource' instead. + */ +function Resource(uri) { + AsyncResource.call(this, uri); +} +Resource.prototype = { + + __proto__: AsyncResource.prototype, + + // ** {{{ Resource.serverTime }}} ** + // + // Caches the latest server timestamp (X-Weave-Timestamp header). + serverTime: null, + + // ** {{{ Resource._request }}} ** + // + // Perform a particular HTTP request on the resource. This method + // is never called directly, but is used by the high-level + // {{{get}}}, {{{put}}}, {{{post}}} and {{delete}} methods. + _request: function Res__request(action, data) { + let [doRequest, cb] = Sync.withCb(this._doRequest, this); + function callback(error, ret) { + if (error) + cb.throw(error); + cb(ret); + } + + // The channel listener might get a failure code + try { + return doRequest(action, data, callback); + } catch(ex) { + // Combine the channel stack with this request stack + let error = Error(ex.message); + let chanStack = []; + if (ex.stack) + chanStack = ex.stack.trim().split(/\n/).slice(1); + let requestStack = error.stack.split(/\n/).slice(1); + + // Strip out the args for the last 2 frames because they're usually HUGE! + for (let i = 0; i <= 1; i++) + requestStack[i] = requestStack[i].replace(/\(".*"\)@/, "(...)@"); + + error.stack = chanStack.concat(requestStack).join("\n"); + throw error; + } }, // ** {{{ Resource.get }}} ** // // Perform an asynchronous HTTP GET for this resource. - // onComplete will be called on completion of the request. get: function Res_get() { return this._request("GET"); }, - // ** {{{ Resource.get }}} ** + // ** {{{ Resource.put }}} ** // // Perform a HTTP PUT for this resource. put: function Res_put(data) { @@ -357,10 +434,12 @@ ChannelListener.prototype = { this._data = null; // Throw the failure code name (and stop execution) - if (!Components.isSuccessCode(status)) - this._onComplete.throw(Error(Components.Exception("", status).name)); + if (!Components.isSuccessCode(status)) { + this._onComplete(Error(Components.Exception("", status).name)); + return; + } - this._onComplete(this._data); + this._onComplete(null, this._data); }, onDataAvailable: function Channel_onDataAvail(req, cb, stream, off, count) { @@ -384,7 +463,7 @@ ChannelListener.prototype = { // Ignore any callbacks if we happen to get any now this.onStopRequest = function() {}; this.onDataAvailable = function() {}; - this._onComplete.throw(Error("Aborting due to channel inactivity.")); + this._onComplete(Error("Aborting due to channel inactivity.")); } };