Fix typo in 9b54bd30006c008b4a951331b273613d5bac3abf
[pm.git] / services / common / storageservice.js
blobb279f655006295d099f57cbf27600554b13cc11a
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/. */
5 /**
6 * This file contains APIs for interacting with the Storage Service API.
8 * The specification for the service is available at.
9 * http://docs.services.mozilla.com/storage/index.html
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.
15 * STORAGE SERVICE OVERVIEW
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.
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.
26 "use strict";
28 this.EXPORTED_SYMBOLS = [
29 "BasicStorageObject",
30 "StorageServiceClient",
31 "StorageServiceRequestError",
34 const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
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");
42 const Prefs = new Preferences("services.common.storageservice.");
44 /**
45 * The data type stored in the storage service.
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.
50 * BSOs belong to named collections.
52 * A single BSO consists of the following fields:
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.
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.
67 * This type is meant to be a dumb container and little more.
69 * @param id
70 * (string) ID of BSO. Can be null.
71 * (string) Collection BSO belongs to. Can be null;
73 this.BasicStorageObject =
74 function BasicStorageObject(id=null, collection=null) {
75 this.data = {};
76 this.id = id;
77 this.collection = collection;
79 BasicStorageObject.prototype = {
80 id: null,
81 collection: null,
82 data: null,
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"]),
89 /**
90 * Get the string payload as-is.
92 get payload() {
93 return this.data.payload;
96 /**
97 * Set the string payload to a new value.
99 set payload(value) {
100 this.data.payload = value;
104 * Get the modified time of the BSO in milliseconds since Unix epoch.
106 * You can convert this to a native JS Date instance easily:
108 * let date = new Date(bso.modified);
110 get modified() {
111 return this.data.modified;
115 * Sets the modified time of the BSO in milliseconds since Unix epoch.
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.
121 set modified(value) {
122 this.data.modified = value;
125 get sortindex() {
126 if (this.data.sortindex) {
127 return this.data.sortindex || 0;
130 return 0;
133 set sortindex(value) {
134 if (!value && value !== 0) {
135 delete this.data.sortindex;
136 return;
139 this.data.sortindex = value;
142 get ttl() {
143 return this.data.ttl;
146 set ttl(value) {
147 if (!value && value !== 0) {
148 delete this.data.ttl;
149 return;
152 this.data.ttl = value;
156 * Deserialize JSON or another object into this instance.
158 * The argument can be a string containing serialized JSON or an object.
160 * If the JSON is invalid or if the object contains unknown fields, an
161 * exception will be thrown.
163 * @param json
164 * (string|object) Value to construct BSO from.
166 deserialize: function deserialize(input) {
167 let data;
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.");
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));
182 for each (let key in Object.keys(data)) {
183 if (key == "id") {
184 this.id = data.id;
185 continue;
188 if (!this._validKeys.has(key)) {
189 throw new Error("Invalid key in object: " + key);
192 this.data[key] = data[key];
197 * Serialize the current BSO to JSON.
199 * @return string
200 * The JSON representation of this BSO.
202 toJSON: function toJSON() {
203 let obj = {};
205 for (let [k, v] in Iterator(this.data)) {
206 obj[k] = v;
209 if (this.id) {
210 obj.id = this.id;
213 return obj;
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 " }";
228 * Represents an error encountered during a StorageServiceRequest request.
230 * Instances of this will be passed to the onComplete callback for any request
231 * that did not succeed.
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.
237 * The following error "classes" are defined by properties on each instance:
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.
243 * notFound - True if the requested URI or resource does not exist.
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.
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.
253 * network - A network error prevented this request from succeeding. If set,
254 * it will be an Error thrown by the Goanna 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.
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.
263 * client - An error occurred which was the client's fault. This typically
264 * means the code in this file is buggy.
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.
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;
282 * Represents a single request to the storage service.
284 * Instances of this type are returned by the APIs on StorageServiceClient.
285 * They should not be created outside of StorageServiceClient.
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.
291 * A number of "public" properties are exposed to allow clients to further
292 * customize behavior. These are documented below.
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.
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.
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.
307 * Usage
308 * =====
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.
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.
322 * For basic requests, the general flow looks something like:
324 * // Obtain a new request instance.
325 * let request = client.getCollectionInfo();
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.
334 * };
336 * // Send the request.
337 * request.dispatch();
339 * Alternatively, we can install the onComplete handler when calling dispatch:
341 * let request = client.getCollectionInfo();
342 * request.dispatch(function onComplete(error, request) {
343 * // Handle response.
344 * });
346 * Please note that installing an `onComplete` handler as the argument to
347 * `dispatch()` will overwrite an existing `handler`.
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.
353 * Most of the complexity for onComplete handlers is error checking.
355 * The first thing you do in your onComplete handler is ensure no error was
356 * seen:
358 * function onComplete(error, request) {
359 * if (error) {
360 * // Handle error.
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.
371 * A robust error handler would look something like:
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.
398 * return;
401 * // Handle successful case.
404 * If `error` is null, the request completed successfully. There may or may not
405 * be additional data available on the request instance.
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.
412 * Conditional Requests
413 * --------------------
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.
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.
426 * For example:
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;
436 * if (request.notModified) {
437 * return;
440 * let info = request.resultObj;
441 * // Do stuff.
442 * });
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.
448 * For example:
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.
459 * return;
462 * // Handle success.
463 * });
465 * Future Features
466 * ---------------
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.
472 function StorageServiceRequest() {
473 this._log = Log.repository.getLogger("Sync.StorageService.Request");
474 this._log.level = Log.Level[Prefs.get("log.level")];
476 this.notModified = false;
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;
489 StorageServiceRequest.prototype = {
491 * The StorageServiceClient this request came from.
493 get client() {
494 return this._client;
498 * The underlying RESTRequest instance.
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.
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.
510 * This accessor may disappear in future versions.
512 get request() {
513 return this._request;
517 * The RESTResponse that resulted from the RESTRequest.
519 get response() {
520 return this._request.response;
524 * HTTP status code from response.
526 get statusCode() {
527 let response = this.response;
528 return response ? response.status : null;
532 * Holds any error that has occurred.
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.
538 get error() {
539 return this._error;
543 * The result from the request.
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.
549 get resultObj() {
550 return this._resultObj;
554 * Define the local version of the entity the client has.
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.
560 * This attribute is not honoured on every request. See the documentation
561 * in the client API to learn where it is valid.
563 set locallyModifiedVersion(value) {
564 // Will eventually become a header, so coerce to string.
565 this._locallyModifiedVersion = "" + value;
569 * Object which holds callbacks and state for this request.
571 * The handler is installed by users of this request. It is simply an object
572 * containing 0 or more of the following properties:
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:
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.
583 * Unless the call to dispatch() throws before returning, this callback
584 * is guaranteed to be invoked.
586 * Every client almost certainly wants to install this handler.
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.
592 * The called function receives the following arguments:
594 * (StorageServiceRequest) The request being issued (this request).
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:
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).
606 * Callers are free to (and encouraged) to store extra state in the supplied
607 * handler.
609 set handler(value) {
610 if (typeof(value) != "object") {
611 throw new Error("Invalid handler. Must be an Object.");
614 this._handler = value;
616 if (!value.onComplete) {
617 this._log.warn("Handler does not contain an onComplete callback!");
621 get handler() {
622 return this._handler;
625 //---------------
626 // General APIs |
627 //---------------
630 * Start the request.
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.
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:
639 * request.handler = {onComplete: value};
640 * request.dispatch();
642 * Please note that any existing handler will be replaced if onComplete is
643 * provided.
645 * @param onComplete
646 * (function) Callback to be invoked when request has completed.
648 dispatch: function dispatch(onComplete) {
649 if (onComplete) {
650 this.handler = {onComplete: onComplete};
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));
662 * This is a synchronous version of dispatch().
664 * THIS IS AN EVIL FUNCTION AND SHOULD NOT BE CALLED. It is provided for
665 * legacy reasons to support evil, synchronous clients.
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.
673 * The `onComplete` argument has the same behavior as for `dispatch()`.
675 * @param onComplete
676 * (function) Callback to be invoked when request has completed.
678 dispatchSynchronous: function dispatchSynchronous(onComplete) {
679 if (onComplete) {
680 this.handler = {onComplete: onComplete};
683 let cb = Async.makeSyncCallback();
684 this._dispatch(cb);
685 let error = Async.waitForSyncCallback(cb);
687 this._onComplete(error);
688 this.completed = true;
691 //-------------------------------------------------------------------------
692 // HIDDEN APIS. DO NOT CHANGE ANYTHING UNDER HERE FROM OUTSIDE THIS TYPE. |
693 //-------------------------------------------------------------------------
696 * Data to include in HTTP request body.
698 _data: null,
701 * StorageServiceRequestError encountered during dispatchy.
703 _error: null,
706 * Handler to parse response body into another object.
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.
712 _completeParser: null,
715 * Dispatch the request.
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.
721 _dispatch: function _dispatch(onComplete) {
722 // RESTRequest throws if the request has already been dispatched, so we
723 // need not bother checking.
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);
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();
744 if (this._handler.onDispatch) {
745 this._handler.onDispatch(this);
748 this._client.runListeners("onDispatch", this);
750 this._log.info("Dispatching request: " + this._method + " " +
751 this._request.uri.asciiSpec);
753 this._request.dispatch(this._method, this._data, onComplete);
757 * RESTRequest onComplete handler for all requests.
759 * This provides common logic for all response handling.
761 _onComplete: function(error) {
762 let onCompleteCalled = false;
764 let callOnComplete = function callOnComplete() {
765 onCompleteCalled = true;
767 if (!this._handler.onComplete) {
768 this._log.warn("No onComplete installed in handler!");
769 return;
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;
779 }.bind(this);
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;
791 let response = this._request.response;
792 this._log.info(response.status + " " + this._request.uri.asciiSpec);
794 this._processHeaders();
796 if (response.status == 200) {
797 this._resultObj = this._completeParser(response);
798 callOnComplete();
799 return;
802 if (response.status == 201) {
803 callOnComplete();
804 return;
807 if (response.status == 204) {
808 callOnComplete();
809 return;
812 if (response.status == 304) {
813 this.notModified = true;
814 callOnComplete();
815 return;
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;
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;
835 if (response.status == 404) {
836 this._error = new StorageServiceRequestError();
837 this._error.notFound = true;
838 callOnComplete();
839 return;
842 if (response.status == 409) {
843 this._error = new StorageServiceRequestError();
844 this._error.conflict = true;
845 callOnComplete();
846 return;
849 if (response.status == 412) {
850 this._error = new StorageServiceRequestError();
851 this._error.serverModified = true;
852 callOnComplete();
853 return;
856 if (response.status == 413) {
857 this._error = new StorageServiceRequestError();
858 this._error.requestTooLarge = true;
859 callOnComplete();
860 return;
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;
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;
882 callOnComplete();
884 } catch (ex) {
885 this._clientError = ex;
886 this._log.info("Exception when processing _onComplete: " + ex);
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);
900 _processHeaders: function _processHeaders() {
901 let headers = this._request.response.headers;
903 if (headers["x-timestamp"]) {
904 this.serverTime = parseFloat(headers["x-timestamp"]);
907 if (headers["x-backoff"]) {
908 this.backoffInterval = 1000 * parseInt(headers["x-backoff"], 10);
911 if (headers["retry-after"]) {
912 this.backoffInterval = 1000 * parseInt(headers["retry-after"], 10);
915 if (this.backoffInterval) {
916 let failure = this._request.response.status == 503;
917 this._client.runListeners("onBackoffReceived", this._client, this,
918 this.backoffInterval, !failure);
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);
930 * Represents a request to fetch from a collection.
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.
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.
942 function StorageCollectionGetRequest() {
943 StorageServiceRequest.call(this);
945 StorageCollectionGetRequest.prototype = {
946 __proto__: StorageServiceRequest.prototype,
948 _namedArgs: {},
950 _streaming: true,
953 * Control whether streaming mode is in effect.
955 * Read the type documentation above for more details.
957 set streaming(value) {
958 this._streaming = !!value;
962 * Define the set of IDs to fetch from the server.
964 set ids(value) {
965 this._namedArgs.ids = value.join(",");
969 * Only retrieve BSOs that were modified strictly before this time.
971 * Defined in milliseconds since UNIX epoch.
973 set older(value) {
974 this._namedArgs.older = value;
978 * Only retrieve BSOs that were modified strictly after this time.
980 * Defined in milliseconds since UNIX epoch.
982 set newer(value) {
983 this._namedArgs.newer = value;
987 * If set to a truthy value, return full BSO information.
989 * If not set (the default), the request will only return the set of BSO
990 * ids.
992 set full(value) {
993 if (value) {
994 this._namedArgs.full = "1";
995 } else {
996 delete this._namedArgs["full"];
1001 * Limit the max number of returned BSOs to this integer number.
1003 set limit(value) {
1004 this._namedArgs.limit = value;
1008 * If set with any value, sort the results based on modification time, oldest
1009 * first.
1011 set sortOldest(value) {
1012 this._namedArgs.sort = "oldest";
1016 * If set with any value, sort the results based on modification time, newest
1017 * first.
1019 set sortNewest(value) {
1020 this._namedArgs.sort = "newest";
1024 * If set with any value, sort the results based on sortindex value, highest
1025 * first.
1027 set sortIndex(value) {
1028 this._namedArgs.sort = "index";
1031 _onDispatch: function _onDispatch() {
1032 let qs = this._getQueryString();
1033 if (!qs.length) {
1034 return;
1037 this._request.uri = CommonUtils.makeURI(this._request.uri.asciiSpec + "?" +
1038 qs);
1041 _getQueryString: function _getQueryString() {
1042 let args = [];
1043 for (let [k, v] in Iterator(this._namedArgs)) {
1044 args.push(encodeURIComponent(k) + "=" + encodeURIComponent(v));
1047 return args.join("&");
1050 _completeParser: function _completeParser(response) {
1051 let obj = JSON.parse(response.body);
1052 let items = obj.items;
1054 if (!Array.isArray(items)) {
1055 throw new Error("Unexpected JSON response. items is missing or not an " +
1056 "array!");
1059 if (!this.handler.onBSORecord) {
1060 return;
1063 for (let bso of items) {
1064 this.handler.onBSORecord(this, bso);
1070 * Represents a request that sets data in a collection
1072 * Instances of this type are returned by StorageServiceClient.setBSOs().
1074 function StorageCollectionSetRequest() {
1075 StorageServiceRequest.call(this);
1077 this.size = 0;
1079 // TODO Bug 775781 convert to Set and Map once iterable.
1080 this.successfulIDs = [];
1081 this.failures = {};
1083 this._lines = [];
1085 StorageCollectionSetRequest.prototype = {
1086 __proto__: StorageServiceRequest.prototype,
1088 get count() {
1089 return this._lines.length;
1093 * Add a BasicStorageObject to this request.
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.
1099 * @param bso
1100 * (BasicStorageObject) BSO to add to the request.
1102 addBSO: function addBSO(bso) {
1103 if (!bso instanceof BasicStorageObject) {
1104 throw new Error("argument must be a BasicStorageObject instance.");
1107 if (!bso.id) {
1108 throw new Error("Passed BSO must have id defined.");
1111 this.addLine(JSON.stringify(bso));
1115 * Add a BSO (represented by its serialized newline-delimited form).
1117 * You probably shouldn't use this. It is used for batching.
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);
1125 _onDispatch: function _onDispatch() {
1126 this._data = this._lines.join("\n");
1127 this.size = this._data.length;
1130 _completeParser: function _completeParser(response) {
1131 let result = JSON.parse(response.body);
1133 for (let id of result.success) {
1134 this.successfulIDs.push(id);
1137 this.allSucceeded = true;
1139 for (let [id, reasons] in Iterator(result.failed)) {
1140 this.failures[id] = reasons;
1141 this.allSucceeded = false;
1147 * Represents a batch upload of BSOs to an individual collection.
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.
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.
1159 * Clients can also explicitly flush pending outgoing BSOs via `flush`. This
1160 * allows callers to control their own batching/chunking.
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.
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.
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.
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`.
1182 * @param client
1183 * (StorageServiceClient) Client instance to use for uploading.
1185 * @param collection
1186 * (string) Collection the batch operation will upload to.
1188 function StorageCollectionBatchedSet(client, collection) {
1189 this.client = client;
1190 this.collection = collection;
1192 this._log = client._log;
1194 this.locallyModifiedVersion = null;
1195 this.serverModifiedVersion = null;
1197 // TODO Bug 775781 convert to Set and Map once iterable.
1198 this.successfulIDs = [];
1199 this.failures = {};
1201 // Request currently being populated.
1202 this._stagingRequest = client.setBSOs(this.collection);
1204 // Requests ready to be sent over the wire.
1205 this._outgoingRequests = [];
1207 // Whether we are waiting for a response.
1208 this._requestInFlight = false;
1210 this._onFinishCallback = null;
1211 this._finished = false;
1212 this._errorEncountered = false;
1214 StorageCollectionBatchedSet.prototype = {
1216 * Add a BSO to be uploaded as part of this batch.
1218 addBSO: function addBSO(bso) {
1219 if (this._errorEncountered) {
1220 return;
1223 let line = JSON.stringify(bso);
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);
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();
1234 this._stagingRequest.addLine(line);
1235 return;
1238 // We are guaranteed to fit within size limits.
1239 this._stagingRequest.addLine(line);
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;
1248 finish: function finish(cb) {
1249 if (this._finished) {
1250 throw new Error("Batch request has already been finished.");
1253 this.flush();
1255 this._onFinishCallback = cb;
1256 this._finished = true;
1257 this._stagingRequest = null;
1260 flush: function flush() {
1261 if (this._finished) {
1262 throw new Error("Batch request has been finished.");
1265 if (!this._stagingRequest.count) {
1266 return;
1269 this._finishStagedRequest();
1272 _finishStagedRequest: function _finishStagedRequest() {
1273 this._outgoingRequests.push(this._stagingRequest);
1274 this._sendOutgoingRequest();
1275 this._stagingRequest = this.client.setBSOs(this.collection);
1278 _sendOutgoingRequest: function _sendOutgoingRequest() {
1279 if (this._requestInFlight || this._errorEncountered) {
1280 return;
1283 if (!this._outgoingRequests.length) {
1284 return;
1287 let request = this._outgoingRequests.shift();
1289 if (this.locallyModifiedVersion) {
1290 request.locallyModifiedVersion = this.locallyModifiedVersion;
1293 request.dispatch(this._onBatchComplete.bind(this));
1294 this._requestInFlight = true;
1297 _onBatchComplete: function _onBatchComplete(error, request) {
1298 this._requestInFlight = false;
1300 this.serverModifiedVersion = request.serverTime;
1302 // Only update if we had a value before. Otherwise, this breaks
1303 // unconditional requests!
1304 if (this.locallyModifiedVersion) {
1305 this.locallyModifiedVersion = request.serverTime;
1308 for (let id of request.successfulIDs) {
1309 this.successfulIDs.push(id);
1312 for (let [id, reason] in Iterator(request.failures)) {
1313 this.failures[id] = reason;
1316 if (request.error) {
1317 this._errorEncountered = true;
1320 this._checkFinish();
1323 _checkFinish: function _checkFinish() {
1324 if (this._outgoingRequests.length && !this._errorEncountered) {
1325 this._sendOutgoingRequest();
1326 return;
1329 if (!this._onFinishCallback) {
1330 return;
1333 try {
1334 this._onFinishCallback(this);
1335 } catch (ex) {
1336 this._log.warn("Exception when calling finished callback: " +
1337 CommonUtils.exceptionStr(ex));
1341 Object.freeze(StorageCollectionBatchedSet.prototype);
1344 * Manages a batch of BSO deletion requests.
1346 * A single instance of this virtual request allows deletion of many individual
1347 * BSOs without having to worry about server limits.
1349 * Instances are obtained by calling `deleteBSOsBatching` on
1350 * StorageServiceClient.
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.
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.
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.
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`.
1372 * Like StorageCollectionBatchedSet, requests are issued serially to avoid
1373 * race conditions on the server.
1375 * @param client
1376 * (StorageServiceClient) Client request is associated with.
1377 * @param collection
1378 * (string) Collection being operated on.
1380 function StorageCollectionBatchedDelete(client, collection) {
1381 this.client = client;
1382 this.collection = collection;
1384 this._log = client._log;
1386 this.locallyModifiedVersion = null;
1387 this.serverModifiedVersion = null;
1388 this.errors = [];
1390 this._pendingIDs = [];
1391 this._requestInFlight = false;
1392 this._finished = false;
1393 this._finishedCallback = null;
1395 StorageCollectionBatchedDelete.prototype = {
1396 addID: function addID(id) {
1397 if (this._finished) {
1398 throw new Error("Cannot add IDs to a finished instance.");
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;
1407 this._pendingIDs.push(id);
1409 if (this._pendingIDs.length >= this.client.REQUEST_BSO_DELETE_LIMIT) {
1410 this._sendRequest();
1415 * Finish this batch operation.
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.
1421 finish: function finish(cb) {
1422 if (this._finished) {
1423 throw new Error("Batch delete instance has already been finished.");
1426 this._finished = true;
1427 this._finishedCallback = cb;
1429 if (this._pendingIDs.length) {
1430 this._sendRequest();
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;
1441 let ids = this._pendingIDs.splice(0, this.client.REQUEST_BSO_DELETE_LIMIT);
1442 let request = this.client.deleteBSOs(this.collection, ids);
1444 if (this.locallyModifiedVersion) {
1445 request.locallyModifiedVersion = this.locallyModifiedVersion;
1448 request.dispatch(this._onRequestComplete.bind(this));
1449 this._requestInFlight = true;
1452 _onRequestComplete: function _onRequestComplete(error, request) {
1453 this._requestInFlight = false;
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);
1462 this.serverModifiedVersion = request.serverTime;
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;
1470 if (this._pendingIDs.length && !this.errors.length) {
1471 this._sendRequest();
1472 return;
1475 if (!this._finishedCallback) {
1476 return;
1479 try {
1480 this._finishedCallback(this);
1481 } catch (ex) {
1482 this._log.warn("Exception when invoking finished callback: " +
1483 CommonUtils.exceptionStr(ex));
1487 Object.freeze(StorageCollectionBatchedDelete.prototype);
1490 * Construct a new client for the SyncStorage API, version 2.0.
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.
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.
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.
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.
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.
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.
1520 * @param baseURI
1521 * (string) Base URI for all requests.
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")];
1527 this._baseURI = baseURI;
1529 if (this._baseURI[this._baseURI.length-1] != "/") {
1530 this._baseURI += "/";
1533 this._log.info("Creating new StorageServiceClient under " + this._baseURI);
1535 this._listeners = [];
1537 StorageServiceClient.prototype = {
1539 * The user agent sent with every request.
1541 * You probably want to change this.
1543 userAgent: "StorageServiceClient",
1546 * Maximum size of entity bodies.
1548 * TODO this should come from the server somehow. See bug 769759.
1550 REQUEST_SIZE_LIMIT: 512000,
1553 * Maximum number of BSOs in requests.
1555 * TODO this should come from the server somehow. See bug 769759.
1557 REQUEST_BSO_COUNT_LIMIT: 100,
1560 * Maximum number of BSOs that can be deleted in a single DELETE.
1562 * TODO this should come from the server. See bug 769759.
1564 REQUEST_BSO_DELETE_LIMIT: 100,
1566 _baseURI: null,
1567 _log: null,
1569 _listeners: null,
1571 //----------------------------
1572 // Event Listener Management |
1573 //----------------------------
1576 * Adds a listener to this client instance.
1578 * Listeners allow other parties to react to and influence execution of the
1579 * client instance.
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:
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.
1592 * onAuthFailure - This is called when any request has experienced an
1593 * authentication failure.
1595 * This callback receives the following arguments:
1597 * (StorageServiceClient) Client that encountered the auth failure.
1598 * (StorageServiceRequest) Request that encountered the auth failure.
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).
1606 * This callback receives the following arguments:
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.
1618 * onNetworkError - This is called for every network error that is
1619 * encountered.
1621 * This callback receives the following arguments:
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.
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.
1633 * This callback receives the following arguments:
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.
1639 addListener: function addListener(listener) {
1640 if (!listener) {
1641 throw new Error("listener argument must be an object.");
1644 if (this._listeners.indexOf(listener) != -1) {
1645 return;
1648 this._listeners.push(listener);
1652 * Remove a previously-installed listener.
1654 removeListener: function removeListener(listener) {
1655 this._listeners = this._listeners.filter(function(a) {
1656 return a != listener;
1661 * Invoke listeners for a specific event.
1663 * @param name
1664 * (string) The name of the listener to invoke.
1665 * @param args
1666 * (array) Arguments to pass to listener functions.
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);
1674 } catch (ex) {
1675 this._log.warn("Listener threw an exception during " + name + ": "
1676 + ex);
1681 //-----------------------------
1682 // Information/Metadata APIs |
1683 //-----------------------------
1686 * Obtain a request that fetches collection info.
1688 * On successful response, the result is placed in the resultObj property
1689 * of the request object.
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.
1695 * This request can be made conditional by defining `locallyModifiedVersion`
1696 * on the returned object to the last known version on the client.
1698 * Example Usage:
1700 * let request = client.getCollectionInfo();
1701 * request.dispatch(function onComplete(error, request) {
1702 * if (!error) {
1703 * return;
1706 * for (let [collection, milliseconds] in Iterator(this.resultObj)) {
1707 * // ...
1709 * });
1711 getCollectionInfo: function getCollectionInfo() {
1712 return this._getJSONGETRequest("info/collections");
1716 * Fetch quota information.
1718 * The result in the callback upon success is a map containing quota
1719 * metadata. It will have the following keys:
1721 * usage - Number of bytes currently utilized.
1722 * quota - Number of bytes available to account.
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.
1728 getQuota: function getQuota() {
1729 return this._getJSONGETRequest("info/quota");
1733 * Fetch information on how much data each collection uses.
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.
1739 getCollectionUsage: function getCollectionUsage() {
1740 return this._getJSONGETRequest("info/collection_usage");
1744 * Fetch the number of records in each collection.
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.
1750 getCollectionCounts: function getCollectionCounts() {
1751 return this._getJSONGETRequest("info/collection_counts");
1754 //--------------------------
1755 // Collection Interaction |
1756 // -------------------------
1759 * Obtain a request to fetch collection information.
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.
1766 * The request can be made conditional by setting `locallyModifiedVersion`
1767 * on the returned request instance.
1769 * Example usage:
1771 * let request = client.getCollection("testcoll");
1773 * // Obtain full BSOs rather than just IDs.
1774 * request.full = true;
1776 * // Only obtain BSOs modified in the last minute.
1777 * request.newer = Date.now() - 60000;
1779 * // Install handler.
1780 * request.handler = {
1781 * onBSORecord: function onBSORecord(request, bso) {
1782 * let id = bso.id;
1783 * let payload = bso.payload;
1785 * // Do something with BSO.
1786 * },
1788 * onComplete: function onComplete(error, req) {
1789 * if (error) {
1790 * // Handle error.
1791 * return;
1794 * // Your onBSORecord handler has processed everything. Now is where
1795 * // you typically signal that everything has been processed and to move
1796 * // on.
1798 * };
1800 * request.dispatch();
1802 * @param collection
1803 * (string) Name of collection to operate on.
1805 getCollection: function getCollection(collection) {
1806 if (!collection) {
1807 throw new Error("collection argument must be defined.");
1810 let uri = this._baseURI + "storage/" + collection;
1812 let request = this._getRequest(uri, "GET", {
1813 accept: "application/json",
1814 allowIfModified: true,
1815 requestType: StorageCollectionGetRequest
1818 return request;
1822 * Fetch a single Basic Storage Object (BSO).
1824 * On success, the BSO may be available in the resultObj property of the
1825 * request as a BasicStorageObject instance.
1827 * The request can be made conditional by setting `locallyModifiedVersion`
1828 * on the returned request instance.*
1830 * Example usage:
1832 * let request = client.getBSO("meta", "global");
1833 * request.dispatch(function onComplete(error, request) {
1834 * if (!error) {
1835 * return;
1838 * if (request.notModified) {
1839 * return;
1842 * let bso = request.bso;
1843 * let payload = bso.payload;
1845 * ...
1846 * };
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.
1856 getBSO: function fetchBSO(collection, id, type=BasicStorageObject) {
1857 if (!collection) {
1858 throw new Error("collection argument must be defined.");
1861 if (!id) {
1862 throw new Error("id argument must be defined.");
1865 let uri = this._baseURI + "storage/" + collection + "/" + id;
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);
1874 return record;
1880 * Add or update a BSO in a collection.
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.
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.
1892 * Example usage:
1894 * let bso = new BasicStorageObject("foo", "coll");
1895 * bso.payload = "payload";
1896 * bso.modified = Date.now();
1898 * let request = client.setBSO(bso);
1899 * request.locallyModifiedVersion = bso.modified;
1901 * request.dispatch(function onComplete(error, req) {
1902 * if (error) {
1903 * if (error.serverModified) {
1904 * // Handle conditional set failure.
1905 * return;
1908 * // Handle other errors.
1909 * return;
1912 * // Record that set worked.
1913 * });
1915 * @param bso
1916 * (BasicStorageObject) BSO to upload. The BSO instance must have the
1917 * `collection` and `id` properties defined.
1919 setBSO: function setBSO(bso) {
1920 if (!bso) {
1921 throw new Error("bso argument must be defined.");
1924 if (!bso.collection) {
1925 throw new Error("BSO instance does not have collection defined.");
1928 if (!bso.id) {
1929 throw new Error("BSO instance does not have ID defined.");
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),
1939 return request;
1943 * Add or update multiple BSOs.
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.
1948 * The request can be made conditional by setting `locallyModifiedVersion`
1949 * on the returned request instance.
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.
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.
1959 * Example usage:
1961 * let request = client.setBSOs("collection0");
1962 * let bso0 = new BasicStorageObject("id0");
1963 * bso0.payload = "payload0";
1965 * let bso1 = new BasicStorageObject("id1");
1966 * bso1.payload = "payload1";
1968 * request.addBSO(bso0);
1969 * request.addBSO(bso1);
1971 * request.dispatch(function onComplete(error, req) {
1972 * if (error) {
1973 * // Handle error.
1974 * return;
1977 * let successful = req.successfulIDs;
1978 * let failed = req.failed;
1980 * // Do additional processing.
1981 * });
1983 * @param collection
1984 * (string) Collection to operate on.
1985 * @return
1986 * (StorageCollectionSetRequest) Request instance.
1988 setBSOs: function setBSOs(collection) {
1989 if (!collection) {
1990 throw new Error("collection argument must be defined.");
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,
2001 return request;
2005 * This is a batching variant of setBSOs.
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.
2012 * Please see the documentation for `StorageCollectionBatchedSet` for
2013 * usage info.
2015 * @param collection
2016 * (string) Collection to operate on.
2017 * @return
2018 * (StorageCollectionBatchedSet) Batched set instance.
2020 setBSOsBatching: function setBSOsBatching(collection) {
2021 if (!collection) {
2022 throw new Error("collection argument must be defined.");
2025 return new StorageCollectionBatchedSet(this, collection);
2029 * Deletes a single BSO from a collection.
2031 * The request can be made conditional by setting `locallyModifiedVersion`
2032 * on the returned request instance.
2034 * @param collection
2035 * (string) Collection to operate on.
2036 * @param id
2037 * (string) ID of BSO to delete.
2039 deleteBSO: function deleteBSO(collection, id) {
2040 if (!collection) {
2041 throw new Error("collection argument must be defined.");
2044 if (!id) {
2045 throw new Error("id argument must be defined.");
2048 let uri = this._baseURI + "storage/" + collection + "/" + id;
2049 return this._getRequest(uri, "DELETE", {
2050 allowIfUnmodified: true,
2055 * Delete multiple BSOs from a specific collection.
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.
2060 * The request can be made conditional by setting `locallyModifiedVersion`
2061 * on the returned request instance.
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.
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.
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(",");
2078 let uri = this._baseURI + "storage/" + collection + "?ids=" + s;
2080 return this._getRequest(uri, "DELETE", {
2081 allowIfUnmodified: true,
2086 * Bulk deletion of BSOs with no size limit.
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.
2091 * @param collection
2092 * (string) Name of collection to delete BSOs from.
2093 * @return StorageCollectionBatchedDelete
2095 deleteBSOsBatching: function deleteBSOsBatching(collection) {
2096 if (!collection) {
2097 throw new Error("collection argument must be defined.");
2100 return new StorageCollectionBatchedDelete(this, collection);
2104 * Deletes a single collection from the server.
2106 * The request can be made conditional by setting `locallyModifiedVersion`
2107 * on the returned request instance.
2109 * @param collection
2110 * (string) Name of collection to delete.
2112 deleteCollection: function deleteCollection(collection) {
2113 let uri = this._baseURI + "storage/" + collection;
2115 return this._getRequest(uri, "DELETE", {
2116 allowIfUnmodified: true
2121 * Deletes all collections data from the server.
2123 deleteCollections: function deleteCollections() {
2124 let uri = this._baseURI + "storage";
2126 return this._getRequest(uri, "DELETE", {});
2130 * Helper that wraps _getRequest for GET requests that return JSON.
2132 _getJSONGETRequest: function _getJSONGETRequest(path) {
2133 let uri = this._baseURI + path;
2135 return this._getRequest(uri, "GET", {
2136 accept: "application/json",
2137 allowIfModified: true,
2138 completeParser: this._jsonResponseParser,
2143 * Common logic for obtaining an HTTP request instance.
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:
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.
2159 * data - Data to be sent in HTTP request body.
2161 * accept - Value for Accept request header.
2163 * contentType - Value for Content-Type request header.
2165 * requestType - Function constructor for request type to initialize.
2166 * Defaults to StorageServiceRequest.
2168 * allowIfModified - Whether to populate X-If-Modified-Since if the
2169 * request contains a locallyModifiedVersion.
2171 * allowIfUnmodified - Whether to populate X-If-Unmodified-Since if
2172 * the request contains a locallyModifiedVersion.
2174 _getRequest: function _getRequest(uri, method, options) {
2175 if (!options.requestType) {
2176 options.requestType = StorageServiceRequest;
2179 let request = new RESTRequest(uri);
2181 if (Prefs.get("sendVersionInfo", true)) {
2182 let ua = this.userAgent + Prefs.get("client.type", "desktop");
2183 request.setHeader("user-agent", ua);
2186 if (options.accept) {
2187 request.setHeader("accept", options.accept);
2190 if (options.contentType) {
2191 request.setHeader("content-type", options.contentType);
2194 let result = new options.requestType();
2195 result._request = request;
2196 result._method = method;
2197 result._client = this;
2198 result._data = options.data;
2200 if (options.completeParser) {
2201 result._completeParser = options.completeParser;
2204 result._allowIfModified = !!options.allowIfModified;
2205 result._allowIfUnmodified = !!options.allowIfUnmodified;
2207 return result;
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!");
2216 if (ct != "application/json" && ct.indexOf("application/json;") != 0) {
2217 throw new Error("Non-JSON media type: " + ct);
2220 return JSON.parse(response.body);