services/common/storageservice.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial