1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/dom/datastore/DataStoreImpl.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,538 @@ 1.4 +/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */ 1.5 +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +'use strict' 1.11 + 1.12 +this.EXPORTED_SYMBOLS = ["DataStore"]; 1.13 + 1.14 +function debug(s) { 1.15 + //dump('DEBUG DataStore: ' + s + '\n'); 1.16 +} 1.17 + 1.18 +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; 1.19 + 1.20 +const REVISION_ADDED = "added"; 1.21 +const REVISION_UPDATED = "updated"; 1.22 +const REVISION_REMOVED = "removed"; 1.23 +const REVISION_VOID = "void"; 1.24 + 1.25 +// This value has to be tuned a bit. Currently it's just a guess 1.26 +// and yet we don't know if it's too low or too high. 1.27 +const MAX_REQUESTS = 25; 1.28 + 1.29 +Cu.import("resource://gre/modules/DataStoreCursorImpl.jsm"); 1.30 +Cu.import("resource://gre/modules/DataStoreDB.jsm"); 1.31 +Cu.import('resource://gre/modules/Services.jsm'); 1.32 +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); 1.33 +Cu.importGlobalProperties(["indexedDB"]); 1.34 + 1.35 +XPCOMUtils.defineLazyServiceGetter(this, "cpmm", 1.36 + "@mozilla.org/childprocessmessagemanager;1", 1.37 + "nsIMessageSender"); 1.38 + 1.39 +/* Helper functions */ 1.40 +function createDOMError(aWindow, aEvent) { 1.41 + return new aWindow.DOMError(aEvent); 1.42 +} 1.43 + 1.44 +function throwInvalidArg(aWindow) { 1.45 + return aWindow.Promise.reject( 1.46 + new aWindow.DOMError("SyntaxError", "Non-numeric or invalid id")); 1.47 +} 1.48 + 1.49 +function throwReadOnly(aWindow) { 1.50 + return aWindow.Promise.reject( 1.51 + new aWindow.DOMError("ReadOnlyError", "DataStore in readonly mode")); 1.52 +} 1.53 + 1.54 +function validateId(aId) { 1.55 + // If string, it cannot be empty. 1.56 + if (typeof(aId) == 'string') { 1.57 + return aId.length; 1.58 + } 1.59 + 1.60 + aId = parseInt(aId); 1.61 + return (!isNaN(aId) && aId > 0); 1.62 +} 1.63 + 1.64 +/* DataStore object */ 1.65 +this.DataStore = function(aWindow, aName, aOwner, aReadOnly) { 1.66 + debug("DataStore created"); 1.67 + this.init(aWindow, aName, aOwner, aReadOnly); 1.68 +} 1.69 + 1.70 +this.DataStore.prototype = { 1.71 + classDescription: "DataStore XPCOM Component", 1.72 + classID: Components.ID("{db5c9602-030f-4bff-a3de-881a8de370f2}"), 1.73 + contractID: "@mozilla.org/dom/datastore-impl;1", 1.74 + QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsISupports, 1.75 + Components.interfaces.nsIObserver]), 1.76 + 1.77 + callbacks: [], 1.78 + 1.79 + _window: null, 1.80 + _name: null, 1.81 + _owner: null, 1.82 + _readOnly: null, 1.83 + _revisionId: null, 1.84 + _exposedObject: null, 1.85 + _cursor: null, 1.86 + _shuttingdown: false, 1.87 + _eventTarget: null, 1.88 + 1.89 + init: function(aWindow, aName, aOwner, aReadOnly) { 1.90 + debug("DataStore init"); 1.91 + 1.92 + this._window = aWindow; 1.93 + this._name = aName; 1.94 + this._owner = aOwner; 1.95 + this._readOnly = aReadOnly; 1.96 + 1.97 + this._db = new DataStoreDB(); 1.98 + this._db.init(aOwner, aName); 1.99 + 1.100 + Services.obs.addObserver(this, "inner-window-destroyed", false); 1.101 + 1.102 + let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) 1.103 + .getInterface(Ci.nsIDOMWindowUtils); 1.104 + this._innerWindowID = util.currentInnerWindowID; 1.105 + 1.106 + cpmm.addMessageListener("DataStore:Changed:Return:OK", this); 1.107 + cpmm.sendAsyncMessage("DataStore:RegisterForMessages", 1.108 + { store: this._name, owner: this._owner }); 1.109 + }, 1.110 + 1.111 + observe: function(aSubject, aTopic, aData) { 1.112 + let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data; 1.113 + if (wId == this._innerWindowID) { 1.114 + Services.obs.removeObserver(this, "inner-window-destroyed"); 1.115 + 1.116 + cpmm.removeMessageListener("DataStore:Changed:Return:OK", this); 1.117 + cpmm.sendAsyncMessage("DataStore:UnregisterForMessages"); 1.118 + this._shuttingdown = true; 1.119 + this._db.close(); 1.120 + } 1.121 + }, 1.122 + 1.123 + setEventTarget: function(aEventTarget) { 1.124 + this._eventTarget = aEventTarget; 1.125 + }, 1.126 + 1.127 + newDBPromise: function(aTxnType, aFunction) { 1.128 + let self = this; 1.129 + return new this._window.Promise(function(aResolve, aReject) { 1.130 + debug("DBPromise started"); 1.131 + self._db.txn( 1.132 + aTxnType, 1.133 + function(aTxn, aStore, aRevisionStore) { 1.134 + debug("DBPromise success"); 1.135 + aFunction(aResolve, aReject, aTxn, aStore, aRevisionStore); 1.136 + }, 1.137 + function(aEvent) { 1.138 + debug("DBPromise error"); 1.139 + aReject(createDOMError(self._window, aEvent)); 1.140 + } 1.141 + ); 1.142 + }); 1.143 + }, 1.144 + 1.145 + checkRevision: function(aReject, aRevisionStore, aRevisionId, aCallback) { 1.146 + if (!aRevisionId) { 1.147 + aCallback(); 1.148 + return; 1.149 + } 1.150 + 1.151 + let self = this; 1.152 + 1.153 + let request = aRevisionStore.openCursor(null, 'prev'); 1.154 + request.onsuccess = function(aEvent) { 1.155 + let cursor = aEvent.target.result; 1.156 + if (!cursor) { 1.157 + dump("This cannot really happen."); 1.158 + return; 1.159 + } 1.160 + 1.161 + if (cursor.value.revisionId != aRevisionId) { 1.162 + aReject(new self._window.DOMError("ConstraintError", 1.163 + "RevisionId is not up-to-date")); 1.164 + return; 1.165 + } 1.166 + 1.167 + aCallback(); 1.168 + } 1.169 + }, 1.170 + 1.171 + getInternal: function(aStore, aIds, aCallback) { 1.172 + debug("GetInternal: " + aIds.toSource()); 1.173 + 1.174 + // Creation of the results array. 1.175 + let results = new Array(aIds.length); 1.176 + 1.177 + // We're going to create this amount of requests. 1.178 + let pendingIds = aIds.length; 1.179 + let indexPos = 0; 1.180 + 1.181 + let self = this; 1.182 + 1.183 + function getInternalSuccess(aEvent, aPos) { 1.184 + debug("GetInternal success. Record: " + aEvent.target.result); 1.185 + results[aPos] = Cu.cloneInto(aEvent.target.result, self._window); 1.186 + if (!--pendingIds) { 1.187 + aCallback(results); 1.188 + return; 1.189 + } 1.190 + 1.191 + if (indexPos < aIds.length) { 1.192 + // Just MAX_REQUESTS requests at the same time. 1.193 + let count = 0; 1.194 + while (indexPos < aIds.length && ++count < MAX_REQUESTS) { 1.195 + getInternalRequest(); 1.196 + } 1.197 + } 1.198 + } 1.199 + 1.200 + function getInternalRequest() { 1.201 + let currentPos = indexPos++; 1.202 + let request = aStore.get(aIds[currentPos]); 1.203 + request.onsuccess = function(aEvent) { 1.204 + getInternalSuccess(aEvent, currentPos); 1.205 + } 1.206 + } 1.207 + 1.208 + getInternalRequest(); 1.209 + }, 1.210 + 1.211 + putInternal: function(aResolve, aStore, aRevisionStore, aObj, aId) { 1.212 + debug("putInternal " + aId); 1.213 + 1.214 + let self = this; 1.215 + let request = aStore.put(aObj, aId); 1.216 + request.onsuccess = function(aEvent) { 1.217 + debug("putInternal success"); 1.218 + 1.219 + self.addRevision(aRevisionStore, aId, REVISION_UPDATED, 1.220 + function() { 1.221 + debug("putInternal - revisionId increased"); 1.222 + // No wrap here because the result is always a int. 1.223 + aResolve(aEvent.target.result); 1.224 + } 1.225 + ); 1.226 + }; 1.227 + }, 1.228 + 1.229 + addInternal: function(aResolve, aStore, aRevisionStore, aObj, aId) { 1.230 + debug("AddInternal"); 1.231 + 1.232 + let self = this; 1.233 + let request = aStore.add(aObj, aId); 1.234 + request.onsuccess = function(aEvent) { 1.235 + debug("Request successful. Id: " + aEvent.target.result); 1.236 + self.addRevision(aRevisionStore, aEvent.target.result, REVISION_ADDED, 1.237 + function() { 1.238 + debug("AddInternal - revisionId increased"); 1.239 + // No wrap here because the result is always a int. 1.240 + aResolve(aEvent.target.result); 1.241 + } 1.242 + ); 1.243 + }; 1.244 + }, 1.245 + 1.246 + removeInternal: function(aResolve, aStore, aRevisionStore, aId) { 1.247 + debug("RemoveInternal"); 1.248 + 1.249 + let self = this; 1.250 + let request = aStore.get(aId); 1.251 + request.onsuccess = function(aEvent) { 1.252 + debug("RemoveInternal success. Record: " + aEvent.target.result); 1.253 + if (aEvent.target.result === undefined) { 1.254 + aResolve(false); 1.255 + return; 1.256 + } 1.257 + 1.258 + let deleteRequest = aStore.delete(aId); 1.259 + deleteRequest.onsuccess = function() { 1.260 + debug("RemoveInternal success"); 1.261 + self.addRevision(aRevisionStore, aId, REVISION_REMOVED, 1.262 + function() { 1.263 + aResolve(true); 1.264 + } 1.265 + ); 1.266 + }; 1.267 + }; 1.268 + }, 1.269 + 1.270 + clearInternal: function(aResolve, aStore, aRevisionStore) { 1.271 + debug("ClearInternal"); 1.272 + 1.273 + let self = this; 1.274 + let request = aStore.clear(); 1.275 + request.onsuccess = function() { 1.276 + debug("ClearInternal success"); 1.277 + self._db.clearRevisions(aRevisionStore, 1.278 + function() { 1.279 + debug("Revisions cleared"); 1.280 + 1.281 + self.addRevision(aRevisionStore, 0, REVISION_VOID, 1.282 + function() { 1.283 + debug("ClearInternal - revisionId increased"); 1.284 + aResolve(); 1.285 + } 1.286 + ); 1.287 + } 1.288 + ); 1.289 + }; 1.290 + }, 1.291 + 1.292 + getLengthInternal: function(aResolve, aStore) { 1.293 + debug("GetLengthInternal"); 1.294 + 1.295 + let request = aStore.count(); 1.296 + request.onsuccess = function(aEvent) { 1.297 + debug("GetLengthInternal success: " + aEvent.target.result); 1.298 + // No wrap here because the result is always a int. 1.299 + aResolve(aEvent.target.result); 1.300 + }; 1.301 + }, 1.302 + 1.303 + addRevision: function(aRevisionStore, aId, aType, aSuccessCb) { 1.304 + let self = this; 1.305 + this._db.addRevision(aRevisionStore, aId, aType, 1.306 + function(aRevisionId) { 1.307 + self._revisionId = aRevisionId; 1.308 + self.sendNotification(aId, aType, aRevisionId); 1.309 + aSuccessCb(); 1.310 + } 1.311 + ); 1.312 + }, 1.313 + 1.314 + retrieveRevisionId: function(aSuccessCb) { 1.315 + let self = this; 1.316 + this._db.revisionTxn( 1.317 + 'readonly', 1.318 + function(aTxn, aRevisionStore) { 1.319 + debug("RetrieveRevisionId transaction success"); 1.320 + 1.321 + let request = aRevisionStore.openCursor(null, 'prev'); 1.322 + request.onsuccess = function(aEvent) { 1.323 + let cursor = aEvent.target.result; 1.324 + if (cursor) { 1.325 + self._revisionId = cursor.value.revisionId; 1.326 + } 1.327 + 1.328 + aSuccessCb(self._revisionId); 1.329 + }; 1.330 + } 1.331 + ); 1.332 + }, 1.333 + 1.334 + sendNotification: function(aId, aOperation, aRevisionId) { 1.335 + debug("SendNotification"); 1.336 + if (aOperation == REVISION_VOID) { 1.337 + aOperation = "cleared"; 1.338 + } 1.339 + 1.340 + cpmm.sendAsyncMessage("DataStore:Changed", 1.341 + { store: this.name, owner: this._owner, 1.342 + message: { revisionId: aRevisionId, id: aId, 1.343 + operation: aOperation, owner: this._owner } } ); 1.344 + }, 1.345 + 1.346 + receiveMessage: function(aMessage) { 1.347 + debug("receiveMessage"); 1.348 + 1.349 + if (aMessage.name != "DataStore:Changed:Return:OK") { 1.350 + debug("Wrong message: " + aMessage.name); 1.351 + return; 1.352 + } 1.353 + 1.354 + // If this message is not for this DataStore, let's ignore it. 1.355 + if (aMessage.data.owner != this._owner || 1.356 + aMessage.data.store != this._name) { 1.357 + return; 1.358 + } 1.359 + 1.360 + let self = this; 1.361 + 1.362 + this.retrieveRevisionId( 1.363 + function() { 1.364 + // If the window has been destroyed we don't emit the events. 1.365 + if (self._shuttingdown) { 1.366 + return; 1.367 + } 1.368 + 1.369 + // If we have an active cursor we don't emit events. 1.370 + if (self._cursor) { 1.371 + return; 1.372 + } 1.373 + 1.374 + let event = new self._window.DataStoreChangeEvent('change', 1.375 + aMessage.data.message); 1.376 + self._eventTarget.dispatchEvent(event); 1.377 + } 1.378 + ); 1.379 + }, 1.380 + 1.381 + get exposedObject() { 1.382 + debug("get exposedObject"); 1.383 + return this._exposedObject; 1.384 + }, 1.385 + 1.386 + set exposedObject(aObject) { 1.387 + debug("set exposedObject"); 1.388 + this._exposedObject = aObject; 1.389 + }, 1.390 + 1.391 + syncTerminated: function(aCursor) { 1.392 + // This checks is to avoid that an invalid cursor stops a sync. 1.393 + if (this._cursor == aCursor) { 1.394 + this._cursor = null; 1.395 + } 1.396 + }, 1.397 + 1.398 + // Public interface : 1.399 + 1.400 + get name() { 1.401 + return this._name; 1.402 + }, 1.403 + 1.404 + get owner() { 1.405 + return this._owner; 1.406 + }, 1.407 + 1.408 + get readOnly() { 1.409 + return this._readOnly; 1.410 + }, 1.411 + 1.412 + get: function() { 1.413 + let ids = Array.prototype.slice.call(arguments); 1.414 + for (let i = 0; i < ids.length; ++i) { 1.415 + if (!validateId(ids[i])) { 1.416 + return throwInvalidArg(this._window); 1.417 + } 1.418 + } 1.419 + 1.420 + let self = this; 1.421 + 1.422 + // Promise<Object> 1.423 + return this.newDBPromise("readonly", 1.424 + function(aResolve, aReject, aTxn, aStore, aRevisionStore) { 1.425 + self.getInternal(aStore, ids, 1.426 + function(aResults) { 1.427 + aResolve(ids.length > 1 ? aResults : aResults[0]); 1.428 + }); 1.429 + } 1.430 + ); 1.431 + }, 1.432 + 1.433 + put: function(aObj, aId, aRevisionId) { 1.434 + if (!validateId(aId)) { 1.435 + return throwInvalidArg(this._window); 1.436 + } 1.437 + 1.438 + if (this._readOnly) { 1.439 + return throwReadOnly(this._window); 1.440 + } 1.441 + 1.442 + let self = this; 1.443 + 1.444 + // Promise<void> 1.445 + return this.newDBPromise("readwrite", 1.446 + function(aResolve, aReject, aTxn, aStore, aRevisionStore) { 1.447 + self.checkRevision(aReject, aRevisionStore, aRevisionId, function() { 1.448 + self.putInternal(aResolve, aStore, aRevisionStore, aObj, aId); 1.449 + }); 1.450 + } 1.451 + ); 1.452 + }, 1.453 + 1.454 + add: function(aObj, aId, aRevisionId) { 1.455 + if (aId) { 1.456 + if (!validateId(aId)) { 1.457 + return throwInvalidArg(this._window); 1.458 + } 1.459 + } 1.460 + 1.461 + if (this._readOnly) { 1.462 + return throwReadOnly(this._window); 1.463 + } 1.464 + 1.465 + let self = this; 1.466 + 1.467 + // Promise<int> 1.468 + return this.newDBPromise("readwrite", 1.469 + function(aResolve, aReject, aTxn, aStore, aRevisionStore) { 1.470 + self.checkRevision(aReject, aRevisionStore, aRevisionId, function() { 1.471 + self.addInternal(aResolve, aStore, aRevisionStore, aObj, aId); 1.472 + }); 1.473 + } 1.474 + ); 1.475 + }, 1.476 + 1.477 + remove: function(aId, aRevisionId) { 1.478 + if (!validateId(aId)) { 1.479 + return throwInvalidArg(this._window); 1.480 + } 1.481 + 1.482 + if (this._readOnly) { 1.483 + return throwReadOnly(this._window); 1.484 + } 1.485 + 1.486 + let self = this; 1.487 + 1.488 + // Promise<void> 1.489 + return this.newDBPromise("readwrite", 1.490 + function(aResolve, aReject, aTxn, aStore, aRevisionStore) { 1.491 + self.checkRevision(aReject, aRevisionStore, aRevisionId, function() { 1.492 + self.removeInternal(aResolve, aStore, aRevisionStore, aId); 1.493 + }); 1.494 + } 1.495 + ); 1.496 + }, 1.497 + 1.498 + clear: function(aRevisionId) { 1.499 + if (this._readOnly) { 1.500 + return throwReadOnly(this._window); 1.501 + } 1.502 + 1.503 + let self = this; 1.504 + 1.505 + // Promise<void> 1.506 + return this.newDBPromise("readwrite", 1.507 + function(aResolve, aReject, aTxn, aStore, aRevisionStore) { 1.508 + self.checkRevision(aReject, aRevisionStore, aRevisionId, function() { 1.509 + self.clearInternal(aResolve, aStore, aRevisionStore); 1.510 + }); 1.511 + } 1.512 + ); 1.513 + }, 1.514 + 1.515 + get revisionId() { 1.516 + return this._revisionId; 1.517 + }, 1.518 + 1.519 + getLength: function() { 1.520 + let self = this; 1.521 + 1.522 + // Promise<int> 1.523 + return this.newDBPromise("readonly", 1.524 + function(aResolve, aReject, aTxn, aStore, aRevisionStore) { 1.525 + self.getLengthInternal(aResolve, aStore); 1.526 + } 1.527 + ); 1.528 + }, 1.529 + 1.530 + sync: function(aRevisionId) { 1.531 + debug("Sync"); 1.532 + this._cursor = new DataStoreCursor(this._window, this, aRevisionId); 1.533 + 1.534 + let cursorImpl = this._window.DataStoreCursorImpl. 1.535 + _create(this._window, this._cursor); 1.536 + 1.537 + let exposedCursor = new this._window.DataStoreCursor(); 1.538 + exposedCursor.setDataStoreCursorImpl(cursorImpl); 1.539 + return exposedCursor; 1.540 + } 1.541 +};