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/. */
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.
28 this.EXPORTED_SYMBOLS
= [
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.");
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
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.
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) {
77 this.collection
= collection
;
79 BasicStorageObject
.prototype = {
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"]),
90 * Get the string payload as-is.
93 return this.data
.payload
;
97 * Set the string payload to a new 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);
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
121 set modified(value
) {
122 this.data
.modified
= value
;
126 if (this.data
.sortindex
) {
127 return this.data
.sortindex
|| 0;
133 set sortindex(value
) {
134 if (!value
&& value
!== 0) {
135 delete this.data
.sortindex
;
139 this.data
.sortindex
= value
;
143 return this.data
.ttl
;
147 if (!value
&& value
!== 0) {
148 delete this.data
.ttl
;
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.
164 * (string|object) Value to construct BSO from.
166 deserialize
: function deserialize(input
) {
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") {
178 throw new Error("Argument must be a JSON string or object: " +
182 for each (let key
in Object
.keys(data
)) {
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.
200 * The JSON representation of this BSO.
202 toJSON
: function toJSON() {
205 for (let [k
, v
] in Iterator(this.data
)) {
216 toString
: function toString() {
218 "id: " + this.id
+ " " +
219 "modified: " + this.modified
+ " " +
220 "ttl: " + this.ttl
+ " " +
221 "index: " + this.sortindex
+ " " +
222 "payload: " + this.payload
+
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
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
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
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;
276 this.authentication
= 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
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.
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
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) {
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.
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
358 * function onComplete(error, request) {
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) {
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
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
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
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
428 * let request = client.getCollectionInfo();
429 * request.locallyModifiedVersion = Date.now() - 60000;
430 * request.dispatch(function onComplete(error, request) {
436 * if (request.notModified) {
440 * let info = request.resultObj;
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.
450 * let request = client.setBSO(bso);
451 * request.locallyModifiedVersion = bso.modified;
452 * request.dispatch(function onComplete(error, request) {
454 * if (error.serverModified) {
455 * // Server data is newer! We should probably fetch it and apply
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;
479 this._request
= 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.
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
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
510 * This accessor may disappear in future versions.
513 return this._request
;
517 * The RESTResponse that resulted from the RESTRequest.
520 return this._request
.response
;
524 * HTTP status code from response.
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.
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
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
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!");
622 return this._handler
;
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
646 * (function) Callback to be invoked when request has completed.
648 dispatch
: function dispatch(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;
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
673 * The `onComplete` argument has the same behavior as for `dispatch()`.
676 * (function) Callback to be invoked when request has completed.
678 dispatchSynchronous
: function dispatchSynchronous(onComplete
) {
680 this.handler
= {onComplete
: onComplete
};
683 let cb
= Async
.makeSyncCallback();
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.
701 * StorageServiceRequestError encountered during dispatchy.
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
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
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
) {
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!");
773 this._handler
.onComplete(this._error
, this);
775 this._log
.warn("Exception when invoking handler's onComplete: " +
776 CommonUtils
.exceptionStr(ex
));
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
);
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
);
802 if (response
.status
== 201) {
807 if (response
.status
== 204) {
812 if (response
.status
== 304) {
813 this.notModified
= true;
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!");
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
,
835 if (response
.status
== 404) {
836 this._error
= new StorageServiceRequestError();
837 this._error
.notFound
= true;
842 if (response
.status
== 409) {
843 this._error
= new StorageServiceRequestError();
844 this._error
.conflict
= true;
849 if (response
.status
== 412) {
850 this._error
= new StorageServiceRequestError();
851 this._error
.serverModified
= true;
856 if (response
.status
== 413) {
857 this._error
= new StorageServiceRequestError();
858 this._error
.requestTooLarge
= true;
863 // If we see this, either the client or the server is buggy. We should
865 if (response
.status
== 415) {
866 this._log
.error("415 HTTP response seen from server! This should " +
868 this._error
= new StorageServiceRequestError();
869 this._error
.client
= new Error("415 Unsupported Media Type received!");
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.");
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!");
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,
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.
965 this._namedArgs
.ids
= value
.join(",");
969 * Only retrieve BSOs that were modified strictly before this time.
971 * Defined in milliseconds since UNIX epoch.
974 this._namedArgs
.older
= value
;
978 * Only retrieve BSOs that were modified strictly after this time.
980 * Defined in milliseconds since UNIX epoch.
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
994 this._namedArgs
.full
= "1";
996 delete this._namedArgs
["full"];
1001 * Limit the max number of returned BSOs to this integer number.
1004 this._namedArgs
.limit
= value
;
1008 * If set with any value, sort the results based on modification time, oldest
1011 set sortOldest(value
) {
1012 this._namedArgs
.sort
= "oldest";
1016 * If set with any value, sort the results based on modification time, newest
1019 set sortNewest(value
) {
1020 this._namedArgs
.sort
= "newest";
1024 * If set with any value, sort the results based on sortindex value, highest
1027 set sortIndex(value
) {
1028 this._namedArgs
.sort
= "index";
1031 _onDispatch
: function _onDispatch() {
1032 let qs
= this._getQueryString();
1037 this._request
.uri
= CommonUtils
.makeURI(this._request
.uri
.asciiSpec
+ "?" +
1041 _getQueryString
: function _getQueryString() {
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 " +
1059 if (!this.handler
.onBSORecord
) {
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);
1079 // TODO Bug 775781 convert to Set and Map once iterable.
1080 this.successfulIDs
= [];
1085 StorageCollectionSetRequest
.prototype = {
1086 __proto__
: StorageServiceRequest
.prototype,
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.
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.");
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
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`.
1183 * (StorageServiceClient) Client instance to use for uploading.
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
= [];
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
) {
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
);
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();
1248 finish
: function finish(cb
) {
1249 if (this._finished
) {
1250 throw new Error("Batch request has already been finished.");
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
) {
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
) {
1283 if (!this._outgoingRequests
.length
) {
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();
1329 if (!this._onFinishCallback
) {
1334 this._onFinishCallback(this);
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.
1376 * (StorageServiceClient) Client request is associated with.
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;
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
) {
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
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
) {
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;
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();
1475 if (!this._finishedCallback
) {
1480 this._finishedCallback(this);
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
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.
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,
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
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
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
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
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
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
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
) {
1641 throw new Error("listener argument must be an object.");
1644 if (this._listeners
.indexOf(listener
) != -1) {
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.
1664 * (string) The name of the listener to invoke.
1666 * (array) Arguments to pass to listener functions.
1668 runListeners
: function runListeners(name
, ...args
) {
1669 for (let listener
of this._listeners
) {
1671 if (name
in listener
) {
1672 listener
[name
].apply(listener
, args
);
1675 this._log
.warn("Listener threw an exception during " + name
+ ": "
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.
1700 * let request = client.getCollectionInfo();
1701 * request.dispatch(function onComplete(error, request) {
1706 * for (let [collection, milliseconds] in Iterator(this.resultObj)) {
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
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
1766 * The request can be made conditional by setting `locallyModifiedVersion`
1767 * on the returned request instance.
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) {
1783 * let payload = bso.payload;
1785 * // Do something with BSO.
1788 * onComplete: function onComplete(error, req) {
1794 * // Your onBSORecord handler has processed everything. Now is where
1795 * // you typically signal that everything has been processed and to move
1800 * request.dispatch();
1803 * (string) Name of collection to operate on.
1805 getCollection
: function getCollection(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
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.*
1832 * let request = client.getBSO("meta", "global");
1833 * request.dispatch(function onComplete(error, request) {
1838 * if (request.notModified) {
1842 * let bso = request.bso;
1843 * let payload = bso.payload;
1849 * (string) Collection to fetch from
1851 * (string) ID of BSO to retrieve.
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
) {
1858 throw new Error("collection argument must be defined.");
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
);
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.
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) {
1903 * if (error.serverModified) {
1904 * // Handle conditional set failure.
1908 * // Handle other errors.
1912 * // Record that set worked.
1916 * (BasicStorageObject) BSO to upload. The BSO instance must have the
1917 * `collection` and `id` properties defined.
1919 setBSO
: function setBSO(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.");
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
),
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.
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) {
1977 * let successful = req.successfulIDs;
1978 * let failed = req.failed;
1980 * // Do additional processing.
1984 * (string) Collection to operate on.
1986 * (StorageCollectionSetRequest) Request instance.
1988 setBSOs
: function setBSOs(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,
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
2012 * Please see the documentation for `StorageCollectionBatchedSet` for
2016 * (string) Collection to operate on.
2018 * (StorageCollectionBatchedSet) Batched set instance.
2020 setBSOsBatching
: function setBSOsBatching(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.
2035 * (string) Collection to operate on.
2037 * (string) ID of BSO to delete.
2039 deleteBSO
: function deleteBSO(collection
, id
) {
2041 throw new Error("collection argument must be defined.");
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.
2068 * (string) Name of collection to delete BSOs from.
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
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.
2092 * (string) Name of collection to delete BSOs from.
2093 * @return StorageCollectionBatchedDelete
2095 deleteBSOsBatching
: function deleteBSOsBatching(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.
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.
2146 * (string) URI to request.
2148 * (string) HTTP method to issue.
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
;
2210 _jsonResponseParser
: function _jsonResponseParser(response
) {
2211 let ct
= response
.headers
["content-type"];
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
);