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