Bug 603301 - Add async resource API. [r=mconnor]
authorPhilipp von Weitershausen <philipp@weitershausen.de>
Fri, 29 Oct 2010 10:20:27 -0700
changeset 57533 d5799add4384fc6b81a0c158fb3ee93c6dde06d7
parent 57532 c69e2357abd2273b0210dbd2c1506f62890a8fef
child 57534 146cf61f11633b7faa74a506a8c8418dafc6164a
push idunknown
push userunknown
push dateunknown
reviewersmconnor
bugs603301
Bug 603301 - Add async resource API. [r=mconnor] The async API is exposed as AsyncResource which is functionally equivalent to Resource. The latter is now a wrapper around AsyncResource.
services/sync/modules/resource.js
--- a/services/sync/modules/resource.js
+++ b/services/sync/modules/resource.js
@@ -15,64 +15,80 @@
  *
  * The Initial Developer of the Original Code is Mozilla.
  * Portions created by the Initial Developer are Copyright (C) 2007
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *  Dan Mills <thunder@mozilla.com>
  *  Anant Narayanan <anant@kix.in>
+ *  Philipp von Weitershausen <philipp@weitershausen.de>
  *
  * 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
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
-const EXPORTED_SYMBOLS = ["Resource"];
+const EXPORTED_SYMBOLS = ["Resource", "AsyncResource"];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://services-sync/auth.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/ext/Observers.js");
 Cu.import("resource://services-sync/ext/Preferences.js");
 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
   // responsible for this particular resource. The authenticator
   // module may modify the headers to perform authentication
   // while performing a request for the resource, for example.
   get authenticator() {
     if (this._authenticator)
@@ -132,17 +148,17 @@ Resource.prototype = {
   get data() this._data,
   set data(value) {
     this._data = value;
   },
 
   // ** {{{ 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() {
     let channel = Svc.IO.newChannel(this.spec, null, null).
       QueryInterface(Ci.nsIRequest).QueryInterface(Ci.nsIHttpChannel);
 
     // Always validate the cache:
     channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
@@ -160,24 +176,19 @@ Resource.prototype = {
         this._log.trace("HTTP Header " + key + ": " + headers[key]);
       channel.setRequestHeader(key, headers[key], false);
     }
     return channel;
   },
 
   _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;
 
     // PUT and POST are trreated differently because
     // they have payload data.
     if ("PUT" == action || "POST" == action) {
       // Convert non-string bodies into JSON
@@ -195,39 +206,31 @@ Resource.prototype = {
       stream.setData(this._data, this._data.length);
 
       channel.QueryInterface(Ci.nsIUploadChannel);
       channel.setUploadStream(stream, type, this._data.length);
     }
 
     // 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 = {};
     let status = 0;
     let success = true;
     try {
       // Read out the response headers if available
       channel.visitResponseHeaders({
@@ -235,40 +238,40 @@ Resource.prototype = {
           headers[header.toLowerCase()] = value;
         }
       });
       status = channel.responseStatus;
       success = channel.requestSucceeded;
 
       // 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.
       if (headers["x-weave-backoff"])
         Observers.notify("weave:service:backoff:interval",
                          parseInt(headers["x-weave-backoff"], 10));
 
       if (success && headers["x-weave-quota-remaining"])
         Observers.notify("weave:service:quota:remaining",
                          parseInt(headers["x-weave-quota-remaining"], 10));
     }
     // Got a response but no header; must be cached (use default values)
     catch(ex) {
       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;
 
     // Make a lazy getter to convert the json response into an object
     Utils.lazy2(ret, "obj", function() JSON.parse(ret));
 
     // Notify if we get a 401 to maybe try again with a new uri
@@ -279,32 +282,106 @@ Resource.prototype = {
         resource: this,
         response: ret
       }
       Observers.notify("weave:resource:status:401", subject);
 
       // 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) {
     return this._request("PUT", data);
   },
 
   // ** {{{ Resource.post }}} **
   //
@@ -352,20 +429,22 @@ ChannelListener.prototype = {
   onStopRequest: function Channel_onStopRequest(channel, context, status) {
     // Clear the abort timer now that the channel is done
     this.abortTimer.clear();
 
     if (this._data == '')
       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) {
     let siStream = Cc["@mozilla.org/scriptableinputstream;1"].
       createInstance(Ci.nsIScriptableInputStream);
     siStream.init(stream);
 
     this._data += siStream.read(count);
@@ -379,17 +458,17 @@ ChannelListener.prototype = {
   delayAbort: function delayAbort() {
     Utils.delay(this.abortRequest, this.ABORT_TIMEOUT, this, "abortTimer");
   },
 
   abortRequest: function abortRequest() {
     // 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."));
   }
 };
 
 // = BadCertListener =
 //
 // We use this listener to ignore bad HTTPS
 // certificates and continue a request on a network
 // channel. Probably not a very smart thing to do,