services/common/storageservice.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/services/common/storageservice.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,2222 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +/**
     1.9 + * This file contains APIs for interacting with the Storage Service API.
    1.10 + *
    1.11 + * The specification for the service is available at.
    1.12 + * http://docs.services.mozilla.com/storage/index.html
    1.13 + *
    1.14 + * Nothing about the spec or the service is Sync-specific. And, that is how
    1.15 + * these APIs are implemented. Instead, it is expected that consumers will
    1.16 + * create a new type inheriting or wrapping those provided by this file.
    1.17 + *
    1.18 + * STORAGE SERVICE OVERVIEW
    1.19 + *
    1.20 + * The storage service is effectively a key-value store where each value is a
    1.21 + * well-defined envelope that stores specific metadata along with a payload.
    1.22 + * These values are called Basic Storage Objects, or BSOs. BSOs are organized
    1.23 + * into named groups called collections.
    1.24 + *
    1.25 + * The service also provides ancillary APIs not related to storage, such as
    1.26 + * looking up the set of stored collections, current quota usage, etc.
    1.27 + */
    1.28 +
    1.29 +"use strict";
    1.30 +
    1.31 +this.EXPORTED_SYMBOLS = [
    1.32 +  "BasicStorageObject",
    1.33 +  "StorageServiceClient",
    1.34 +  "StorageServiceRequestError",
    1.35 +];
    1.36 +
    1.37 +const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
    1.38 +
    1.39 +Cu.import("resource://gre/modules/Preferences.jsm");
    1.40 +Cu.import("resource://services-common/async.js");
    1.41 +Cu.import("resource://gre/modules/Log.jsm");
    1.42 +Cu.import("resource://services-common/rest.js");
    1.43 +Cu.import("resource://services-common/utils.js");
    1.44 +
    1.45 +const Prefs = new Preferences("services.common.storageservice.");
    1.46 +
    1.47 +/**
    1.48 + * The data type stored in the storage service.
    1.49 + *
    1.50 + * A Basic Storage Object (BSO) is the primitive type stored in the storage
    1.51 + * service. BSO's are simply maps with a well-defined set of keys.
    1.52 + *
    1.53 + * BSOs belong to named collections.
    1.54 + *
    1.55 + * A single BSO consists of the following fields:
    1.56 + *
    1.57 + *   id - An identifying string. This is how a BSO is uniquely identified within
    1.58 + *     a single collection.
    1.59 + *   modified - Integer milliseconds since Unix epoch BSO was modified.
    1.60 + *   payload - String contents of BSO. The format of the string is undefined
    1.61 + *     (although JSON is typically used).
    1.62 + *   ttl - The number of seconds to keep this record.
    1.63 + *   sortindex - Integer indicating relative importance of record within the
    1.64 + *     collection.
    1.65 + *
    1.66 + * The constructor simply creates an empty BSO having the specified ID (which
    1.67 + * can be null or undefined). It also takes an optional collection. This is
    1.68 + * purely for convenience.
    1.69 + *
    1.70 + * This type is meant to be a dumb container and little more.
    1.71 + *
    1.72 + * @param id
    1.73 + *        (string) ID of BSO. Can be null.
    1.74 + *        (string) Collection BSO belongs to. Can be null;
    1.75 + */
    1.76 +this.BasicStorageObject =
    1.77 + function BasicStorageObject(id=null, collection=null) {
    1.78 +  this.data       = {};
    1.79 +  this.id         = id;
    1.80 +  this.collection = collection;
    1.81 +}
    1.82 +BasicStorageObject.prototype = {
    1.83 +  id: null,
    1.84 +  collection: null,
    1.85 +  data: null,
    1.86 +
    1.87 +  // At the time this was written, the convention for constructor arguments
    1.88 +  // was not adopted by Harmony. It could break in the future. We have test
    1.89 +  // coverage that will break if SpiderMonkey changes, just in case.
    1.90 +  _validKeys: new Set(["id", "payload", "modified", "sortindex", "ttl"]),
    1.91 +
    1.92 +  /**
    1.93 +   * Get the string payload as-is.
    1.94 +   */
    1.95 +  get payload() {
    1.96 +    return this.data.payload;
    1.97 +  },
    1.98 +
    1.99 +  /**
   1.100 +   * Set the string payload to a new value.
   1.101 +   */
   1.102 +  set payload(value) {
   1.103 +    this.data.payload = value;
   1.104 +  },
   1.105 +
   1.106 +  /**
   1.107 +   * Get the modified time of the BSO in milliseconds since Unix epoch.
   1.108 +   *
   1.109 +   * You can convert this to a native JS Date instance easily:
   1.110 +   *
   1.111 +   *   let date = new Date(bso.modified);
   1.112 +   */
   1.113 +  get modified() {
   1.114 +    return this.data.modified;
   1.115 +  },
   1.116 +
   1.117 +  /**
   1.118 +   * Sets the modified time of the BSO in milliseconds since Unix epoch.
   1.119 +   *
   1.120 +   * Please note that if this value is sent to the server it will be ignored.
   1.121 +   * The server will use its time at the time of the operation when storing the
   1.122 +   * BSO.
   1.123 +   */
   1.124 +  set modified(value) {
   1.125 +    this.data.modified = value;
   1.126 +  },
   1.127 +
   1.128 +  get sortindex() {
   1.129 +    if (this.data.sortindex) {
   1.130 +      return this.data.sortindex || 0;
   1.131 +    }
   1.132 +
   1.133 +    return 0;
   1.134 +  },
   1.135 +
   1.136 +  set sortindex(value) {
   1.137 +    if (!value && value !== 0) {
   1.138 +      delete this.data.sortindex;
   1.139 +      return;
   1.140 +    }
   1.141 +
   1.142 +    this.data.sortindex = value;
   1.143 +  },
   1.144 +
   1.145 +  get ttl() {
   1.146 +    return this.data.ttl;
   1.147 +  },
   1.148 +
   1.149 +  set ttl(value) {
   1.150 +    if (!value && value !== 0) {
   1.151 +      delete this.data.ttl;
   1.152 +      return;
   1.153 +    }
   1.154 +
   1.155 +    this.data.ttl = value;
   1.156 +  },
   1.157 +
   1.158 +  /**
   1.159 +   * Deserialize JSON or another object into this instance.
   1.160 +   *
   1.161 +   * The argument can be a string containing serialized JSON or an object.
   1.162 +   *
   1.163 +   * If the JSON is invalid or if the object contains unknown fields, an
   1.164 +   * exception will be thrown.
   1.165 +   *
   1.166 +   * @param json
   1.167 +   *        (string|object) Value to construct BSO from.
   1.168 +   */
   1.169 +  deserialize: function deserialize(input) {
   1.170 +    let data;
   1.171 +
   1.172 +    if (typeof(input) == "string") {
   1.173 +      data = JSON.parse(input);
   1.174 +      if (typeof(data) != "object") {
   1.175 +        throw new Error("Supplied JSON is valid but is not a JS-Object.");
   1.176 +      }
   1.177 +    }
   1.178 +    else if (typeof(input) == "object") {
   1.179 +      data = input;
   1.180 +    } else {
   1.181 +      throw new Error("Argument must be a JSON string or object: " +
   1.182 +                      typeof(input));
   1.183 +    }
   1.184 +
   1.185 +    for each (let key in Object.keys(data)) {
   1.186 +      if (key == "id") {
   1.187 +        this.id = data.id;
   1.188 +        continue;
   1.189 +      }
   1.190 +
   1.191 +      if (!this._validKeys.has(key)) {
   1.192 +        throw new Error("Invalid key in object: " + key);
   1.193 +      }
   1.194 +
   1.195 +      this.data[key] = data[key];
   1.196 +    }
   1.197 +  },
   1.198 +
   1.199 +  /**
   1.200 +   * Serialize the current BSO to JSON.
   1.201 +   *
   1.202 +   * @return string
   1.203 +   *         The JSON representation of this BSO.
   1.204 +   */
   1.205 +  toJSON: function toJSON() {
   1.206 +    let obj = {};
   1.207 +
   1.208 +    for (let [k, v] in Iterator(this.data)) {
   1.209 +      obj[k] = v;
   1.210 +    }
   1.211 +
   1.212 +    if (this.id) {
   1.213 +      obj.id = this.id;
   1.214 +    }
   1.215 +
   1.216 +    return obj;
   1.217 +  },
   1.218 +
   1.219 +  toString: function toString() {
   1.220 +    return "{ " +
   1.221 +      "id: "       + this.id        + " " +
   1.222 +      "modified: " + this.modified  + " " +
   1.223 +      "ttl: "      + this.ttl       + " " +
   1.224 +      "index: "    + this.sortindex + " " +
   1.225 +      "payload: "  + this.payload   +
   1.226 +      " }";
   1.227 +  },
   1.228 +};
   1.229 +
   1.230 +/**
   1.231 + * Represents an error encountered during a StorageServiceRequest request.
   1.232 + *
   1.233 + * Instances of this will be passed to the onComplete callback for any request
   1.234 + * that did not succeed.
   1.235 + *
   1.236 + * This type effectively wraps other error conditions. It is up to the client
   1.237 + * to determine the appropriate course of action for each error type
   1.238 + * encountered.
   1.239 + *
   1.240 + * The following error "classes" are defined by properties on each instance:
   1.241 + *
   1.242 + *   serverModified - True if the request to modify data was conditional and
   1.243 + *     the server rejected the request because it has newer data than the
   1.244 + *     client.
   1.245 + *
   1.246 + *   notFound - True if the requested URI or resource does not exist.
   1.247 + *
   1.248 + *   conflict - True if the server reported that a resource being operated on
   1.249 + *     was in conflict. If this occurs, the client should typically wait a
   1.250 + *     little and try the request again.
   1.251 + *
   1.252 + *   requestTooLarge - True if the request was too large for the server. If
   1.253 + *     this happens on batch requests, the client should retry the request with
   1.254 + *     smaller batches.
   1.255 + *
   1.256 + *   network - A network error prevented this request from succeeding. If set,
   1.257 + *     it will be an Error thrown by the Gecko network stack. If set, it could
   1.258 + *     mean that the request could not be performed or that an error occurred
   1.259 + *     when the request was in flight. It is also possible the request
   1.260 + *     succeeded on the server but the response was lost in transit.
   1.261 + *
   1.262 + *   authentication - If defined, an authentication error has occurred. If
   1.263 + *     defined, it will be an Error instance. If seen, the client should not
   1.264 + *     retry the request without first correcting the authentication issue.
   1.265 + *
   1.266 + *   client - An error occurred which was the client's fault. This typically
   1.267 + *     means the code in this file is buggy.
   1.268 + *
   1.269 + *   server - An error occurred on the server. In the ideal world, this should
   1.270 + *     never happen. But, it does. If set, this will be an Error which
   1.271 + *     describes the error as reported by the server.
   1.272 + */
   1.273 +this.StorageServiceRequestError = function StorageServiceRequestError() {
   1.274 +  this.serverModified  = false;
   1.275 +  this.notFound        = false;
   1.276 +  this.conflict        = false;
   1.277 +  this.requestToolarge = false;
   1.278 +  this.network         = null;
   1.279 +  this.authentication  = null;
   1.280 +  this.client          = null;
   1.281 +  this.server          = null;
   1.282 +}
   1.283 +
   1.284 +/**
   1.285 + * Represents a single request to the storage service.
   1.286 + *
   1.287 + * Instances of this type are returned by the APIs on StorageServiceClient.
   1.288 + * They should not be created outside of StorageServiceClient.
   1.289 + *
   1.290 + * This type encapsulates common storage API request and response handling.
   1.291 + * Metadata required to perform the request is stored inside each instance and
   1.292 + * should be treated as invisible by consumers.
   1.293 + *
   1.294 + * A number of "public" properties are exposed to allow clients to further
   1.295 + * customize behavior. These are documented below.
   1.296 + *
   1.297 + * Some APIs in StorageServiceClient define their own types which inherit from
   1.298 + * this one. Read the API documentation to see which types those are and when
   1.299 + * they apply.
   1.300 + *
   1.301 + * This type wraps RESTRequest rather than extending it. The reason is mainly
   1.302 + * to avoid the fragile base class problem. We implement considerable extra
   1.303 + * functionality on top of RESTRequest and don't want this to accidentally
   1.304 + * trample on RESTRequest's members.
   1.305 + *
   1.306 + * If this were a C++ class, it and StorageServiceClient would be friend
   1.307 + * classes. Each touches "protected" APIs of the other. Thus, each should be
   1.308 + * considered when making changes to the other.
   1.309 + *
   1.310 + * Usage
   1.311 + * =====
   1.312 + *
   1.313 + * When you obtain a request instance, it is waiting to be dispatched. It may
   1.314 + * have additional settings available for tuning. See the documentation in
   1.315 + * StorageServiceClient for more.
   1.316 + *
   1.317 + * There are essentially two types of requests: "basic" and "streaming."
   1.318 + * "Basic" requests encapsulate the traditional request-response paradigm:
   1.319 + * a request is issued and we get a response later once the full response
   1.320 + * is available. Most of the APIs in StorageServiceClient issue these "basic"
   1.321 + * requests. Streaming requests typically involve the transport of multiple
   1.322 + * BasicStorageObject instances. When a new BSO instance is available, a
   1.323 + * callback is fired.
   1.324 + *
   1.325 + * For basic requests, the general flow looks something like:
   1.326 + *
   1.327 + *   // Obtain a new request instance.
   1.328 + *   let request = client.getCollectionInfo();
   1.329 + *
   1.330 + *   // Install a handler which provides callbacks for request events. The most
   1.331 + *   // important is `onComplete`, which is called when the request has
   1.332 + *   // finished and the response is completely received.
   1.333 + *   request.handler = {
   1.334 + *     onComplete: function onComplete(error, request) {
   1.335 + *       // Do something.
   1.336 + *     }
   1.337 + *   };
   1.338 + *
   1.339 + *   // Send the request.
   1.340 + *   request.dispatch();
   1.341 + *
   1.342 + * Alternatively, we can install the onComplete handler when calling dispatch:
   1.343 + *
   1.344 + *   let request = client.getCollectionInfo();
   1.345 + *   request.dispatch(function onComplete(error, request) {
   1.346 + *     // Handle response.
   1.347 + *   });
   1.348 + *
   1.349 + * Please note that installing an `onComplete` handler as the argument to
   1.350 + * `dispatch()` will overwrite an existing `handler`.
   1.351 + *
   1.352 + * In both of the above example, the two `request` variables are identical. The
   1.353 + * original `StorageServiceRequest` is passed into the callback so callers
   1.354 + * don't need to rely on closures.
   1.355 + *
   1.356 + * Most of the complexity for onComplete handlers is error checking.
   1.357 + *
   1.358 + * The first thing you do in your onComplete handler is ensure no error was
   1.359 + * seen:
   1.360 + *
   1.361 + *   function onComplete(error, request) {
   1.362 + *     if (error) {
   1.363 + *       // Handle error.
   1.364 + *     }
   1.365 + *   }
   1.366 + *
   1.367 + * If `error` is defined, it will be an instance of
   1.368 + * `StorageServiceRequestError`. An error will be set if the request didn't
   1.369 + * complete successfully. This means the transport layer must have succeeded
   1.370 + * and the application protocol (HTTP) must have returned a successful status
   1.371 + * code (2xx and some 3xx). Please see the documentation for
   1.372 + * `StorageServiceRequestError` for more.
   1.373 + *
   1.374 + * A robust error handler would look something like:
   1.375 + *
   1.376 + *   function onComplete(error, request) {
   1.377 + *     if (error) {
   1.378 + *       if (error.network) {
   1.379 + *         // Network error encountered!
   1.380 + *       } else if (error.server) {
   1.381 + *         // Something went wrong on the server (HTTP 5xx).
   1.382 + *       } else if (error.authentication) {
   1.383 + *         // Server rejected request due to bad credentials.
   1.384 + *       } else if (error.serverModified) {
   1.385 + *         // The conditional request was rejected because the server has newer
   1.386 + *         // data than what the client reported.
   1.387 + *       } else if (error.conflict) {
   1.388 + *         // The server reported that the operation could not be completed
   1.389 + *         // because another client is also updating it.
   1.390 + *       } else if (error.requestTooLarge) {
   1.391 + *         // The server rejected the request because it was too large.
   1.392 + *       } else if (error.notFound) {
   1.393 + *         // The requested resource was not found.
   1.394 + *       } else if (error.client) {
   1.395 + *         // Something is wrong with the client's request. You should *never*
   1.396 + *         // see this, as it means this client is likely buggy. It could also
   1.397 + *         // mean the server is buggy or misconfigured. Either way, something
   1.398 + *         // is buggy.
   1.399 + *       }
   1.400 + *
   1.401 + *       return;
   1.402 + *     }
   1.403 + *
   1.404 + *     // Handle successful case.
   1.405 + *   }
   1.406 + *
   1.407 + * If `error` is null, the request completed successfully. There may or may not
   1.408 + * be additional data available on the request instance.
   1.409 + *
   1.410 + * For requests that obtain data, this data is typically made available through
   1.411 + * the `resultObj` property on the request instance. The API that was called
   1.412 + * will install its own response hander and ensure this property is decoded to
   1.413 + * what you expect.
   1.414 + *
   1.415 + * Conditional Requests
   1.416 + * --------------------
   1.417 + *
   1.418 + * Many of the APIs on `StorageServiceClient` support conditional requests.
   1.419 + * That is, the client defines the last version of data it has (the version
   1.420 + * comes from a previous response from the server) and sends this as part of
   1.421 + * the request.
   1.422 + *
   1.423 + * For query requests, if the server hasn't changed, no new data will be
   1.424 + * returned. If issuing a conditional query request, the caller should check
   1.425 + * the `notModified` property on the request in the response callback. If this
   1.426 + * property is true, the server has no new data and there is obviously no data
   1.427 + * on the response.
   1.428 + *
   1.429 + * For example:
   1.430 + *
   1.431 + *   let request = client.getCollectionInfo();
   1.432 + *   request.locallyModifiedVersion = Date.now() - 60000;
   1.433 + *   request.dispatch(function onComplete(error, request) {
   1.434 + *     if (error) {
   1.435 + *       // Handle error.
   1.436 + *       return;
   1.437 + *     }
   1.438 + *
   1.439 + *     if (request.notModified) {
   1.440 + *       return;
   1.441 + *     }
   1.442 + *
   1.443 + *     let info = request.resultObj;
   1.444 + *     // Do stuff.
   1.445 + *   });
   1.446 + *
   1.447 + * For modification requests, if the server has changed, the request will be
   1.448 + * rejected. When this happens, `error`will be defined and the `serverModified`
   1.449 + * property on it will be true.
   1.450 + *
   1.451 + * For example:
   1.452 + *
   1.453 + *   let request = client.setBSO(bso);
   1.454 + *   request.locallyModifiedVersion = bso.modified;
   1.455 + *   request.dispatch(function onComplete(error, request) {
   1.456 + *     if (error) {
   1.457 + *       if (error.serverModified) {
   1.458 + *         // Server data is newer! We should probably fetch it and apply
   1.459 + *         // locally.
   1.460 + *       }
   1.461 + *
   1.462 + *       return;
   1.463 + *     }
   1.464 + *
   1.465 + *     // Handle success.
   1.466 + *   });
   1.467 + *
   1.468 + * Future Features
   1.469 + * ---------------
   1.470 + *
   1.471 + * The current implementation does not support true streaming for things like
   1.472 + * multi-BSO retrieval. However, the API supports it, so we should be able
   1.473 + * to implement it transparently.
   1.474 + */
   1.475 +function StorageServiceRequest() {
   1.476 +  this._log = Log.repository.getLogger("Sync.StorageService.Request");
   1.477 +  this._log.level = Log.Level[Prefs.get("log.level")];
   1.478 +
   1.479 +  this.notModified = false;
   1.480 +
   1.481 +  this._client                 = null;
   1.482 +  this._request                = null;
   1.483 +  this._method                 = null;
   1.484 +  this._handler                = {};
   1.485 +  this._data                   = null;
   1.486 +  this._error                  = null;
   1.487 +  this._resultObj              = null;
   1.488 +  this._locallyModifiedVersion = null;
   1.489 +  this._allowIfModified        = false;
   1.490 +  this._allowIfUnmodified      = false;
   1.491 +}
   1.492 +StorageServiceRequest.prototype = {
   1.493 +  /**
   1.494 +   * The StorageServiceClient this request came from.
   1.495 +   */
   1.496 +  get client() {
   1.497 +    return this._client;
   1.498 +  },
   1.499 +
   1.500 +  /**
   1.501 +   * The underlying RESTRequest instance.
   1.502 +   *
   1.503 +   * This should be treated as read only and should not be modified
   1.504 +   * directly by external callers. While modification would probably work, this
   1.505 +   * would defeat the purpose of the API and the abstractions it is meant to
   1.506 +   * provide.
   1.507 +   *
   1.508 +   * If a consumer needs to modify the underlying request object, it is
   1.509 +   * recommended for them to implement a new type that inherits from
   1.510 +   * StorageServiceClient and override the necessary APIs to modify the request
   1.511 +   * there.
   1.512 +   *
   1.513 +   * This accessor may disappear in future versions.
   1.514 +   */
   1.515 +  get request() {
   1.516 +    return this._request;
   1.517 +  },
   1.518 +
   1.519 +  /**
   1.520 +   * The RESTResponse that resulted from the RESTRequest.
   1.521 +   */
   1.522 +  get response() {
   1.523 +    return this._request.response;
   1.524 +  },
   1.525 +
   1.526 +  /**
   1.527 +   * HTTP status code from response.
   1.528 +   */
   1.529 +  get statusCode() {
   1.530 +    let response = this.response;
   1.531 +    return response ? response.status : null;
   1.532 +  },
   1.533 +
   1.534 +  /**
   1.535 +   * Holds any error that has occurred.
   1.536 +   *
   1.537 +   * If a network error occurred, that will be returned. If no network error
   1.538 +   * occurred, the client error will be returned. If no error occurred (yet),
   1.539 +   * null will be returned.
   1.540 +   */
   1.541 +  get error() {
   1.542 +    return this._error;
   1.543 +  },
   1.544 +
   1.545 +  /**
   1.546 +   * The result from the request.
   1.547 +   *
   1.548 +   * This stores the object returned from the server. The type of object depends
   1.549 +   * on the request type. See the per-API documentation in StorageServiceClient
   1.550 +   * for details.
   1.551 +   */
   1.552 +  get resultObj() {
   1.553 +    return this._resultObj;
   1.554 +  },
   1.555 +
   1.556 +  /**
   1.557 +   * Define the local version of the entity the client has.
   1.558 +   *
   1.559 +   * This is used to enable conditional requests. Depending on the request
   1.560 +   * type, the value set here could be reflected in the X-If-Modified-Since or
   1.561 +   * X-If-Unmodified-Since headers.
   1.562 +   *
   1.563 +   * This attribute is not honoured on every request. See the documentation
   1.564 +   * in the client API to learn where it is valid.
   1.565 +   */
   1.566 +  set locallyModifiedVersion(value) {
   1.567 +    // Will eventually become a header, so coerce to string.
   1.568 +    this._locallyModifiedVersion = "" + value;
   1.569 +  },
   1.570 +
   1.571 +  /**
   1.572 +   * Object which holds callbacks and state for this request.
   1.573 +   *
   1.574 +   * The handler is installed by users of this request. It is simply an object
   1.575 +   * containing 0 or more of the following properties:
   1.576 +   *
   1.577 +   *   onComplete - A function called when the request has completed and all
   1.578 +   *     data has been received from the server. The function receives the
   1.579 +   *     following arguments:
   1.580 +   *
   1.581 +   *       (StorageServiceRequestError) Error encountered during request. null
   1.582 +   *         if no error was encountered.
   1.583 +   *       (StorageServiceRequest) The request that was sent (this instance).
   1.584 +   *         Response information is available via properties and functions.
   1.585 +   *
   1.586 +   *     Unless the call to dispatch() throws before returning, this callback
   1.587 +   *     is guaranteed to be invoked.
   1.588 +   *
   1.589 +   *     Every client almost certainly wants to install this handler.
   1.590 +   *
   1.591 +   *   onDispatch - A function called immediately before the request is
   1.592 +   *     dispatched. This hook can be used to inspect or modify the request
   1.593 +   *     before it is issued.
   1.594 +   *
   1.595 +   *     The called function receives the following arguments:
   1.596 +   *
   1.597 +   *       (StorageServiceRequest) The request being issued (this request).
   1.598 +   *
   1.599 +   *   onBSORecord - When retrieving multiple BSOs from the server, this
   1.600 +   *     function is invoked when a new BSO record has been read. This function
   1.601 +   *     will be invoked 0 to N times before onComplete is invoked. onComplete
   1.602 +   *     signals that the last BSO has been processed or that an error
   1.603 +   *     occurred. The function receives the following arguments:
   1.604 +   *
   1.605 +   *       (StorageServiceRequest) The request that was sent (this instance).
   1.606 +   *       (BasicStorageObject|string) The received BSO instance (when in full
   1.607 +   *         mode) or the string ID of the BSO (when not in full mode).
   1.608 +   *
   1.609 +   * Callers are free to (and encouraged) to store extra state in the supplied
   1.610 +   * handler.
   1.611 +   */
   1.612 +  set handler(value) {
   1.613 +    if (typeof(value) != "object") {
   1.614 +      throw new Error("Invalid handler. Must be an Object.");
   1.615 +    }
   1.616 +
   1.617 +    this._handler = value;
   1.618 +
   1.619 +    if (!value.onComplete) {
   1.620 +      this._log.warn("Handler does not contain an onComplete callback!");
   1.621 +    }
   1.622 +  },
   1.623 +
   1.624 +  get handler() {
   1.625 +    return this._handler;
   1.626 +  },
   1.627 +
   1.628 +  //---------------
   1.629 +  // General APIs |
   1.630 +  //---------------
   1.631 +
   1.632 +  /**
   1.633 +   * Start the request.
   1.634 +   *
   1.635 +   * The request is dispatched asynchronously. The installed handler will have
   1.636 +   * one or more of its callbacks invoked as the state of the request changes.
   1.637 +   *
   1.638 +   * The `onComplete` argument is optional. If provided, the supplied function
   1.639 +   * will be installed on a *new* handler before the request is dispatched. This
   1.640 +   * is equivalent to calling:
   1.641 +   *
   1.642 +   *   request.handler = {onComplete: value};
   1.643 +   *   request.dispatch();
   1.644 +   *
   1.645 +   * Please note that any existing handler will be replaced if onComplete is
   1.646 +   * provided.
   1.647 +   *
   1.648 +   * @param onComplete
   1.649 +   *        (function) Callback to be invoked when request has completed.
   1.650 +   */
   1.651 +  dispatch: function dispatch(onComplete) {
   1.652 +    if (onComplete) {
   1.653 +      this.handler = {onComplete: onComplete};
   1.654 +    }
   1.655 +
   1.656 +    // Installing the dummy callback makes implementation easier in _onComplete
   1.657 +    // because we can then blindly call.
   1.658 +    this._dispatch(function _internalOnComplete(error) {
   1.659 +      this._onComplete(error);
   1.660 +      this.completed = true;
   1.661 +    }.bind(this));
   1.662 +  },
   1.663 +
   1.664 +  /**
   1.665 +   * This is a synchronous version of dispatch().
   1.666 +   *
   1.667 +   * THIS IS AN EVIL FUNCTION AND SHOULD NOT BE CALLED. It is provided for
   1.668 +   * legacy reasons to support evil, synchronous clients.
   1.669 +   *
   1.670 +   * Please note that onComplete callbacks are executed from this JS thread.
   1.671 +   * We dispatch the request, spin the event loop until it comes back. Then,
   1.672 +   * we execute callbacks ourselves then return. In other words, there is no
   1.673 +   * potential for spinning between callback execution and this function
   1.674 +   * returning.
   1.675 +   *
   1.676 +   * The `onComplete` argument has the same behavior as for `dispatch()`.
   1.677 +   *
   1.678 +   * @param onComplete
   1.679 +   *        (function) Callback to be invoked when request has completed.
   1.680 +   */
   1.681 +  dispatchSynchronous: function dispatchSynchronous(onComplete) {
   1.682 +    if (onComplete) {
   1.683 +      this.handler = {onComplete: onComplete};
   1.684 +    }
   1.685 +
   1.686 +    let cb = Async.makeSyncCallback();
   1.687 +    this._dispatch(cb);
   1.688 +    let error = Async.waitForSyncCallback(cb);
   1.689 +
   1.690 +    this._onComplete(error);
   1.691 +    this.completed = true;
   1.692 +  },
   1.693 +
   1.694 +  //-------------------------------------------------------------------------
   1.695 +  // HIDDEN APIS. DO NOT CHANGE ANYTHING UNDER HERE FROM OUTSIDE THIS TYPE. |
   1.696 +  //-------------------------------------------------------------------------
   1.697 +
   1.698 +  /**
   1.699 +   * Data to include in HTTP request body.
   1.700 +   */
   1.701 +  _data: null,
   1.702 +
   1.703 +  /**
   1.704 +   * StorageServiceRequestError encountered during dispatchy.
   1.705 +   */
   1.706 +  _error: null,
   1.707 +
   1.708 +  /**
   1.709 +   * Handler to parse response body into another object.
   1.710 +   *
   1.711 +   * This is installed by the client API. It should return the value the body
   1.712 +   * parses to on success. If a failure is encountered, an exception should be
   1.713 +   * thrown.
   1.714 +   */
   1.715 +  _completeParser: null,
   1.716 +
   1.717 +  /**
   1.718 +   * Dispatch the request.
   1.719 +   *
   1.720 +   * This contains common functionality for dispatching requests. It should
   1.721 +   * ideally be part of dispatch, but since dispatchSynchronous exists, we
   1.722 +   * factor out common code.
   1.723 +   */
   1.724 +  _dispatch: function _dispatch(onComplete) {
   1.725 +    // RESTRequest throws if the request has already been dispatched, so we
   1.726 +    // need not bother checking.
   1.727 +
   1.728 +    // Inject conditional headers into request if they are allowed and if a
   1.729 +    // value is set. Note that _locallyModifiedVersion is always a string and
   1.730 +    // if("0") is true.
   1.731 +    if (this._allowIfModified && this._locallyModifiedVersion) {
   1.732 +      this._log.trace("Making request conditional.");
   1.733 +      this._request.setHeader("X-If-Modified-Since",
   1.734 +                              this._locallyModifiedVersion);
   1.735 +    } else if (this._allowIfUnmodified && this._locallyModifiedVersion) {
   1.736 +      this._log.trace("Making request conditional.");
   1.737 +      this._request.setHeader("X-If-Unmodified-Since",
   1.738 +                              this._locallyModifiedVersion);
   1.739 +    }
   1.740 +
   1.741 +    // We have both an internal and public hook.
   1.742 +    // If these throw, it is OK since we are not in a callback.
   1.743 +    if (this._onDispatch) {
   1.744 +      this._onDispatch();
   1.745 +    }
   1.746 +
   1.747 +    if (this._handler.onDispatch) {
   1.748 +      this._handler.onDispatch(this);
   1.749 +    }
   1.750 +
   1.751 +    this._client.runListeners("onDispatch", this);
   1.752 +
   1.753 +    this._log.info("Dispatching request: " + this._method + " " +
   1.754 +                   this._request.uri.asciiSpec);
   1.755 +
   1.756 +    this._request.dispatch(this._method, this._data, onComplete);
   1.757 +  },
   1.758 +
   1.759 +  /**
   1.760 +   * RESTRequest onComplete handler for all requests.
   1.761 +   *
   1.762 +   * This provides common logic for all response handling.
   1.763 +   */
   1.764 +  _onComplete: function(error) {
   1.765 +    let onCompleteCalled = false;
   1.766 +
   1.767 +    let callOnComplete = function callOnComplete() {
   1.768 +      onCompleteCalled = true;
   1.769 +
   1.770 +      if (!this._handler.onComplete) {
   1.771 +        this._log.warn("No onComplete installed in handler!");
   1.772 +        return;
   1.773 +      }
   1.774 +
   1.775 +      try {
   1.776 +        this._handler.onComplete(this._error, this);
   1.777 +      } catch (ex) {
   1.778 +        this._log.warn("Exception when invoking handler's onComplete: " +
   1.779 +                       CommonUtils.exceptionStr(ex));
   1.780 +        throw ex;
   1.781 +      }
   1.782 +    }.bind(this);
   1.783 +
   1.784 +    try {
   1.785 +      if (error) {
   1.786 +        this._error = new StorageServiceRequestError();
   1.787 +        this._error.network = error;
   1.788 +        this._log.info("Network error during request: " + error);
   1.789 +        this._client.runListeners("onNetworkError", this._client, this, error);
   1.790 +        callOnComplete();
   1.791 +        return;
   1.792 +      }
   1.793 +
   1.794 +      let response = this._request.response;
   1.795 +      this._log.info(response.status + " " + this._request.uri.asciiSpec);
   1.796 +
   1.797 +      this._processHeaders();
   1.798 +
   1.799 +      if (response.status == 200) {
   1.800 +        this._resultObj = this._completeParser(response);
   1.801 +        callOnComplete();
   1.802 +        return;
   1.803 +      }
   1.804 +
   1.805 +      if (response.status == 201) {
   1.806 +        callOnComplete();
   1.807 +        return;
   1.808 +      }
   1.809 +
   1.810 +      if (response.status == 204) {
   1.811 +        callOnComplete();
   1.812 +        return;
   1.813 +      }
   1.814 +
   1.815 +      if (response.status == 304) {
   1.816 +        this.notModified = true;
   1.817 +        callOnComplete();
   1.818 +        return;
   1.819 +      }
   1.820 +
   1.821 +      // TODO handle numeric response code from server.
   1.822 +      if (response.status == 400) {
   1.823 +        this._error = new StorageServiceRequestError();
   1.824 +        this._error.client = new Error("Client error!");
   1.825 +        callOnComplete();
   1.826 +        return;
   1.827 +      }
   1.828 +
   1.829 +      if (response.status == 401) {
   1.830 +        this._error = new StorageServiceRequestError();
   1.831 +        this._error.authentication = new Error("401 Received.");
   1.832 +        this._client.runListeners("onAuthFailure", this._error.authentication,
   1.833 +                                  this);
   1.834 +        callOnComplete();
   1.835 +        return;
   1.836 +      }
   1.837 +
   1.838 +      if (response.status == 404) {
   1.839 +        this._error = new StorageServiceRequestError();
   1.840 +        this._error.notFound = true;
   1.841 +        callOnComplete();
   1.842 +        return;
   1.843 +      }
   1.844 +
   1.845 +      if (response.status == 409) {
   1.846 +        this._error = new StorageServiceRequestError();
   1.847 +        this._error.conflict = true;
   1.848 +        callOnComplete();
   1.849 +        return;
   1.850 +      }
   1.851 +
   1.852 +      if (response.status == 412) {
   1.853 +        this._error = new StorageServiceRequestError();
   1.854 +        this._error.serverModified = true;
   1.855 +        callOnComplete();
   1.856 +        return;
   1.857 +      }
   1.858 +
   1.859 +      if (response.status == 413) {
   1.860 +        this._error = new StorageServiceRequestError();
   1.861 +        this._error.requestTooLarge = true;
   1.862 +        callOnComplete();
   1.863 +        return;
   1.864 +      }
   1.865 +
   1.866 +      // If we see this, either the client or the server is buggy. We should
   1.867 +      // never see this.
   1.868 +      if (response.status == 415) {
   1.869 +        this._log.error("415 HTTP response seen from server! This should " +
   1.870 +                        "never happen!");
   1.871 +        this._error = new StorageServiceRequestError();
   1.872 +        this._error.client = new Error("415 Unsupported Media Type received!");
   1.873 +        callOnComplete();
   1.874 +        return;
   1.875 +      }
   1.876 +
   1.877 +      if (response.status >= 500 && response.status <= 599) {
   1.878 +        this._log.error(response.status + " seen from server!");
   1.879 +        this._error = new StorageServiceRequestError();
   1.880 +        this._error.server = new Error(response.status + " status code.");
   1.881 +        callOnComplete();
   1.882 +        return;
   1.883 +      }
   1.884 +
   1.885 +      callOnComplete();
   1.886 +
   1.887 +    } catch (ex) {
   1.888 +      this._clientError = ex;
   1.889 +      this._log.info("Exception when processing _onComplete: " + ex);
   1.890 +
   1.891 +      if (!onCompleteCalled) {
   1.892 +        this._log.warn("Exception in internal response handling logic!");
   1.893 +        try {
   1.894 +          callOnComplete();
   1.895 +        } catch (ex) {
   1.896 +          this._log.warn("An additional exception was encountered when " +
   1.897 +                         "calling the handler's onComplete: " + ex);
   1.898 +        }
   1.899 +      }
   1.900 +    }
   1.901 +  },
   1.902 +
   1.903 +  _processHeaders: function _processHeaders() {
   1.904 +    let headers = this._request.response.headers;
   1.905 +
   1.906 +    if (headers["x-timestamp"]) {
   1.907 +      this.serverTime = parseFloat(headers["x-timestamp"]);
   1.908 +    }
   1.909 +
   1.910 +    if (headers["x-backoff"]) {
   1.911 +      this.backoffInterval = 1000 * parseInt(headers["x-backoff"], 10);
   1.912 +    }
   1.913 +
   1.914 +    if (headers["retry-after"]) {
   1.915 +      this.backoffInterval = 1000 * parseInt(headers["retry-after"], 10);
   1.916 +    }
   1.917 +
   1.918 +    if (this.backoffInterval) {
   1.919 +      let failure = this._request.response.status == 503;
   1.920 +      this._client.runListeners("onBackoffReceived", this._client, this,
   1.921 +                               this.backoffInterval, !failure);
   1.922 +    }
   1.923 +
   1.924 +    if (headers["x-quota-remaining"]) {
   1.925 +      this.quotaRemaining = parseInt(headers["x-quota-remaining"], 10);
   1.926 +      this._client.runListeners("onQuotaRemaining", this._client, this,
   1.927 +                               this.quotaRemaining);
   1.928 +    }
   1.929 +  },
   1.930 +};
   1.931 +
   1.932 +/**
   1.933 + * Represents a request to fetch from a collection.
   1.934 + *
   1.935 + * These requests are highly configurable so they are given their own type.
   1.936 + * This type inherits from StorageServiceRequest and provides additional
   1.937 + * controllable parameters.
   1.938 + *
   1.939 + * By default, requests are issued in "streaming" mode. As the client receives
   1.940 + * data from the server, it will invoke the caller-supplied onBSORecord
   1.941 + * callback for each record as it is ready. When all records have been received,
   1.942 + * it will invoke onComplete as normal. To change this behavior, modify the
   1.943 + * "streaming" property before the request is dispatched.
   1.944 + */
   1.945 +function StorageCollectionGetRequest() {
   1.946 +  StorageServiceRequest.call(this);
   1.947 +}
   1.948 +StorageCollectionGetRequest.prototype = {
   1.949 +  __proto__: StorageServiceRequest.prototype,
   1.950 +
   1.951 +  _namedArgs: {},
   1.952 +
   1.953 +  _streaming: true,
   1.954 +
   1.955 +  /**
   1.956 +   * Control whether streaming mode is in effect.
   1.957 +   *
   1.958 +   * Read the type documentation above for more details.
   1.959 +   */
   1.960 +  set streaming(value) {
   1.961 +    this._streaming = !!value;
   1.962 +  },
   1.963 +
   1.964 +  /**
   1.965 +   * Define the set of IDs to fetch from the server.
   1.966 +   */
   1.967 +  set ids(value) {
   1.968 +    this._namedArgs.ids = value.join(",");
   1.969 +  },
   1.970 +
   1.971 +  /**
   1.972 +   * Only retrieve BSOs that were modified strictly before this time.
   1.973 +   *
   1.974 +   * Defined in milliseconds since UNIX epoch.
   1.975 +   */
   1.976 +  set older(value) {
   1.977 +    this._namedArgs.older = value;
   1.978 +  },
   1.979 +
   1.980 +  /**
   1.981 +   * Only retrieve BSOs that were modified strictly after this time.
   1.982 +   *
   1.983 +   * Defined in milliseconds since UNIX epoch.
   1.984 +   */
   1.985 +  set newer(value) {
   1.986 +    this._namedArgs.newer = value;
   1.987 +  },
   1.988 +
   1.989 +  /**
   1.990 +   * If set to a truthy value, return full BSO information.
   1.991 +   *
   1.992 +   * If not set (the default), the request will only return the set of BSO
   1.993 +   * ids.
   1.994 +   */
   1.995 +  set full(value) {
   1.996 +    if (value) {
   1.997 +      this._namedArgs.full = "1";
   1.998 +    } else {
   1.999 +      delete this._namedArgs["full"];
  1.1000 +    }
  1.1001 +  },
  1.1002 +
  1.1003 +  /**
  1.1004 +   * Limit the max number of returned BSOs to this integer number.
  1.1005 +   */
  1.1006 +  set limit(value) {
  1.1007 +    this._namedArgs.limit = value;
  1.1008 +  },
  1.1009 +
  1.1010 +  /**
  1.1011 +   * If set with any value, sort the results based on modification time, oldest
  1.1012 +   * first.
  1.1013 +   */
  1.1014 +  set sortOldest(value) {
  1.1015 +    this._namedArgs.sort = "oldest";
  1.1016 +  },
  1.1017 +
  1.1018 +  /**
  1.1019 +   * If set with any value, sort the results based on modification time, newest
  1.1020 +   * first.
  1.1021 +   */
  1.1022 +  set sortNewest(value) {
  1.1023 +    this._namedArgs.sort = "newest";
  1.1024 +  },
  1.1025 +
  1.1026 +  /**
  1.1027 +   * If set with any value, sort the results based on sortindex value, highest
  1.1028 +   * first.
  1.1029 +   */
  1.1030 +  set sortIndex(value) {
  1.1031 +    this._namedArgs.sort = "index";
  1.1032 +  },
  1.1033 +
  1.1034 +  _onDispatch: function _onDispatch() {
  1.1035 +    let qs = this._getQueryString();
  1.1036 +    if (!qs.length) {
  1.1037 +      return;
  1.1038 +    }
  1.1039 +
  1.1040 +    this._request.uri = CommonUtils.makeURI(this._request.uri.asciiSpec + "?" +
  1.1041 +                                            qs);
  1.1042 +  },
  1.1043 +
  1.1044 +  _getQueryString: function _getQueryString() {
  1.1045 +    let args = [];
  1.1046 +    for (let [k, v] in Iterator(this._namedArgs)) {
  1.1047 +      args.push(encodeURIComponent(k) + "=" + encodeURIComponent(v));
  1.1048 +    }
  1.1049 +
  1.1050 +    return args.join("&");
  1.1051 +  },
  1.1052 +
  1.1053 +  _completeParser: function _completeParser(response) {
  1.1054 +    let obj = JSON.parse(response.body);
  1.1055 +    let items = obj.items;
  1.1056 +
  1.1057 +    if (!Array.isArray(items)) {
  1.1058 +      throw new Error("Unexpected JSON response. items is missing or not an " +
  1.1059 +                      "array!");
  1.1060 +    }
  1.1061 +
  1.1062 +    if (!this.handler.onBSORecord) {
  1.1063 +      return;
  1.1064 +    }
  1.1065 +
  1.1066 +    for (let bso of items) {
  1.1067 +      this.handler.onBSORecord(this, bso);
  1.1068 +    }
  1.1069 +  },
  1.1070 +};
  1.1071 +
  1.1072 +/**
  1.1073 + * Represents a request that sets data in a collection
  1.1074 + *
  1.1075 + * Instances of this type are returned by StorageServiceClient.setBSOs().
  1.1076 + */
  1.1077 +function StorageCollectionSetRequest() {
  1.1078 +  StorageServiceRequest.call(this);
  1.1079 +
  1.1080 +  this.size = 0;
  1.1081 +
  1.1082 +  // TODO Bug 775781 convert to Set and Map once iterable.
  1.1083 +  this.successfulIDs = [];
  1.1084 +  this.failures      = {};
  1.1085 +
  1.1086 +  this._lines = [];
  1.1087 +}
  1.1088 +StorageCollectionSetRequest.prototype = {
  1.1089 +  __proto__: StorageServiceRequest.prototype,
  1.1090 +
  1.1091 +  get count() {
  1.1092 +    return this._lines.length;
  1.1093 +  },
  1.1094 +
  1.1095 +  /**
  1.1096 +   * Add a BasicStorageObject to this request.
  1.1097 +   *
  1.1098 +   * Please note that the BSO content is retrieved when the BSO is added to
  1.1099 +   * the request. If the BSO changes after it is added to a request, those
  1.1100 +   * changes will not be reflected in the request.
  1.1101 +   *
  1.1102 +   * @param bso
  1.1103 +   *        (BasicStorageObject) BSO to add to the request.
  1.1104 +   */
  1.1105 +  addBSO: function addBSO(bso) {
  1.1106 +    if (!bso instanceof BasicStorageObject) {
  1.1107 +      throw new Error("argument must be a BasicStorageObject instance.");
  1.1108 +    }
  1.1109 +
  1.1110 +    if (!bso.id) {
  1.1111 +      throw new Error("Passed BSO must have id defined.");
  1.1112 +    }
  1.1113 +
  1.1114 +    this.addLine(JSON.stringify(bso));
  1.1115 +  },
  1.1116 +
  1.1117 +  /**
  1.1118 +   * Add a BSO (represented by its serialized newline-delimited form).
  1.1119 +   *
  1.1120 +   * You probably shouldn't use this. It is used for batching.
  1.1121 +   */
  1.1122 +  addLine: function addLine(line) {
  1.1123 +    // This is off by 1 in the larger direction. We don't care.
  1.1124 +    this.size += line.length + 1;
  1.1125 +    this._lines.push(line);
  1.1126 +  },
  1.1127 +
  1.1128 +  _onDispatch: function _onDispatch() {
  1.1129 +    this._data = this._lines.join("\n");
  1.1130 +    this.size = this._data.length;
  1.1131 +  },
  1.1132 +
  1.1133 +  _completeParser: function _completeParser(response) {
  1.1134 +    let result = JSON.parse(response.body);
  1.1135 +
  1.1136 +    for (let id of result.success) {
  1.1137 +      this.successfulIDs.push(id);
  1.1138 +    }
  1.1139 +
  1.1140 +    this.allSucceeded = true;
  1.1141 +
  1.1142 +    for (let [id, reasons] in Iterator(result.failed)) {
  1.1143 +      this.failures[id] = reasons;
  1.1144 +      this.allSucceeded = false;
  1.1145 +    }
  1.1146 +  },
  1.1147 +};
  1.1148 +
  1.1149 +/**
  1.1150 + * Represents a batch upload of BSOs to an individual collection.
  1.1151 + *
  1.1152 + * This is a more intelligent way to upload may BSOs to the server. It will
  1.1153 + * split the uploaded data into multiple requests so size limits, etc aren't
  1.1154 + * exceeded.
  1.1155 + *
  1.1156 + * Once a client obtains an instance of this type, it calls `addBSO` for each
  1.1157 + * BSO to be uploaded. When the client is done providing BSOs to be uploaded,
  1.1158 + * it calls `finish`. When `finish` is called, no more BSOs can be added to the
  1.1159 + * batch. When all requests created from this batch have finished, the callback
  1.1160 + * provided to `finish` will be invoked.
  1.1161 + *
  1.1162 + * Clients can also explicitly flush pending outgoing BSOs via `flush`. This
  1.1163 + * allows callers to control their own batching/chunking.
  1.1164 + *
  1.1165 + * Interally, this maintains a queue of StorageCollectionSetRequest to be
  1.1166 + * issued. At most one request is allowed to be in-flight at once. This is to
  1.1167 + * avoid potential conflicts on the server. And, in the case of conditional
  1.1168 + * requests, it prevents requests from being declined due to the server being
  1.1169 + * updated by another request issued by us.
  1.1170 + *
  1.1171 + * If a request errors for any reason, all queued uploads are abandoned and the
  1.1172 + * `finish` callback is invoked as soon as possible. The `successfulIDs` and
  1.1173 + * `failures` properties will contain data from all requests that had this
  1.1174 + * response data. In other words, the IDs have BSOs that were never sent to the
  1.1175 + * server are not lumped in to either property.
  1.1176 + *
  1.1177 + * Requests can be made conditional by setting `locallyModifiedVersion` to the
  1.1178 + * most recent version of server data. As responses from the server are seen,
  1.1179 + * the last server version is carried forward to subsequent requests.
  1.1180 + *
  1.1181 + * The server version from the last request is available in the
  1.1182 + * `serverModifiedVersion` property. It should only be accessed during or
  1.1183 + * after the callback passed to `finish`.
  1.1184 + *
  1.1185 + * @param client
  1.1186 + *        (StorageServiceClient) Client instance to use for uploading.
  1.1187 + *
  1.1188 + * @param collection
  1.1189 + *        (string) Collection the batch operation will upload to.
  1.1190 + */
  1.1191 +function StorageCollectionBatchedSet(client, collection) {
  1.1192 +  this.client     = client;
  1.1193 +  this.collection = collection;
  1.1194 +
  1.1195 +  this._log = client._log;
  1.1196 +
  1.1197 +  this.locallyModifiedVersion = null;
  1.1198 +  this.serverModifiedVersion  = null;
  1.1199 +
  1.1200 +  // TODO Bug 775781 convert to Set and Map once iterable.
  1.1201 +  this.successfulIDs = [];
  1.1202 +  this.failures      = {};
  1.1203 +
  1.1204 +  // Request currently being populated.
  1.1205 +  this._stagingRequest = client.setBSOs(this.collection);
  1.1206 +
  1.1207 +  // Requests ready to be sent over the wire.
  1.1208 +  this._outgoingRequests = [];
  1.1209 +
  1.1210 +  // Whether we are waiting for a response.
  1.1211 +  this._requestInFlight = false;
  1.1212 +
  1.1213 +  this._onFinishCallback = null;
  1.1214 +  this._finished         = false;
  1.1215 +  this._errorEncountered = false;
  1.1216 +}
  1.1217 +StorageCollectionBatchedSet.prototype = {
  1.1218 +  /**
  1.1219 +   * Add a BSO to be uploaded as part of this batch.
  1.1220 +   */
  1.1221 +  addBSO: function addBSO(bso) {
  1.1222 +    if (this._errorEncountered) {
  1.1223 +      return;
  1.1224 +    }
  1.1225 +
  1.1226 +    let line = JSON.stringify(bso);
  1.1227 +
  1.1228 +    if (line.length > this.client.REQUEST_SIZE_LIMIT) {
  1.1229 +      throw new Error("BSO is larger than allowed limit: " + line.length +
  1.1230 +                      " > " + this.client.REQUEST_SIZE_LIMIT);
  1.1231 +    }
  1.1232 +
  1.1233 +    if (this._stagingRequest.size + line.length > this.client.REQUEST_SIZE_LIMIT) {
  1.1234 +      this._log.debug("Sending request because payload size would be exceeded");
  1.1235 +      this._finishStagedRequest();
  1.1236 +
  1.1237 +      this._stagingRequest.addLine(line);
  1.1238 +      return;
  1.1239 +    }
  1.1240 +
  1.1241 +    // We are guaranteed to fit within size limits.
  1.1242 +    this._stagingRequest.addLine(line);
  1.1243 +
  1.1244 +    if (this._stagingRequest.count >= this.client.REQUEST_BSO_COUNT_LIMIT) {
  1.1245 +      this._log.debug("Sending request because BSO count threshold reached.");
  1.1246 +      this._finishStagedRequest();
  1.1247 +      return;
  1.1248 +    }
  1.1249 +  },
  1.1250 +
  1.1251 +  finish: function finish(cb) {
  1.1252 +    if (this._finished) {
  1.1253 +      throw new Error("Batch request has already been finished.");
  1.1254 +    }
  1.1255 +
  1.1256 +    this.flush();
  1.1257 +
  1.1258 +    this._onFinishCallback = cb;
  1.1259 +    this._finished = true;
  1.1260 +    this._stagingRequest = null;
  1.1261 +  },
  1.1262 +
  1.1263 +  flush: function flush() {
  1.1264 +    if (this._finished) {
  1.1265 +      throw new Error("Batch request has been finished.");
  1.1266 +    }
  1.1267 +
  1.1268 +    if (!this._stagingRequest.count) {
  1.1269 +      return;
  1.1270 +    }
  1.1271 +
  1.1272 +    this._finishStagedRequest();
  1.1273 +  },
  1.1274 +
  1.1275 +  _finishStagedRequest: function _finishStagedRequest() {
  1.1276 +    this._outgoingRequests.push(this._stagingRequest);
  1.1277 +    this._sendOutgoingRequest();
  1.1278 +    this._stagingRequest = this.client.setBSOs(this.collection);
  1.1279 +  },
  1.1280 +
  1.1281 +  _sendOutgoingRequest: function _sendOutgoingRequest() {
  1.1282 +    if (this._requestInFlight || this._errorEncountered) {
  1.1283 +      return;
  1.1284 +    }
  1.1285 +
  1.1286 +    if (!this._outgoingRequests.length) {
  1.1287 +      return;
  1.1288 +    }
  1.1289 +
  1.1290 +    let request = this._outgoingRequests.shift();
  1.1291 +
  1.1292 +    if (this.locallyModifiedVersion) {
  1.1293 +      request.locallyModifiedVersion = this.locallyModifiedVersion;
  1.1294 +    }
  1.1295 +
  1.1296 +    request.dispatch(this._onBatchComplete.bind(this));
  1.1297 +    this._requestInFlight = true;
  1.1298 +  },
  1.1299 +
  1.1300 +  _onBatchComplete: function _onBatchComplete(error, request) {
  1.1301 +    this._requestInFlight = false;
  1.1302 +
  1.1303 +    this.serverModifiedVersion = request.serverTime;
  1.1304 +
  1.1305 +    // Only update if we had a value before. Otherwise, this breaks
  1.1306 +    // unconditional requests!
  1.1307 +    if (this.locallyModifiedVersion) {
  1.1308 +      this.locallyModifiedVersion = request.serverTime;
  1.1309 +    }
  1.1310 +
  1.1311 +    for (let id of request.successfulIDs) {
  1.1312 +      this.successfulIDs.push(id);
  1.1313 +    }
  1.1314 +
  1.1315 +    for (let [id, reason] in Iterator(request.failures)) {
  1.1316 +      this.failures[id] = reason;
  1.1317 +    }
  1.1318 +
  1.1319 +    if (request.error) {
  1.1320 +      this._errorEncountered = true;
  1.1321 +    }
  1.1322 +
  1.1323 +    this._checkFinish();
  1.1324 +  },
  1.1325 +
  1.1326 +  _checkFinish: function _checkFinish() {
  1.1327 +    if (this._outgoingRequests.length && !this._errorEncountered) {
  1.1328 +      this._sendOutgoingRequest();
  1.1329 +      return;
  1.1330 +    }
  1.1331 +
  1.1332 +    if (!this._onFinishCallback) {
  1.1333 +      return;
  1.1334 +    }
  1.1335 +
  1.1336 +    try {
  1.1337 +      this._onFinishCallback(this);
  1.1338 +    } catch (ex) {
  1.1339 +      this._log.warn("Exception when calling finished callback: " +
  1.1340 +                     CommonUtils.exceptionStr(ex));
  1.1341 +    }
  1.1342 +  },
  1.1343 +};
  1.1344 +Object.freeze(StorageCollectionBatchedSet.prototype);
  1.1345 +
  1.1346 +/**
  1.1347 + * Manages a batch of BSO deletion requests.
  1.1348 + *
  1.1349 + * A single instance of this virtual request allows deletion of many individual
  1.1350 + * BSOs without having to worry about server limits.
  1.1351 + *
  1.1352 + * Instances are obtained by calling `deleteBSOsBatching` on
  1.1353 + * StorageServiceClient.
  1.1354 + *
  1.1355 + * Usage is roughly the same as StorageCollectionBatchedSet. Callers obtain
  1.1356 + * an instance and select individual BSOs for deletion by calling `addID`.
  1.1357 + * When the caller is finished marking BSOs for deletion, they call `finish`
  1.1358 + * with a callback which will be invoked when all deletion requests finish.
  1.1359 + *
  1.1360 + * When the finished callback is invoked, any encountered errors will be stored
  1.1361 + * in the `errors` property of this instance (which is passed to the callback).
  1.1362 + * This will be an empty array if no errors were encountered. Else, it will
  1.1363 + * contain the errors from the `onComplete` handler of request instances. The
  1.1364 + * set of succeeded and failed IDs is not currently available.
  1.1365 + *
  1.1366 + * Deletes can be made conditional by setting `locallyModifiedVersion`. The
  1.1367 + * behavior is the same as request types. The only difference is that the
  1.1368 + * updated version from the server as a result of requests is carried forward
  1.1369 + * to subsequent requests.
  1.1370 + *
  1.1371 + * The server version from the last request is stored in the
  1.1372 + * `serverModifiedVersion` property. It is not safe to access this until the
  1.1373 + * callback from `finish`.
  1.1374 + *
  1.1375 + * Like StorageCollectionBatchedSet, requests are issued serially to avoid
  1.1376 + * race conditions on the server.
  1.1377 + *
  1.1378 + * @param client
  1.1379 + *        (StorageServiceClient) Client request is associated with.
  1.1380 + * @param collection
  1.1381 + *        (string) Collection being operated on.
  1.1382 + */
  1.1383 +function StorageCollectionBatchedDelete(client, collection) {
  1.1384 +  this.client     = client;
  1.1385 +  this.collection = collection;
  1.1386 +
  1.1387 +  this._log = client._log;
  1.1388 +
  1.1389 +  this.locallyModifiedVersion = null;
  1.1390 +  this.serverModifiedVersion  = null;
  1.1391 +  this.errors                 = [];
  1.1392 +
  1.1393 +  this._pendingIDs          = [];
  1.1394 +  this._requestInFlight     = false;
  1.1395 +  this._finished            = false;
  1.1396 +  this._finishedCallback    = null;
  1.1397 +}
  1.1398 +StorageCollectionBatchedDelete.prototype = {
  1.1399 +  addID: function addID(id) {
  1.1400 +    if (this._finished) {
  1.1401 +      throw new Error("Cannot add IDs to a finished instance.");
  1.1402 +    }
  1.1403 +
  1.1404 +    // If we saw errors already, don't do any work. This is an optimization
  1.1405 +    // and isn't strictly required, as _sendRequest() should no-op.
  1.1406 +    if (this.errors.length) {
  1.1407 +      return;
  1.1408 +    }
  1.1409 +
  1.1410 +    this._pendingIDs.push(id);
  1.1411 +
  1.1412 +    if (this._pendingIDs.length >= this.client.REQUEST_BSO_DELETE_LIMIT) {
  1.1413 +      this._sendRequest();
  1.1414 +    }
  1.1415 +  },
  1.1416 +
  1.1417 +  /**
  1.1418 +   * Finish this batch operation.
  1.1419 +   *
  1.1420 +   * No more IDs can be added to this operation. Existing IDs are flushed as
  1.1421 +   * a request. The passed callback will be called when all requests have
  1.1422 +   * finished.
  1.1423 +   */
  1.1424 +  finish: function finish(cb) {
  1.1425 +    if (this._finished) {
  1.1426 +      throw new Error("Batch delete instance has already been finished.");
  1.1427 +    }
  1.1428 +
  1.1429 +    this._finished = true;
  1.1430 +    this._finishedCallback = cb;
  1.1431 +
  1.1432 +    if (this._pendingIDs.length) {
  1.1433 +      this._sendRequest();
  1.1434 +    }
  1.1435 +  },
  1.1436 +
  1.1437 +  _sendRequest: function _sendRequest() {
  1.1438 +    // Only allow 1 active request at a time and don't send additional
  1.1439 +    // requests if one has failed.
  1.1440 +    if (this._requestInFlight || this.errors.length) {
  1.1441 +      return;
  1.1442 +    }
  1.1443 +
  1.1444 +    let ids = this._pendingIDs.splice(0, this.client.REQUEST_BSO_DELETE_LIMIT);
  1.1445 +    let request = this.client.deleteBSOs(this.collection, ids);
  1.1446 +
  1.1447 +    if (this.locallyModifiedVersion) {
  1.1448 +      request.locallyModifiedVersion = this.locallyModifiedVersion;
  1.1449 +    }
  1.1450 +
  1.1451 +    request.dispatch(this._onRequestComplete.bind(this));
  1.1452 +    this._requestInFlight = true;
  1.1453 +  },
  1.1454 +
  1.1455 +  _onRequestComplete: function _onRequestComplete(error, request) {
  1.1456 +    this._requestInFlight = false;
  1.1457 +
  1.1458 +    if (error) {
  1.1459 +      // We don't currently track metadata of what failed. This is an obvious
  1.1460 +      // feature that could be added.
  1.1461 +      this._log.warn("Error received from server: " + error);
  1.1462 +      this.errors.push(error);
  1.1463 +    }
  1.1464 +
  1.1465 +    this.serverModifiedVersion = request.serverTime;
  1.1466 +
  1.1467 +    // If performing conditional requests, carry forward the new server version
  1.1468 +    // so subsequent conditional requests work.
  1.1469 +    if (this.locallyModifiedVersion) {
  1.1470 +      this.locallyModifiedVersion = request.serverTime;
  1.1471 +    }
  1.1472 +
  1.1473 +    if (this._pendingIDs.length && !this.errors.length) {
  1.1474 +      this._sendRequest();
  1.1475 +      return;
  1.1476 +    }
  1.1477 +
  1.1478 +    if (!this._finishedCallback) {
  1.1479 +      return;
  1.1480 +    }
  1.1481 +
  1.1482 +    try {
  1.1483 +      this._finishedCallback(this);
  1.1484 +    } catch (ex) {
  1.1485 +      this._log.warn("Exception when invoking finished callback: " +
  1.1486 +                     CommonUtils.exceptionStr(ex));
  1.1487 +    }
  1.1488 +  },
  1.1489 +};
  1.1490 +Object.freeze(StorageCollectionBatchedDelete.prototype);
  1.1491 +
  1.1492 +/**
  1.1493 + * Construct a new client for the SyncStorage API, version 2.0.
  1.1494 + *
  1.1495 + * Clients are constructed against a base URI. This URI is typically obtained
  1.1496 + * from the token server via the endpoint component of a successful token
  1.1497 + * response.
  1.1498 + *
  1.1499 + * The purpose of this type is to serve as a middleware between a client's core
  1.1500 + * logic and the HTTP API. It hides the details of how the storage API is
  1.1501 + * implemented but exposes important events, such as when auth goes bad or the
  1.1502 + * server requests the client to back off.
  1.1503 + *
  1.1504 + * All request APIs operate by returning a StorageServiceRequest instance. The
  1.1505 + * caller then installs the appropriate callbacks on each instance and then
  1.1506 + * dispatches the request.
  1.1507 + *
  1.1508 + * Each client instance also serves as a controller and coordinator for
  1.1509 + * associated requests. Callers can install listeners for common events on the
  1.1510 + * client and take the appropriate action whenever any associated request
  1.1511 + * observes them. For example, you will only need to register one listener for
  1.1512 + * backoff observation as opposed to one on each request.
  1.1513 + *
  1.1514 + * While not currently supported, a future goal of this type is to support
  1.1515 + * more advanced transport channels - such as SPDY - to allow for faster and
  1.1516 + * more efficient API calls. The API is thus designed to abstract transport
  1.1517 + * specifics away from the caller.
  1.1518 + *
  1.1519 + * Storage API consumers almost certainly have added functionality on top of the
  1.1520 + * storage service. It is encouraged to create a child type which adds
  1.1521 + * functionality to this layer.
  1.1522 + *
  1.1523 + * @param baseURI
  1.1524 + *        (string) Base URI for all requests.
  1.1525 + */
  1.1526 +this.StorageServiceClient = function StorageServiceClient(baseURI) {
  1.1527 +  this._log = Log.repository.getLogger("Services.Common.StorageServiceClient");
  1.1528 +  this._log.level = Log.Level[Prefs.get("log.level")];
  1.1529 +
  1.1530 +  this._baseURI = baseURI;
  1.1531 +
  1.1532 +  if (this._baseURI[this._baseURI.length-1] != "/") {
  1.1533 +    this._baseURI += "/";
  1.1534 +  }
  1.1535 +
  1.1536 +  this._log.info("Creating new StorageServiceClient under " + this._baseURI);
  1.1537 +
  1.1538 +  this._listeners = [];
  1.1539 +}
  1.1540 +StorageServiceClient.prototype = {
  1.1541 +  /**
  1.1542 +   * The user agent sent with every request.
  1.1543 +   *
  1.1544 +   * You probably want to change this.
  1.1545 +   */
  1.1546 +  userAgent: "StorageServiceClient",
  1.1547 +
  1.1548 +  /**
  1.1549 +   * Maximum size of entity bodies.
  1.1550 +   *
  1.1551 +   * TODO this should come from the server somehow. See bug 769759.
  1.1552 +   */
  1.1553 +  REQUEST_SIZE_LIMIT: 512000,
  1.1554 +
  1.1555 +  /**
  1.1556 +   * Maximum number of BSOs in requests.
  1.1557 +   *
  1.1558 +   * TODO this should come from the server somehow. See bug 769759.
  1.1559 +   */
  1.1560 +  REQUEST_BSO_COUNT_LIMIT: 100,
  1.1561 +
  1.1562 +  /**
  1.1563 +   * Maximum number of BSOs that can be deleted in a single DELETE.
  1.1564 +   *
  1.1565 +   * TODO this should come from the server. See bug 769759.
  1.1566 +   */
  1.1567 +  REQUEST_BSO_DELETE_LIMIT: 100,
  1.1568 +
  1.1569 +  _baseURI: null,
  1.1570 +  _log: null,
  1.1571 +
  1.1572 +  _listeners: null,
  1.1573 +
  1.1574 +  //----------------------------
  1.1575 +  // Event Listener Management |
  1.1576 +  //----------------------------
  1.1577 +
  1.1578 +  /**
  1.1579 +   * Adds a listener to this client instance.
  1.1580 +   *
  1.1581 +   * Listeners allow other parties to react to and influence execution of the
  1.1582 +   * client instance.
  1.1583 +   *
  1.1584 +   * An event listener is simply an object that exposes functions which get
  1.1585 +   * executed during client execution. Objects can expose 0 or more of the
  1.1586 +   * following keys:
  1.1587 +   *
  1.1588 +   *   onDispatch - Callback notified immediately before a request is
  1.1589 +   *     dispatched. This gets called for every outgoing request. The function
  1.1590 +   *     receives as its arguments the client instance and the outgoing
  1.1591 +   *     StorageServiceRequest. This listener is useful for global
  1.1592 +   *     authentication handlers, which can modify the request before it is
  1.1593 +   *     sent.
  1.1594 +   *
  1.1595 +   *   onAuthFailure - This is called when any request has experienced an
  1.1596 +   *     authentication failure.
  1.1597 +   *
  1.1598 +   *     This callback receives the following arguments:
  1.1599 +   *
  1.1600 +   *       (StorageServiceClient) Client that encountered the auth failure.
  1.1601 +   *       (StorageServiceRequest) Request that encountered the auth failure.
  1.1602 +   *
  1.1603 +   *   onBackoffReceived - This is called when a backoff request is issued by
  1.1604 +   *     the server. Backoffs are issued either when the service is completely
  1.1605 +   *     unavailable (and the client should abort all activity) or if the server
  1.1606 +   *     is under heavy load (and has completed the current request but is
  1.1607 +   *     asking clients to be kind and stop issuing requests for a while).
  1.1608 +   *
  1.1609 +   *     This callback receives the following arguments:
  1.1610 +   *
  1.1611 +   *       (StorageServiceClient) Client that encountered the backoff.
  1.1612 +   *       (StorageServiceRequest) Request that received the backoff.
  1.1613 +   *       (number) Integer milliseconds the server is requesting us to back off
  1.1614 +   *         for.
  1.1615 +   *       (bool) Whether the request completed successfully. If false, the
  1.1616 +   *         client should cease sending additional requests immediately, as
  1.1617 +   *         they will likely fail. If true, the client is allowed to continue
  1.1618 +   *         to put the server in a proper state. But, it should stop and heed
  1.1619 +   *         the backoff as soon as possible.
  1.1620 +   *
  1.1621 +   *   onNetworkError - This is called for every network error that is
  1.1622 +   *     encountered.
  1.1623 +   *
  1.1624 +   *     This callback receives the following arguments:
  1.1625 +   *
  1.1626 +   *       (StorageServiceClient) Client that encountered the network error.
  1.1627 +   *       (StorageServiceRequest) Request that encountered the error.
  1.1628 +   *       (Error) Error passed in to RESTRequest's onComplete handler. It has
  1.1629 +   *         a result property, which is a Components.Results enumeration.
  1.1630 +   *
  1.1631 +   *   onQuotaRemaining - This is called if any request sees updated quota
  1.1632 +   *     information from the server. This provides an update mechanism so
  1.1633 +   *     listeners can immediately find out quota changes as soon as they
  1.1634 +   *     are made.
  1.1635 +   *
  1.1636 +   *     This callback receives the following arguments:
  1.1637 +   *
  1.1638 +   *       (StorageServiceClient) Client that encountered the quota change.
  1.1639 +   *       (StorageServiceRequest) Request that received the quota change.
  1.1640 +   *       (number) Integer number of kilobytes remaining for the user.
  1.1641 +   */
  1.1642 +  addListener: function addListener(listener) {
  1.1643 +    if (!listener) {
  1.1644 +      throw new Error("listener argument must be an object.");
  1.1645 +    }
  1.1646 +
  1.1647 +    if (this._listeners.indexOf(listener) != -1) {
  1.1648 +      return;
  1.1649 +    }
  1.1650 +
  1.1651 +    this._listeners.push(listener);
  1.1652 +  },
  1.1653 +
  1.1654 +  /**
  1.1655 +   * Remove a previously-installed listener.
  1.1656 +   */
  1.1657 +  removeListener: function removeListener(listener) {
  1.1658 +    this._listeners = this._listeners.filter(function(a) {
  1.1659 +      return a != listener;
  1.1660 +    });
  1.1661 +  },
  1.1662 +
  1.1663 +  /**
  1.1664 +   * Invoke listeners for a specific event.
  1.1665 +   *
  1.1666 +   * @param name
  1.1667 +   *        (string) The name of the listener to invoke.
  1.1668 +   * @param args
  1.1669 +   *        (array) Arguments to pass to listener functions.
  1.1670 +   */
  1.1671 +  runListeners: function runListeners(name, ...args) {
  1.1672 +    for (let listener of this._listeners) {
  1.1673 +      try {
  1.1674 +        if (name in listener) {
  1.1675 +          listener[name].apply(listener, args);
  1.1676 +        }
  1.1677 +      } catch (ex) {
  1.1678 +        this._log.warn("Listener threw an exception during " + name + ": "
  1.1679 +                       + ex);
  1.1680 +      }
  1.1681 +    }
  1.1682 +  },
  1.1683 +
  1.1684 +  //-----------------------------
  1.1685 +  // Information/Metadata APIs  |
  1.1686 +  //-----------------------------
  1.1687 +
  1.1688 +  /**
  1.1689 +   * Obtain a request that fetches collection info.
  1.1690 +   *
  1.1691 +   * On successful response, the result is placed in the resultObj property
  1.1692 +   * of the request object.
  1.1693 +   *
  1.1694 +   * The result value is a map of strings to numbers. The string keys represent
  1.1695 +   * collection names. The number values are integer milliseconds since Unix
  1.1696 +   * epoch that hte collection was last modified.
  1.1697 +   *
  1.1698 +   * This request can be made conditional by defining `locallyModifiedVersion`
  1.1699 +   * on the returned object to the last known version on the client.
  1.1700 +   *
  1.1701 +   * Example Usage:
  1.1702 +   *
  1.1703 +   *   let request = client.getCollectionInfo();
  1.1704 +   *   request.dispatch(function onComplete(error, request) {
  1.1705 +   *     if (!error) {
  1.1706 +   *       return;
  1.1707 +   *     }
  1.1708 +   *
  1.1709 +   *     for (let [collection, milliseconds] in Iterator(this.resultObj)) {
  1.1710 +   *       // ...
  1.1711 +   *     }
  1.1712 +   *   });
  1.1713 +   */
  1.1714 +  getCollectionInfo: function getCollectionInfo() {
  1.1715 +    return this._getJSONGETRequest("info/collections");
  1.1716 +  },
  1.1717 +
  1.1718 +  /**
  1.1719 +   * Fetch quota information.
  1.1720 +   *
  1.1721 +   * The result in the callback upon success is a map containing quota
  1.1722 +   * metadata. It will have the following keys:
  1.1723 +   *
  1.1724 +   *   usage - Number of bytes currently utilized.
  1.1725 +   *   quota - Number of bytes available to account.
  1.1726 +   *
  1.1727 +   * The request can be made conditional by populating `locallyModifiedVersion`
  1.1728 +   * on the returned request instance with the most recently known version of
  1.1729 +   * server data.
  1.1730 +   */
  1.1731 +  getQuota: function getQuota() {
  1.1732 +    return this._getJSONGETRequest("info/quota");
  1.1733 +  },
  1.1734 +
  1.1735 +  /**
  1.1736 +   * Fetch information on how much data each collection uses.
  1.1737 +   *
  1.1738 +   * The result on success is a map of strings to numbers. The string keys
  1.1739 +   * are collection names. The values are numbers corresponding to the number
  1.1740 +   * of kilobytes used by that collection.
  1.1741 +   */
  1.1742 +  getCollectionUsage: function getCollectionUsage() {
  1.1743 +    return this._getJSONGETRequest("info/collection_usage");
  1.1744 +  },
  1.1745 +
  1.1746 +  /**
  1.1747 +   * Fetch the number of records in each collection.
  1.1748 +   *
  1.1749 +   * The result on success is a map of strings to numbers. The string keys are
  1.1750 +   * collection names. The values are numbers corresponding to the integer
  1.1751 +   * number of items in that collection.
  1.1752 +   */
  1.1753 +  getCollectionCounts: function getCollectionCounts() {
  1.1754 +    return this._getJSONGETRequest("info/collection_counts");
  1.1755 +  },
  1.1756 +
  1.1757 +  //--------------------------
  1.1758 +  // Collection Interaction  |
  1.1759 +  // -------------------------
  1.1760 +
  1.1761 +  /**
  1.1762 +   * Obtain a request to fetch collection information.
  1.1763 +   *
  1.1764 +   * The returned request instance is a StorageCollectionGetRequest instance.
  1.1765 +   * This is a sub-type of StorageServiceRequest and offers a number of setters
  1.1766 +   * to control how the request is performed. See the documentation for that
  1.1767 +   * type for more.
  1.1768 +   *
  1.1769 +   * The request can be made conditional by setting `locallyModifiedVersion`
  1.1770 +   * on the returned request instance.
  1.1771 +   *
  1.1772 +   * Example usage:
  1.1773 +   *
  1.1774 +   *   let request = client.getCollection("testcoll");
  1.1775 +   *
  1.1776 +   *   // Obtain full BSOs rather than just IDs.
  1.1777 +   *   request.full = true;
  1.1778 +   *
  1.1779 +   *   // Only obtain BSOs modified in the last minute.
  1.1780 +   *   request.newer = Date.now() - 60000;
  1.1781 +   *
  1.1782 +   *   // Install handler.
  1.1783 +   *   request.handler = {
  1.1784 +   *     onBSORecord: function onBSORecord(request, bso) {
  1.1785 +   *       let id = bso.id;
  1.1786 +   *       let payload = bso.payload;
  1.1787 +   *
  1.1788 +   *       // Do something with BSO.
  1.1789 +   *     },
  1.1790 +   *
  1.1791 +   *     onComplete: function onComplete(error, req) {
  1.1792 +   *       if (error) {
  1.1793 +   *         // Handle error.
  1.1794 +   *         return;
  1.1795 +   *       }
  1.1796 +   *
  1.1797 +   *       // Your onBSORecord handler has processed everything. Now is where
  1.1798 +   *       // you typically signal that everything has been processed and to move
  1.1799 +   *       // on.
  1.1800 +   *     }
  1.1801 +   *   };
  1.1802 +   *
  1.1803 +   *   request.dispatch();
  1.1804 +   *
  1.1805 +   * @param collection
  1.1806 +   *        (string) Name of collection to operate on.
  1.1807 +   */
  1.1808 +  getCollection: function getCollection(collection) {
  1.1809 +    if (!collection) {
  1.1810 +      throw new Error("collection argument must be defined.");
  1.1811 +    }
  1.1812 +
  1.1813 +    let uri = this._baseURI + "storage/" + collection;
  1.1814 +
  1.1815 +    let request = this._getRequest(uri, "GET", {
  1.1816 +      accept:          "application/json",
  1.1817 +      allowIfModified: true,
  1.1818 +      requestType:     StorageCollectionGetRequest
  1.1819 +    });
  1.1820 +
  1.1821 +    return request;
  1.1822 +  },
  1.1823 +
  1.1824 +  /**
  1.1825 +   * Fetch a single Basic Storage Object (BSO).
  1.1826 +   *
  1.1827 +   * On success, the BSO may be available in the resultObj property of the
  1.1828 +   * request as a BasicStorageObject instance.
  1.1829 +   *
  1.1830 +   * The request can be made conditional by setting `locallyModifiedVersion`
  1.1831 +   * on the returned request instance.*
  1.1832 +   *
  1.1833 +   * Example usage:
  1.1834 +   *
  1.1835 +   *   let request = client.getBSO("meta", "global");
  1.1836 +   *   request.dispatch(function onComplete(error, request) {
  1.1837 +   *     if (!error) {
  1.1838 +   *       return;
  1.1839 +   *     }
  1.1840 +   *
  1.1841 +   *     if (request.notModified) {
  1.1842 +   *       return;
  1.1843 +   *     }
  1.1844 +   *
  1.1845 +   *     let bso = request.bso;
  1.1846 +   *     let payload = bso.payload;
  1.1847 +   *
  1.1848 +   *     ...
  1.1849 +   *   };
  1.1850 +   *
  1.1851 +   * @param collection
  1.1852 +   *        (string) Collection to fetch from
  1.1853 +   * @param id
  1.1854 +   *        (string) ID of BSO to retrieve.
  1.1855 +   * @param type
  1.1856 +   *        (constructor) Constructor to call to create returned object. This
  1.1857 +   *        is optional and defaults to BasicStorageObject.
  1.1858 +   */
  1.1859 +  getBSO: function fetchBSO(collection, id, type=BasicStorageObject) {
  1.1860 +    if (!collection) {
  1.1861 +      throw new Error("collection argument must be defined.");
  1.1862 +    }
  1.1863 +
  1.1864 +    if (!id) {
  1.1865 +      throw new Error("id argument must be defined.");
  1.1866 +    }
  1.1867 +
  1.1868 +    let uri = this._baseURI + "storage/" + collection + "/" + id;
  1.1869 +
  1.1870 +    return this._getRequest(uri, "GET", {
  1.1871 +      accept: "application/json",
  1.1872 +      allowIfModified: true,
  1.1873 +      completeParser: function completeParser(response) {
  1.1874 +        let record = new type(id, collection);
  1.1875 +        record.deserialize(response.body);
  1.1876 +
  1.1877 +        return record;
  1.1878 +      },
  1.1879 +    });
  1.1880 +  },
  1.1881 +
  1.1882 +  /**
  1.1883 +   * Add or update a BSO in a collection.
  1.1884 +   *
  1.1885 +   * To make the request conditional (i.e. don't allow server changes if the
  1.1886 +   * server has a newer version), set request.locallyModifiedVersion to the
  1.1887 +   * last known version of the BSO. While this could be done automatically by
  1.1888 +   * this API, it is intentionally omitted because there are valid conditions
  1.1889 +   * where a client may wish to forcefully update the server.
  1.1890 +   *
  1.1891 +   * If a conditional request fails because the server has newer data, the
  1.1892 +   * StorageServiceRequestError passed to the callback will have the
  1.1893 +   * `serverModified` property set to true.
  1.1894 +   *
  1.1895 +   * Example usage:
  1.1896 +   *
  1.1897 +   *   let bso = new BasicStorageObject("foo", "coll");
  1.1898 +   *   bso.payload = "payload";
  1.1899 +   *   bso.modified = Date.now();
  1.1900 +   *
  1.1901 +   *   let request = client.setBSO(bso);
  1.1902 +   *   request.locallyModifiedVersion = bso.modified;
  1.1903 +   *
  1.1904 +   *   request.dispatch(function onComplete(error, req) {
  1.1905 +   *     if (error) {
  1.1906 +   *       if (error.serverModified) {
  1.1907 +   *         // Handle conditional set failure.
  1.1908 +   *         return;
  1.1909 +   *       }
  1.1910 +   *
  1.1911 +   *       // Handle other errors.
  1.1912 +   *       return;
  1.1913 +   *     }
  1.1914 +   *
  1.1915 +   *     // Record that set worked.
  1.1916 +   *   });
  1.1917 +   *
  1.1918 +   * @param bso
  1.1919 +   *        (BasicStorageObject) BSO to upload. The BSO instance must have the
  1.1920 +   *        `collection` and `id` properties defined.
  1.1921 +   */
  1.1922 +  setBSO: function setBSO(bso) {
  1.1923 +    if (!bso) {
  1.1924 +      throw new Error("bso argument must be defined.");
  1.1925 +    }
  1.1926 +
  1.1927 +    if (!bso.collection) {
  1.1928 +      throw new Error("BSO instance does not have collection defined.");
  1.1929 +    }
  1.1930 +
  1.1931 +    if (!bso.id) {
  1.1932 +      throw new Error("BSO instance does not have ID defined.");
  1.1933 +    }
  1.1934 +
  1.1935 +    let uri = this._baseURI + "storage/" + bso.collection + "/" + bso.id;
  1.1936 +    let request = this._getRequest(uri, "PUT", {
  1.1937 +      contentType:       "application/json",
  1.1938 +      allowIfUnmodified: true,
  1.1939 +      data:              JSON.stringify(bso),
  1.1940 +    });
  1.1941 +
  1.1942 +    return request;
  1.1943 +  },
  1.1944 +
  1.1945 +  /**
  1.1946 +   * Add or update multiple BSOs.
  1.1947 +   *
  1.1948 +   * This is roughly equivalent to calling setBSO multiple times except it is
  1.1949 +   * much more effecient because there is only 1 round trip to the server.
  1.1950 +   *
  1.1951 +   * The request can be made conditional by setting `locallyModifiedVersion`
  1.1952 +   * on the returned request instance.
  1.1953 +   *
  1.1954 +   * This function returns a StorageCollectionSetRequest instance. This type
  1.1955 +   * has additional functions and properties specific to this operation. See
  1.1956 +   * its documentation for more.
  1.1957 +   *
  1.1958 +   * Most consumers interested in submitting multiple BSOs to the server will
  1.1959 +   * want to use `setBSOsBatching` instead. That API intelligently splits up
  1.1960 +   * requests as necessary, etc.
  1.1961 +   *
  1.1962 +   * Example usage:
  1.1963 +   *
  1.1964 +   *   let request = client.setBSOs("collection0");
  1.1965 +   *   let bso0 = new BasicStorageObject("id0");
  1.1966 +   *   bso0.payload = "payload0";
  1.1967 +   *
  1.1968 +   *   let bso1 = new BasicStorageObject("id1");
  1.1969 +   *   bso1.payload = "payload1";
  1.1970 +   *
  1.1971 +   *   request.addBSO(bso0);
  1.1972 +   *   request.addBSO(bso1);
  1.1973 +   *
  1.1974 +   *   request.dispatch(function onComplete(error, req) {
  1.1975 +   *     if (error) {
  1.1976 +   *       // Handle error.
  1.1977 +   *       return;
  1.1978 +   *     }
  1.1979 +   *
  1.1980 +   *     let successful = req.successfulIDs;
  1.1981 +   *     let failed = req.failed;
  1.1982 +   *
  1.1983 +   *     // Do additional processing.
  1.1984 +   *   });
  1.1985 +   *
  1.1986 +   * @param collection
  1.1987 +   *        (string) Collection to operate on.
  1.1988 +   * @return
  1.1989 +   *        (StorageCollectionSetRequest) Request instance.
  1.1990 +   */
  1.1991 +  setBSOs: function setBSOs(collection) {
  1.1992 +    if (!collection) {
  1.1993 +      throw new Error("collection argument must be defined.");
  1.1994 +    }
  1.1995 +
  1.1996 +    let uri = this._baseURI + "storage/" + collection;
  1.1997 +    let request = this._getRequest(uri, "POST", {
  1.1998 +      requestType:       StorageCollectionSetRequest,
  1.1999 +      contentType:       "application/newlines",
  1.2000 +      accept:            "application/json",
  1.2001 +      allowIfUnmodified: true,
  1.2002 +    });
  1.2003 +
  1.2004 +    return request;
  1.2005 +  },
  1.2006 +
  1.2007 +  /**
  1.2008 +   * This is a batching variant of setBSOs.
  1.2009 +   *
  1.2010 +   * Whereas `setBSOs` is a 1:1 mapping between function calls and HTTP
  1.2011 +   * requests issued, this one is a 1:N mapping. It will intelligently break
  1.2012 +   * up outgoing BSOs into multiple requests so size limits, etc aren't
  1.2013 +   * exceeded.
  1.2014 +   *
  1.2015 +   * Please see the documentation for `StorageCollectionBatchedSet` for
  1.2016 +   * usage info.
  1.2017 +   *
  1.2018 +   * @param collection
  1.2019 +   *        (string) Collection to operate on.
  1.2020 +   * @return
  1.2021 +   *        (StorageCollectionBatchedSet) Batched set instance.
  1.2022 +   */
  1.2023 +  setBSOsBatching: function setBSOsBatching(collection) {
  1.2024 +    if (!collection) {
  1.2025 +      throw new Error("collection argument must be defined.");
  1.2026 +    }
  1.2027 +
  1.2028 +    return new StorageCollectionBatchedSet(this, collection);
  1.2029 +  },
  1.2030 +
  1.2031 +  /**
  1.2032 +   * Deletes a single BSO from a collection.
  1.2033 +   *
  1.2034 +   * The request can be made conditional by setting `locallyModifiedVersion`
  1.2035 +   * on the returned request instance.
  1.2036 +   *
  1.2037 +   * @param collection
  1.2038 +   *        (string) Collection to operate on.
  1.2039 +   * @param id
  1.2040 +   *        (string) ID of BSO to delete.
  1.2041 +   */
  1.2042 +  deleteBSO: function deleteBSO(collection, id) {
  1.2043 +    if (!collection) {
  1.2044 +      throw new Error("collection argument must be defined.");
  1.2045 +    }
  1.2046 +
  1.2047 +    if (!id) {
  1.2048 +      throw new Error("id argument must be defined.");
  1.2049 +    }
  1.2050 +
  1.2051 +    let uri = this._baseURI + "storage/" + collection + "/" + id;
  1.2052 +    return this._getRequest(uri, "DELETE", {
  1.2053 +      allowIfUnmodified: true,
  1.2054 +    });
  1.2055 +  },
  1.2056 +
  1.2057 +  /**
  1.2058 +   * Delete multiple BSOs from a specific collection.
  1.2059 +   *
  1.2060 +   * This is functional equivalent to calling deleteBSO() for every ID but
  1.2061 +   * much more efficient because it only results in 1 round trip to the server.
  1.2062 +   *
  1.2063 +   * The request can be made conditional by setting `locallyModifiedVersion`
  1.2064 +   * on the returned request instance.
  1.2065 +   *
  1.2066 +   * If the number of BSOs to delete is potentially large, it is preferred to
  1.2067 +   * use `deleteBSOsBatching`. That API automatically splits the operation into
  1.2068 +   * multiple requests so server limits aren't exceeded.
  1.2069 +   *
  1.2070 +   * @param collection
  1.2071 +   *        (string) Name of collection to delete BSOs from.
  1.2072 +   * @param ids
  1.2073 +   *        (iterable of strings) Set of BSO IDs to delete.
  1.2074 +   */
  1.2075 +  deleteBSOs: function deleteBSOs(collection, ids) {
  1.2076 +    // In theory we should URL encode. However, IDs are supposed to be URL
  1.2077 +    // safe. If we get garbage in, we'll get garbage out and the server will
  1.2078 +    // reject it.
  1.2079 +    let s = ids.join(",");
  1.2080 +
  1.2081 +    let uri = this._baseURI + "storage/" + collection + "?ids=" + s;
  1.2082 +
  1.2083 +    return this._getRequest(uri, "DELETE", {
  1.2084 +      allowIfUnmodified: true,
  1.2085 +    });
  1.2086 +  },
  1.2087 +
  1.2088 +  /**
  1.2089 +   * Bulk deletion of BSOs with no size limit.
  1.2090 +   *
  1.2091 +   * This allows a large amount of BSOs to be deleted easily. It will formulate
  1.2092 +   * multiple `deleteBSOs` queries so the client does not exceed server limits.
  1.2093 +   *
  1.2094 +   * @param collection
  1.2095 +   *        (string) Name of collection to delete BSOs from.
  1.2096 +   * @return StorageCollectionBatchedDelete
  1.2097 +   */
  1.2098 +  deleteBSOsBatching: function deleteBSOsBatching(collection) {
  1.2099 +    if (!collection) {
  1.2100 +      throw new Error("collection argument must be defined.");
  1.2101 +    }
  1.2102 +
  1.2103 +    return new StorageCollectionBatchedDelete(this, collection);
  1.2104 +  },
  1.2105 +
  1.2106 +  /**
  1.2107 +   * Deletes a single collection from the server.
  1.2108 +   *
  1.2109 +   * The request can be made conditional by setting `locallyModifiedVersion`
  1.2110 +   * on the returned request instance.
  1.2111 +   *
  1.2112 +   * @param collection
  1.2113 +   *        (string) Name of collection to delete.
  1.2114 +   */
  1.2115 +  deleteCollection: function deleteCollection(collection) {
  1.2116 +    let uri = this._baseURI + "storage/" + collection;
  1.2117 +
  1.2118 +    return this._getRequest(uri, "DELETE", {
  1.2119 +      allowIfUnmodified: true
  1.2120 +    });
  1.2121 +  },
  1.2122 +
  1.2123 +  /**
  1.2124 +   * Deletes all collections data from the server.
  1.2125 +   */
  1.2126 +  deleteCollections: function deleteCollections() {
  1.2127 +    let uri = this._baseURI + "storage";
  1.2128 +
  1.2129 +    return this._getRequest(uri, "DELETE", {});
  1.2130 +  },
  1.2131 +
  1.2132 +  /**
  1.2133 +   * Helper that wraps _getRequest for GET requests that return JSON.
  1.2134 +   */
  1.2135 +  _getJSONGETRequest: function _getJSONGETRequest(path) {
  1.2136 +    let uri = this._baseURI + path;
  1.2137 +
  1.2138 +    return this._getRequest(uri, "GET", {
  1.2139 +      accept:          "application/json",
  1.2140 +      allowIfModified: true,
  1.2141 +      completeParser:  this._jsonResponseParser,
  1.2142 +    });
  1.2143 +  },
  1.2144 +
  1.2145 +  /**
  1.2146 +   * Common logic for obtaining an HTTP request instance.
  1.2147 +   *
  1.2148 +   * @param uri
  1.2149 +   *        (string) URI to request.
  1.2150 +   * @param method
  1.2151 +   *        (string) HTTP method to issue.
  1.2152 +   * @param options
  1.2153 +   *        (object) Additional options to control request and response
  1.2154 +   *          handling. Keys influencing behavior are:
  1.2155 +   *
  1.2156 +   *          completeParser - Function that parses a HTTP response body into a
  1.2157 +   *            value. This function receives the RESTResponse object and
  1.2158 +   *            returns a value that is added to a StorageResponse instance.
  1.2159 +   *            If the response cannot be parsed or is invalid, this function
  1.2160 +   *            should throw an exception.
  1.2161 +   *
  1.2162 +   *          data - Data to be sent in HTTP request body.
  1.2163 +   *
  1.2164 +   *          accept - Value for Accept request header.
  1.2165 +   *
  1.2166 +   *          contentType - Value for Content-Type request header.
  1.2167 +   *
  1.2168 +   *          requestType - Function constructor for request type to initialize.
  1.2169 +   *            Defaults to StorageServiceRequest.
  1.2170 +   *
  1.2171 +   *          allowIfModified - Whether to populate X-If-Modified-Since if the
  1.2172 +   *            request contains a locallyModifiedVersion.
  1.2173 +   *
  1.2174 +   *          allowIfUnmodified - Whether to populate X-If-Unmodified-Since if
  1.2175 +   *            the request contains a locallyModifiedVersion.
  1.2176 +   */
  1.2177 +  _getRequest: function _getRequest(uri, method, options) {
  1.2178 +    if (!options.requestType) {
  1.2179 +      options.requestType = StorageServiceRequest;
  1.2180 +    }
  1.2181 +
  1.2182 +    let request = new RESTRequest(uri);
  1.2183 +
  1.2184 +    if (Prefs.get("sendVersionInfo", true)) {
  1.2185 +      let ua = this.userAgent + Prefs.get("client.type", "desktop");
  1.2186 +      request.setHeader("user-agent", ua);
  1.2187 +    }
  1.2188 +
  1.2189 +    if (options.accept) {
  1.2190 +      request.setHeader("accept", options.accept);
  1.2191 +    }
  1.2192 +
  1.2193 +    if (options.contentType) {
  1.2194 +      request.setHeader("content-type", options.contentType);
  1.2195 +    }
  1.2196 +
  1.2197 +    let result = new options.requestType();
  1.2198 +    result._request = request;
  1.2199 +    result._method = method;
  1.2200 +    result._client = this;
  1.2201 +    result._data = options.data;
  1.2202 +
  1.2203 +    if (options.completeParser) {
  1.2204 +      result._completeParser = options.completeParser;
  1.2205 +    }
  1.2206 +
  1.2207 +    result._allowIfModified = !!options.allowIfModified;
  1.2208 +    result._allowIfUnmodified = !!options.allowIfUnmodified;
  1.2209 +
  1.2210 +    return result;
  1.2211 +  },
  1.2212 +
  1.2213 +  _jsonResponseParser: function _jsonResponseParser(response) {
  1.2214 +    let ct = response.headers["content-type"];
  1.2215 +    if (!ct) {
  1.2216 +      throw new Error("No Content-Type response header! Misbehaving server!");
  1.2217 +    }
  1.2218 +
  1.2219 +    if (ct != "application/json" && ct.indexOf("application/json;") != 0) {
  1.2220 +      throw new Error("Non-JSON media type: " + ct);
  1.2221 +    }
  1.2222 +
  1.2223 +    return JSON.parse(response.body);
  1.2224 +  },
  1.2225 +};

mercurial