michael@0: /* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */ michael@0: /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ 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 michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: 'use strict' michael@0: michael@0: this.EXPORTED_SYMBOLS = ["DataStore"]; michael@0: michael@0: function debug(s) { michael@0: //dump('DEBUG DataStore: ' + s + '\n'); michael@0: } michael@0: michael@0: const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; michael@0: michael@0: const REVISION_ADDED = "added"; michael@0: const REVISION_UPDATED = "updated"; michael@0: const REVISION_REMOVED = "removed"; michael@0: const REVISION_VOID = "void"; michael@0: michael@0: // This value has to be tuned a bit. Currently it's just a guess michael@0: // and yet we don't know if it's too low or too high. michael@0: const MAX_REQUESTS = 25; michael@0: michael@0: Cu.import("resource://gre/modules/DataStoreCursorImpl.jsm"); michael@0: Cu.import("resource://gre/modules/DataStoreDB.jsm"); michael@0: Cu.import('resource://gre/modules/Services.jsm'); michael@0: Cu.import('resource://gre/modules/XPCOMUtils.jsm'); michael@0: Cu.importGlobalProperties(["indexedDB"]); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "cpmm", michael@0: "@mozilla.org/childprocessmessagemanager;1", michael@0: "nsIMessageSender"); michael@0: michael@0: /* Helper functions */ michael@0: function createDOMError(aWindow, aEvent) { michael@0: return new aWindow.DOMError(aEvent); michael@0: } michael@0: michael@0: function throwInvalidArg(aWindow) { michael@0: return aWindow.Promise.reject( michael@0: new aWindow.DOMError("SyntaxError", "Non-numeric or invalid id")); michael@0: } michael@0: michael@0: function throwReadOnly(aWindow) { michael@0: return aWindow.Promise.reject( michael@0: new aWindow.DOMError("ReadOnlyError", "DataStore in readonly mode")); michael@0: } michael@0: michael@0: function validateId(aId) { michael@0: // If string, it cannot be empty. michael@0: if (typeof(aId) == 'string') { michael@0: return aId.length; michael@0: } michael@0: michael@0: aId = parseInt(aId); michael@0: return (!isNaN(aId) && aId > 0); michael@0: } michael@0: michael@0: /* DataStore object */ michael@0: this.DataStore = function(aWindow, aName, aOwner, aReadOnly) { michael@0: debug("DataStore created"); michael@0: this.init(aWindow, aName, aOwner, aReadOnly); michael@0: } michael@0: michael@0: this.DataStore.prototype = { michael@0: classDescription: "DataStore XPCOM Component", michael@0: classID: Components.ID("{db5c9602-030f-4bff-a3de-881a8de370f2}"), michael@0: contractID: "@mozilla.org/dom/datastore-impl;1", michael@0: QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsISupports, michael@0: Components.interfaces.nsIObserver]), michael@0: michael@0: callbacks: [], michael@0: michael@0: _window: null, michael@0: _name: null, michael@0: _owner: null, michael@0: _readOnly: null, michael@0: _revisionId: null, michael@0: _exposedObject: null, michael@0: _cursor: null, michael@0: _shuttingdown: false, michael@0: _eventTarget: null, michael@0: michael@0: init: function(aWindow, aName, aOwner, aReadOnly) { michael@0: debug("DataStore init"); michael@0: michael@0: this._window = aWindow; michael@0: this._name = aName; michael@0: this._owner = aOwner; michael@0: this._readOnly = aReadOnly; michael@0: michael@0: this._db = new DataStoreDB(); michael@0: this._db.init(aOwner, aName); michael@0: michael@0: Services.obs.addObserver(this, "inner-window-destroyed", false); michael@0: michael@0: let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils); michael@0: this._innerWindowID = util.currentInnerWindowID; michael@0: michael@0: cpmm.addMessageListener("DataStore:Changed:Return:OK", this); michael@0: cpmm.sendAsyncMessage("DataStore:RegisterForMessages", michael@0: { store: this._name, owner: this._owner }); michael@0: }, michael@0: michael@0: observe: function(aSubject, aTopic, aData) { michael@0: let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data; michael@0: if (wId == this._innerWindowID) { michael@0: Services.obs.removeObserver(this, "inner-window-destroyed"); michael@0: michael@0: cpmm.removeMessageListener("DataStore:Changed:Return:OK", this); michael@0: cpmm.sendAsyncMessage("DataStore:UnregisterForMessages"); michael@0: this._shuttingdown = true; michael@0: this._db.close(); michael@0: } michael@0: }, michael@0: michael@0: setEventTarget: function(aEventTarget) { michael@0: this._eventTarget = aEventTarget; michael@0: }, michael@0: michael@0: newDBPromise: function(aTxnType, aFunction) { michael@0: let self = this; michael@0: return new this._window.Promise(function(aResolve, aReject) { michael@0: debug("DBPromise started"); michael@0: self._db.txn( michael@0: aTxnType, michael@0: function(aTxn, aStore, aRevisionStore) { michael@0: debug("DBPromise success"); michael@0: aFunction(aResolve, aReject, aTxn, aStore, aRevisionStore); michael@0: }, michael@0: function(aEvent) { michael@0: debug("DBPromise error"); michael@0: aReject(createDOMError(self._window, aEvent)); michael@0: } michael@0: ); michael@0: }); michael@0: }, michael@0: michael@0: checkRevision: function(aReject, aRevisionStore, aRevisionId, aCallback) { michael@0: if (!aRevisionId) { michael@0: aCallback(); michael@0: return; michael@0: } michael@0: michael@0: let self = this; michael@0: michael@0: let request = aRevisionStore.openCursor(null, 'prev'); michael@0: request.onsuccess = function(aEvent) { michael@0: let cursor = aEvent.target.result; michael@0: if (!cursor) { michael@0: dump("This cannot really happen."); michael@0: return; michael@0: } michael@0: michael@0: if (cursor.value.revisionId != aRevisionId) { michael@0: aReject(new self._window.DOMError("ConstraintError", michael@0: "RevisionId is not up-to-date")); michael@0: return; michael@0: } michael@0: michael@0: aCallback(); michael@0: } michael@0: }, michael@0: michael@0: getInternal: function(aStore, aIds, aCallback) { michael@0: debug("GetInternal: " + aIds.toSource()); michael@0: michael@0: // Creation of the results array. michael@0: let results = new Array(aIds.length); michael@0: michael@0: // We're going to create this amount of requests. michael@0: let pendingIds = aIds.length; michael@0: let indexPos = 0; michael@0: michael@0: let self = this; michael@0: michael@0: function getInternalSuccess(aEvent, aPos) { michael@0: debug("GetInternal success. Record: " + aEvent.target.result); michael@0: results[aPos] = Cu.cloneInto(aEvent.target.result, self._window); michael@0: if (!--pendingIds) { michael@0: aCallback(results); michael@0: return; michael@0: } michael@0: michael@0: if (indexPos < aIds.length) { michael@0: // Just MAX_REQUESTS requests at the same time. michael@0: let count = 0; michael@0: while (indexPos < aIds.length && ++count < MAX_REQUESTS) { michael@0: getInternalRequest(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: function getInternalRequest() { michael@0: let currentPos = indexPos++; michael@0: let request = aStore.get(aIds[currentPos]); michael@0: request.onsuccess = function(aEvent) { michael@0: getInternalSuccess(aEvent, currentPos); michael@0: } michael@0: } michael@0: michael@0: getInternalRequest(); michael@0: }, michael@0: michael@0: putInternal: function(aResolve, aStore, aRevisionStore, aObj, aId) { michael@0: debug("putInternal " + aId); michael@0: michael@0: let self = this; michael@0: let request = aStore.put(aObj, aId); michael@0: request.onsuccess = function(aEvent) { michael@0: debug("putInternal success"); michael@0: michael@0: self.addRevision(aRevisionStore, aId, REVISION_UPDATED, michael@0: function() { michael@0: debug("putInternal - revisionId increased"); michael@0: // No wrap here because the result is always a int. michael@0: aResolve(aEvent.target.result); michael@0: } michael@0: ); michael@0: }; michael@0: }, michael@0: michael@0: addInternal: function(aResolve, aStore, aRevisionStore, aObj, aId) { michael@0: debug("AddInternal"); michael@0: michael@0: let self = this; michael@0: let request = aStore.add(aObj, aId); michael@0: request.onsuccess = function(aEvent) { michael@0: debug("Request successful. Id: " + aEvent.target.result); michael@0: self.addRevision(aRevisionStore, aEvent.target.result, REVISION_ADDED, michael@0: function() { michael@0: debug("AddInternal - revisionId increased"); michael@0: // No wrap here because the result is always a int. michael@0: aResolve(aEvent.target.result); michael@0: } michael@0: ); michael@0: }; michael@0: }, michael@0: michael@0: removeInternal: function(aResolve, aStore, aRevisionStore, aId) { michael@0: debug("RemoveInternal"); michael@0: michael@0: let self = this; michael@0: let request = aStore.get(aId); michael@0: request.onsuccess = function(aEvent) { michael@0: debug("RemoveInternal success. Record: " + aEvent.target.result); michael@0: if (aEvent.target.result === undefined) { michael@0: aResolve(false); michael@0: return; michael@0: } michael@0: michael@0: let deleteRequest = aStore.delete(aId); michael@0: deleteRequest.onsuccess = function() { michael@0: debug("RemoveInternal success"); michael@0: self.addRevision(aRevisionStore, aId, REVISION_REMOVED, michael@0: function() { michael@0: aResolve(true); michael@0: } michael@0: ); michael@0: }; michael@0: }; michael@0: }, michael@0: michael@0: clearInternal: function(aResolve, aStore, aRevisionStore) { michael@0: debug("ClearInternal"); michael@0: michael@0: let self = this; michael@0: let request = aStore.clear(); michael@0: request.onsuccess = function() { michael@0: debug("ClearInternal success"); michael@0: self._db.clearRevisions(aRevisionStore, michael@0: function() { michael@0: debug("Revisions cleared"); michael@0: michael@0: self.addRevision(aRevisionStore, 0, REVISION_VOID, michael@0: function() { michael@0: debug("ClearInternal - revisionId increased"); michael@0: aResolve(); michael@0: } michael@0: ); michael@0: } michael@0: ); michael@0: }; michael@0: }, michael@0: michael@0: getLengthInternal: function(aResolve, aStore) { michael@0: debug("GetLengthInternal"); michael@0: michael@0: let request = aStore.count(); michael@0: request.onsuccess = function(aEvent) { michael@0: debug("GetLengthInternal success: " + aEvent.target.result); michael@0: // No wrap here because the result is always a int. michael@0: aResolve(aEvent.target.result); michael@0: }; michael@0: }, michael@0: michael@0: addRevision: function(aRevisionStore, aId, aType, aSuccessCb) { michael@0: let self = this; michael@0: this._db.addRevision(aRevisionStore, aId, aType, michael@0: function(aRevisionId) { michael@0: self._revisionId = aRevisionId; michael@0: self.sendNotification(aId, aType, aRevisionId); michael@0: aSuccessCb(); michael@0: } michael@0: ); michael@0: }, michael@0: michael@0: retrieveRevisionId: function(aSuccessCb) { michael@0: let self = this; michael@0: this._db.revisionTxn( michael@0: 'readonly', michael@0: function(aTxn, aRevisionStore) { michael@0: debug("RetrieveRevisionId transaction success"); michael@0: michael@0: let request = aRevisionStore.openCursor(null, 'prev'); michael@0: request.onsuccess = function(aEvent) { michael@0: let cursor = aEvent.target.result; michael@0: if (cursor) { michael@0: self._revisionId = cursor.value.revisionId; michael@0: } michael@0: michael@0: aSuccessCb(self._revisionId); michael@0: }; michael@0: } michael@0: ); michael@0: }, michael@0: michael@0: sendNotification: function(aId, aOperation, aRevisionId) { michael@0: debug("SendNotification"); michael@0: if (aOperation == REVISION_VOID) { michael@0: aOperation = "cleared"; michael@0: } michael@0: michael@0: cpmm.sendAsyncMessage("DataStore:Changed", michael@0: { store: this.name, owner: this._owner, michael@0: message: { revisionId: aRevisionId, id: aId, michael@0: operation: aOperation, owner: this._owner } } ); michael@0: }, michael@0: michael@0: receiveMessage: function(aMessage) { michael@0: debug("receiveMessage"); michael@0: michael@0: if (aMessage.name != "DataStore:Changed:Return:OK") { michael@0: debug("Wrong message: " + aMessage.name); michael@0: return; michael@0: } michael@0: michael@0: // If this message is not for this DataStore, let's ignore it. michael@0: if (aMessage.data.owner != this._owner || michael@0: aMessage.data.store != this._name) { michael@0: return; michael@0: } michael@0: michael@0: let self = this; michael@0: michael@0: this.retrieveRevisionId( michael@0: function() { michael@0: // If the window has been destroyed we don't emit the events. michael@0: if (self._shuttingdown) { michael@0: return; michael@0: } michael@0: michael@0: // If we have an active cursor we don't emit events. michael@0: if (self._cursor) { michael@0: return; michael@0: } michael@0: michael@0: let event = new self._window.DataStoreChangeEvent('change', michael@0: aMessage.data.message); michael@0: self._eventTarget.dispatchEvent(event); michael@0: } michael@0: ); michael@0: }, michael@0: michael@0: get exposedObject() { michael@0: debug("get exposedObject"); michael@0: return this._exposedObject; michael@0: }, michael@0: michael@0: set exposedObject(aObject) { michael@0: debug("set exposedObject"); michael@0: this._exposedObject = aObject; michael@0: }, michael@0: michael@0: syncTerminated: function(aCursor) { michael@0: // This checks is to avoid that an invalid cursor stops a sync. michael@0: if (this._cursor == aCursor) { michael@0: this._cursor = null; michael@0: } michael@0: }, michael@0: michael@0: // Public interface : michael@0: michael@0: get name() { michael@0: return this._name; michael@0: }, michael@0: michael@0: get owner() { michael@0: return this._owner; michael@0: }, michael@0: michael@0: get readOnly() { michael@0: return this._readOnly; michael@0: }, michael@0: michael@0: get: function() { michael@0: let ids = Array.prototype.slice.call(arguments); michael@0: for (let i = 0; i < ids.length; ++i) { michael@0: if (!validateId(ids[i])) { michael@0: return throwInvalidArg(this._window); michael@0: } michael@0: } michael@0: michael@0: let self = this; michael@0: michael@0: // Promise michael@0: return this.newDBPromise("readonly", michael@0: function(aResolve, aReject, aTxn, aStore, aRevisionStore) { michael@0: self.getInternal(aStore, ids, michael@0: function(aResults) { michael@0: aResolve(ids.length > 1 ? aResults : aResults[0]); michael@0: }); michael@0: } michael@0: ); michael@0: }, michael@0: michael@0: put: function(aObj, aId, aRevisionId) { michael@0: if (!validateId(aId)) { michael@0: return throwInvalidArg(this._window); michael@0: } michael@0: michael@0: if (this._readOnly) { michael@0: return throwReadOnly(this._window); michael@0: } michael@0: michael@0: let self = this; michael@0: michael@0: // Promise michael@0: return this.newDBPromise("readwrite", michael@0: function(aResolve, aReject, aTxn, aStore, aRevisionStore) { michael@0: self.checkRevision(aReject, aRevisionStore, aRevisionId, function() { michael@0: self.putInternal(aResolve, aStore, aRevisionStore, aObj, aId); michael@0: }); michael@0: } michael@0: ); michael@0: }, michael@0: michael@0: add: function(aObj, aId, aRevisionId) { michael@0: if (aId) { michael@0: if (!validateId(aId)) { michael@0: return throwInvalidArg(this._window); michael@0: } michael@0: } michael@0: michael@0: if (this._readOnly) { michael@0: return throwReadOnly(this._window); michael@0: } michael@0: michael@0: let self = this; michael@0: michael@0: // Promise michael@0: return this.newDBPromise("readwrite", michael@0: function(aResolve, aReject, aTxn, aStore, aRevisionStore) { michael@0: self.checkRevision(aReject, aRevisionStore, aRevisionId, function() { michael@0: self.addInternal(aResolve, aStore, aRevisionStore, aObj, aId); michael@0: }); michael@0: } michael@0: ); michael@0: }, michael@0: michael@0: remove: function(aId, aRevisionId) { michael@0: if (!validateId(aId)) { michael@0: return throwInvalidArg(this._window); michael@0: } michael@0: michael@0: if (this._readOnly) { michael@0: return throwReadOnly(this._window); michael@0: } michael@0: michael@0: let self = this; michael@0: michael@0: // Promise michael@0: return this.newDBPromise("readwrite", michael@0: function(aResolve, aReject, aTxn, aStore, aRevisionStore) { michael@0: self.checkRevision(aReject, aRevisionStore, aRevisionId, function() { michael@0: self.removeInternal(aResolve, aStore, aRevisionStore, aId); michael@0: }); michael@0: } michael@0: ); michael@0: }, michael@0: michael@0: clear: function(aRevisionId) { michael@0: if (this._readOnly) { michael@0: return throwReadOnly(this._window); michael@0: } michael@0: michael@0: let self = this; michael@0: michael@0: // Promise michael@0: return this.newDBPromise("readwrite", michael@0: function(aResolve, aReject, aTxn, aStore, aRevisionStore) { michael@0: self.checkRevision(aReject, aRevisionStore, aRevisionId, function() { michael@0: self.clearInternal(aResolve, aStore, aRevisionStore); michael@0: }); michael@0: } michael@0: ); michael@0: }, michael@0: michael@0: get revisionId() { michael@0: return this._revisionId; michael@0: }, michael@0: michael@0: getLength: function() { michael@0: let self = this; michael@0: michael@0: // Promise michael@0: return this.newDBPromise("readonly", michael@0: function(aResolve, aReject, aTxn, aStore, aRevisionStore) { michael@0: self.getLengthInternal(aResolve, aStore); michael@0: } michael@0: ); michael@0: }, michael@0: michael@0: sync: function(aRevisionId) { michael@0: debug("Sync"); michael@0: this._cursor = new DataStoreCursor(this._window, this, aRevisionId); michael@0: michael@0: let cursorImpl = this._window.DataStoreCursorImpl. michael@0: _create(this._window, this._cursor); michael@0: michael@0: let exposedCursor = new this._window.DataStoreCursor(); michael@0: exposedCursor.setDataStoreCursorImpl(cursorImpl); michael@0: return exposedCursor; michael@0: } michael@0: };