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