1 /* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
10 //dump('DEBUG DataStore: ' + s + '\n');
13 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
15 const REVISION_ADDED = "added";
16 const REVISION_UPDATED = "updated";
17 const REVISION_REMOVED = "removed";
18 const REVISION_VOID = "void";
20 // This value has to be tuned a bit. Currently it's just a guess
21 // and yet we don't know if it's too low or too high.
22 const MAX_REQUESTS = 25;
24 Cu.import("resource://gre/modules/DataStoreCursorImpl.jsm");
25 Cu.import("resource://gre/modules/DataStoreDB.jsm");
26 Cu.import('resource://gre/modules/Services.jsm');
27 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
28 Cu.importGlobalProperties(["indexedDB"]);
30 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
31 "@mozilla.org/childprocessmessagemanager;1",
34 /* Helper functions */
35 function createDOMError(aWindow, aEvent) {
36 return new aWindow.DOMError(aEvent);
39 function throwInvalidArg(aWindow) {
40 return aWindow.Promise.reject(
41 new aWindow.DOMError("SyntaxError", "Non-numeric or invalid id"));
44 function throwReadOnly(aWindow) {
45 return aWindow.Promise.reject(
46 new aWindow.DOMError("ReadOnlyError", "DataStore in readonly mode"));
49 function validateId(aId) {
50 // If string, it cannot be empty.
51 if (typeof(aId) == 'string') {
56 return (!isNaN(aId) && aId > 0);
59 /* DataStore object */
60 function DataStore() {
61 debug("DataStore created");
64 DataStore.prototype = {
65 classDescription: "DataStore XPCOM Component",
66 classID: Components.ID("{db5c9602-030f-4bff-a3de-881a8de370f2}"),
67 contractID: "@mozilla.org/dom/datastore-impl;1",
68 QueryInterface: XPCOMUtils.generateQI([Ci.nsIDataStore, Ci.nsISupports,
83 init: function(aWindow, aName, aOwner, aReadOnly) {
84 debug("DataStore init");
86 this._window = aWindow;
89 this._readOnly = aReadOnly;
91 this._db = new DataStoreDB();
92 this._db.init(aOwner, aName);
94 Services.obs.addObserver(this, "inner-window-destroyed", false);
96 let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
97 .getInterface(Ci.nsIDOMWindowUtils);
98 this._innerWindowID = util.currentInnerWindowID;
100 cpmm.addMessageListener("DataStore:Changed:Return:OK", this);
101 cpmm.sendAsyncMessage("DataStore:RegisterForMessages",
102 { store: this._name, owner: this._owner,
103 innerWindowID: this._innerWindowID },
105 this._window.document.nodePrincipal);
108 observe: function(aSubject, aTopic, aData) {
109 let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
110 if (wId == this._innerWindowID) {
111 Services.obs.removeObserver(this, "inner-window-destroyed");
113 cpmm.removeMessageListener("DataStore:Changed:Return:OK", this);
114 cpmm.sendAsyncMessage("DataStore:UnregisterForMessages",
115 { innerWindowID: this._innerWindowID },
117 this._window.document.nodePrincipal);
118 this._shuttingdown = true;
123 setEventTarget: function(aEventTarget) {
124 this._eventTarget = aEventTarget;
127 newDBPromise: function(aTxnType, aFunction) {
129 return new this._window.Promise(function(aResolve, aReject) {
130 debug("DBPromise started");
133 function(aTxn, aStore, aRevisionStore) {
134 debug("DBPromise success");
135 aFunction(aResolve, aReject, aTxn, aStore, aRevisionStore);
138 debug("DBPromise error");
139 aReject(createDOMError(self._window, aEvent));
145 checkRevision: function(aReject, aRevisionStore, aRevisionId, aCallback) {
153 let request = aRevisionStore.openCursor(null, 'prev');
154 request.onsuccess = function(aEvent) {
155 let cursor = aEvent.target.result;
157 dump("This cannot really happen.");
161 if (cursor.value.revisionId != aRevisionId) {
162 aReject(new self._window.DOMError("ConstraintError",
163 "RevisionId is not up-to-date"));
171 getInternal: function(aStore, aIds, aCallback) {
172 debug("GetInternal: " + aIds.toSource());
174 // Creation of the results array.
175 let results = new this._window.Array(aIds.length);
177 // We're going to create this amount of requests.
178 let pendingIds = aIds.length;
183 function getInternalSuccess(aEvent, aPos) {
184 debug("GetInternal success. Record: " + aEvent.target.result);
185 results[aPos] = Cu.cloneInto(aEvent.target.result, self._window);
191 if (indexPos < aIds.length) {
192 // Just MAX_REQUESTS requests at the same time.
194 while (indexPos < aIds.length && ++count < MAX_REQUESTS) {
195 getInternalRequest();
200 function getInternalRequest() {
201 let currentPos = indexPos++;
202 let request = aStore.get(aIds[currentPos]);
203 request.onsuccess = function(aEvent) {
204 getInternalSuccess(aEvent, currentPos);
208 getInternalRequest();
211 putInternal: function(aResolve, aStore, aRevisionStore, aObj, aId) {
212 debug("putInternal " + aId);
215 let request = aStore.put(aObj, aId);
216 request.onsuccess = function(aEvent) {
217 debug("putInternal success");
219 self.addRevision(aRevisionStore, aId, REVISION_UPDATED,
221 debug("putInternal - revisionId increased");
222 // No wrap here because the result is always a int.
223 aResolve(aEvent.target.result);
229 addInternal: function(aResolve, aStore, aRevisionStore, aObj, aId) {
230 debug("AddInternal");
233 let request = aStore.add(aObj, aId);
234 request.onsuccess = function(aEvent) {
235 debug("Request successful. Id: " + aEvent.target.result);
236 self.addRevision(aRevisionStore, aEvent.target.result, REVISION_ADDED,
238 debug("AddInternal - revisionId increased");
239 // No wrap here because the result is always a int.
240 aResolve(aEvent.target.result);
246 removeInternal: function(aResolve, aStore, aRevisionStore, aId) {
247 debug("RemoveInternal");
250 let request = aStore.get(aId);
251 request.onsuccess = function(aEvent) {
252 debug("RemoveInternal success. Record: " + aEvent.target.result);
253 if (aEvent.target.result === undefined) {
258 let deleteRequest = aStore.delete(aId);
259 deleteRequest.onsuccess = function() {
260 debug("RemoveInternal success");
261 self.addRevision(aRevisionStore, aId, REVISION_REMOVED,
270 clearInternal: function(aResolve, aStore, aRevisionStore) {
271 debug("ClearInternal");
274 let request = aStore.clear();
275 request.onsuccess = function() {
276 debug("ClearInternal success");
277 self._db.clearRevisions(aRevisionStore,
279 debug("Revisions cleared");
281 self.addRevision(aRevisionStore, null, REVISION_VOID,
283 debug("ClearInternal - revisionId increased");
292 getLengthInternal: function(aResolve, aStore) {
293 debug("GetLengthInternal");
295 let request = aStore.count();
296 request.onsuccess = function(aEvent) {
297 debug("GetLengthInternal success: " + aEvent.target.result);
298 // No wrap here because the result is always a int.
299 aResolve(aEvent.target.result);
303 addRevision: function(aRevisionStore, aId, aType, aSuccessCb) {
305 this._db.addRevision(aRevisionStore, aId, aType,
306 function(aRevisionId) {
307 self._revisionId = aRevisionId;
308 self.sendNotification(aId, aType, aRevisionId);
314 retrieveRevisionId: function(aSuccessCb) {
316 this._db.revisionTxn(
318 function(aTxn, aRevisionStore) {
319 debug("RetrieveRevisionId transaction success");
321 let request = aRevisionStore.openCursor(null, 'prev');
322 request.onsuccess = function(aEvent) {
323 let cursor = aEvent.target.result;
325 self._revisionId = cursor.value.revisionId;
328 aSuccessCb(self._revisionId);
334 sendNotification: function(aId, aOperation, aRevisionId) {
335 debug("SendNotification");
336 if (aOperation == REVISION_VOID) {
337 aOperation = "cleared";
340 cpmm.sendAsyncMessage("DataStore:Changed",
341 { store: this.name, owner: this._owner,
342 message: { revisionId: aRevisionId, id: aId,
343 operation: aOperation, owner: this._owner } },
345 this._window.document.nodePrincipal);
348 receiveMessage: function(aMessage) {
349 debug("receiveMessage");
351 if (aMessage.name != "DataStore:Changed:Return:OK") {
352 debug("Wrong message: " + aMessage.name);
356 // If this message is not for this DataStore, let's ignore it.
357 if (aMessage.data.owner != this._owner ||
358 aMessage.data.store != this._name) {
364 this.retrieveRevisionId(
366 // If the window has been destroyed we don't emit the events.
367 if (self._shuttingdown) {
371 // If we have an active cursor we don't emit events.
376 let event = new self._window.DataStoreChangeEvent('change',
377 aMessage.data.message);
378 self._eventTarget.dispatchEvent(event);
383 get exposedObject() {
384 debug("get exposedObject");
385 return this._exposedObject;
388 set exposedObject(aObject) {
389 debug("set exposedObject");
390 this._exposedObject = aObject;
393 syncTerminated: function(aCursor) {
394 // This checks is to avoid that an invalid cursor stops a sync.
395 if (this._cursor == aCursor) {
400 // Public interface :
411 return this._readOnly;
415 let ids = Array.prototype.slice.call(arguments);
416 for (let i = 0; i < ids.length; ++i) {
417 if (!validateId(ids[i])) {
418 return throwInvalidArg(this._window);
422 if (ids.length == 0) {
423 return this._window.Promise.resolve(new this._window.Array());
429 return this.newDBPromise("readonly",
430 function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
431 self.getInternal(aStore, ids,
433 aResolve(ids.length > 1 ? aResults : aResults[0]);
439 put: function(aObj, aId, aRevisionId) {
440 if (!validateId(aId)) {
441 return throwInvalidArg(this._window);
444 if (this._readOnly) {
445 return throwReadOnly(this._window);
451 return this.newDBPromise("readwrite",
452 function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
453 self.checkRevision(aReject, aRevisionStore, aRevisionId, function() {
454 self.putInternal(aResolve, aStore, aRevisionStore, aObj, aId);
460 add: function(aObj, aId, aRevisionId) {
462 if (!validateId(aId)) {
463 return throwInvalidArg(this._window);
467 if (this._readOnly) {
468 return throwReadOnly(this._window);
474 return this.newDBPromise("readwrite",
475 function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
476 self.checkRevision(aReject, aRevisionStore, aRevisionId, function() {
477 self.addInternal(aResolve, aStore, aRevisionStore, aObj, aId);
483 remove: function(aId, aRevisionId) {
484 if (!validateId(aId)) {
485 return throwInvalidArg(this._window);
488 if (this._readOnly) {
489 return throwReadOnly(this._window);
495 return this.newDBPromise("readwrite",
496 function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
497 self.checkRevision(aReject, aRevisionStore, aRevisionId, function() {
498 self.removeInternal(aResolve, aStore, aRevisionStore, aId);
504 clear: function(aRevisionId) {
505 if (this._readOnly) {
506 return throwReadOnly(this._window);
512 return this.newDBPromise("readwrite",
513 function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
514 self.checkRevision(aReject, aRevisionStore, aRevisionId, function() {
515 self.clearInternal(aResolve, aStore, aRevisionStore);
522 return this._revisionId;
525 getLength: function() {
529 return this.newDBPromise("readonly",
530 function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
531 self.getLengthInternal(aResolve, aStore);
536 sync: function(aRevisionId) {
538 this._cursor = new DataStoreCursor(this._window, this, aRevisionId);
540 let cursorImpl = this._window.DataStoreCursorImpl.
541 _create(this._window, this._cursor);
543 let exposedCursor = new this._window.DataStoreCursor();
544 exposedCursor.setDataStoreCursorImpl(cursorImpl);
545 return exposedCursor;
549 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DataStore]);