Wed, 31 Dec 2014 06:09:35 +0100
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 | }; |