/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */ /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ /* 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/. */ 'use strict' this.EXPORTED_SYMBOLS = ["DataStore"]; function debug(s) { // dump('DEBUG DataStore: ' + s + '\n'); } const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; const REVISION_ADDED = "added"; const REVISION_UPDATED = "updated"; const REVISION_REMOVED = "removed"; const REVISION_VOID = "void"; // This value has to be tuned a bit. Currently it's just a guess // and yet we don't know if it's too low or too high. const MAX_REQUESTS = 25; Cu.import("resource://gre/modules/DataStoreCursor.jsm"); Cu.import("resource://gre/modules/DataStoreDB.jsm"); Cu.import("resource://gre/modules/ObjectWrapper.jsm"); Cu.import('resource://gre/modules/Services.jsm'); Cu.import('resource://gre/modules/XPCOMUtils.jsm'); XPCOMUtils.defineLazyServiceGetter(this, "cpmm", "@mozilla.org/childprocessmessagemanager;1", "nsIMessageSender"); /* Helper functions */ function createDOMError(aWindow, aEvent) { return new aWindow.DOMError(aEvent.target.error.name); } function throwInvalidArg(aWindow) { return aWindow.Promise.reject( new aWindow.DOMError("SyntaxError", "Non-numeric or invalid id")); } function throwReadOnly(aWindow) { return aWindow.Promise.reject( new aWindow.DOMError("ReadOnlyError", "DataStore in readonly mode")); } function parseIds(aId) { function parseId(aId) { aId = parseInt(aId); return (isNaN(aId) || aId <= 0) ? null : aId; } if (!Array.isArray(aId)) { return parseId(aId); } for (let i = 0; i < aId.length; ++i) { aId[i] = parseId(aId[i]); if (aId[i] === null) { return null; } } return aId; } /* DataStore object */ this.DataStore = function(aWindow, aName, aOwner, aReadOnly) { debug("DataStore created"); this.init(aWindow, aName, aOwner, aReadOnly); } this.DataStore.prototype = { classDescription: "DataStore XPCOM Component", classID: Components.ID("{db5c9602-030f-4bff-a3de-881a8de370f2}"), contractID: "@mozilla.org/dom/datastore;1", QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsISupports]), callbacks: [], _window: null, _name: null, _owner: null, _readOnly: null, _revisionId: null, _exposedObject: null, _cursor: null, init: function(aWindow, aName, aOwner, aReadOnly) { debug("DataStore init"); this._window = aWindow; this._name = aName; this._owner = aOwner; this._readOnly = aReadOnly; this._db = new DataStoreDB(); this._db.init(aOwner, aName); let self = this; Services.obs.addObserver(function(aSubject, aTopic, aData) { let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data; if (wId == self._innerWindowID) { cpmm.removeMessageListener("DataStore:Changed:Return:OK", self); self._db.close(); } }, "inner-window-destroyed", false); let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); this._innerWindowID = util.currentInnerWindowID; cpmm.addMessageListener("DataStore:Changed:Return:OK", this); cpmm.sendAsyncMessage("DataStore:RegisterForMessages", { store: this._name, owner: this._owner }); }, newDBPromise: function(aTxnType, aFunction) { let self = this; return new this._window.Promise(function(aResolve, aReject) { debug("DBPromise started"); self._db.txn( aTxnType, function(aTxn, aStore, aRevisionStore) { debug("DBPromise success"); aFunction(aResolve, aReject, aTxn, aStore, aRevisionStore); }, function(aEvent) { debug("DBPromise error"); aReject(createDOMError(self._window, aEvent)); } ); }); }, getInternal: function(aStore, aIds, aCallback) { debug("GetInternal: " + aIds.toSource()); // Creation of the results array. let results = new Array(aIds.length); // We're going to create this amount of requests. let pendingIds = aIds.length; let indexPos = 0; let self = this; function getInternalSuccess(aEvent, aPos) { debug("GetInternal success. Record: " + aEvent.target.result); results[aPos] = ObjectWrapper.wrap(aEvent.target.result, self._window); if (!--pendingIds) { aCallback(results); return; } if (indexPos < aIds.length) { // Just MAX_REQUESTS requests at the same time. let count = 0; while (indexPos < aIds.length && ++count < MAX_REQUESTS) { getInternalRequest(); } } } function getInternalRequest() { let currentPos = indexPos++; let request = aStore.get(aIds[currentPos]); request.onsuccess = function(aEvent) { getInternalSuccess(aEvent, currentPos); } } getInternalRequest(); }, updateInternal: function(aResolve, aStore, aRevisionStore, aId, aObj) { debug("UpdateInternal " + aId); let self = this; let request = aStore.put(aObj, aId); request.onsuccess = function(aEvent) { debug("UpdateInternal success"); self.addRevision(aRevisionStore, aId, REVISION_UPDATED, function() { debug("UpdateInternal - revisionId increased"); // No wrap here because the result is always a int. aResolve(aEvent.target.result); } ); }; }, addInternal: function(aResolve, aStore, aRevisionStore, aObj) { debug("AddInternal"); let self = this; let request = aStore.put(aObj); request.onsuccess = function(aEvent) { debug("Request successful. Id: " + aEvent.target.result); self.addRevision(aRevisionStore, aEvent.target.result, REVISION_ADDED, function() { debug("AddInternal - revisionId increased"); // No wrap here because the result is always a int. aResolve(aEvent.target.result); } ); }; }, removeInternal: function(aResolve, aStore, aRevisionStore, aId) { debug("RemoveInternal"); let self = this; let request = aStore.get(aId); request.onsuccess = function(aEvent) { debug("RemoveInternal success. Record: " + aEvent.target.result); if (aEvent.target.result === undefined) { aResolve(false); return; } let deleteRequest = aStore.delete(aId); deleteRequest.onsuccess = function() { debug("RemoveInternal success"); self.addRevision(aRevisionStore, aId, REVISION_REMOVED, function() { aResolve(true); } ); }; }; }, clearInternal: function(aResolve, aStore, aRevisionStore) { debug("ClearInternal"); let self = this; let request = aStore.clear(); request.onsuccess = function() { debug("ClearInternal success"); self._db.clearRevisions(aRevisionStore, function() { debug("Revisions cleared"); self.addRevision(aRevisionStore, 0, REVISION_VOID, function() { debug("ClearInternal - revisionId increased"); aResolve(); } ); } ); }; }, getLengthInternal: function(aResolve, aStore) { debug("GetLengthInternal"); let request = aStore.count(); request.onsuccess = function(aEvent) { debug("GetLengthInternal success: " + aEvent.target.result); // No wrap here because the result is always a int. aResolve(aEvent.target.result); }; }, addRevision: function(aRevisionStore, aId, aType, aSuccessCb) { let self = this; this._db.addRevision(aRevisionStore, aId, aType, function(aRevisionId) { self._revisionId = aRevisionId; self.sendNotification(aId, aType, aRevisionId); aSuccessCb(); } ); }, retrieveRevisionId: function(aSuccessCb) { let self = this; this._db.revisionTxn( 'readonly', function(aTxn, aRevisionStore) { debug("RetrieveRevisionId transaction success"); let request = aRevisionStore.openCursor(null, 'prev'); request.onsuccess = function(aEvent) { let cursor = aEvent.target.result; if (cursor) { self._revisionId = cursor.value.revisionId; } aSuccessCb(self._revisionId); }; } ); }, sendNotification: function(aId, aOperation, aRevisionId) { debug("SendNotification"); if (aOperation != REVISION_VOID) { cpmm.sendAsyncMessage("DataStore:Changed", { store: this.name, owner: this.owner, message: { revisionId: aRevisionId, id: aId, operation: aOperation } } ); } }, receiveMessage: function(aMessage) { debug("receiveMessage"); if (aMessage.name != "DataStore:Changed:Return:OK") { debug("Wrong message: " + aMessage.name); return; } let self = this; this.retrieveRevisionId( function() { // If we have an active cursor we don't emit events. if (self._cursor) { return; } let event = new self._window.DataStoreChangeEvent('change', aMessage.data); self.__DOM_IMPL__.dispatchEvent(event); } ); }, get exposedObject() { debug("get exposedObject"); return this._exposedObject; }, set exposedObject(aObject) { debug("set exposedObject"); this._exposedObject = aObject; }, syncTerminated: function(aCursor) { // This checks is to avoid that an invalid cursor stops a sync. if (this._cursor == aCursor) { this._cursor = null; } }, // Public interface : get name() { return this._name; }, get owner() { return this._owner; }, get readOnly() { return this._readOnly; }, get: function(aId) { aId = parseIds(aId); if (aId === null) { return throwInvalidArg(this._window); } let self = this; // Promise return this.newDBPromise("readonly", function(aResolve, aReject, aTxn, aStore, aRevisionStore) { self.getInternal(aStore, Array.isArray(aId) ? aId : [ aId ], function(aResults) { aResolve(Array.isArray(aId) ? aResults : aResults[0]); }); } ); }, update: function(aId, aObj) { aId = parseInt(aId); if (isNaN(aId) || aId <= 0) { return throwInvalidArg(this._window); } if (this._readOnly) { return throwReadOnly(this._window); } let self = this; // Promise return this.newDBPromise("readwrite", function(aResolve, aReject, aTxn, aStore, aRevisionStore) { self.updateInternal(aResolve, aStore, aRevisionStore, aId, aObj); } ); }, add: function(aObj) { if (this._readOnly) { return throwReadOnly(this._window); } let self = this; // Promise return this.newDBPromise("readwrite", function(aResolve, aReject, aTxn, aStore, aRevisionStore) { self.addInternal(aResolve, aStore, aRevisionStore, aObj); } ); }, remove: function(aId) { aId = parseInt(aId); if (isNaN(aId) || aId <= 0) { return throwInvalidArg(this._window); } if (this._readOnly) { return throwReadOnly(this._window); } let self = this; // Promise return this.newDBPromise("readwrite", function(aResolve, aReject, aTxn, aStore, aRevisionStore) { self.removeInternal(aResolve, aStore, aRevisionStore, aId); } ); }, clear: function() { if (this._readOnly) { return throwReadOnly(this._window); } let self = this; // Promise return this.newDBPromise("readwrite", function(aResolve, aReject, aTxn, aStore, aRevisionStore) { self.clearInternal(aResolve, aStore, aRevisionStore); } ); }, get revisionId() { return this._revisionId; }, getChanges: function(aRevisionId) { debug("GetChanges: " + aRevisionId); if (aRevisionId === null || aRevisionId === undefined) { return this._window.Promise.reject( new this._window.DOMError("SyntaxError", "Invalid revisionId")); } let self = this; // Promise return new this._window.Promise(function(aResolve, aReject) { debug("GetChanges promise started"); self._db.revisionTxn( 'readonly', function(aTxn, aStore) { debug("GetChanges transaction success"); let request = self._db.getInternalRevisionId( aRevisionId, aStore, function(aInternalRevisionId) { if (aInternalRevisionId == undefined) { aResolve(undefined); return; } // This object is the return value of this promise. // Initially we use maps, and then we convert them in array. let changes = { revisionId: '', addedIds: {}, updatedIds: {}, removedIds: {} }; let request = aStore.mozGetAll(IDBKeyRange.lowerBound(aInternalRevisionId, true)); request.onsuccess = function(aEvent) { for (let i = 0; i < aEvent.target.result.length; ++i) { let data = aEvent.target.result[i]; switch (data.operation) { case REVISION_ADDED: changes.addedIds[data.objectId] = true; break; case REVISION_UPDATED: // We don't consider an update if this object has been added // or if it has been already modified by a previous // operation. if (!(data.objectId in changes.addedIds) && !(data.objectId in changes.updatedIds)) { changes.updatedIds[data.objectId] = true; } break; case REVISION_REMOVED: let id = data.objectId; // If the object has been added in this range of revisions // we can ignore it and remove it from the list. if (id in changes.addedIds) { delete changes.addedIds[id]; } else { changes.removedIds[id] = true; } if (id in changes.updatedIds) { delete changes.updatedIds[id]; } break; } } // The last revisionId. if (aEvent.target.result.length) { changes.revisionId = aEvent.target.result[aEvent.target.result.length - 1].revisionId; } // From maps to arrays. changes.addedIds = Object.keys(changes.addedIds).map(function(aKey) { return parseInt(aKey, 10); }); changes.updatedIds = Object.keys(changes.updatedIds).map(function(aKey) { return parseInt(aKey, 10); }); changes.removedIds = Object.keys(changes.removedIds).map(function(aKey) { return parseInt(aKey, 10); }); let wrappedObject = ObjectWrapper.wrap(changes, self._window); aResolve(wrappedObject); }; } ); }, function(aEvent) { debug("GetChanges transaction failed"); aReject(createDOMError(self._window, aEvent)); } ); }); }, getLength: function() { let self = this; // Promise return this.newDBPromise("readonly", function(aResolve, aReject, aTxn, aStore, aRevisionStore) { self.getLengthInternal(aResolve, aStore); } ); }, set onchange(aCallback) { debug("Set OnChange"); this.__DOM_IMPL__.setEventHandler("onchange", aCallback); }, get onchange() { debug("Get OnChange"); return this.__DOM_IMPL__.getEventHandler("onchange"); }, sync: function(aRevisionId) { debug("Sync"); this._cursor = new DataStoreCursor(this._window, this, aRevisionId); return this._window.DataStoreCursor._create(this._window, this._cursor); } };