diff -r 000000000000 -r 6474c204b198 dom/datastore/DataStoreImpl.jsm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dom/datastore/DataStoreImpl.jsm Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,538 @@ +/* -*- 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/DataStoreCursorImpl.jsm"); +Cu.import("resource://gre/modules/DataStoreDB.jsm"); +Cu.import('resource://gre/modules/Services.jsm'); +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); +Cu.importGlobalProperties(["indexedDB"]); + +XPCOMUtils.defineLazyServiceGetter(this, "cpmm", + "@mozilla.org/childprocessmessagemanager;1", + "nsIMessageSender"); + +/* Helper functions */ +function createDOMError(aWindow, aEvent) { + return new aWindow.DOMError(aEvent); +} + +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 validateId(aId) { + // If string, it cannot be empty. + if (typeof(aId) == 'string') { + return aId.length; + } + + aId = parseInt(aId); + return (!isNaN(aId) && aId > 0); +} + +/* 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-impl;1", + QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsISupports, + Components.interfaces.nsIObserver]), + + callbacks: [], + + _window: null, + _name: null, + _owner: null, + _readOnly: null, + _revisionId: null, + _exposedObject: null, + _cursor: null, + _shuttingdown: false, + _eventTarget: 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); + + Services.obs.addObserver(this, "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 }); + }, + + observe: function(aSubject, aTopic, aData) { + let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data; + if (wId == this._innerWindowID) { + Services.obs.removeObserver(this, "inner-window-destroyed"); + + cpmm.removeMessageListener("DataStore:Changed:Return:OK", this); + cpmm.sendAsyncMessage("DataStore:UnregisterForMessages"); + this._shuttingdown = true; + this._db.close(); + } + }, + + setEventTarget: function(aEventTarget) { + this._eventTarget = aEventTarget; + }, + + 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)); + } + ); + }); + }, + + checkRevision: function(aReject, aRevisionStore, aRevisionId, aCallback) { + if (!aRevisionId) { + aCallback(); + return; + } + + let self = this; + + let request = aRevisionStore.openCursor(null, 'prev'); + request.onsuccess = function(aEvent) { + let cursor = aEvent.target.result; + if (!cursor) { + dump("This cannot really happen."); + return; + } + + if (cursor.value.revisionId != aRevisionId) { + aReject(new self._window.DOMError("ConstraintError", + "RevisionId is not up-to-date")); + return; + } + + aCallback(); + } + }, + + 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] = Cu.cloneInto(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(); + }, + + putInternal: function(aResolve, aStore, aRevisionStore, aObj, aId) { + debug("putInternal " + aId); + + let self = this; + let request = aStore.put(aObj, aId); + request.onsuccess = function(aEvent) { + debug("putInternal success"); + + self.addRevision(aRevisionStore, aId, REVISION_UPDATED, + function() { + debug("putInternal - revisionId increased"); + // No wrap here because the result is always a int. + aResolve(aEvent.target.result); + } + ); + }; + }, + + addInternal: function(aResolve, aStore, aRevisionStore, aObj, aId) { + debug("AddInternal"); + + let self = this; + let request = aStore.add(aObj, aId); + 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) { + aOperation = "cleared"; + } + + cpmm.sendAsyncMessage("DataStore:Changed", + { store: this.name, owner: this._owner, + message: { revisionId: aRevisionId, id: aId, + operation: aOperation, owner: this._owner } } ); + }, + + receiveMessage: function(aMessage) { + debug("receiveMessage"); + + if (aMessage.name != "DataStore:Changed:Return:OK") { + debug("Wrong message: " + aMessage.name); + return; + } + + // If this message is not for this DataStore, let's ignore it. + if (aMessage.data.owner != this._owner || + aMessage.data.store != this._name) { + return; + } + + let self = this; + + this.retrieveRevisionId( + function() { + // If the window has been destroyed we don't emit the events. + if (self._shuttingdown) { + return; + } + + // If we have an active cursor we don't emit events. + if (self._cursor) { + return; + } + + let event = new self._window.DataStoreChangeEvent('change', + aMessage.data.message); + self._eventTarget.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() { + let ids = Array.prototype.slice.call(arguments); + for (let i = 0; i < ids.length; ++i) { + if (!validateId(ids[i])) { + return throwInvalidArg(this._window); + } + } + + let self = this; + + // Promise + return this.newDBPromise("readonly", + function(aResolve, aReject, aTxn, aStore, aRevisionStore) { + self.getInternal(aStore, ids, + function(aResults) { + aResolve(ids.length > 1 ? aResults : aResults[0]); + }); + } + ); + }, + + put: function(aObj, aId, aRevisionId) { + if (!validateId(aId)) { + 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.checkRevision(aReject, aRevisionStore, aRevisionId, function() { + self.putInternal(aResolve, aStore, aRevisionStore, aObj, aId); + }); + } + ); + }, + + add: function(aObj, aId, aRevisionId) { + if (aId) { + if (!validateId(aId)) { + 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.checkRevision(aReject, aRevisionStore, aRevisionId, function() { + self.addInternal(aResolve, aStore, aRevisionStore, aObj, aId); + }); + } + ); + }, + + remove: function(aId, aRevisionId) { + if (!validateId(aId)) { + 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.checkRevision(aReject, aRevisionStore, aRevisionId, function() { + self.removeInternal(aResolve, aStore, aRevisionStore, aId); + }); + } + ); + }, + + clear: function(aRevisionId) { + if (this._readOnly) { + return throwReadOnly(this._window); + } + + let self = this; + + // Promise + return this.newDBPromise("readwrite", + function(aResolve, aReject, aTxn, aStore, aRevisionStore) { + self.checkRevision(aReject, aRevisionStore, aRevisionId, function() { + self.clearInternal(aResolve, aStore, aRevisionStore); + }); + } + ); + }, + + get revisionId() { + return this._revisionId; + }, + + getLength: function() { + let self = this; + + // Promise + return this.newDBPromise("readonly", + function(aResolve, aReject, aTxn, aStore, aRevisionStore) { + self.getLengthInternal(aResolve, aStore); + } + ); + }, + + sync: function(aRevisionId) { + debug("Sync"); + this._cursor = new DataStoreCursor(this._window, this, aRevisionId); + + let cursorImpl = this._window.DataStoreCursorImpl. + _create(this._window, this._cursor); + + let exposedCursor = new this._window.DataStoreCursor(); + exposedCursor.setDataStoreCursorImpl(cursorImpl); + return exposedCursor; + } +};