mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-01 14:45:29 +00:00
89d1cbef27
--HG-- rename : services/common/log4moz.js => toolkit/modules/Log.jsm rename : services/common/tests/unit/test_log4moz.js => toolkit/modules/tests/xpcshell/test_Log.js
2223 lines
68 KiB
JavaScript
2223 lines
68 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
/**
|
|
* This file contains APIs for interacting with the Storage Service API.
|
|
*
|
|
* The specification for the service is available at.
|
|
* http://docs.services.mozilla.com/storage/index.html
|
|
*
|
|
* Nothing about the spec or the service is Sync-specific. And, that is how
|
|
* these APIs are implemented. Instead, it is expected that consumers will
|
|
* create a new type inheriting or wrapping those provided by this file.
|
|
*
|
|
* STORAGE SERVICE OVERVIEW
|
|
*
|
|
* The storage service is effectively a key-value store where each value is a
|
|
* well-defined envelope that stores specific metadata along with a payload.
|
|
* These values are called Basic Storage Objects, or BSOs. BSOs are organized
|
|
* into named groups called collections.
|
|
*
|
|
* The service also provides ancillary APIs not related to storage, such as
|
|
* looking up the set of stored collections, current quota usage, etc.
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
this.EXPORTED_SYMBOLS = [
|
|
"BasicStorageObject",
|
|
"StorageServiceClient",
|
|
"StorageServiceRequestError",
|
|
];
|
|
|
|
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
|
|
|
|
Cu.import("resource://gre/modules/Preferences.jsm");
|
|
Cu.import("resource://services-common/async.js");
|
|
Cu.import("resource://gre/modules/Log.jsm");
|
|
Cu.import("resource://services-common/rest.js");
|
|
Cu.import("resource://services-common/utils.js");
|
|
|
|
const Prefs = new Preferences("services.common.storageservice.");
|
|
|
|
/**
|
|
* The data type stored in the storage service.
|
|
*
|
|
* A Basic Storage Object (BSO) is the primitive type stored in the storage
|
|
* service. BSO's are simply maps with a well-defined set of keys.
|
|
*
|
|
* BSOs belong to named collections.
|
|
*
|
|
* A single BSO consists of the following fields:
|
|
*
|
|
* id - An identifying string. This is how a BSO is uniquely identified within
|
|
* a single collection.
|
|
* modified - Integer milliseconds since Unix epoch BSO was modified.
|
|
* payload - String contents of BSO. The format of the string is undefined
|
|
* (although JSON is typically used).
|
|
* ttl - The number of seconds to keep this record.
|
|
* sortindex - Integer indicating relative importance of record within the
|
|
* collection.
|
|
*
|
|
* The constructor simply creates an empty BSO having the specified ID (which
|
|
* can be null or undefined). It also takes an optional collection. This is
|
|
* purely for convenience.
|
|
*
|
|
* This type is meant to be a dumb container and little more.
|
|
*
|
|
* @param id
|
|
* (string) ID of BSO. Can be null.
|
|
* (string) Collection BSO belongs to. Can be null;
|
|
*/
|
|
this.BasicStorageObject =
|
|
function BasicStorageObject(id=null, collection=null) {
|
|
this.data = {};
|
|
this.id = id;
|
|
this.collection = collection;
|
|
}
|
|
BasicStorageObject.prototype = {
|
|
id: null,
|
|
collection: null,
|
|
data: null,
|
|
|
|
// At the time this was written, the convention for constructor arguments
|
|
// was not adopted by Harmony. It could break in the future. We have test
|
|
// coverage that will break if SpiderMonkey changes, just in case.
|
|
_validKeys: new Set(["id", "payload", "modified", "sortindex", "ttl"]),
|
|
|
|
/**
|
|
* Get the string payload as-is.
|
|
*/
|
|
get payload() {
|
|
return this.data.payload;
|
|
},
|
|
|
|
/**
|
|
* Set the string payload to a new value.
|
|
*/
|
|
set payload(value) {
|
|
this.data.payload = value;
|
|
},
|
|
|
|
/**
|
|
* Get the modified time of the BSO in milliseconds since Unix epoch.
|
|
*
|
|
* You can convert this to a native JS Date instance easily:
|
|
*
|
|
* let date = new Date(bso.modified);
|
|
*/
|
|
get modified() {
|
|
return this.data.modified;
|
|
},
|
|
|
|
/**
|
|
* Sets the modified time of the BSO in milliseconds since Unix epoch.
|
|
*
|
|
* Please note that if this value is sent to the server it will be ignored.
|
|
* The server will use its time at the time of the operation when storing the
|
|
* BSO.
|
|
*/
|
|
set modified(value) {
|
|
this.data.modified = value;
|
|
},
|
|
|
|
get sortindex() {
|
|
if (this.data.sortindex) {
|
|
return this.data.sortindex || 0;
|
|
}
|
|
|
|
return 0;
|
|
},
|
|
|
|
set sortindex(value) {
|
|
if (!value && value !== 0) {
|
|
delete this.data.sortindex;
|
|
return;
|
|
}
|
|
|
|
this.data.sortindex = value;
|
|
},
|
|
|
|
get ttl() {
|
|
return this.data.ttl;
|
|
},
|
|
|
|
set ttl(value) {
|
|
if (!value && value !== 0) {
|
|
delete this.data.ttl;
|
|
return;
|
|
}
|
|
|
|
this.data.ttl = value;
|
|
},
|
|
|
|
/**
|
|
* Deserialize JSON or another object into this instance.
|
|
*
|
|
* The argument can be a string containing serialized JSON or an object.
|
|
*
|
|
* If the JSON is invalid or if the object contains unknown fields, an
|
|
* exception will be thrown.
|
|
*
|
|
* @param json
|
|
* (string|object) Value to construct BSO from.
|
|
*/
|
|
deserialize: function deserialize(input) {
|
|
let data;
|
|
|
|
if (typeof(input) == "string") {
|
|
data = JSON.parse(input);
|
|
if (typeof(data) != "object") {
|
|
throw new Error("Supplied JSON is valid but is not a JS-Object.");
|
|
}
|
|
}
|
|
else if (typeof(input) == "object") {
|
|
data = input;
|
|
} else {
|
|
throw new Error("Argument must be a JSON string or object: " +
|
|
typeof(input));
|
|
}
|
|
|
|
for each (let key in Object.keys(data)) {
|
|
if (key == "id") {
|
|
this.id = data.id;
|
|
continue;
|
|
}
|
|
|
|
if (!this._validKeys.has(key)) {
|
|
throw new Error("Invalid key in object: " + key);
|
|
}
|
|
|
|
this.data[key] = data[key];
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Serialize the current BSO to JSON.
|
|
*
|
|
* @return string
|
|
* The JSON representation of this BSO.
|
|
*/
|
|
toJSON: function toJSON() {
|
|
let obj = {};
|
|
|
|
for (let [k, v] in Iterator(this.data)) {
|
|
obj[k] = v;
|
|
}
|
|
|
|
if (this.id) {
|
|
obj.id = this.id;
|
|
}
|
|
|
|
return obj;
|
|
},
|
|
|
|
toString: function toString() {
|
|
return "{ " +
|
|
"id: " + this.id + " " +
|
|
"modified: " + this.modified + " " +
|
|
"ttl: " + this.ttl + " " +
|
|
"index: " + this.sortindex + " " +
|
|
"payload: " + this.payload +
|
|
" }";
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Represents an error encountered during a StorageServiceRequest request.
|
|
*
|
|
* Instances of this will be passed to the onComplete callback for any request
|
|
* that did not succeed.
|
|
*
|
|
* This type effectively wraps other error conditions. It is up to the client
|
|
* to determine the appropriate course of action for each error type
|
|
* encountered.
|
|
*
|
|
* The following error "classes" are defined by properties on each instance:
|
|
*
|
|
* serverModified - True if the request to modify data was conditional and
|
|
* the server rejected the request because it has newer data than the
|
|
* client.
|
|
*
|
|
* notFound - True if the requested URI or resource does not exist.
|
|
*
|
|
* conflict - True if the server reported that a resource being operated on
|
|
* was in conflict. If this occurs, the client should typically wait a
|
|
* little and try the request again.
|
|
*
|
|
* requestTooLarge - True if the request was too large for the server. If
|
|
* this happens on batch requests, the client should retry the request with
|
|
* smaller batches.
|
|
*
|
|
* network - A network error prevented this request from succeeding. If set,
|
|
* it will be an Error thrown by the Gecko network stack. If set, it could
|
|
* mean that the request could not be performed or that an error occurred
|
|
* when the request was in flight. It is also possible the request
|
|
* succeeded on the server but the response was lost in transit.
|
|
*
|
|
* authentication - If defined, an authentication error has occurred. If
|
|
* defined, it will be an Error instance. If seen, the client should not
|
|
* retry the request without first correcting the authentication issue.
|
|
*
|
|
* client - An error occurred which was the client's fault. This typically
|
|
* means the code in this file is buggy.
|
|
*
|
|
* server - An error occurred on the server. In the ideal world, this should
|
|
* never happen. But, it does. If set, this will be an Error which
|
|
* describes the error as reported by the server.
|
|
*/
|
|
this.StorageServiceRequestError = function StorageServiceRequestError() {
|
|
this.serverModified = false;
|
|
this.notFound = false;
|
|
this.conflict = false;
|
|
this.requestToolarge = false;
|
|
this.network = null;
|
|
this.authentication = null;
|
|
this.client = null;
|
|
this.server = null;
|
|
}
|
|
|
|
/**
|
|
* Represents a single request to the storage service.
|
|
*
|
|
* Instances of this type are returned by the APIs on StorageServiceClient.
|
|
* They should not be created outside of StorageServiceClient.
|
|
*
|
|
* This type encapsulates common storage API request and response handling.
|
|
* Metadata required to perform the request is stored inside each instance and
|
|
* should be treated as invisible by consumers.
|
|
*
|
|
* A number of "public" properties are exposed to allow clients to further
|
|
* customize behavior. These are documented below.
|
|
*
|
|
* Some APIs in StorageServiceClient define their own types which inherit from
|
|
* this one. Read the API documentation to see which types those are and when
|
|
* they apply.
|
|
*
|
|
* This type wraps RESTRequest rather than extending it. The reason is mainly
|
|
* to avoid the fragile base class problem. We implement considerable extra
|
|
* functionality on top of RESTRequest and don't want this to accidentally
|
|
* trample on RESTRequest's members.
|
|
*
|
|
* If this were a C++ class, it and StorageServiceClient would be friend
|
|
* classes. Each touches "protected" APIs of the other. Thus, each should be
|
|
* considered when making changes to the other.
|
|
*
|
|
* Usage
|
|
* =====
|
|
*
|
|
* When you obtain a request instance, it is waiting to be dispatched. It may
|
|
* have additional settings available for tuning. See the documentation in
|
|
* StorageServiceClient for more.
|
|
*
|
|
* There are essentially two types of requests: "basic" and "streaming."
|
|
* "Basic" requests encapsulate the traditional request-response paradigm:
|
|
* a request is issued and we get a response later once the full response
|
|
* is available. Most of the APIs in StorageServiceClient issue these "basic"
|
|
* requests. Streaming requests typically involve the transport of multiple
|
|
* BasicStorageObject instances. When a new BSO instance is available, a
|
|
* callback is fired.
|
|
*
|
|
* For basic requests, the general flow looks something like:
|
|
*
|
|
* // Obtain a new request instance.
|
|
* let request = client.getCollectionInfo();
|
|
*
|
|
* // Install a handler which provides callbacks for request events. The most
|
|
* // important is `onComplete`, which is called when the request has
|
|
* // finished and the response is completely received.
|
|
* request.handler = {
|
|
* onComplete: function onComplete(error, request) {
|
|
* // Do something.
|
|
* }
|
|
* };
|
|
*
|
|
* // Send the request.
|
|
* request.dispatch();
|
|
*
|
|
* Alternatively, we can install the onComplete handler when calling dispatch:
|
|
*
|
|
* let request = client.getCollectionInfo();
|
|
* request.dispatch(function onComplete(error, request) {
|
|
* // Handle response.
|
|
* });
|
|
*
|
|
* Please note that installing an `onComplete` handler as the argument to
|
|
* `dispatch()` will overwrite an existing `handler`.
|
|
*
|
|
* In both of the above example, the two `request` variables are identical. The
|
|
* original `StorageServiceRequest` is passed into the callback so callers
|
|
* don't need to rely on closures.
|
|
*
|
|
* Most of the complexity for onComplete handlers is error checking.
|
|
*
|
|
* The first thing you do in your onComplete handler is ensure no error was
|
|
* seen:
|
|
*
|
|
* function onComplete(error, request) {
|
|
* if (error) {
|
|
* // Handle error.
|
|
* }
|
|
* }
|
|
*
|
|
* If `error` is defined, it will be an instance of
|
|
* `StorageServiceRequestError`. An error will be set if the request didn't
|
|
* complete successfully. This means the transport layer must have succeeded
|
|
* and the application protocol (HTTP) must have returned a successful status
|
|
* code (2xx and some 3xx). Please see the documentation for
|
|
* `StorageServiceRequestError` for more.
|
|
*
|
|
* A robust error handler would look something like:
|
|
*
|
|
* function onComplete(error, request) {
|
|
* if (error) {
|
|
* if (error.network) {
|
|
* // Network error encountered!
|
|
* } else if (error.server) {
|
|
* // Something went wrong on the server (HTTP 5xx).
|
|
* } else if (error.authentication) {
|
|
* // Server rejected request due to bad credentials.
|
|
* } else if (error.serverModified) {
|
|
* // The conditional request was rejected because the server has newer
|
|
* // data than what the client reported.
|
|
* } else if (error.conflict) {
|
|
* // The server reported that the operation could not be completed
|
|
* // because another client is also updating it.
|
|
* } else if (error.requestTooLarge) {
|
|
* // The server rejected the request because it was too large.
|
|
* } else if (error.notFound) {
|
|
* // The requested resource was not found.
|
|
* } else if (error.client) {
|
|
* // Something is wrong with the client's request. You should *never*
|
|
* // see this, as it means this client is likely buggy. It could also
|
|
* // mean the server is buggy or misconfigured. Either way, something
|
|
* // is buggy.
|
|
* }
|
|
*
|
|
* return;
|
|
* }
|
|
*
|
|
* // Handle successful case.
|
|
* }
|
|
*
|
|
* If `error` is null, the request completed successfully. There may or may not
|
|
* be additional data available on the request instance.
|
|
*
|
|
* For requests that obtain data, this data is typically made available through
|
|
* the `resultObj` property on the request instance. The API that was called
|
|
* will install its own response hander and ensure this property is decoded to
|
|
* what you expect.
|
|
*
|
|
* Conditional Requests
|
|
* --------------------
|
|
*
|
|
* Many of the APIs on `StorageServiceClient` support conditional requests.
|
|
* That is, the client defines the last version of data it has (the version
|
|
* comes from a previous response from the server) and sends this as part of
|
|
* the request.
|
|
*
|
|
* For query requests, if the server hasn't changed, no new data will be
|
|
* returned. If issuing a conditional query request, the caller should check
|
|
* the `notModified` property on the request in the response callback. If this
|
|
* property is true, the server has no new data and there is obviously no data
|
|
* on the response.
|
|
*
|
|
* For example:
|
|
*
|
|
* let request = client.getCollectionInfo();
|
|
* request.locallyModifiedVersion = Date.now() - 60000;
|
|
* request.dispatch(function onComplete(error, request) {
|
|
* if (error) {
|
|
* // Handle error.
|
|
* return;
|
|
* }
|
|
*
|
|
* if (request.notModified) {
|
|
* return;
|
|
* }
|
|
*
|
|
* let info = request.resultObj;
|
|
* // Do stuff.
|
|
* });
|
|
*
|
|
* For modification requests, if the server has changed, the request will be
|
|
* rejected. When this happens, `error`will be defined and the `serverModified`
|
|
* property on it will be true.
|
|
*
|
|
* For example:
|
|
*
|
|
* let request = client.setBSO(bso);
|
|
* request.locallyModifiedVersion = bso.modified;
|
|
* request.dispatch(function onComplete(error, request) {
|
|
* if (error) {
|
|
* if (error.serverModified) {
|
|
* // Server data is newer! We should probably fetch it and apply
|
|
* // locally.
|
|
* }
|
|
*
|
|
* return;
|
|
* }
|
|
*
|
|
* // Handle success.
|
|
* });
|
|
*
|
|
* Future Features
|
|
* ---------------
|
|
*
|
|
* The current implementation does not support true streaming for things like
|
|
* multi-BSO retrieval. However, the API supports it, so we should be able
|
|
* to implement it transparently.
|
|
*/
|
|
function StorageServiceRequest() {
|
|
this._log = Log.repository.getLogger("Sync.StorageService.Request");
|
|
this._log.level = Log.Level[Prefs.get("log.level")];
|
|
|
|
this.notModified = false;
|
|
|
|
this._client = null;
|
|
this._request = null;
|
|
this._method = null;
|
|
this._handler = {};
|
|
this._data = null;
|
|
this._error = null;
|
|
this._resultObj = null;
|
|
this._locallyModifiedVersion = null;
|
|
this._allowIfModified = false;
|
|
this._allowIfUnmodified = false;
|
|
}
|
|
StorageServiceRequest.prototype = {
|
|
/**
|
|
* The StorageServiceClient this request came from.
|
|
*/
|
|
get client() {
|
|
return this._client;
|
|
},
|
|
|
|
/**
|
|
* The underlying RESTRequest instance.
|
|
*
|
|
* This should be treated as read only and should not be modified
|
|
* directly by external callers. While modification would probably work, this
|
|
* would defeat the purpose of the API and the abstractions it is meant to
|
|
* provide.
|
|
*
|
|
* If a consumer needs to modify the underlying request object, it is
|
|
* recommended for them to implement a new type that inherits from
|
|
* StorageServiceClient and override the necessary APIs to modify the request
|
|
* there.
|
|
*
|
|
* This accessor may disappear in future versions.
|
|
*/
|
|
get request() {
|
|
return this._request;
|
|
},
|
|
|
|
/**
|
|
* The RESTResponse that resulted from the RESTRequest.
|
|
*/
|
|
get response() {
|
|
return this._request.response;
|
|
},
|
|
|
|
/**
|
|
* HTTP status code from response.
|
|
*/
|
|
get statusCode() {
|
|
let response = this.response;
|
|
return response ? response.status : null;
|
|
},
|
|
|
|
/**
|
|
* Holds any error that has occurred.
|
|
*
|
|
* If a network error occurred, that will be returned. If no network error
|
|
* occurred, the client error will be returned. If no error occurred (yet),
|
|
* null will be returned.
|
|
*/
|
|
get error() {
|
|
return this._error;
|
|
},
|
|
|
|
/**
|
|
* The result from the request.
|
|
*
|
|
* This stores the object returned from the server. The type of object depends
|
|
* on the request type. See the per-API documentation in StorageServiceClient
|
|
* for details.
|
|
*/
|
|
get resultObj() {
|
|
return this._resultObj;
|
|
},
|
|
|
|
/**
|
|
* Define the local version of the entity the client has.
|
|
*
|
|
* This is used to enable conditional requests. Depending on the request
|
|
* type, the value set here could be reflected in the X-If-Modified-Since or
|
|
* X-If-Unmodified-Since headers.
|
|
*
|
|
* This attribute is not honoured on every request. See the documentation
|
|
* in the client API to learn where it is valid.
|
|
*/
|
|
set locallyModifiedVersion(value) {
|
|
// Will eventually become a header, so coerce to string.
|
|
this._locallyModifiedVersion = "" + value;
|
|
},
|
|
|
|
/**
|
|
* Object which holds callbacks and state for this request.
|
|
*
|
|
* The handler is installed by users of this request. It is simply an object
|
|
* containing 0 or more of the following properties:
|
|
*
|
|
* onComplete - A function called when the request has completed and all
|
|
* data has been received from the server. The function receives the
|
|
* following arguments:
|
|
*
|
|
* (StorageServiceRequestError) Error encountered during request. null
|
|
* if no error was encountered.
|
|
* (StorageServiceRequest) The request that was sent (this instance).
|
|
* Response information is available via properties and functions.
|
|
*
|
|
* Unless the call to dispatch() throws before returning, this callback
|
|
* is guaranteed to be invoked.
|
|
*
|
|
* Every client almost certainly wants to install this handler.
|
|
*
|
|
* onDispatch - A function called immediately before the request is
|
|
* dispatched. This hook can be used to inspect or modify the request
|
|
* before it is issued.
|
|
*
|
|
* The called function receives the following arguments:
|
|
*
|
|
* (StorageServiceRequest) The request being issued (this request).
|
|
*
|
|
* onBSORecord - When retrieving multiple BSOs from the server, this
|
|
* function is invoked when a new BSO record has been read. This function
|
|
* will be invoked 0 to N times before onComplete is invoked. onComplete
|
|
* signals that the last BSO has been processed or that an error
|
|
* occurred. The function receives the following arguments:
|
|
*
|
|
* (StorageServiceRequest) The request that was sent (this instance).
|
|
* (BasicStorageObject|string) The received BSO instance (when in full
|
|
* mode) or the string ID of the BSO (when not in full mode).
|
|
*
|
|
* Callers are free to (and encouraged) to store extra state in the supplied
|
|
* handler.
|
|
*/
|
|
set handler(value) {
|
|
if (typeof(value) != "object") {
|
|
throw new Error("Invalid handler. Must be an Object.");
|
|
}
|
|
|
|
this._handler = value;
|
|
|
|
if (!value.onComplete) {
|
|
this._log.warn("Handler does not contain an onComplete callback!");
|
|
}
|
|
},
|
|
|
|
get handler() {
|
|
return this._handler;
|
|
},
|
|
|
|
//---------------
|
|
// General APIs |
|
|
//---------------
|
|
|
|
/**
|
|
* Start the request.
|
|
*
|
|
* The request is dispatched asynchronously. The installed handler will have
|
|
* one or more of its callbacks invoked as the state of the request changes.
|
|
*
|
|
* The `onComplete` argument is optional. If provided, the supplied function
|
|
* will be installed on a *new* handler before the request is dispatched. This
|
|
* is equivalent to calling:
|
|
*
|
|
* request.handler = {onComplete: value};
|
|
* request.dispatch();
|
|
*
|
|
* Please note that any existing handler will be replaced if onComplete is
|
|
* provided.
|
|
*
|
|
* @param onComplete
|
|
* (function) Callback to be invoked when request has completed.
|
|
*/
|
|
dispatch: function dispatch(onComplete) {
|
|
if (onComplete) {
|
|
this.handler = {onComplete: onComplete};
|
|
}
|
|
|
|
// Installing the dummy callback makes implementation easier in _onComplete
|
|
// because we can then blindly call.
|
|
this._dispatch(function _internalOnComplete(error) {
|
|
this._onComplete(error);
|
|
this.completed = true;
|
|
}.bind(this));
|
|
},
|
|
|
|
/**
|
|
* This is a synchronous version of dispatch().
|
|
*
|
|
* THIS IS AN EVIL FUNCTION AND SHOULD NOT BE CALLED. It is provided for
|
|
* legacy reasons to support evil, synchronous clients.
|
|
*
|
|
* Please note that onComplete callbacks are executed from this JS thread.
|
|
* We dispatch the request, spin the event loop until it comes back. Then,
|
|
* we execute callbacks ourselves then return. In other words, there is no
|
|
* potential for spinning between callback execution and this function
|
|
* returning.
|
|
*
|
|
* The `onComplete` argument has the same behavior as for `dispatch()`.
|
|
*
|
|
* @param onComplete
|
|
* (function) Callback to be invoked when request has completed.
|
|
*/
|
|
dispatchSynchronous: function dispatchSynchronous(onComplete) {
|
|
if (onComplete) {
|
|
this.handler = {onComplete: onComplete};
|
|
}
|
|
|
|
let cb = Async.makeSyncCallback();
|
|
this._dispatch(cb);
|
|
let error = Async.waitForSyncCallback(cb);
|
|
|
|
this._onComplete(error);
|
|
this.completed = true;
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
// HIDDEN APIS. DO NOT CHANGE ANYTHING UNDER HERE FROM OUTSIDE THIS TYPE. |
|
|
//-------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Data to include in HTTP request body.
|
|
*/
|
|
_data: null,
|
|
|
|
/**
|
|
* StorageServiceRequestError encountered during dispatchy.
|
|
*/
|
|
_error: null,
|
|
|
|
/**
|
|
* Handler to parse response body into another object.
|
|
*
|
|
* This is installed by the client API. It should return the value the body
|
|
* parses to on success. If a failure is encountered, an exception should be
|
|
* thrown.
|
|
*/
|
|
_completeParser: null,
|
|
|
|
/**
|
|
* Dispatch the request.
|
|
*
|
|
* This contains common functionality for dispatching requests. It should
|
|
* ideally be part of dispatch, but since dispatchSynchronous exists, we
|
|
* factor out common code.
|
|
*/
|
|
_dispatch: function _dispatch(onComplete) {
|
|
// RESTRequest throws if the request has already been dispatched, so we
|
|
// need not bother checking.
|
|
|
|
// Inject conditional headers into request if they are allowed and if a
|
|
// value is set. Note that _locallyModifiedVersion is always a string and
|
|
// if("0") is true.
|
|
if (this._allowIfModified && this._locallyModifiedVersion) {
|
|
this._log.trace("Making request conditional.");
|
|
this._request.setHeader("X-If-Modified-Since",
|
|
this._locallyModifiedVersion);
|
|
} else if (this._allowIfUnmodified && this._locallyModifiedVersion) {
|
|
this._log.trace("Making request conditional.");
|
|
this._request.setHeader("X-If-Unmodified-Since",
|
|
this._locallyModifiedVersion);
|
|
}
|
|
|
|
// We have both an internal and public hook.
|
|
// If these throw, it is OK since we are not in a callback.
|
|
if (this._onDispatch) {
|
|
this._onDispatch();
|
|
}
|
|
|
|
if (this._handler.onDispatch) {
|
|
this._handler.onDispatch(this);
|
|
}
|
|
|
|
this._client.runListeners("onDispatch", this);
|
|
|
|
this._log.info("Dispatching request: " + this._method + " " +
|
|
this._request.uri.asciiSpec);
|
|
|
|
this._request.dispatch(this._method, this._data, onComplete);
|
|
},
|
|
|
|
/**
|
|
* RESTRequest onComplete handler for all requests.
|
|
*
|
|
* This provides common logic for all response handling.
|
|
*/
|
|
_onComplete: function(error) {
|
|
let onCompleteCalled = false;
|
|
|
|
let callOnComplete = function callOnComplete() {
|
|
onCompleteCalled = true;
|
|
|
|
if (!this._handler.onComplete) {
|
|
this._log.warn("No onComplete installed in handler!");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this._handler.onComplete(this._error, this);
|
|
} catch (ex) {
|
|
this._log.warn("Exception when invoking handler's onComplete: " +
|
|
CommonUtils.exceptionStr(ex));
|
|
throw ex;
|
|
}
|
|
}.bind(this);
|
|
|
|
try {
|
|
if (error) {
|
|
this._error = new StorageServiceRequestError();
|
|
this._error.network = error;
|
|
this._log.info("Network error during request: " + error);
|
|
this._client.runListeners("onNetworkError", this._client, this, error);
|
|
callOnComplete();
|
|
return;
|
|
}
|
|
|
|
let response = this._request.response;
|
|
this._log.info(response.status + " " + this._request.uri.asciiSpec);
|
|
|
|
this._processHeaders();
|
|
|
|
if (response.status == 200) {
|
|
this._resultObj = this._completeParser(response);
|
|
callOnComplete();
|
|
return;
|
|
}
|
|
|
|
if (response.status == 201) {
|
|
callOnComplete();
|
|
return;
|
|
}
|
|
|
|
if (response.status == 204) {
|
|
callOnComplete();
|
|
return;
|
|
}
|
|
|
|
if (response.status == 304) {
|
|
this.notModified = true;
|
|
callOnComplete();
|
|
return;
|
|
}
|
|
|
|
// TODO handle numeric response code from server.
|
|
if (response.status == 400) {
|
|
this._error = new StorageServiceRequestError();
|
|
this._error.client = new Error("Client error!");
|
|
callOnComplete();
|
|
return;
|
|
}
|
|
|
|
if (response.status == 401) {
|
|
this._error = new StorageServiceRequestError();
|
|
this._error.authentication = new Error("401 Received.");
|
|
this._client.runListeners("onAuthFailure", this._error.authentication,
|
|
this);
|
|
callOnComplete();
|
|
return;
|
|
}
|
|
|
|
if (response.status == 404) {
|
|
this._error = new StorageServiceRequestError();
|
|
this._error.notFound = true;
|
|
callOnComplete();
|
|
return;
|
|
}
|
|
|
|
if (response.status == 409) {
|
|
this._error = new StorageServiceRequestError();
|
|
this._error.conflict = true;
|
|
callOnComplete();
|
|
return;
|
|
}
|
|
|
|
if (response.status == 412) {
|
|
this._error = new StorageServiceRequestError();
|
|
this._error.serverModified = true;
|
|
callOnComplete();
|
|
return;
|
|
}
|
|
|
|
if (response.status == 413) {
|
|
this._error = new StorageServiceRequestError();
|
|
this._error.requestTooLarge = true;
|
|
callOnComplete();
|
|
return;
|
|
}
|
|
|
|
// If we see this, either the client or the server is buggy. We should
|
|
// never see this.
|
|
if (response.status == 415) {
|
|
this._log.error("415 HTTP response seen from server! This should " +
|
|
"never happen!");
|
|
this._error = new StorageServiceRequestError();
|
|
this._error.client = new Error("415 Unsupported Media Type received!");
|
|
callOnComplete();
|
|
return;
|
|
}
|
|
|
|
if (response.status >= 500 && response.status <= 599) {
|
|
this._log.error(response.status + " seen from server!");
|
|
this._error = new StorageServiceRequestError();
|
|
this._error.server = new Error(response.status + " status code.");
|
|
callOnComplete();
|
|
return;
|
|
}
|
|
|
|
callOnComplete();
|
|
|
|
} catch (ex) {
|
|
this._clientError = ex;
|
|
this._log.info("Exception when processing _onComplete: " + ex);
|
|
|
|
if (!onCompleteCalled) {
|
|
this._log.warn("Exception in internal response handling logic!");
|
|
try {
|
|
callOnComplete();
|
|
} catch (ex) {
|
|
this._log.warn("An additional exception was encountered when " +
|
|
"calling the handler's onComplete: " + ex);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
_processHeaders: function _processHeaders() {
|
|
let headers = this._request.response.headers;
|
|
|
|
if (headers["x-timestamp"]) {
|
|
this.serverTime = parseFloat(headers["x-timestamp"]);
|
|
}
|
|
|
|
if (headers["x-backoff"]) {
|
|
this.backoffInterval = 1000 * parseInt(headers["x-backoff"], 10);
|
|
}
|
|
|
|
if (headers["retry-after"]) {
|
|
this.backoffInterval = 1000 * parseInt(headers["retry-after"], 10);
|
|
}
|
|
|
|
if (this.backoffInterval) {
|
|
let failure = this._request.response.status == 503;
|
|
this._client.runListeners("onBackoffReceived", this._client, this,
|
|
this.backoffInterval, !failure);
|
|
}
|
|
|
|
if (headers["x-quota-remaining"]) {
|
|
this.quotaRemaining = parseInt(headers["x-quota-remaining"], 10);
|
|
this._client.runListeners("onQuotaRemaining", this._client, this,
|
|
this.quotaRemaining);
|
|
}
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Represents a request to fetch from a collection.
|
|
*
|
|
* These requests are highly configurable so they are given their own type.
|
|
* This type inherits from StorageServiceRequest and provides additional
|
|
* controllable parameters.
|
|
*
|
|
* By default, requests are issued in "streaming" mode. As the client receives
|
|
* data from the server, it will invoke the caller-supplied onBSORecord
|
|
* callback for each record as it is ready. When all records have been received,
|
|
* it will invoke onComplete as normal. To change this behavior, modify the
|
|
* "streaming" property before the request is dispatched.
|
|
*/
|
|
function StorageCollectionGetRequest() {
|
|
StorageServiceRequest.call(this);
|
|
}
|
|
StorageCollectionGetRequest.prototype = {
|
|
__proto__: StorageServiceRequest.prototype,
|
|
|
|
_namedArgs: {},
|
|
|
|
_streaming: true,
|
|
|
|
/**
|
|
* Control whether streaming mode is in effect.
|
|
*
|
|
* Read the type documentation above for more details.
|
|
*/
|
|
set streaming(value) {
|
|
this._streaming = !!value;
|
|
},
|
|
|
|
/**
|
|
* Define the set of IDs to fetch from the server.
|
|
*/
|
|
set ids(value) {
|
|
this._namedArgs.ids = value.join(",");
|
|
},
|
|
|
|
/**
|
|
* Only retrieve BSOs that were modified strictly before this time.
|
|
*
|
|
* Defined in milliseconds since UNIX epoch.
|
|
*/
|
|
set older(value) {
|
|
this._namedArgs.older = value;
|
|
},
|
|
|
|
/**
|
|
* Only retrieve BSOs that were modified strictly after this time.
|
|
*
|
|
* Defined in milliseconds since UNIX epoch.
|
|
*/
|
|
set newer(value) {
|
|
this._namedArgs.newer = value;
|
|
},
|
|
|
|
/**
|
|
* If set to a truthy value, return full BSO information.
|
|
*
|
|
* If not set (the default), the request will only return the set of BSO
|
|
* ids.
|
|
*/
|
|
set full(value) {
|
|
if (value) {
|
|
this._namedArgs.full = "1";
|
|
} else {
|
|
delete this._namedArgs["full"];
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Limit the max number of returned BSOs to this integer number.
|
|
*/
|
|
set limit(value) {
|
|
this._namedArgs.limit = value;
|
|
},
|
|
|
|
/**
|
|
* If set with any value, sort the results based on modification time, oldest
|
|
* first.
|
|
*/
|
|
set sortOldest(value) {
|
|
this._namedArgs.sort = "oldest";
|
|
},
|
|
|
|
/**
|
|
* If set with any value, sort the results based on modification time, newest
|
|
* first.
|
|
*/
|
|
set sortNewest(value) {
|
|
this._namedArgs.sort = "newest";
|
|
},
|
|
|
|
/**
|
|
* If set with any value, sort the results based on sortindex value, highest
|
|
* first.
|
|
*/
|
|
set sortIndex(value) {
|
|
this._namedArgs.sort = "index";
|
|
},
|
|
|
|
_onDispatch: function _onDispatch() {
|
|
let qs = this._getQueryString();
|
|
if (!qs.length) {
|
|
return;
|
|
}
|
|
|
|
this._request.uri = CommonUtils.makeURI(this._request.uri.asciiSpec + "?" +
|
|
qs);
|
|
},
|
|
|
|
_getQueryString: function _getQueryString() {
|
|
let args = [];
|
|
for (let [k, v] in Iterator(this._namedArgs)) {
|
|
args.push(encodeURIComponent(k) + "=" + encodeURIComponent(v));
|
|
}
|
|
|
|
return args.join("&");
|
|
},
|
|
|
|
_completeParser: function _completeParser(response) {
|
|
let obj = JSON.parse(response.body);
|
|
let items = obj.items;
|
|
|
|
if (!Array.isArray(items)) {
|
|
throw new Error("Unexpected JSON response. items is missing or not an " +
|
|
"array!");
|
|
}
|
|
|
|
if (!this.handler.onBSORecord) {
|
|
return;
|
|
}
|
|
|
|
for (let bso of items) {
|
|
this.handler.onBSORecord(this, bso);
|
|
}
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Represents a request that sets data in a collection
|
|
*
|
|
* Instances of this type are returned by StorageServiceClient.setBSOs().
|
|
*/
|
|
function StorageCollectionSetRequest() {
|
|
StorageServiceRequest.call(this);
|
|
|
|
this.size = 0;
|
|
|
|
// TODO Bug 775781 convert to Set and Map once iterable.
|
|
this.successfulIDs = [];
|
|
this.failures = {};
|
|
|
|
this._lines = [];
|
|
}
|
|
StorageCollectionSetRequest.prototype = {
|
|
__proto__: StorageServiceRequest.prototype,
|
|
|
|
get count() {
|
|
return this._lines.length;
|
|
},
|
|
|
|
/**
|
|
* Add a BasicStorageObject to this request.
|
|
*
|
|
* Please note that the BSO content is retrieved when the BSO is added to
|
|
* the request. If the BSO changes after it is added to a request, those
|
|
* changes will not be reflected in the request.
|
|
*
|
|
* @param bso
|
|
* (BasicStorageObject) BSO to add to the request.
|
|
*/
|
|
addBSO: function addBSO(bso) {
|
|
if (!bso instanceof BasicStorageObject) {
|
|
throw new Error("argument must be a BasicStorageObject instance.");
|
|
}
|
|
|
|
if (!bso.id) {
|
|
throw new Error("Passed BSO must have id defined.");
|
|
}
|
|
|
|
this.addLine(JSON.stringify(bso));
|
|
},
|
|
|
|
/**
|
|
* Add a BSO (represented by its serialized newline-delimited form).
|
|
*
|
|
* You probably shouldn't use this. It is used for batching.
|
|
*/
|
|
addLine: function addLine(line) {
|
|
// This is off by 1 in the larger direction. We don't care.
|
|
this.size += line.length + 1;
|
|
this._lines.push(line);
|
|
},
|
|
|
|
_onDispatch: function _onDispatch() {
|
|
this._data = this._lines.join("\n");
|
|
this.size = this._data.length;
|
|
},
|
|
|
|
_completeParser: function _completeParser(response) {
|
|
let result = JSON.parse(response.body);
|
|
|
|
for (let id of result.success) {
|
|
this.successfulIDs.push(id);
|
|
}
|
|
|
|
this.allSucceeded = true;
|
|
|
|
for (let [id, reasons] in Iterator(result.failed)) {
|
|
this.failures[id] = reasons;
|
|
this.allSucceeded = false;
|
|
}
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Represents a batch upload of BSOs to an individual collection.
|
|
*
|
|
* This is a more intelligent way to upload may BSOs to the server. It will
|
|
* split the uploaded data into multiple requests so size limits, etc aren't
|
|
* exceeded.
|
|
*
|
|
* Once a client obtains an instance of this type, it calls `addBSO` for each
|
|
* BSO to be uploaded. When the client is done providing BSOs to be uploaded,
|
|
* it calls `finish`. When `finish` is called, no more BSOs can be added to the
|
|
* batch. When all requests created from this batch have finished, the callback
|
|
* provided to `finish` will be invoked.
|
|
*
|
|
* Clients can also explicitly flush pending outgoing BSOs via `flush`. This
|
|
* allows callers to control their own batching/chunking.
|
|
*
|
|
* Interally, this maintains a queue of StorageCollectionSetRequest to be
|
|
* issued. At most one request is allowed to be in-flight at once. This is to
|
|
* avoid potential conflicts on the server. And, in the case of conditional
|
|
* requests, it prevents requests from being declined due to the server being
|
|
* updated by another request issued by us.
|
|
*
|
|
* If a request errors for any reason, all queued uploads are abandoned and the
|
|
* `finish` callback is invoked as soon as possible. The `successfulIDs` and
|
|
* `failures` properties will contain data from all requests that had this
|
|
* response data. In other words, the IDs have BSOs that were never sent to the
|
|
* server are not lumped in to either property.
|
|
*
|
|
* Requests can be made conditional by setting `locallyModifiedVersion` to the
|
|
* most recent version of server data. As responses from the server are seen,
|
|
* the last server version is carried forward to subsequent requests.
|
|
*
|
|
* The server version from the last request is available in the
|
|
* `serverModifiedVersion` property. It should only be accessed during or
|
|
* after the callback passed to `finish`.
|
|
*
|
|
* @param client
|
|
* (StorageServiceClient) Client instance to use for uploading.
|
|
*
|
|
* @param collection
|
|
* (string) Collection the batch operation will upload to.
|
|
*/
|
|
function StorageCollectionBatchedSet(client, collection) {
|
|
this.client = client;
|
|
this.collection = collection;
|
|
|
|
this._log = client._log;
|
|
|
|
this.locallyModifiedVersion = null;
|
|
this.serverModifiedVersion = null;
|
|
|
|
// TODO Bug 775781 convert to Set and Map once iterable.
|
|
this.successfulIDs = [];
|
|
this.failures = {};
|
|
|
|
// Request currently being populated.
|
|
this._stagingRequest = client.setBSOs(this.collection);
|
|
|
|
// Requests ready to be sent over the wire.
|
|
this._outgoingRequests = [];
|
|
|
|
// Whether we are waiting for a response.
|
|
this._requestInFlight = false;
|
|
|
|
this._onFinishCallback = null;
|
|
this._finished = false;
|
|
this._errorEncountered = false;
|
|
}
|
|
StorageCollectionBatchedSet.prototype = {
|
|
/**
|
|
* Add a BSO to be uploaded as part of this batch.
|
|
*/
|
|
addBSO: function addBSO(bso) {
|
|
if (this._errorEncountered) {
|
|
return;
|
|
}
|
|
|
|
let line = JSON.stringify(bso);
|
|
|
|
if (line.length > this.client.REQUEST_SIZE_LIMIT) {
|
|
throw new Error("BSO is larger than allowed limit: " + line.length +
|
|
" > " + this.client.REQUEST_SIZE_LIMIT);
|
|
}
|
|
|
|
if (this._stagingRequest.size + line.length > this.client.REQUEST_SIZE_LIMIT) {
|
|
this._log.debug("Sending request because payload size would be exceeded");
|
|
this._finishStagedRequest();
|
|
|
|
this._stagingRequest.addLine(line);
|
|
return;
|
|
}
|
|
|
|
// We are guaranteed to fit within size limits.
|
|
this._stagingRequest.addLine(line);
|
|
|
|
if (this._stagingRequest.count >= this.client.REQUEST_BSO_COUNT_LIMIT) {
|
|
this._log.debug("Sending request because BSO count threshold reached.");
|
|
this._finishStagedRequest();
|
|
return;
|
|
}
|
|
},
|
|
|
|
finish: function finish(cb) {
|
|
if (this._finished) {
|
|
throw new Error("Batch request has already been finished.");
|
|
}
|
|
|
|
this.flush();
|
|
|
|
this._onFinishCallback = cb;
|
|
this._finished = true;
|
|
this._stagingRequest = null;
|
|
},
|
|
|
|
flush: function flush() {
|
|
if (this._finished) {
|
|
throw new Error("Batch request has been finished.");
|
|
}
|
|
|
|
if (!this._stagingRequest.count) {
|
|
return;
|
|
}
|
|
|
|
this._finishStagedRequest();
|
|
},
|
|
|
|
_finishStagedRequest: function _finishStagedRequest() {
|
|
this._outgoingRequests.push(this._stagingRequest);
|
|
this._sendOutgoingRequest();
|
|
this._stagingRequest = this.client.setBSOs(this.collection);
|
|
},
|
|
|
|
_sendOutgoingRequest: function _sendOutgoingRequest() {
|
|
if (this._requestInFlight || this._errorEncountered) {
|
|
return;
|
|
}
|
|
|
|
if (!this._outgoingRequests.length) {
|
|
return;
|
|
}
|
|
|
|
let request = this._outgoingRequests.shift();
|
|
|
|
if (this.locallyModifiedVersion) {
|
|
request.locallyModifiedVersion = this.locallyModifiedVersion;
|
|
}
|
|
|
|
request.dispatch(this._onBatchComplete.bind(this));
|
|
this._requestInFlight = true;
|
|
},
|
|
|
|
_onBatchComplete: function _onBatchComplete(error, request) {
|
|
this._requestInFlight = false;
|
|
|
|
this.serverModifiedVersion = request.serverTime;
|
|
|
|
// Only update if we had a value before. Otherwise, this breaks
|
|
// unconditional requests!
|
|
if (this.locallyModifiedVersion) {
|
|
this.locallyModifiedVersion = request.serverTime;
|
|
}
|
|
|
|
for (let id of request.successfulIDs) {
|
|
this.successfulIDs.push(id);
|
|
}
|
|
|
|
for (let [id, reason] in Iterator(request.failures)) {
|
|
this.failures[id] = reason;
|
|
}
|
|
|
|
if (request.error) {
|
|
this._errorEncountered = true;
|
|
}
|
|
|
|
this._checkFinish();
|
|
},
|
|
|
|
_checkFinish: function _checkFinish() {
|
|
if (this._outgoingRequests.length && !this._errorEncountered) {
|
|
this._sendOutgoingRequest();
|
|
return;
|
|
}
|
|
|
|
if (!this._onFinishCallback) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this._onFinishCallback(this);
|
|
} catch (ex) {
|
|
this._log.warn("Exception when calling finished callback: " +
|
|
CommonUtils.exceptionStr(ex));
|
|
}
|
|
},
|
|
};
|
|
Object.freeze(StorageCollectionBatchedSet.prototype);
|
|
|
|
/**
|
|
* Manages a batch of BSO deletion requests.
|
|
*
|
|
* A single instance of this virtual request allows deletion of many individual
|
|
* BSOs without having to worry about server limits.
|
|
*
|
|
* Instances are obtained by calling `deleteBSOsBatching` on
|
|
* StorageServiceClient.
|
|
*
|
|
* Usage is roughly the same as StorageCollectionBatchedSet. Callers obtain
|
|
* an instance and select individual BSOs for deletion by calling `addID`.
|
|
* When the caller is finished marking BSOs for deletion, they call `finish`
|
|
* with a callback which will be invoked when all deletion requests finish.
|
|
*
|
|
* When the finished callback is invoked, any encountered errors will be stored
|
|
* in the `errors` property of this instance (which is passed to the callback).
|
|
* This will be an empty array if no errors were encountered. Else, it will
|
|
* contain the errors from the `onComplete` handler of request instances. The
|
|
* set of succeeded and failed IDs is not currently available.
|
|
*
|
|
* Deletes can be made conditional by setting `locallyModifiedVersion`. The
|
|
* behavior is the same as request types. The only difference is that the
|
|
* updated version from the server as a result of requests is carried forward
|
|
* to subsequent requests.
|
|
*
|
|
* The server version from the last request is stored in the
|
|
* `serverModifiedVersion` property. It is not safe to access this until the
|
|
* callback from `finish`.
|
|
*
|
|
* Like StorageCollectionBatchedSet, requests are issued serially to avoid
|
|
* race conditions on the server.
|
|
*
|
|
* @param client
|
|
* (StorageServiceClient) Client request is associated with.
|
|
* @param collection
|
|
* (string) Collection being operated on.
|
|
*/
|
|
function StorageCollectionBatchedDelete(client, collection) {
|
|
this.client = client;
|
|
this.collection = collection;
|
|
|
|
this._log = client._log;
|
|
|
|
this.locallyModifiedVersion = null;
|
|
this.serverModifiedVersion = null;
|
|
this.errors = [];
|
|
|
|
this._pendingIDs = [];
|
|
this._requestInFlight = false;
|
|
this._finished = false;
|
|
this._finishedCallback = null;
|
|
}
|
|
StorageCollectionBatchedDelete.prototype = {
|
|
addID: function addID(id) {
|
|
if (this._finished) {
|
|
throw new Error("Cannot add IDs to a finished instance.");
|
|
}
|
|
|
|
// If we saw errors already, don't do any work. This is an optimization
|
|
// and isn't strictly required, as _sendRequest() should no-op.
|
|
if (this.errors.length) {
|
|
return;
|
|
}
|
|
|
|
this._pendingIDs.push(id);
|
|
|
|
if (this._pendingIDs.length >= this.client.REQUEST_BSO_DELETE_LIMIT) {
|
|
this._sendRequest();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Finish this batch operation.
|
|
*
|
|
* No more IDs can be added to this operation. Existing IDs are flushed as
|
|
* a request. The passed callback will be called when all requests have
|
|
* finished.
|
|
*/
|
|
finish: function finish(cb) {
|
|
if (this._finished) {
|
|
throw new Error("Batch delete instance has already been finished.");
|
|
}
|
|
|
|
this._finished = true;
|
|
this._finishedCallback = cb;
|
|
|
|
if (this._pendingIDs.length) {
|
|
this._sendRequest();
|
|
}
|
|
},
|
|
|
|
_sendRequest: function _sendRequest() {
|
|
// Only allow 1 active request at a time and don't send additional
|
|
// requests if one has failed.
|
|
if (this._requestInFlight || this.errors.length) {
|
|
return;
|
|
}
|
|
|
|
let ids = this._pendingIDs.splice(0, this.client.REQUEST_BSO_DELETE_LIMIT);
|
|
let request = this.client.deleteBSOs(this.collection, ids);
|
|
|
|
if (this.locallyModifiedVersion) {
|
|
request.locallyModifiedVersion = this.locallyModifiedVersion;
|
|
}
|
|
|
|
request.dispatch(this._onRequestComplete.bind(this));
|
|
this._requestInFlight = true;
|
|
},
|
|
|
|
_onRequestComplete: function _onRequestComplete(error, request) {
|
|
this._requestInFlight = false;
|
|
|
|
if (error) {
|
|
// We don't currently track metadata of what failed. This is an obvious
|
|
// feature that could be added.
|
|
this._log.warn("Error received from server: " + error);
|
|
this.errors.push(error);
|
|
}
|
|
|
|
this.serverModifiedVersion = request.serverTime;
|
|
|
|
// If performing conditional requests, carry forward the new server version
|
|
// so subsequent conditional requests work.
|
|
if (this.locallyModifiedVersion) {
|
|
this.locallyModifiedVersion = request.serverTime;
|
|
}
|
|
|
|
if (this._pendingIDs.length && !this.errors.length) {
|
|
this._sendRequest();
|
|
return;
|
|
}
|
|
|
|
if (!this._finishedCallback) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this._finishedCallback(this);
|
|
} catch (ex) {
|
|
this._log.warn("Exception when invoking finished callback: " +
|
|
CommonUtils.exceptionStr(ex));
|
|
}
|
|
},
|
|
};
|
|
Object.freeze(StorageCollectionBatchedDelete.prototype);
|
|
|
|
/**
|
|
* Construct a new client for the SyncStorage API, version 2.0.
|
|
*
|
|
* Clients are constructed against a base URI. This URI is typically obtained
|
|
* from the token server via the endpoint component of a successful token
|
|
* response.
|
|
*
|
|
* The purpose of this type is to serve as a middleware between a client's core
|
|
* logic and the HTTP API. It hides the details of how the storage API is
|
|
* implemented but exposes important events, such as when auth goes bad or the
|
|
* server requests the client to back off.
|
|
*
|
|
* All request APIs operate by returning a StorageServiceRequest instance. The
|
|
* caller then installs the appropriate callbacks on each instance and then
|
|
* dispatches the request.
|
|
*
|
|
* Each client instance also serves as a controller and coordinator for
|
|
* associated requests. Callers can install listeners for common events on the
|
|
* client and take the appropriate action whenever any associated request
|
|
* observes them. For example, you will only need to register one listener for
|
|
* backoff observation as opposed to one on each request.
|
|
*
|
|
* While not currently supported, a future goal of this type is to support
|
|
* more advanced transport channels - such as SPDY - to allow for faster and
|
|
* more efficient API calls. The API is thus designed to abstract transport
|
|
* specifics away from the caller.
|
|
*
|
|
* Storage API consumers almost certainly have added functionality on top of the
|
|
* storage service. It is encouraged to create a child type which adds
|
|
* functionality to this layer.
|
|
*
|
|
* @param baseURI
|
|
* (string) Base URI for all requests.
|
|
*/
|
|
this.StorageServiceClient = function StorageServiceClient(baseURI) {
|
|
this._log = Log.repository.getLogger("Services.Common.StorageServiceClient");
|
|
this._log.level = Log.Level[Prefs.get("log.level")];
|
|
|
|
this._baseURI = baseURI;
|
|
|
|
if (this._baseURI[this._baseURI.length-1] != "/") {
|
|
this._baseURI += "/";
|
|
}
|
|
|
|
this._log.info("Creating new StorageServiceClient under " + this._baseURI);
|
|
|
|
this._listeners = [];
|
|
}
|
|
StorageServiceClient.prototype = {
|
|
/**
|
|
* The user agent sent with every request.
|
|
*
|
|
* You probably want to change this.
|
|
*/
|
|
userAgent: "StorageServiceClient",
|
|
|
|
/**
|
|
* Maximum size of entity bodies.
|
|
*
|
|
* TODO this should come from the server somehow. See bug 769759.
|
|
*/
|
|
REQUEST_SIZE_LIMIT: 512000,
|
|
|
|
/**
|
|
* Maximum number of BSOs in requests.
|
|
*
|
|
* TODO this should come from the server somehow. See bug 769759.
|
|
*/
|
|
REQUEST_BSO_COUNT_LIMIT: 100,
|
|
|
|
/**
|
|
* Maximum number of BSOs that can be deleted in a single DELETE.
|
|
*
|
|
* TODO this should come from the server. See bug 769759.
|
|
*/
|
|
REQUEST_BSO_DELETE_LIMIT: 100,
|
|
|
|
_baseURI: null,
|
|
_log: null,
|
|
|
|
_listeners: null,
|
|
|
|
//----------------------------
|
|
// Event Listener Management |
|
|
//----------------------------
|
|
|
|
/**
|
|
* Adds a listener to this client instance.
|
|
*
|
|
* Listeners allow other parties to react to and influence execution of the
|
|
* client instance.
|
|
*
|
|
* An event listener is simply an object that exposes functions which get
|
|
* executed during client execution. Objects can expose 0 or more of the
|
|
* following keys:
|
|
*
|
|
* onDispatch - Callback notified immediately before a request is
|
|
* dispatched. This gets called for every outgoing request. The function
|
|
* receives as its arguments the client instance and the outgoing
|
|
* StorageServiceRequest. This listener is useful for global
|
|
* authentication handlers, which can modify the request before it is
|
|
* sent.
|
|
*
|
|
* onAuthFailure - This is called when any request has experienced an
|
|
* authentication failure.
|
|
*
|
|
* This callback receives the following arguments:
|
|
*
|
|
* (StorageServiceClient) Client that encountered the auth failure.
|
|
* (StorageServiceRequest) Request that encountered the auth failure.
|
|
*
|
|
* onBackoffReceived - This is called when a backoff request is issued by
|
|
* the server. Backoffs are issued either when the service is completely
|
|
* unavailable (and the client should abort all activity) or if the server
|
|
* is under heavy load (and has completed the current request but is
|
|
* asking clients to be kind and stop issuing requests for a while).
|
|
*
|
|
* This callback receives the following arguments:
|
|
*
|
|
* (StorageServiceClient) Client that encountered the backoff.
|
|
* (StorageServiceRequest) Request that received the backoff.
|
|
* (number) Integer milliseconds the server is requesting us to back off
|
|
* for.
|
|
* (bool) Whether the request completed successfully. If false, the
|
|
* client should cease sending additional requests immediately, as
|
|
* they will likely fail. If true, the client is allowed to continue
|
|
* to put the server in a proper state. But, it should stop and heed
|
|
* the backoff as soon as possible.
|
|
*
|
|
* onNetworkError - This is called for every network error that is
|
|
* encountered.
|
|
*
|
|
* This callback receives the following arguments:
|
|
*
|
|
* (StorageServiceClient) Client that encountered the network error.
|
|
* (StorageServiceRequest) Request that encountered the error.
|
|
* (Error) Error passed in to RESTRequest's onComplete handler. It has
|
|
* a result property, which is a Components.Results enumeration.
|
|
*
|
|
* onQuotaRemaining - This is called if any request sees updated quota
|
|
* information from the server. This provides an update mechanism so
|
|
* listeners can immediately find out quota changes as soon as they
|
|
* are made.
|
|
*
|
|
* This callback receives the following arguments:
|
|
*
|
|
* (StorageServiceClient) Client that encountered the quota change.
|
|
* (StorageServiceRequest) Request that received the quota change.
|
|
* (number) Integer number of kilobytes remaining for the user.
|
|
*/
|
|
addListener: function addListener(listener) {
|
|
if (!listener) {
|
|
throw new Error("listener argument must be an object.");
|
|
}
|
|
|
|
if (this._listeners.indexOf(listener) != -1) {
|
|
return;
|
|
}
|
|
|
|
this._listeners.push(listener);
|
|
},
|
|
|
|
/**
|
|
* Remove a previously-installed listener.
|
|
*/
|
|
removeListener: function removeListener(listener) {
|
|
this._listeners = this._listeners.filter(function(a) {
|
|
return a != listener;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Invoke listeners for a specific event.
|
|
*
|
|
* @param name
|
|
* (string) The name of the listener to invoke.
|
|
* @param args
|
|
* (array) Arguments to pass to listener functions.
|
|
*/
|
|
runListeners: function runListeners(name, ...args) {
|
|
for (let listener of this._listeners) {
|
|
try {
|
|
if (name in listener) {
|
|
listener[name].apply(listener, args);
|
|
}
|
|
} catch (ex) {
|
|
this._log.warn("Listener threw an exception during " + name + ": "
|
|
+ ex);
|
|
}
|
|
}
|
|
},
|
|
|
|
//-----------------------------
|
|
// Information/Metadata APIs |
|
|
//-----------------------------
|
|
|
|
/**
|
|
* Obtain a request that fetches collection info.
|
|
*
|
|
* On successful response, the result is placed in the resultObj property
|
|
* of the request object.
|
|
*
|
|
* The result value is a map of strings to numbers. The string keys represent
|
|
* collection names. The number values are integer milliseconds since Unix
|
|
* epoch that hte collection was last modified.
|
|
*
|
|
* This request can be made conditional by defining `locallyModifiedVersion`
|
|
* on the returned object to the last known version on the client.
|
|
*
|
|
* Example Usage:
|
|
*
|
|
* let request = client.getCollectionInfo();
|
|
* request.dispatch(function onComplete(error, request) {
|
|
* if (!error) {
|
|
* return;
|
|
* }
|
|
*
|
|
* for (let [collection, milliseconds] in Iterator(this.resultObj)) {
|
|
* // ...
|
|
* }
|
|
* });
|
|
*/
|
|
getCollectionInfo: function getCollectionInfo() {
|
|
return this._getJSONGETRequest("info/collections");
|
|
},
|
|
|
|
/**
|
|
* Fetch quota information.
|
|
*
|
|
* The result in the callback upon success is a map containing quota
|
|
* metadata. It will have the following keys:
|
|
*
|
|
* usage - Number of bytes currently utilized.
|
|
* quota - Number of bytes available to account.
|
|
*
|
|
* The request can be made conditional by populating `locallyModifiedVersion`
|
|
* on the returned request instance with the most recently known version of
|
|
* server data.
|
|
*/
|
|
getQuota: function getQuota() {
|
|
return this._getJSONGETRequest("info/quota");
|
|
},
|
|
|
|
/**
|
|
* Fetch information on how much data each collection uses.
|
|
*
|
|
* The result on success is a map of strings to numbers. The string keys
|
|
* are collection names. The values are numbers corresponding to the number
|
|
* of kilobytes used by that collection.
|
|
*/
|
|
getCollectionUsage: function getCollectionUsage() {
|
|
return this._getJSONGETRequest("info/collection_usage");
|
|
},
|
|
|
|
/**
|
|
* Fetch the number of records in each collection.
|
|
*
|
|
* The result on success is a map of strings to numbers. The string keys are
|
|
* collection names. The values are numbers corresponding to the integer
|
|
* number of items in that collection.
|
|
*/
|
|
getCollectionCounts: function getCollectionCounts() {
|
|
return this._getJSONGETRequest("info/collection_counts");
|
|
},
|
|
|
|
//--------------------------
|
|
// Collection Interaction |
|
|
// -------------------------
|
|
|
|
/**
|
|
* Obtain a request to fetch collection information.
|
|
*
|
|
* The returned request instance is a StorageCollectionGetRequest instance.
|
|
* This is a sub-type of StorageServiceRequest and offers a number of setters
|
|
* to control how the request is performed. See the documentation for that
|
|
* type for more.
|
|
*
|
|
* The request can be made conditional by setting `locallyModifiedVersion`
|
|
* on the returned request instance.
|
|
*
|
|
* Example usage:
|
|
*
|
|
* let request = client.getCollection("testcoll");
|
|
*
|
|
* // Obtain full BSOs rather than just IDs.
|
|
* request.full = true;
|
|
*
|
|
* // Only obtain BSOs modified in the last minute.
|
|
* request.newer = Date.now() - 60000;
|
|
*
|
|
* // Install handler.
|
|
* request.handler = {
|
|
* onBSORecord: function onBSORecord(request, bso) {
|
|
* let id = bso.id;
|
|
* let payload = bso.payload;
|
|
*
|
|
* // Do something with BSO.
|
|
* },
|
|
*
|
|
* onComplete: function onComplete(error, req) {
|
|
* if (error) {
|
|
* // Handle error.
|
|
* return;
|
|
* }
|
|
*
|
|
* // Your onBSORecord handler has processed everything. Now is where
|
|
* // you typically signal that everything has been processed and to move
|
|
* // on.
|
|
* }
|
|
* };
|
|
*
|
|
* request.dispatch();
|
|
*
|
|
* @param collection
|
|
* (string) Name of collection to operate on.
|
|
*/
|
|
getCollection: function getCollection(collection) {
|
|
if (!collection) {
|
|
throw new Error("collection argument must be defined.");
|
|
}
|
|
|
|
let uri = this._baseURI + "storage/" + collection;
|
|
|
|
let request = this._getRequest(uri, "GET", {
|
|
accept: "application/json",
|
|
allowIfModified: true,
|
|
requestType: StorageCollectionGetRequest
|
|
});
|
|
|
|
return request;
|
|
},
|
|
|
|
/**
|
|
* Fetch a single Basic Storage Object (BSO).
|
|
*
|
|
* On success, the BSO may be available in the resultObj property of the
|
|
* request as a BasicStorageObject instance.
|
|
*
|
|
* The request can be made conditional by setting `locallyModifiedVersion`
|
|
* on the returned request instance.*
|
|
*
|
|
* Example usage:
|
|
*
|
|
* let request = client.getBSO("meta", "global");
|
|
* request.dispatch(function onComplete(error, request) {
|
|
* if (!error) {
|
|
* return;
|
|
* }
|
|
*
|
|
* if (request.notModified) {
|
|
* return;
|
|
* }
|
|
*
|
|
* let bso = request.bso;
|
|
* let payload = bso.payload;
|
|
*
|
|
* ...
|
|
* };
|
|
*
|
|
* @param collection
|
|
* (string) Collection to fetch from
|
|
* @param id
|
|
* (string) ID of BSO to retrieve.
|
|
* @param type
|
|
* (constructor) Constructor to call to create returned object. This
|
|
* is optional and defaults to BasicStorageObject.
|
|
*/
|
|
getBSO: function fetchBSO(collection, id, type=BasicStorageObject) {
|
|
if (!collection) {
|
|
throw new Error("collection argument must be defined.");
|
|
}
|
|
|
|
if (!id) {
|
|
throw new Error("id argument must be defined.");
|
|
}
|
|
|
|
let uri = this._baseURI + "storage/" + collection + "/" + id;
|
|
|
|
return this._getRequest(uri, "GET", {
|
|
accept: "application/json",
|
|
allowIfModified: true,
|
|
completeParser: function completeParser(response) {
|
|
let record = new type(id, collection);
|
|
record.deserialize(response.body);
|
|
|
|
return record;
|
|
},
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Add or update a BSO in a collection.
|
|
*
|
|
* To make the request conditional (i.e. don't allow server changes if the
|
|
* server has a newer version), set request.locallyModifiedVersion to the
|
|
* last known version of the BSO. While this could be done automatically by
|
|
* this API, it is intentionally omitted because there are valid conditions
|
|
* where a client may wish to forcefully update the server.
|
|
*
|
|
* If a conditional request fails because the server has newer data, the
|
|
* StorageServiceRequestError passed to the callback will have the
|
|
* `serverModified` property set to true.
|
|
*
|
|
* Example usage:
|
|
*
|
|
* let bso = new BasicStorageObject("foo", "coll");
|
|
* bso.payload = "payload";
|
|
* bso.modified = Date.now();
|
|
*
|
|
* let request = client.setBSO(bso);
|
|
* request.locallyModifiedVersion = bso.modified;
|
|
*
|
|
* request.dispatch(function onComplete(error, req) {
|
|
* if (error) {
|
|
* if (error.serverModified) {
|
|
* // Handle conditional set failure.
|
|
* return;
|
|
* }
|
|
*
|
|
* // Handle other errors.
|
|
* return;
|
|
* }
|
|
*
|
|
* // Record that set worked.
|
|
* });
|
|
*
|
|
* @param bso
|
|
* (BasicStorageObject) BSO to upload. The BSO instance must have the
|
|
* `collection` and `id` properties defined.
|
|
*/
|
|
setBSO: function setBSO(bso) {
|
|
if (!bso) {
|
|
throw new Error("bso argument must be defined.");
|
|
}
|
|
|
|
if (!bso.collection) {
|
|
throw new Error("BSO instance does not have collection defined.");
|
|
}
|
|
|
|
if (!bso.id) {
|
|
throw new Error("BSO instance does not have ID defined.");
|
|
}
|
|
|
|
let uri = this._baseURI + "storage/" + bso.collection + "/" + bso.id;
|
|
let request = this._getRequest(uri, "PUT", {
|
|
contentType: "application/json",
|
|
allowIfUnmodified: true,
|
|
data: JSON.stringify(bso),
|
|
});
|
|
|
|
return request;
|
|
},
|
|
|
|
/**
|
|
* Add or update multiple BSOs.
|
|
*
|
|
* This is roughly equivalent to calling setBSO multiple times except it is
|
|
* much more effecient because there is only 1 round trip to the server.
|
|
*
|
|
* The request can be made conditional by setting `locallyModifiedVersion`
|
|
* on the returned request instance.
|
|
*
|
|
* This function returns a StorageCollectionSetRequest instance. This type
|
|
* has additional functions and properties specific to this operation. See
|
|
* its documentation for more.
|
|
*
|
|
* Most consumers interested in submitting multiple BSOs to the server will
|
|
* want to use `setBSOsBatching` instead. That API intelligently splits up
|
|
* requests as necessary, etc.
|
|
*
|
|
* Example usage:
|
|
*
|
|
* let request = client.setBSOs("collection0");
|
|
* let bso0 = new BasicStorageObject("id0");
|
|
* bso0.payload = "payload0";
|
|
*
|
|
* let bso1 = new BasicStorageObject("id1");
|
|
* bso1.payload = "payload1";
|
|
*
|
|
* request.addBSO(bso0);
|
|
* request.addBSO(bso1);
|
|
*
|
|
* request.dispatch(function onComplete(error, req) {
|
|
* if (error) {
|
|
* // Handle error.
|
|
* return;
|
|
* }
|
|
*
|
|
* let successful = req.successfulIDs;
|
|
* let failed = req.failed;
|
|
*
|
|
* // Do additional processing.
|
|
* });
|
|
*
|
|
* @param collection
|
|
* (string) Collection to operate on.
|
|
* @return
|
|
* (StorageCollectionSetRequest) Request instance.
|
|
*/
|
|
setBSOs: function setBSOs(collection) {
|
|
if (!collection) {
|
|
throw new Error("collection argument must be defined.");
|
|
}
|
|
|
|
let uri = this._baseURI + "storage/" + collection;
|
|
let request = this._getRequest(uri, "POST", {
|
|
requestType: StorageCollectionSetRequest,
|
|
contentType: "application/newlines",
|
|
accept: "application/json",
|
|
allowIfUnmodified: true,
|
|
});
|
|
|
|
return request;
|
|
},
|
|
|
|
/**
|
|
* This is a batching variant of setBSOs.
|
|
*
|
|
* Whereas `setBSOs` is a 1:1 mapping between function calls and HTTP
|
|
* requests issued, this one is a 1:N mapping. It will intelligently break
|
|
* up outgoing BSOs into multiple requests so size limits, etc aren't
|
|
* exceeded.
|
|
*
|
|
* Please see the documentation for `StorageCollectionBatchedSet` for
|
|
* usage info.
|
|
*
|
|
* @param collection
|
|
* (string) Collection to operate on.
|
|
* @return
|
|
* (StorageCollectionBatchedSet) Batched set instance.
|
|
*/
|
|
setBSOsBatching: function setBSOsBatching(collection) {
|
|
if (!collection) {
|
|
throw new Error("collection argument must be defined.");
|
|
}
|
|
|
|
return new StorageCollectionBatchedSet(this, collection);
|
|
},
|
|
|
|
/**
|
|
* Deletes a single BSO from a collection.
|
|
*
|
|
* The request can be made conditional by setting `locallyModifiedVersion`
|
|
* on the returned request instance.
|
|
*
|
|
* @param collection
|
|
* (string) Collection to operate on.
|
|
* @param id
|
|
* (string) ID of BSO to delete.
|
|
*/
|
|
deleteBSO: function deleteBSO(collection, id) {
|
|
if (!collection) {
|
|
throw new Error("collection argument must be defined.");
|
|
}
|
|
|
|
if (!id) {
|
|
throw new Error("id argument must be defined.");
|
|
}
|
|
|
|
let uri = this._baseURI + "storage/" + collection + "/" + id;
|
|
return this._getRequest(uri, "DELETE", {
|
|
allowIfUnmodified: true,
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Delete multiple BSOs from a specific collection.
|
|
*
|
|
* This is functional equivalent to calling deleteBSO() for every ID but
|
|
* much more efficient because it only results in 1 round trip to the server.
|
|
*
|
|
* The request can be made conditional by setting `locallyModifiedVersion`
|
|
* on the returned request instance.
|
|
*
|
|
* If the number of BSOs to delete is potentially large, it is preferred to
|
|
* use `deleteBSOsBatching`. That API automatically splits the operation into
|
|
* multiple requests so server limits aren't exceeded.
|
|
*
|
|
* @param collection
|
|
* (string) Name of collection to delete BSOs from.
|
|
* @param ids
|
|
* (iterable of strings) Set of BSO IDs to delete.
|
|
*/
|
|
deleteBSOs: function deleteBSOs(collection, ids) {
|
|
// In theory we should URL encode. However, IDs are supposed to be URL
|
|
// safe. If we get garbage in, we'll get garbage out and the server will
|
|
// reject it.
|
|
let s = ids.join(",");
|
|
|
|
let uri = this._baseURI + "storage/" + collection + "?ids=" + s;
|
|
|
|
return this._getRequest(uri, "DELETE", {
|
|
allowIfUnmodified: true,
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Bulk deletion of BSOs with no size limit.
|
|
*
|
|
* This allows a large amount of BSOs to be deleted easily. It will formulate
|
|
* multiple `deleteBSOs` queries so the client does not exceed server limits.
|
|
*
|
|
* @param collection
|
|
* (string) Name of collection to delete BSOs from.
|
|
* @return StorageCollectionBatchedDelete
|
|
*/
|
|
deleteBSOsBatching: function deleteBSOsBatching(collection) {
|
|
if (!collection) {
|
|
throw new Error("collection argument must be defined.");
|
|
}
|
|
|
|
return new StorageCollectionBatchedDelete(this, collection);
|
|
},
|
|
|
|
/**
|
|
* Deletes a single collection from the server.
|
|
*
|
|
* The request can be made conditional by setting `locallyModifiedVersion`
|
|
* on the returned request instance.
|
|
*
|
|
* @param collection
|
|
* (string) Name of collection to delete.
|
|
*/
|
|
deleteCollection: function deleteCollection(collection) {
|
|
let uri = this._baseURI + "storage/" + collection;
|
|
|
|
return this._getRequest(uri, "DELETE", {
|
|
allowIfUnmodified: true
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Deletes all collections data from the server.
|
|
*/
|
|
deleteCollections: function deleteCollections() {
|
|
let uri = this._baseURI + "storage";
|
|
|
|
return this._getRequest(uri, "DELETE", {});
|
|
},
|
|
|
|
/**
|
|
* Helper that wraps _getRequest for GET requests that return JSON.
|
|
*/
|
|
_getJSONGETRequest: function _getJSONGETRequest(path) {
|
|
let uri = this._baseURI + path;
|
|
|
|
return this._getRequest(uri, "GET", {
|
|
accept: "application/json",
|
|
allowIfModified: true,
|
|
completeParser: this._jsonResponseParser,
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Common logic for obtaining an HTTP request instance.
|
|
*
|
|
* @param uri
|
|
* (string) URI to request.
|
|
* @param method
|
|
* (string) HTTP method to issue.
|
|
* @param options
|
|
* (object) Additional options to control request and response
|
|
* handling. Keys influencing behavior are:
|
|
*
|
|
* completeParser - Function that parses a HTTP response body into a
|
|
* value. This function receives the RESTResponse object and
|
|
* returns a value that is added to a StorageResponse instance.
|
|
* If the response cannot be parsed or is invalid, this function
|
|
* should throw an exception.
|
|
*
|
|
* data - Data to be sent in HTTP request body.
|
|
*
|
|
* accept - Value for Accept request header.
|
|
*
|
|
* contentType - Value for Content-Type request header.
|
|
*
|
|
* requestType - Function constructor for request type to initialize.
|
|
* Defaults to StorageServiceRequest.
|
|
*
|
|
* allowIfModified - Whether to populate X-If-Modified-Since if the
|
|
* request contains a locallyModifiedVersion.
|
|
*
|
|
* allowIfUnmodified - Whether to populate X-If-Unmodified-Since if
|
|
* the request contains a locallyModifiedVersion.
|
|
*/
|
|
_getRequest: function _getRequest(uri, method, options) {
|
|
if (!options.requestType) {
|
|
options.requestType = StorageServiceRequest;
|
|
}
|
|
|
|
let request = new RESTRequest(uri);
|
|
|
|
if (Prefs.get("sendVersionInfo", true)) {
|
|
let ua = this.userAgent + Prefs.get("client.type", "desktop");
|
|
request.setHeader("user-agent", ua);
|
|
}
|
|
|
|
if (options.accept) {
|
|
request.setHeader("accept", options.accept);
|
|
}
|
|
|
|
if (options.contentType) {
|
|
request.setHeader("content-type", options.contentType);
|
|
}
|
|
|
|
let result = new options.requestType();
|
|
result._request = request;
|
|
result._method = method;
|
|
result._client = this;
|
|
result._data = options.data;
|
|
|
|
if (options.completeParser) {
|
|
result._completeParser = options.completeParser;
|
|
}
|
|
|
|
result._allowIfModified = !!options.allowIfModified;
|
|
result._allowIfUnmodified = !!options.allowIfUnmodified;
|
|
|
|
return result;
|
|
},
|
|
|
|
_jsonResponseParser: function _jsonResponseParser(response) {
|
|
let ct = response.headers["content-type"];
|
|
if (!ct) {
|
|
throw new Error("No Content-Type response header! Misbehaving server!");
|
|
}
|
|
|
|
if (ct != "application/json" && ct.indexOf("application/json;") != 0) {
|
|
throw new Error("Non-JSON media type: " + ct);
|
|
}
|
|
|
|
return JSON.parse(response.body);
|
|
},
|
|
};
|