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