services/common/storageservice.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 /**
     6  * This file contains APIs for interacting with the Storage Service API.
     7  *
     8  * The specification for the service is available at.
     9  * http://docs.services.mozilla.com/storage/index.html
    10  *
    11  * Nothing about the spec or the service is Sync-specific. And, that is how
    12  * these APIs are implemented. Instead, it is expected that consumers will
    13  * create a new type inheriting or wrapping those provided by this file.
    14  *
    15  * STORAGE SERVICE OVERVIEW
    16  *
    17  * The storage service is effectively a key-value store where each value is a
    18  * well-defined envelope that stores specific metadata along with a payload.
    19  * These values are called Basic Storage Objects, or BSOs. BSOs are organized
    20  * into named groups called collections.
    21  *
    22  * The service also provides ancillary APIs not related to storage, such as
    23  * looking up the set of stored collections, current quota usage, etc.
    24  */
    26 "use strict";
    28 this.EXPORTED_SYMBOLS = [
    29   "BasicStorageObject",
    30   "StorageServiceClient",
    31   "StorageServiceRequestError",
    32 ];
    34 const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
    36 Cu.import("resource://gre/modules/Preferences.jsm");
    37 Cu.import("resource://services-common/async.js");
    38 Cu.import("resource://gre/modules/Log.jsm");
    39 Cu.import("resource://services-common/rest.js");
    40 Cu.import("resource://services-common/utils.js");
    42 const Prefs = new Preferences("services.common.storageservice.");
    44 /**
    45  * The data type stored in the storage service.
    46  *
    47  * A Basic Storage Object (BSO) is the primitive type stored in the storage
    48  * service. BSO's are simply maps with a well-defined set of keys.
    49  *
    50  * BSOs belong to named collections.
    51  *
    52  * A single BSO consists of the following fields:
    53  *
    54  *   id - An identifying string. This is how a BSO is uniquely identified within
    55  *     a single collection.
    56  *   modified - Integer milliseconds since Unix epoch BSO was modified.
    57  *   payload - String contents of BSO. The format of the string is undefined
    58  *     (although JSON is typically used).
    59  *   ttl - The number of seconds to keep this record.
    60  *   sortindex - Integer indicating relative importance of record within the
    61  *     collection.
    62  *
    63  * The constructor simply creates an empty BSO having the specified ID (which
    64  * can be null or undefined). It also takes an optional collection. This is
    65  * purely for convenience.
    66  *
    67  * This type is meant to be a dumb container and little more.
    68  *
    69  * @param id
    70  *        (string) ID of BSO. Can be null.
    71  *        (string) Collection BSO belongs to. Can be null;
    72  */
    73 this.BasicStorageObject =
    74  function BasicStorageObject(id=null, collection=null) {
    75   this.data       = {};
    76   this.id         = id;
    77   this.collection = collection;
    78 }
    79 BasicStorageObject.prototype = {
    80   id: null,
    81   collection: null,
    82   data: null,
    84   // At the time this was written, the convention for constructor arguments
    85   // was not adopted by Harmony. It could break in the future. We have test
    86   // coverage that will break if SpiderMonkey changes, just in case.
    87   _validKeys: new Set(["id", "payload", "modified", "sortindex", "ttl"]),
    89   /**
    90    * Get the string payload as-is.
    91    */
    92   get payload() {
    93     return this.data.payload;
    94   },
    96   /**
    97    * Set the string payload to a new value.
    98    */
    99   set payload(value) {
   100     this.data.payload = value;
   101   },
   103   /**
   104    * Get the modified time of the BSO in milliseconds since Unix epoch.
   105    *
   106    * You can convert this to a native JS Date instance easily:
   107    *
   108    *   let date = new Date(bso.modified);
   109    */
   110   get modified() {
   111     return this.data.modified;
   112   },
   114   /**
   115    * Sets the modified time of the BSO in milliseconds since Unix epoch.
   116    *
   117    * Please note that if this value is sent to the server it will be ignored.
   118    * The server will use its time at the time of the operation when storing the
   119    * BSO.
   120    */
   121   set modified(value) {
   122     this.data.modified = value;
   123   },
   125   get sortindex() {
   126     if (this.data.sortindex) {
   127       return this.data.sortindex || 0;
   128     }
   130     return 0;
   131   },
   133   set sortindex(value) {
   134     if (!value && value !== 0) {
   135       delete this.data.sortindex;
   136       return;
   137     }
   139     this.data.sortindex = value;
   140   },
   142   get ttl() {
   143     return this.data.ttl;
   144   },
   146   set ttl(value) {
   147     if (!value && value !== 0) {
   148       delete this.data.ttl;
   149       return;
   150     }
   152     this.data.ttl = value;
   153   },
   155   /**
   156    * Deserialize JSON or another object into this instance.
   157    *
   158    * The argument can be a string containing serialized JSON or an object.
   159    *
   160    * If the JSON is invalid or if the object contains unknown fields, an
   161    * exception will be thrown.
   162    *
   163    * @param json
   164    *        (string|object) Value to construct BSO from.
   165    */
   166   deserialize: function deserialize(input) {
   167     let data;
   169     if (typeof(input) == "string") {
   170       data = JSON.parse(input);
   171       if (typeof(data) != "object") {
   172         throw new Error("Supplied JSON is valid but is not a JS-Object.");
   173       }
   174     }
   175     else if (typeof(input) == "object") {
   176       data = input;
   177     } else {
   178       throw new Error("Argument must be a JSON string or object: " +
   179                       typeof(input));
   180     }
   182     for each (let key in Object.keys(data)) {
   183       if (key == "id") {
   184         this.id = data.id;
   185         continue;
   186       }
   188       if (!this._validKeys.has(key)) {
   189         throw new Error("Invalid key in object: " + key);
   190       }
   192       this.data[key] = data[key];
   193     }
   194   },
   196   /**
   197    * Serialize the current BSO to JSON.
   198    *
   199    * @return string
   200    *         The JSON representation of this BSO.
   201    */
   202   toJSON: function toJSON() {
   203     let obj = {};
   205     for (let [k, v] in Iterator(this.data)) {
   206       obj[k] = v;
   207     }
   209     if (this.id) {
   210       obj.id = this.id;
   211     }
   213     return obj;
   214   },
   216   toString: function toString() {
   217     return "{ " +
   218       "id: "       + this.id        + " " +
   219       "modified: " + this.modified  + " " +
   220       "ttl: "      + this.ttl       + " " +
   221       "index: "    + this.sortindex + " " +
   222       "payload: "  + this.payload   +
   223       " }";
   224   },
   225 };
   227 /**
   228  * Represents an error encountered during a StorageServiceRequest request.
   229  *
   230  * Instances of this will be passed to the onComplete callback for any request
   231  * that did not succeed.
   232  *
   233  * This type effectively wraps other error conditions. It is up to the client
   234  * to determine the appropriate course of action for each error type
   235  * encountered.
   236  *
   237  * The following error "classes" are defined by properties on each instance:
   238  *
   239  *   serverModified - True if the request to modify data was conditional and
   240  *     the server rejected the request because it has newer data than the
   241  *     client.
   242  *
   243  *   notFound - True if the requested URI or resource does not exist.
   244  *
   245  *   conflict - True if the server reported that a resource being operated on
   246  *     was in conflict. If this occurs, the client should typically wait a
   247  *     little and try the request again.
   248  *
   249  *   requestTooLarge - True if the request was too large for the server. If
   250  *     this happens on batch requests, the client should retry the request with
   251  *     smaller batches.
   252  *
   253  *   network - A network error prevented this request from succeeding. If set,
   254  *     it will be an Error thrown by the Gecko network stack. If set, it could
   255  *     mean that the request could not be performed or that an error occurred
   256  *     when the request was in flight. It is also possible the request
   257  *     succeeded on the server but the response was lost in transit.
   258  *
   259  *   authentication - If defined, an authentication error has occurred. If
   260  *     defined, it will be an Error instance. If seen, the client should not
   261  *     retry the request without first correcting the authentication issue.
   262  *
   263  *   client - An error occurred which was the client's fault. This typically
   264  *     means the code in this file is buggy.
   265  *
   266  *   server - An error occurred on the server. In the ideal world, this should
   267  *     never happen. But, it does. If set, this will be an Error which
   268  *     describes the error as reported by the server.
   269  */
   270 this.StorageServiceRequestError = function StorageServiceRequestError() {
   271   this.serverModified  = false;
   272   this.notFound        = false;
   273   this.conflict        = false;
   274   this.requestToolarge = false;
   275   this.network         = null;
   276   this.authentication  = null;
   277   this.client          = null;
   278   this.server          = null;
   279 }
   281 /**
   282  * Represents a single request to the storage service.
   283  *
   284  * Instances of this type are returned by the APIs on StorageServiceClient.
   285  * They should not be created outside of StorageServiceClient.
   286  *
   287  * This type encapsulates common storage API request and response handling.
   288  * Metadata required to perform the request is stored inside each instance and
   289  * should be treated as invisible by consumers.
   290  *
   291  * A number of "public" properties are exposed to allow clients to further
   292  * customize behavior. These are documented below.
   293  *
   294  * Some APIs in StorageServiceClient define their own types which inherit from
   295  * this one. Read the API documentation to see which types those are and when
   296  * they apply.
   297  *
   298  * This type wraps RESTRequest rather than extending it. The reason is mainly
   299  * to avoid the fragile base class problem. We implement considerable extra
   300  * functionality on top of RESTRequest and don't want this to accidentally
   301  * trample on RESTRequest's members.
   302  *
   303  * If this were a C++ class, it and StorageServiceClient would be friend
   304  * classes. Each touches "protected" APIs of the other. Thus, each should be
   305  * considered when making changes to the other.
   306  *
   307  * Usage
   308  * =====
   309  *
   310  * When you obtain a request instance, it is waiting to be dispatched. It may
   311  * have additional settings available for tuning. See the documentation in
   312  * StorageServiceClient for more.
   313  *
   314  * There are essentially two types of requests: "basic" and "streaming."
   315  * "Basic" requests encapsulate the traditional request-response paradigm:
   316  * a request is issued and we get a response later once the full response
   317  * is available. Most of the APIs in StorageServiceClient issue these "basic"
   318  * requests. Streaming requests typically involve the transport of multiple
   319  * BasicStorageObject instances. When a new BSO instance is available, a
   320  * callback is fired.
   321  *
   322  * For basic requests, the general flow looks something like:
   323  *
   324  *   // Obtain a new request instance.
   325  *   let request = client.getCollectionInfo();
   326  *
   327  *   // Install a handler which provides callbacks for request events. The most
   328  *   // important is `onComplete`, which is called when the request has
   329  *   // finished and the response is completely received.
   330  *   request.handler = {
   331  *     onComplete: function onComplete(error, request) {
   332  *       // Do something.
   333  *     }
   334  *   };
   335  *
   336  *   // Send the request.
   337  *   request.dispatch();
   338  *
   339  * Alternatively, we can install the onComplete handler when calling dispatch:
   340  *
   341  *   let request = client.getCollectionInfo();
   342  *   request.dispatch(function onComplete(error, request) {
   343  *     // Handle response.
   344  *   });
   345  *
   346  * Please note that installing an `onComplete` handler as the argument to
   347  * `dispatch()` will overwrite an existing `handler`.
   348  *
   349  * In both of the above example, the two `request` variables are identical. The
   350  * original `StorageServiceRequest` is passed into the callback so callers
   351  * don't need to rely on closures.
   352  *
   353  * Most of the complexity for onComplete handlers is error checking.
   354  *
   355  * The first thing you do in your onComplete handler is ensure no error was
   356  * seen:
   357  *
   358  *   function onComplete(error, request) {
   359  *     if (error) {
   360  *       // Handle error.
   361  *     }
   362  *   }
   363  *
   364  * If `error` is defined, it will be an instance of
   365  * `StorageServiceRequestError`. An error will be set if the request didn't
   366  * complete successfully. This means the transport layer must have succeeded
   367  * and the application protocol (HTTP) must have returned a successful status
   368  * code (2xx and some 3xx). Please see the documentation for
   369  * `StorageServiceRequestError` for more.
   370  *
   371  * A robust error handler would look something like:
   372  *
   373  *   function onComplete(error, request) {
   374  *     if (error) {
   375  *       if (error.network) {
   376  *         // Network error encountered!
   377  *       } else if (error.server) {
   378  *         // Something went wrong on the server (HTTP 5xx).
   379  *       } else if (error.authentication) {
   380  *         // Server rejected request due to bad credentials.
   381  *       } else if (error.serverModified) {
   382  *         // The conditional request was rejected because the server has newer
   383  *         // data than what the client reported.
   384  *       } else if (error.conflict) {
   385  *         // The server reported that the operation could not be completed
   386  *         // because another client is also updating it.
   387  *       } else if (error.requestTooLarge) {
   388  *         // The server rejected the request because it was too large.
   389  *       } else if (error.notFound) {
   390  *         // The requested resource was not found.
   391  *       } else if (error.client) {
   392  *         // Something is wrong with the client's request. You should *never*
   393  *         // see this, as it means this client is likely buggy. It could also
   394  *         // mean the server is buggy or misconfigured. Either way, something
   395  *         // is buggy.
   396  *       }
   397  *
   398  *       return;
   399  *     }
   400  *
   401  *     // Handle successful case.
   402  *   }
   403  *
   404  * If `error` is null, the request completed successfully. There may or may not
   405  * be additional data available on the request instance.
   406  *
   407  * For requests that obtain data, this data is typically made available through
   408  * the `resultObj` property on the request instance. The API that was called
   409  * will install its own response hander and ensure this property is decoded to
   410  * what you expect.
   411  *
   412  * Conditional Requests
   413  * --------------------
   414  *
   415  * Many of the APIs on `StorageServiceClient` support conditional requests.
   416  * That is, the client defines the last version of data it has (the version
   417  * comes from a previous response from the server) and sends this as part of
   418  * the request.
   419  *
   420  * For query requests, if the server hasn't changed, no new data will be
   421  * returned. If issuing a conditional query request, the caller should check
   422  * the `notModified` property on the request in the response callback. If this
   423  * property is true, the server has no new data and there is obviously no data
   424  * on the response.
   425  *
   426  * For example:
   427  *
   428  *   let request = client.getCollectionInfo();
   429  *   request.locallyModifiedVersion = Date.now() - 60000;
   430  *   request.dispatch(function onComplete(error, request) {
   431  *     if (error) {
   432  *       // Handle error.
   433  *       return;
   434  *     }
   435  *
   436  *     if (request.notModified) {
   437  *       return;
   438  *     }
   439  *
   440  *     let info = request.resultObj;
   441  *     // Do stuff.
   442  *   });
   443  *
   444  * For modification requests, if the server has changed, the request will be
   445  * rejected. When this happens, `error`will be defined and the `serverModified`
   446  * property on it will be true.
   447  *
   448  * For example:
   449  *
   450  *   let request = client.setBSO(bso);
   451  *   request.locallyModifiedVersion = bso.modified;
   452  *   request.dispatch(function onComplete(error, request) {
   453  *     if (error) {
   454  *       if (error.serverModified) {
   455  *         // Server data is newer! We should probably fetch it and apply
   456  *         // locally.
   457  *       }
   458  *
   459  *       return;
   460  *     }
   461  *
   462  *     // Handle success.
   463  *   });
   464  *
   465  * Future Features
   466  * ---------------
   467  *
   468  * The current implementation does not support true streaming for things like
   469  * multi-BSO retrieval. However, the API supports it, so we should be able
   470  * to implement it transparently.
   471  */
   472 function StorageServiceRequest() {
   473   this._log = Log.repository.getLogger("Sync.StorageService.Request");
   474   this._log.level = Log.Level[Prefs.get("log.level")];
   476   this.notModified = false;
   478   this._client                 = null;
   479   this._request                = null;
   480   this._method                 = null;
   481   this._handler                = {};
   482   this._data                   = null;
   483   this._error                  = null;
   484   this._resultObj              = null;
   485   this._locallyModifiedVersion = null;
   486   this._allowIfModified        = false;
   487   this._allowIfUnmodified      = false;
   488 }
   489 StorageServiceRequest.prototype = {
   490   /**
   491    * The StorageServiceClient this request came from.
   492    */
   493   get client() {
   494     return this._client;
   495   },
   497   /**
   498    * The underlying RESTRequest instance.
   499    *
   500    * This should be treated as read only and should not be modified
   501    * directly by external callers. While modification would probably work, this
   502    * would defeat the purpose of the API and the abstractions it is meant to
   503    * provide.
   504    *
   505    * If a consumer needs to modify the underlying request object, it is
   506    * recommended for them to implement a new type that inherits from
   507    * StorageServiceClient and override the necessary APIs to modify the request
   508    * there.
   509    *
   510    * This accessor may disappear in future versions.
   511    */
   512   get request() {
   513     return this._request;
   514   },
   516   /**
   517    * The RESTResponse that resulted from the RESTRequest.
   518    */
   519   get response() {
   520     return this._request.response;
   521   },
   523   /**
   524    * HTTP status code from response.
   525    */
   526   get statusCode() {
   527     let response = this.response;
   528     return response ? response.status : null;
   529   },
   531   /**
   532    * Holds any error that has occurred.
   533    *
   534    * If a network error occurred, that will be returned. If no network error
   535    * occurred, the client error will be returned. If no error occurred (yet),
   536    * null will be returned.
   537    */
   538   get error() {
   539     return this._error;
   540   },
   542   /**
   543    * The result from the request.
   544    *
   545    * This stores the object returned from the server. The type of object depends
   546    * on the request type. See the per-API documentation in StorageServiceClient
   547    * for details.
   548    */
   549   get resultObj() {
   550     return this._resultObj;
   551   },
   553   /**
   554    * Define the local version of the entity the client has.
   555    *
   556    * This is used to enable conditional requests. Depending on the request
   557    * type, the value set here could be reflected in the X-If-Modified-Since or
   558    * X-If-Unmodified-Since headers.
   559    *
   560    * This attribute is not honoured on every request. See the documentation
   561    * in the client API to learn where it is valid.
   562    */
   563   set locallyModifiedVersion(value) {
   564     // Will eventually become a header, so coerce to string.
   565     this._locallyModifiedVersion = "" + value;
   566   },
   568   /**
   569    * Object which holds callbacks and state for this request.
   570    *
   571    * The handler is installed by users of this request. It is simply an object
   572    * containing 0 or more of the following properties:
   573    *
   574    *   onComplete - A function called when the request has completed and all
   575    *     data has been received from the server. The function receives the
   576    *     following arguments:
   577    *
   578    *       (StorageServiceRequestError) Error encountered during request. null
   579    *         if no error was encountered.
   580    *       (StorageServiceRequest) The request that was sent (this instance).
   581    *         Response information is available via properties and functions.
   582    *
   583    *     Unless the call to dispatch() throws before returning, this callback
   584    *     is guaranteed to be invoked.
   585    *
   586    *     Every client almost certainly wants to install this handler.
   587    *
   588    *   onDispatch - A function called immediately before the request is
   589    *     dispatched. This hook can be used to inspect or modify the request
   590    *     before it is issued.
   591    *
   592    *     The called function receives the following arguments:
   593    *
   594    *       (StorageServiceRequest) The request being issued (this request).
   595    *
   596    *   onBSORecord - When retrieving multiple BSOs from the server, this
   597    *     function is invoked when a new BSO record has been read. This function
   598    *     will be invoked 0 to N times before onComplete is invoked. onComplete
   599    *     signals that the last BSO has been processed or that an error
   600    *     occurred. The function receives the following arguments:
   601    *
   602    *       (StorageServiceRequest) The request that was sent (this instance).
   603    *       (BasicStorageObject|string) The received BSO instance (when in full
   604    *         mode) or the string ID of the BSO (when not in full mode).
   605    *
   606    * Callers are free to (and encouraged) to store extra state in the supplied
   607    * handler.
   608    */
   609   set handler(value) {
   610     if (typeof(value) != "object") {
   611       throw new Error("Invalid handler. Must be an Object.");
   612     }
   614     this._handler = value;
   616     if (!value.onComplete) {
   617       this._log.warn("Handler does not contain an onComplete callback!");
   618     }
   619   },
   621   get handler() {
   622     return this._handler;
   623   },
   625   //---------------
   626   // General APIs |
   627   //---------------
   629   /**
   630    * Start the request.
   631    *
   632    * The request is dispatched asynchronously. The installed handler will have
   633    * one or more of its callbacks invoked as the state of the request changes.
   634    *
   635    * The `onComplete` argument is optional. If provided, the supplied function
   636    * will be installed on a *new* handler before the request is dispatched. This
   637    * is equivalent to calling:
   638    *
   639    *   request.handler = {onComplete: value};
   640    *   request.dispatch();
   641    *
   642    * Please note that any existing handler will be replaced if onComplete is
   643    * provided.
   644    *
   645    * @param onComplete
   646    *        (function) Callback to be invoked when request has completed.
   647    */
   648   dispatch: function dispatch(onComplete) {
   649     if (onComplete) {
   650       this.handler = {onComplete: onComplete};
   651     }
   653     // Installing the dummy callback makes implementation easier in _onComplete
   654     // because we can then blindly call.
   655     this._dispatch(function _internalOnComplete(error) {
   656       this._onComplete(error);
   657       this.completed = true;
   658     }.bind(this));
   659   },
   661   /**
   662    * This is a synchronous version of dispatch().
   663    *
   664    * THIS IS AN EVIL FUNCTION AND SHOULD NOT BE CALLED. It is provided for
   665    * legacy reasons to support evil, synchronous clients.
   666    *
   667    * Please note that onComplete callbacks are executed from this JS thread.
   668    * We dispatch the request, spin the event loop until it comes back. Then,
   669    * we execute callbacks ourselves then return. In other words, there is no
   670    * potential for spinning between callback execution and this function
   671    * returning.
   672    *
   673    * The `onComplete` argument has the same behavior as for `dispatch()`.
   674    *
   675    * @param onComplete
   676    *        (function) Callback to be invoked when request has completed.
   677    */
   678   dispatchSynchronous: function dispatchSynchronous(onComplete) {
   679     if (onComplete) {
   680       this.handler = {onComplete: onComplete};
   681     }
   683     let cb = Async.makeSyncCallback();
   684     this._dispatch(cb);
   685     let error = Async.waitForSyncCallback(cb);
   687     this._onComplete(error);
   688     this.completed = true;
   689   },
   691   //-------------------------------------------------------------------------
   692   // HIDDEN APIS. DO NOT CHANGE ANYTHING UNDER HERE FROM OUTSIDE THIS TYPE. |
   693   //-------------------------------------------------------------------------
   695   /**
   696    * Data to include in HTTP request body.
   697    */
   698   _data: null,
   700   /**
   701    * StorageServiceRequestError encountered during dispatchy.
   702    */
   703   _error: null,
   705   /**
   706    * Handler to parse response body into another object.
   707    *
   708    * This is installed by the client API. It should return the value the body
   709    * parses to on success. If a failure is encountered, an exception should be
   710    * thrown.
   711    */
   712   _completeParser: null,
   714   /**
   715    * Dispatch the request.
   716    *
   717    * This contains common functionality for dispatching requests. It should
   718    * ideally be part of dispatch, but since dispatchSynchronous exists, we
   719    * factor out common code.
   720    */
   721   _dispatch: function _dispatch(onComplete) {
   722     // RESTRequest throws if the request has already been dispatched, so we
   723     // need not bother checking.
   725     // Inject conditional headers into request if they are allowed and if a
   726     // value is set. Note that _locallyModifiedVersion is always a string and
   727     // if("0") is true.
   728     if (this._allowIfModified && this._locallyModifiedVersion) {
   729       this._log.trace("Making request conditional.");
   730       this._request.setHeader("X-If-Modified-Since",
   731                               this._locallyModifiedVersion);
   732     } else if (this._allowIfUnmodified && this._locallyModifiedVersion) {
   733       this._log.trace("Making request conditional.");
   734       this._request.setHeader("X-If-Unmodified-Since",
   735                               this._locallyModifiedVersion);
   736     }
   738     // We have both an internal and public hook.
   739     // If these throw, it is OK since we are not in a callback.
   740     if (this._onDispatch) {
   741       this._onDispatch();
   742     }
   744     if (this._handler.onDispatch) {
   745       this._handler.onDispatch(this);
   746     }
   748     this._client.runListeners("onDispatch", this);
   750     this._log.info("Dispatching request: " + this._method + " " +
   751                    this._request.uri.asciiSpec);
   753     this._request.dispatch(this._method, this._data, onComplete);
   754   },
   756   /**
   757    * RESTRequest onComplete handler for all requests.
   758    *
   759    * This provides common logic for all response handling.
   760    */
   761   _onComplete: function(error) {
   762     let onCompleteCalled = false;
   764     let callOnComplete = function callOnComplete() {
   765       onCompleteCalled = true;
   767       if (!this._handler.onComplete) {
   768         this._log.warn("No onComplete installed in handler!");
   769         return;
   770       }
   772       try {
   773         this._handler.onComplete(this._error, this);
   774       } catch (ex) {
   775         this._log.warn("Exception when invoking handler's onComplete: " +
   776                        CommonUtils.exceptionStr(ex));
   777         throw ex;
   778       }
   779     }.bind(this);
   781     try {
   782       if (error) {
   783         this._error = new StorageServiceRequestError();
   784         this._error.network = error;
   785         this._log.info("Network error during request: " + error);
   786         this._client.runListeners("onNetworkError", this._client, this, error);
   787         callOnComplete();
   788         return;
   789       }
   791       let response = this._request.response;
   792       this._log.info(response.status + " " + this._request.uri.asciiSpec);
   794       this._processHeaders();
   796       if (response.status == 200) {
   797         this._resultObj = this._completeParser(response);
   798         callOnComplete();
   799         return;
   800       }
   802       if (response.status == 201) {
   803         callOnComplete();
   804         return;
   805       }
   807       if (response.status == 204) {
   808         callOnComplete();
   809         return;
   810       }
   812       if (response.status == 304) {
   813         this.notModified = true;
   814         callOnComplete();
   815         return;
   816       }
   818       // TODO handle numeric response code from server.
   819       if (response.status == 400) {
   820         this._error = new StorageServiceRequestError();
   821         this._error.client = new Error("Client error!");
   822         callOnComplete();
   823         return;
   824       }
   826       if (response.status == 401) {
   827         this._error = new StorageServiceRequestError();
   828         this._error.authentication = new Error("401 Received.");
   829         this._client.runListeners("onAuthFailure", this._error.authentication,
   830                                   this);
   831         callOnComplete();
   832         return;
   833       }
   835       if (response.status == 404) {
   836         this._error = new StorageServiceRequestError();
   837         this._error.notFound = true;
   838         callOnComplete();
   839         return;
   840       }
   842       if (response.status == 409) {
   843         this._error = new StorageServiceRequestError();
   844         this._error.conflict = true;
   845         callOnComplete();
   846         return;
   847       }
   849       if (response.status == 412) {
   850         this._error = new StorageServiceRequestError();
   851         this._error.serverModified = true;
   852         callOnComplete();
   853         return;
   854       }
   856       if (response.status == 413) {
   857         this._error = new StorageServiceRequestError();
   858         this._error.requestTooLarge = true;
   859         callOnComplete();
   860         return;
   861       }
   863       // If we see this, either the client or the server is buggy. We should
   864       // never see this.
   865       if (response.status == 415) {
   866         this._log.error("415 HTTP response seen from server! This should " +
   867                         "never happen!");
   868         this._error = new StorageServiceRequestError();
   869         this._error.client = new Error("415 Unsupported Media Type received!");
   870         callOnComplete();
   871         return;
   872       }
   874       if (response.status >= 500 && response.status <= 599) {
   875         this._log.error(response.status + " seen from server!");
   876         this._error = new StorageServiceRequestError();
   877         this._error.server = new Error(response.status + " status code.");
   878         callOnComplete();
   879         return;
   880       }
   882       callOnComplete();
   884     } catch (ex) {
   885       this._clientError = ex;
   886       this._log.info("Exception when processing _onComplete: " + ex);
   888       if (!onCompleteCalled) {
   889         this._log.warn("Exception in internal response handling logic!");
   890         try {
   891           callOnComplete();
   892         } catch (ex) {
   893           this._log.warn("An additional exception was encountered when " +
   894                          "calling the handler's onComplete: " + ex);
   895         }
   896       }
   897     }
   898   },
   900   _processHeaders: function _processHeaders() {
   901     let headers = this._request.response.headers;
   903     if (headers["x-timestamp"]) {
   904       this.serverTime = parseFloat(headers["x-timestamp"]);
   905     }
   907     if (headers["x-backoff"]) {
   908       this.backoffInterval = 1000 * parseInt(headers["x-backoff"], 10);
   909     }
   911     if (headers["retry-after"]) {
   912       this.backoffInterval = 1000 * parseInt(headers["retry-after"], 10);
   913     }
   915     if (this.backoffInterval) {
   916       let failure = this._request.response.status == 503;
   917       this._client.runListeners("onBackoffReceived", this._client, this,
   918                                this.backoffInterval, !failure);
   919     }
   921     if (headers["x-quota-remaining"]) {
   922       this.quotaRemaining = parseInt(headers["x-quota-remaining"], 10);
   923       this._client.runListeners("onQuotaRemaining", this._client, this,
   924                                this.quotaRemaining);
   925     }
   926   },
   927 };
   929 /**
   930  * Represents a request to fetch from a collection.
   931  *
   932  * These requests are highly configurable so they are given their own type.
   933  * This type inherits from StorageServiceRequest and provides additional
   934  * controllable parameters.
   935  *
   936  * By default, requests are issued in "streaming" mode. As the client receives
   937  * data from the server, it will invoke the caller-supplied onBSORecord
   938  * callback for each record as it is ready. When all records have been received,
   939  * it will invoke onComplete as normal. To change this behavior, modify the
   940  * "streaming" property before the request is dispatched.
   941  */
   942 function StorageCollectionGetRequest() {
   943   StorageServiceRequest.call(this);
   944 }
   945 StorageCollectionGetRequest.prototype = {
   946   __proto__: StorageServiceRequest.prototype,
   948   _namedArgs: {},
   950   _streaming: true,
   952   /**
   953    * Control whether streaming mode is in effect.
   954    *
   955    * Read the type documentation above for more details.
   956    */
   957   set streaming(value) {
   958     this._streaming = !!value;
   959   },
   961   /**
   962    * Define the set of IDs to fetch from the server.
   963    */
   964   set ids(value) {
   965     this._namedArgs.ids = value.join(",");
   966   },
   968   /**
   969    * Only retrieve BSOs that were modified strictly before this time.
   970    *
   971    * Defined in milliseconds since UNIX epoch.
   972    */
   973   set older(value) {
   974     this._namedArgs.older = value;
   975   },
   977   /**
   978    * Only retrieve BSOs that were modified strictly after this time.
   979    *
   980    * Defined in milliseconds since UNIX epoch.
   981    */
   982   set newer(value) {
   983     this._namedArgs.newer = value;
   984   },
   986   /**
   987    * If set to a truthy value, return full BSO information.
   988    *
   989    * If not set (the default), the request will only return the set of BSO
   990    * ids.
   991    */
   992   set full(value) {
   993     if (value) {
   994       this._namedArgs.full = "1";
   995     } else {
   996       delete this._namedArgs["full"];
   997     }
   998   },
  1000   /**
  1001    * Limit the max number of returned BSOs to this integer number.
  1002    */
  1003   set limit(value) {
  1004     this._namedArgs.limit = value;
  1005   },
  1007   /**
  1008    * If set with any value, sort the results based on modification time, oldest
  1009    * first.
  1010    */
  1011   set sortOldest(value) {
  1012     this._namedArgs.sort = "oldest";
  1013   },
  1015   /**
  1016    * If set with any value, sort the results based on modification time, newest
  1017    * first.
  1018    */
  1019   set sortNewest(value) {
  1020     this._namedArgs.sort = "newest";
  1021   },
  1023   /**
  1024    * If set with any value, sort the results based on sortindex value, highest
  1025    * first.
  1026    */
  1027   set sortIndex(value) {
  1028     this._namedArgs.sort = "index";
  1029   },
  1031   _onDispatch: function _onDispatch() {
  1032     let qs = this._getQueryString();
  1033     if (!qs.length) {
  1034       return;
  1037     this._request.uri = CommonUtils.makeURI(this._request.uri.asciiSpec + "?" +
  1038                                             qs);
  1039   },
  1041   _getQueryString: function _getQueryString() {
  1042     let args = [];
  1043     for (let [k, v] in Iterator(this._namedArgs)) {
  1044       args.push(encodeURIComponent(k) + "=" + encodeURIComponent(v));
  1047     return args.join("&");
  1048   },
  1050   _completeParser: function _completeParser(response) {
  1051     let obj = JSON.parse(response.body);
  1052     let items = obj.items;
  1054     if (!Array.isArray(items)) {
  1055       throw new Error("Unexpected JSON response. items is missing or not an " +
  1056                       "array!");
  1059     if (!this.handler.onBSORecord) {
  1060       return;
  1063     for (let bso of items) {
  1064       this.handler.onBSORecord(this, bso);
  1066   },
  1067 };
  1069 /**
  1070  * Represents a request that sets data in a collection
  1072  * Instances of this type are returned by StorageServiceClient.setBSOs().
  1073  */
  1074 function StorageCollectionSetRequest() {
  1075   StorageServiceRequest.call(this);
  1077   this.size = 0;
  1079   // TODO Bug 775781 convert to Set and Map once iterable.
  1080   this.successfulIDs = [];
  1081   this.failures      = {};
  1083   this._lines = [];
  1085 StorageCollectionSetRequest.prototype = {
  1086   __proto__: StorageServiceRequest.prototype,
  1088   get count() {
  1089     return this._lines.length;
  1090   },
  1092   /**
  1093    * Add a BasicStorageObject to this request.
  1095    * Please note that the BSO content is retrieved when the BSO is added to
  1096    * the request. If the BSO changes after it is added to a request, those
  1097    * changes will not be reflected in the request.
  1099    * @param bso
  1100    *        (BasicStorageObject) BSO to add to the request.
  1101    */
  1102   addBSO: function addBSO(bso) {
  1103     if (!bso instanceof BasicStorageObject) {
  1104       throw new Error("argument must be a BasicStorageObject instance.");
  1107     if (!bso.id) {
  1108       throw new Error("Passed BSO must have id defined.");
  1111     this.addLine(JSON.stringify(bso));
  1112   },
  1114   /**
  1115    * Add a BSO (represented by its serialized newline-delimited form).
  1117    * You probably shouldn't use this. It is used for batching.
  1118    */
  1119   addLine: function addLine(line) {
  1120     // This is off by 1 in the larger direction. We don't care.
  1121     this.size += line.length + 1;
  1122     this._lines.push(line);
  1123   },
  1125   _onDispatch: function _onDispatch() {
  1126     this._data = this._lines.join("\n");
  1127     this.size = this._data.length;
  1128   },
  1130   _completeParser: function _completeParser(response) {
  1131     let result = JSON.parse(response.body);
  1133     for (let id of result.success) {
  1134       this.successfulIDs.push(id);
  1137     this.allSucceeded = true;
  1139     for (let [id, reasons] in Iterator(result.failed)) {
  1140       this.failures[id] = reasons;
  1141       this.allSucceeded = false;
  1143   },
  1144 };
  1146 /**
  1147  * Represents a batch upload of BSOs to an individual collection.
  1149  * This is a more intelligent way to upload may BSOs to the server. It will
  1150  * split the uploaded data into multiple requests so size limits, etc aren't
  1151  * exceeded.
  1153  * Once a client obtains an instance of this type, it calls `addBSO` for each
  1154  * BSO to be uploaded. When the client is done providing BSOs to be uploaded,
  1155  * it calls `finish`. When `finish` is called, no more BSOs can be added to the
  1156  * batch. When all requests created from this batch have finished, the callback
  1157  * provided to `finish` will be invoked.
  1159  * Clients can also explicitly flush pending outgoing BSOs via `flush`. This
  1160  * allows callers to control their own batching/chunking.
  1162  * Interally, this maintains a queue of StorageCollectionSetRequest to be
  1163  * issued. At most one request is allowed to be in-flight at once. This is to
  1164  * avoid potential conflicts on the server. And, in the case of conditional
  1165  * requests, it prevents requests from being declined due to the server being
  1166  * updated by another request issued by us.
  1168  * If a request errors for any reason, all queued uploads are abandoned and the
  1169  * `finish` callback is invoked as soon as possible. The `successfulIDs` and
  1170  * `failures` properties will contain data from all requests that had this
  1171  * response data. In other words, the IDs have BSOs that were never sent to the
  1172  * server are not lumped in to either property.
  1174  * Requests can be made conditional by setting `locallyModifiedVersion` to the
  1175  * most recent version of server data. As responses from the server are seen,
  1176  * the last server version is carried forward to subsequent requests.
  1178  * The server version from the last request is available in the
  1179  * `serverModifiedVersion` property. It should only be accessed during or
  1180  * after the callback passed to `finish`.
  1182  * @param client
  1183  *        (StorageServiceClient) Client instance to use for uploading.
  1185  * @param collection
  1186  *        (string) Collection the batch operation will upload to.
  1187  */
  1188 function StorageCollectionBatchedSet(client, collection) {
  1189   this.client     = client;
  1190   this.collection = collection;
  1192   this._log = client._log;
  1194   this.locallyModifiedVersion = null;
  1195   this.serverModifiedVersion  = null;
  1197   // TODO Bug 775781 convert to Set and Map once iterable.
  1198   this.successfulIDs = [];
  1199   this.failures      = {};
  1201   // Request currently being populated.
  1202   this._stagingRequest = client.setBSOs(this.collection);
  1204   // Requests ready to be sent over the wire.
  1205   this._outgoingRequests = [];
  1207   // Whether we are waiting for a response.
  1208   this._requestInFlight = false;
  1210   this._onFinishCallback = null;
  1211   this._finished         = false;
  1212   this._errorEncountered = false;
  1214 StorageCollectionBatchedSet.prototype = {
  1215   /**
  1216    * Add a BSO to be uploaded as part of this batch.
  1217    */
  1218   addBSO: function addBSO(bso) {
  1219     if (this._errorEncountered) {
  1220       return;
  1223     let line = JSON.stringify(bso);
  1225     if (line.length > this.client.REQUEST_SIZE_LIMIT) {
  1226       throw new Error("BSO is larger than allowed limit: " + line.length +
  1227                       " > " + this.client.REQUEST_SIZE_LIMIT);
  1230     if (this._stagingRequest.size + line.length > this.client.REQUEST_SIZE_LIMIT) {
  1231       this._log.debug("Sending request because payload size would be exceeded");
  1232       this._finishStagedRequest();
  1234       this._stagingRequest.addLine(line);
  1235       return;
  1238     // We are guaranteed to fit within size limits.
  1239     this._stagingRequest.addLine(line);
  1241     if (this._stagingRequest.count >= this.client.REQUEST_BSO_COUNT_LIMIT) {
  1242       this._log.debug("Sending request because BSO count threshold reached.");
  1243       this._finishStagedRequest();
  1244       return;
  1246   },
  1248   finish: function finish(cb) {
  1249     if (this._finished) {
  1250       throw new Error("Batch request has already been finished.");
  1253     this.flush();
  1255     this._onFinishCallback = cb;
  1256     this._finished = true;
  1257     this._stagingRequest = null;
  1258   },
  1260   flush: function flush() {
  1261     if (this._finished) {
  1262       throw new Error("Batch request has been finished.");
  1265     if (!this._stagingRequest.count) {
  1266       return;
  1269     this._finishStagedRequest();
  1270   },
  1272   _finishStagedRequest: function _finishStagedRequest() {
  1273     this._outgoingRequests.push(this._stagingRequest);
  1274     this._sendOutgoingRequest();
  1275     this._stagingRequest = this.client.setBSOs(this.collection);
  1276   },
  1278   _sendOutgoingRequest: function _sendOutgoingRequest() {
  1279     if (this._requestInFlight || this._errorEncountered) {
  1280       return;
  1283     if (!this._outgoingRequests.length) {
  1284       return;
  1287     let request = this._outgoingRequests.shift();
  1289     if (this.locallyModifiedVersion) {
  1290       request.locallyModifiedVersion = this.locallyModifiedVersion;
  1293     request.dispatch(this._onBatchComplete.bind(this));
  1294     this._requestInFlight = true;
  1295   },
  1297   _onBatchComplete: function _onBatchComplete(error, request) {
  1298     this._requestInFlight = false;
  1300     this.serverModifiedVersion = request.serverTime;
  1302     // Only update if we had a value before. Otherwise, this breaks
  1303     // unconditional requests!
  1304     if (this.locallyModifiedVersion) {
  1305       this.locallyModifiedVersion = request.serverTime;
  1308     for (let id of request.successfulIDs) {
  1309       this.successfulIDs.push(id);
  1312     for (let [id, reason] in Iterator(request.failures)) {
  1313       this.failures[id] = reason;
  1316     if (request.error) {
  1317       this._errorEncountered = true;
  1320     this._checkFinish();
  1321   },
  1323   _checkFinish: function _checkFinish() {
  1324     if (this._outgoingRequests.length && !this._errorEncountered) {
  1325       this._sendOutgoingRequest();
  1326       return;
  1329     if (!this._onFinishCallback) {
  1330       return;
  1333     try {
  1334       this._onFinishCallback(this);
  1335     } catch (ex) {
  1336       this._log.warn("Exception when calling finished callback: " +
  1337                      CommonUtils.exceptionStr(ex));
  1339   },
  1340 };
  1341 Object.freeze(StorageCollectionBatchedSet.prototype);
  1343 /**
  1344  * Manages a batch of BSO deletion requests.
  1346  * A single instance of this virtual request allows deletion of many individual
  1347  * BSOs without having to worry about server limits.
  1349  * Instances are obtained by calling `deleteBSOsBatching` on
  1350  * StorageServiceClient.
  1352  * Usage is roughly the same as StorageCollectionBatchedSet. Callers obtain
  1353  * an instance and select individual BSOs for deletion by calling `addID`.
  1354  * When the caller is finished marking BSOs for deletion, they call `finish`
  1355  * with a callback which will be invoked when all deletion requests finish.
  1357  * When the finished callback is invoked, any encountered errors will be stored
  1358  * in the `errors` property of this instance (which is passed to the callback).
  1359  * This will be an empty array if no errors were encountered. Else, it will
  1360  * contain the errors from the `onComplete` handler of request instances. The
  1361  * set of succeeded and failed IDs is not currently available.
  1363  * Deletes can be made conditional by setting `locallyModifiedVersion`. The
  1364  * behavior is the same as request types. The only difference is that the
  1365  * updated version from the server as a result of requests is carried forward
  1366  * to subsequent requests.
  1368  * The server version from the last request is stored in the
  1369  * `serverModifiedVersion` property. It is not safe to access this until the
  1370  * callback from `finish`.
  1372  * Like StorageCollectionBatchedSet, requests are issued serially to avoid
  1373  * race conditions on the server.
  1375  * @param client
  1376  *        (StorageServiceClient) Client request is associated with.
  1377  * @param collection
  1378  *        (string) Collection being operated on.
  1379  */
  1380 function StorageCollectionBatchedDelete(client, collection) {
  1381   this.client     = client;
  1382   this.collection = collection;
  1384   this._log = client._log;
  1386   this.locallyModifiedVersion = null;
  1387   this.serverModifiedVersion  = null;
  1388   this.errors                 = [];
  1390   this._pendingIDs          = [];
  1391   this._requestInFlight     = false;
  1392   this._finished            = false;
  1393   this._finishedCallback    = null;
  1395 StorageCollectionBatchedDelete.prototype = {
  1396   addID: function addID(id) {
  1397     if (this._finished) {
  1398       throw new Error("Cannot add IDs to a finished instance.");
  1401     // If we saw errors already, don't do any work. This is an optimization
  1402     // and isn't strictly required, as _sendRequest() should no-op.
  1403     if (this.errors.length) {
  1404       return;
  1407     this._pendingIDs.push(id);
  1409     if (this._pendingIDs.length >= this.client.REQUEST_BSO_DELETE_LIMIT) {
  1410       this._sendRequest();
  1412   },
  1414   /**
  1415    * Finish this batch operation.
  1417    * No more IDs can be added to this operation. Existing IDs are flushed as
  1418    * a request. The passed callback will be called when all requests have
  1419    * finished.
  1420    */
  1421   finish: function finish(cb) {
  1422     if (this._finished) {
  1423       throw new Error("Batch delete instance has already been finished.");
  1426     this._finished = true;
  1427     this._finishedCallback = cb;
  1429     if (this._pendingIDs.length) {
  1430       this._sendRequest();
  1432   },
  1434   _sendRequest: function _sendRequest() {
  1435     // Only allow 1 active request at a time and don't send additional
  1436     // requests if one has failed.
  1437     if (this._requestInFlight || this.errors.length) {
  1438       return;
  1441     let ids = this._pendingIDs.splice(0, this.client.REQUEST_BSO_DELETE_LIMIT);
  1442     let request = this.client.deleteBSOs(this.collection, ids);
  1444     if (this.locallyModifiedVersion) {
  1445       request.locallyModifiedVersion = this.locallyModifiedVersion;
  1448     request.dispatch(this._onRequestComplete.bind(this));
  1449     this._requestInFlight = true;
  1450   },
  1452   _onRequestComplete: function _onRequestComplete(error, request) {
  1453     this._requestInFlight = false;
  1455     if (error) {
  1456       // We don't currently track metadata of what failed. This is an obvious
  1457       // feature that could be added.
  1458       this._log.warn("Error received from server: " + error);
  1459       this.errors.push(error);
  1462     this.serverModifiedVersion = request.serverTime;
  1464     // If performing conditional requests, carry forward the new server version
  1465     // so subsequent conditional requests work.
  1466     if (this.locallyModifiedVersion) {
  1467       this.locallyModifiedVersion = request.serverTime;
  1470     if (this._pendingIDs.length && !this.errors.length) {
  1471       this._sendRequest();
  1472       return;
  1475     if (!this._finishedCallback) {
  1476       return;
  1479     try {
  1480       this._finishedCallback(this);
  1481     } catch (ex) {
  1482       this._log.warn("Exception when invoking finished callback: " +
  1483                      CommonUtils.exceptionStr(ex));
  1485   },
  1486 };
  1487 Object.freeze(StorageCollectionBatchedDelete.prototype);
  1489 /**
  1490  * Construct a new client for the SyncStorage API, version 2.0.
  1492  * Clients are constructed against a base URI. This URI is typically obtained
  1493  * from the token server via the endpoint component of a successful token
  1494  * response.
  1496  * The purpose of this type is to serve as a middleware between a client's core
  1497  * logic and the HTTP API. It hides the details of how the storage API is
  1498  * implemented but exposes important events, such as when auth goes bad or the
  1499  * server requests the client to back off.
  1501  * All request APIs operate by returning a StorageServiceRequest instance. The
  1502  * caller then installs the appropriate callbacks on each instance and then
  1503  * dispatches the request.
  1505  * Each client instance also serves as a controller and coordinator for
  1506  * associated requests. Callers can install listeners for common events on the
  1507  * client and take the appropriate action whenever any associated request
  1508  * observes them. For example, you will only need to register one listener for
  1509  * backoff observation as opposed to one on each request.
  1511  * While not currently supported, a future goal of this type is to support
  1512  * more advanced transport channels - such as SPDY - to allow for faster and
  1513  * more efficient API calls. The API is thus designed to abstract transport
  1514  * specifics away from the caller.
  1516  * Storage API consumers almost certainly have added functionality on top of the
  1517  * storage service. It is encouraged to create a child type which adds
  1518  * functionality to this layer.
  1520  * @param baseURI
  1521  *        (string) Base URI for all requests.
  1522  */
  1523 this.StorageServiceClient = function StorageServiceClient(baseURI) {
  1524   this._log = Log.repository.getLogger("Services.Common.StorageServiceClient");
  1525   this._log.level = Log.Level[Prefs.get("log.level")];
  1527   this._baseURI = baseURI;
  1529   if (this._baseURI[this._baseURI.length-1] != "/") {
  1530     this._baseURI += "/";
  1533   this._log.info("Creating new StorageServiceClient under " + this._baseURI);
  1535   this._listeners = [];
  1537 StorageServiceClient.prototype = {
  1538   /**
  1539    * The user agent sent with every request.
  1541    * You probably want to change this.
  1542    */
  1543   userAgent: "StorageServiceClient",
  1545   /**
  1546    * Maximum size of entity bodies.
  1548    * TODO this should come from the server somehow. See bug 769759.
  1549    */
  1550   REQUEST_SIZE_LIMIT: 512000,
  1552   /**
  1553    * Maximum number of BSOs in requests.
  1555    * TODO this should come from the server somehow. See bug 769759.
  1556    */
  1557   REQUEST_BSO_COUNT_LIMIT: 100,
  1559   /**
  1560    * Maximum number of BSOs that can be deleted in a single DELETE.
  1562    * TODO this should come from the server. See bug 769759.
  1563    */
  1564   REQUEST_BSO_DELETE_LIMIT: 100,
  1566   _baseURI: null,
  1567   _log: null,
  1569   _listeners: null,
  1571   //----------------------------
  1572   // Event Listener Management |
  1573   //----------------------------
  1575   /**
  1576    * Adds a listener to this client instance.
  1578    * Listeners allow other parties to react to and influence execution of the
  1579    * client instance.
  1581    * An event listener is simply an object that exposes functions which get
  1582    * executed during client execution. Objects can expose 0 or more of the
  1583    * following keys:
  1585    *   onDispatch - Callback notified immediately before a request is
  1586    *     dispatched. This gets called for every outgoing request. The function
  1587    *     receives as its arguments the client instance and the outgoing
  1588    *     StorageServiceRequest. This listener is useful for global
  1589    *     authentication handlers, which can modify the request before it is
  1590    *     sent.
  1592    *   onAuthFailure - This is called when any request has experienced an
  1593    *     authentication failure.
  1595    *     This callback receives the following arguments:
  1597    *       (StorageServiceClient) Client that encountered the auth failure.
  1598    *       (StorageServiceRequest) Request that encountered the auth failure.
  1600    *   onBackoffReceived - This is called when a backoff request is issued by
  1601    *     the server. Backoffs are issued either when the service is completely
  1602    *     unavailable (and the client should abort all activity) or if the server
  1603    *     is under heavy load (and has completed the current request but is
  1604    *     asking clients to be kind and stop issuing requests for a while).
  1606    *     This callback receives the following arguments:
  1608    *       (StorageServiceClient) Client that encountered the backoff.
  1609    *       (StorageServiceRequest) Request that received the backoff.
  1610    *       (number) Integer milliseconds the server is requesting us to back off
  1611    *         for.
  1612    *       (bool) Whether the request completed successfully. If false, the
  1613    *         client should cease sending additional requests immediately, as
  1614    *         they will likely fail. If true, the client is allowed to continue
  1615    *         to put the server in a proper state. But, it should stop and heed
  1616    *         the backoff as soon as possible.
  1618    *   onNetworkError - This is called for every network error that is
  1619    *     encountered.
  1621    *     This callback receives the following arguments:
  1623    *       (StorageServiceClient) Client that encountered the network error.
  1624    *       (StorageServiceRequest) Request that encountered the error.
  1625    *       (Error) Error passed in to RESTRequest's onComplete handler. It has
  1626    *         a result property, which is a Components.Results enumeration.
  1628    *   onQuotaRemaining - This is called if any request sees updated quota
  1629    *     information from the server. This provides an update mechanism so
  1630    *     listeners can immediately find out quota changes as soon as they
  1631    *     are made.
  1633    *     This callback receives the following arguments:
  1635    *       (StorageServiceClient) Client that encountered the quota change.
  1636    *       (StorageServiceRequest) Request that received the quota change.
  1637    *       (number) Integer number of kilobytes remaining for the user.
  1638    */
  1639   addListener: function addListener(listener) {
  1640     if (!listener) {
  1641       throw new Error("listener argument must be an object.");
  1644     if (this._listeners.indexOf(listener) != -1) {
  1645       return;
  1648     this._listeners.push(listener);
  1649   },
  1651   /**
  1652    * Remove a previously-installed listener.
  1653    */
  1654   removeListener: function removeListener(listener) {
  1655     this._listeners = this._listeners.filter(function(a) {
  1656       return a != listener;
  1657     });
  1658   },
  1660   /**
  1661    * Invoke listeners for a specific event.
  1663    * @param name
  1664    *        (string) The name of the listener to invoke.
  1665    * @param args
  1666    *        (array) Arguments to pass to listener functions.
  1667    */
  1668   runListeners: function runListeners(name, ...args) {
  1669     for (let listener of this._listeners) {
  1670       try {
  1671         if (name in listener) {
  1672           listener[name].apply(listener, args);
  1674       } catch (ex) {
  1675         this._log.warn("Listener threw an exception during " + name + ": "
  1676                        + ex);
  1679   },
  1681   //-----------------------------
  1682   // Information/Metadata APIs  |
  1683   //-----------------------------
  1685   /**
  1686    * Obtain a request that fetches collection info.
  1688    * On successful response, the result is placed in the resultObj property
  1689    * of the request object.
  1691    * The result value is a map of strings to numbers. The string keys represent
  1692    * collection names. The number values are integer milliseconds since Unix
  1693    * epoch that hte collection was last modified.
  1695    * This request can be made conditional by defining `locallyModifiedVersion`
  1696    * on the returned object to the last known version on the client.
  1698    * Example Usage:
  1700    *   let request = client.getCollectionInfo();
  1701    *   request.dispatch(function onComplete(error, request) {
  1702    *     if (!error) {
  1703    *       return;
  1704    *     }
  1706    *     for (let [collection, milliseconds] in Iterator(this.resultObj)) {
  1707    *       // ...
  1708    *     }
  1709    *   });
  1710    */
  1711   getCollectionInfo: function getCollectionInfo() {
  1712     return this._getJSONGETRequest("info/collections");
  1713   },
  1715   /**
  1716    * Fetch quota information.
  1718    * The result in the callback upon success is a map containing quota
  1719    * metadata. It will have the following keys:
  1721    *   usage - Number of bytes currently utilized.
  1722    *   quota - Number of bytes available to account.
  1724    * The request can be made conditional by populating `locallyModifiedVersion`
  1725    * on the returned request instance with the most recently known version of
  1726    * server data.
  1727    */
  1728   getQuota: function getQuota() {
  1729     return this._getJSONGETRequest("info/quota");
  1730   },
  1732   /**
  1733    * Fetch information on how much data each collection uses.
  1735    * The result on success is a map of strings to numbers. The string keys
  1736    * are collection names. The values are numbers corresponding to the number
  1737    * of kilobytes used by that collection.
  1738    */
  1739   getCollectionUsage: function getCollectionUsage() {
  1740     return this._getJSONGETRequest("info/collection_usage");
  1741   },
  1743   /**
  1744    * Fetch the number of records in each collection.
  1746    * The result on success is a map of strings to numbers. The string keys are
  1747    * collection names. The values are numbers corresponding to the integer
  1748    * number of items in that collection.
  1749    */
  1750   getCollectionCounts: function getCollectionCounts() {
  1751     return this._getJSONGETRequest("info/collection_counts");
  1752   },
  1754   //--------------------------
  1755   // Collection Interaction  |
  1756   // -------------------------
  1758   /**
  1759    * Obtain a request to fetch collection information.
  1761    * The returned request instance is a StorageCollectionGetRequest instance.
  1762    * This is a sub-type of StorageServiceRequest and offers a number of setters
  1763    * to control how the request is performed. See the documentation for that
  1764    * type for more.
  1766    * The request can be made conditional by setting `locallyModifiedVersion`
  1767    * on the returned request instance.
  1769    * Example usage:
  1771    *   let request = client.getCollection("testcoll");
  1773    *   // Obtain full BSOs rather than just IDs.
  1774    *   request.full = true;
  1776    *   // Only obtain BSOs modified in the last minute.
  1777    *   request.newer = Date.now() - 60000;
  1779    *   // Install handler.
  1780    *   request.handler = {
  1781    *     onBSORecord: function onBSORecord(request, bso) {
  1782    *       let id = bso.id;
  1783    *       let payload = bso.payload;
  1785    *       // Do something with BSO.
  1786    *     },
  1788    *     onComplete: function onComplete(error, req) {
  1789    *       if (error) {
  1790    *         // Handle error.
  1791    *         return;
  1792    *       }
  1794    *       // Your onBSORecord handler has processed everything. Now is where
  1795    *       // you typically signal that everything has been processed and to move
  1796    *       // on.
  1797    *     }
  1798    *   };
  1800    *   request.dispatch();
  1802    * @param collection
  1803    *        (string) Name of collection to operate on.
  1804    */
  1805   getCollection: function getCollection(collection) {
  1806     if (!collection) {
  1807       throw new Error("collection argument must be defined.");
  1810     let uri = this._baseURI + "storage/" + collection;
  1812     let request = this._getRequest(uri, "GET", {
  1813       accept:          "application/json",
  1814       allowIfModified: true,
  1815       requestType:     StorageCollectionGetRequest
  1816     });
  1818     return request;
  1819   },
  1821   /**
  1822    * Fetch a single Basic Storage Object (BSO).
  1824    * On success, the BSO may be available in the resultObj property of the
  1825    * request as a BasicStorageObject instance.
  1827    * The request can be made conditional by setting `locallyModifiedVersion`
  1828    * on the returned request instance.*
  1830    * Example usage:
  1832    *   let request = client.getBSO("meta", "global");
  1833    *   request.dispatch(function onComplete(error, request) {
  1834    *     if (!error) {
  1835    *       return;
  1836    *     }
  1838    *     if (request.notModified) {
  1839    *       return;
  1840    *     }
  1842    *     let bso = request.bso;
  1843    *     let payload = bso.payload;
  1845    *     ...
  1846    *   };
  1848    * @param collection
  1849    *        (string) Collection to fetch from
  1850    * @param id
  1851    *        (string) ID of BSO to retrieve.
  1852    * @param type
  1853    *        (constructor) Constructor to call to create returned object. This
  1854    *        is optional and defaults to BasicStorageObject.
  1855    */
  1856   getBSO: function fetchBSO(collection, id, type=BasicStorageObject) {
  1857     if (!collection) {
  1858       throw new Error("collection argument must be defined.");
  1861     if (!id) {
  1862       throw new Error("id argument must be defined.");
  1865     let uri = this._baseURI + "storage/" + collection + "/" + id;
  1867     return this._getRequest(uri, "GET", {
  1868       accept: "application/json",
  1869       allowIfModified: true,
  1870       completeParser: function completeParser(response) {
  1871         let record = new type(id, collection);
  1872         record.deserialize(response.body);
  1874         return record;
  1875       },
  1876     });
  1877   },
  1879   /**
  1880    * Add or update a BSO in a collection.
  1882    * To make the request conditional (i.e. don't allow server changes if the
  1883    * server has a newer version), set request.locallyModifiedVersion to the
  1884    * last known version of the BSO. While this could be done automatically by
  1885    * this API, it is intentionally omitted because there are valid conditions
  1886    * where a client may wish to forcefully update the server.
  1888    * If a conditional request fails because the server has newer data, the
  1889    * StorageServiceRequestError passed to the callback will have the
  1890    * `serverModified` property set to true.
  1892    * Example usage:
  1894    *   let bso = new BasicStorageObject("foo", "coll");
  1895    *   bso.payload = "payload";
  1896    *   bso.modified = Date.now();
  1898    *   let request = client.setBSO(bso);
  1899    *   request.locallyModifiedVersion = bso.modified;
  1901    *   request.dispatch(function onComplete(error, req) {
  1902    *     if (error) {
  1903    *       if (error.serverModified) {
  1904    *         // Handle conditional set failure.
  1905    *         return;
  1906    *       }
  1908    *       // Handle other errors.
  1909    *       return;
  1910    *     }
  1912    *     // Record that set worked.
  1913    *   });
  1915    * @param bso
  1916    *        (BasicStorageObject) BSO to upload. The BSO instance must have the
  1917    *        `collection` and `id` properties defined.
  1918    */
  1919   setBSO: function setBSO(bso) {
  1920     if (!bso) {
  1921       throw new Error("bso argument must be defined.");
  1924     if (!bso.collection) {
  1925       throw new Error("BSO instance does not have collection defined.");
  1928     if (!bso.id) {
  1929       throw new Error("BSO instance does not have ID defined.");
  1932     let uri = this._baseURI + "storage/" + bso.collection + "/" + bso.id;
  1933     let request = this._getRequest(uri, "PUT", {
  1934       contentType:       "application/json",
  1935       allowIfUnmodified: true,
  1936       data:              JSON.stringify(bso),
  1937     });
  1939     return request;
  1940   },
  1942   /**
  1943    * Add or update multiple BSOs.
  1945    * This is roughly equivalent to calling setBSO multiple times except it is
  1946    * much more effecient because there is only 1 round trip to the server.
  1948    * The request can be made conditional by setting `locallyModifiedVersion`
  1949    * on the returned request instance.
  1951    * This function returns a StorageCollectionSetRequest instance. This type
  1952    * has additional functions and properties specific to this operation. See
  1953    * its documentation for more.
  1955    * Most consumers interested in submitting multiple BSOs to the server will
  1956    * want to use `setBSOsBatching` instead. That API intelligently splits up
  1957    * requests as necessary, etc.
  1959    * Example usage:
  1961    *   let request = client.setBSOs("collection0");
  1962    *   let bso0 = new BasicStorageObject("id0");
  1963    *   bso0.payload = "payload0";
  1965    *   let bso1 = new BasicStorageObject("id1");
  1966    *   bso1.payload = "payload1";
  1968    *   request.addBSO(bso0);
  1969    *   request.addBSO(bso1);
  1971    *   request.dispatch(function onComplete(error, req) {
  1972    *     if (error) {
  1973    *       // Handle error.
  1974    *       return;
  1975    *     }
  1977    *     let successful = req.successfulIDs;
  1978    *     let failed = req.failed;
  1980    *     // Do additional processing.
  1981    *   });
  1983    * @param collection
  1984    *        (string) Collection to operate on.
  1985    * @return
  1986    *        (StorageCollectionSetRequest) Request instance.
  1987    */
  1988   setBSOs: function setBSOs(collection) {
  1989     if (!collection) {
  1990       throw new Error("collection argument must be defined.");
  1993     let uri = this._baseURI + "storage/" + collection;
  1994     let request = this._getRequest(uri, "POST", {
  1995       requestType:       StorageCollectionSetRequest,
  1996       contentType:       "application/newlines",
  1997       accept:            "application/json",
  1998       allowIfUnmodified: true,
  1999     });
  2001     return request;
  2002   },
  2004   /**
  2005    * This is a batching variant of setBSOs.
  2007    * Whereas `setBSOs` is a 1:1 mapping between function calls and HTTP
  2008    * requests issued, this one is a 1:N mapping. It will intelligently break
  2009    * up outgoing BSOs into multiple requests so size limits, etc aren't
  2010    * exceeded.
  2012    * Please see the documentation for `StorageCollectionBatchedSet` for
  2013    * usage info.
  2015    * @param collection
  2016    *        (string) Collection to operate on.
  2017    * @return
  2018    *        (StorageCollectionBatchedSet) Batched set instance.
  2019    */
  2020   setBSOsBatching: function setBSOsBatching(collection) {
  2021     if (!collection) {
  2022       throw new Error("collection argument must be defined.");
  2025     return new StorageCollectionBatchedSet(this, collection);
  2026   },
  2028   /**
  2029    * Deletes a single BSO from a collection.
  2031    * The request can be made conditional by setting `locallyModifiedVersion`
  2032    * on the returned request instance.
  2034    * @param collection
  2035    *        (string) Collection to operate on.
  2036    * @param id
  2037    *        (string) ID of BSO to delete.
  2038    */
  2039   deleteBSO: function deleteBSO(collection, id) {
  2040     if (!collection) {
  2041       throw new Error("collection argument must be defined.");
  2044     if (!id) {
  2045       throw new Error("id argument must be defined.");
  2048     let uri = this._baseURI + "storage/" + collection + "/" + id;
  2049     return this._getRequest(uri, "DELETE", {
  2050       allowIfUnmodified: true,
  2051     });
  2052   },
  2054   /**
  2055    * Delete multiple BSOs from a specific collection.
  2057    * This is functional equivalent to calling deleteBSO() for every ID but
  2058    * much more efficient because it only results in 1 round trip to the server.
  2060    * The request can be made conditional by setting `locallyModifiedVersion`
  2061    * on the returned request instance.
  2063    * If the number of BSOs to delete is potentially large, it is preferred to
  2064    * use `deleteBSOsBatching`. That API automatically splits the operation into
  2065    * multiple requests so server limits aren't exceeded.
  2067    * @param collection
  2068    *        (string) Name of collection to delete BSOs from.
  2069    * @param ids
  2070    *        (iterable of strings) Set of BSO IDs to delete.
  2071    */
  2072   deleteBSOs: function deleteBSOs(collection, ids) {
  2073     // In theory we should URL encode. However, IDs are supposed to be URL
  2074     // safe. If we get garbage in, we'll get garbage out and the server will
  2075     // reject it.
  2076     let s = ids.join(",");
  2078     let uri = this._baseURI + "storage/" + collection + "?ids=" + s;
  2080     return this._getRequest(uri, "DELETE", {
  2081       allowIfUnmodified: true,
  2082     });
  2083   },
  2085   /**
  2086    * Bulk deletion of BSOs with no size limit.
  2088    * This allows a large amount of BSOs to be deleted easily. It will formulate
  2089    * multiple `deleteBSOs` queries so the client does not exceed server limits.
  2091    * @param collection
  2092    *        (string) Name of collection to delete BSOs from.
  2093    * @return StorageCollectionBatchedDelete
  2094    */
  2095   deleteBSOsBatching: function deleteBSOsBatching(collection) {
  2096     if (!collection) {
  2097       throw new Error("collection argument must be defined.");
  2100     return new StorageCollectionBatchedDelete(this, collection);
  2101   },
  2103   /**
  2104    * Deletes a single collection from the server.
  2106    * The request can be made conditional by setting `locallyModifiedVersion`
  2107    * on the returned request instance.
  2109    * @param collection
  2110    *        (string) Name of collection to delete.
  2111    */
  2112   deleteCollection: function deleteCollection(collection) {
  2113     let uri = this._baseURI + "storage/" + collection;
  2115     return this._getRequest(uri, "DELETE", {
  2116       allowIfUnmodified: true
  2117     });
  2118   },
  2120   /**
  2121    * Deletes all collections data from the server.
  2122    */
  2123   deleteCollections: function deleteCollections() {
  2124     let uri = this._baseURI + "storage";
  2126     return this._getRequest(uri, "DELETE", {});
  2127   },
  2129   /**
  2130    * Helper that wraps _getRequest for GET requests that return JSON.
  2131    */
  2132   _getJSONGETRequest: function _getJSONGETRequest(path) {
  2133     let uri = this._baseURI + path;
  2135     return this._getRequest(uri, "GET", {
  2136       accept:          "application/json",
  2137       allowIfModified: true,
  2138       completeParser:  this._jsonResponseParser,
  2139     });
  2140   },
  2142   /**
  2143    * Common logic for obtaining an HTTP request instance.
  2145    * @param uri
  2146    *        (string) URI to request.
  2147    * @param method
  2148    *        (string) HTTP method to issue.
  2149    * @param options
  2150    *        (object) Additional options to control request and response
  2151    *          handling. Keys influencing behavior are:
  2153    *          completeParser - Function that parses a HTTP response body into a
  2154    *            value. This function receives the RESTResponse object and
  2155    *            returns a value that is added to a StorageResponse instance.
  2156    *            If the response cannot be parsed or is invalid, this function
  2157    *            should throw an exception.
  2159    *          data - Data to be sent in HTTP request body.
  2161    *          accept - Value for Accept request header.
  2163    *          contentType - Value for Content-Type request header.
  2165    *          requestType - Function constructor for request type to initialize.
  2166    *            Defaults to StorageServiceRequest.
  2168    *          allowIfModified - Whether to populate X-If-Modified-Since if the
  2169    *            request contains a locallyModifiedVersion.
  2171    *          allowIfUnmodified - Whether to populate X-If-Unmodified-Since if
  2172    *            the request contains a locallyModifiedVersion.
  2173    */
  2174   _getRequest: function _getRequest(uri, method, options) {
  2175     if (!options.requestType) {
  2176       options.requestType = StorageServiceRequest;
  2179     let request = new RESTRequest(uri);
  2181     if (Prefs.get("sendVersionInfo", true)) {
  2182       let ua = this.userAgent + Prefs.get("client.type", "desktop");
  2183       request.setHeader("user-agent", ua);
  2186     if (options.accept) {
  2187       request.setHeader("accept", options.accept);
  2190     if (options.contentType) {
  2191       request.setHeader("content-type", options.contentType);
  2194     let result = new options.requestType();
  2195     result._request = request;
  2196     result._method = method;
  2197     result._client = this;
  2198     result._data = options.data;
  2200     if (options.completeParser) {
  2201       result._completeParser = options.completeParser;
  2204     result._allowIfModified = !!options.allowIfModified;
  2205     result._allowIfUnmodified = !!options.allowIfUnmodified;
  2207     return result;
  2208   },
  2210   _jsonResponseParser: function _jsonResponseParser(response) {
  2211     let ct = response.headers["content-type"];
  2212     if (!ct) {
  2213       throw new Error("No Content-Type response header! Misbehaving server!");
  2216     if (ct != "application/json" && ct.indexOf("application/json;") != 0) {
  2217       throw new Error("Non-JSON media type: " + ct);
  2220     return JSON.parse(response.body);
  2221   },
  2222 };

mercurial