Bug 1073336 part 5 - Add AnimationPlayerCollection::PlayerUpdated; r=dbaron
[gecko.git] / dom / datastore / DataStoreImpl.js
blob6c47605c320c8b95e9fff2341cbaf0e763948117
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/. */
7 'use strict'
9 function debug(s) {
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",
32                                    "nsIMessageSender");
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') {
52     return aId.length;
53   }
55   aId = parseInt(aId);
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,
69                                          Ci.nsIObserver]),
71   callbacks: [],
73   _window: null,
74   _name: null,
75   _owner: null,
76   _readOnly: null,
77   _revisionId: null,
78   _exposedObject: null,
79   _cursor: null,
80   _shuttingdown: false,
81   _eventTarget: null,
83   init: function(aWindow, aName, aOwner, aReadOnly) {
84     debug("DataStore init");
86     this._window = aWindow;
87     this._name = aName;
88     this._owner = aOwner;
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 },
104                           null,
105                           this._window.document.nodePrincipal);
106   },
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 },
116                             null,
117                             this._window.document.nodePrincipal);
118       this._shuttingdown = true;
119       this._db.close();
120     }
121   },
123   setEventTarget: function(aEventTarget) {
124     this._eventTarget = aEventTarget;
125   },
127   newDBPromise: function(aTxnType, aFunction) {
128     let self = this;
129     return new this._window.Promise(function(aResolve, aReject) {
130       debug("DBPromise started");
131       self._db.txn(
132         aTxnType,
133         function(aTxn, aStore, aRevisionStore) {
134           debug("DBPromise success");
135           aFunction(aResolve, aReject, aTxn, aStore, aRevisionStore);
136         },
137         function(aEvent) {
138           debug("DBPromise error");
139           aReject(createDOMError(self._window, aEvent));
140         }
141       );
142     });
143   },
145   checkRevision: function(aReject, aRevisionStore, aRevisionId, aCallback) {
146     if (!aRevisionId) {
147       aCallback();
148       return;
149     }
151     let self = this;
153     let request = aRevisionStore.openCursor(null, 'prev');
154     request.onsuccess = function(aEvent) {
155       let cursor = aEvent.target.result;
156       if (!cursor) {
157         dump("This cannot really happen.");
158         return;
159       }
161       if (cursor.value.revisionId != aRevisionId) {
162         aReject(new self._window.DOMError("ConstraintError",
163                                           "RevisionId is not up-to-date"));
164         return;
165       }
167       aCallback();
168     }
169   },
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;
179     let indexPos = 0;
181     let self = this;
183     function getInternalSuccess(aEvent, aPos) {
184       debug("GetInternal success. Record: " + aEvent.target.result);
185       results[aPos] = Cu.cloneInto(aEvent.target.result, self._window);
186       if (!--pendingIds) {
187         aCallback(results);
188         return;
189       }
191       if (indexPos < aIds.length) {
192         // Just MAX_REQUESTS requests at the same time.
193         let count = 0;
194         while (indexPos < aIds.length && ++count < MAX_REQUESTS) {
195           getInternalRequest();
196         }
197       }
198     }
200     function getInternalRequest() {
201       let currentPos = indexPos++;
202       let request = aStore.get(aIds[currentPos]);
203       request.onsuccess = function(aEvent) {
204         getInternalSuccess(aEvent, currentPos);
205       }
206     }
208     getInternalRequest();
209   },
211   putInternal: function(aResolve, aStore, aRevisionStore, aObj, aId) {
212     debug("putInternal " + aId);
214     let self = this;
215     let request = aStore.put(aObj, aId);
216     request.onsuccess = function(aEvent) {
217       debug("putInternal success");
219       self.addRevision(aRevisionStore, aId, REVISION_UPDATED,
220         function() {
221           debug("putInternal - revisionId increased");
222           // No wrap here because the result is always a int.
223           aResolve(aEvent.target.result);
224         }
225       );
226     };
227   },
229   addInternal: function(aResolve, aStore, aRevisionStore, aObj, aId) {
230     debug("AddInternal");
232     let self = this;
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,
237         function() {
238           debug("AddInternal - revisionId increased");
239           // No wrap here because the result is always a int.
240           aResolve(aEvent.target.result);
241         }
242       );
243     };
244   },
246   removeInternal: function(aResolve, aStore, aRevisionStore, aId) {
247     debug("RemoveInternal");
249     let self = this;
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) {
254         aResolve(false);
255         return;
256       }
258       let deleteRequest = aStore.delete(aId);
259       deleteRequest.onsuccess = function() {
260         debug("RemoveInternal success");
261         self.addRevision(aRevisionStore, aId, REVISION_REMOVED,
262           function() {
263             aResolve(true);
264           }
265         );
266       };
267     };
268   },
270   clearInternal: function(aResolve, aStore, aRevisionStore) {
271     debug("ClearInternal");
273     let self = this;
274     let request = aStore.clear();
275     request.onsuccess = function() {
276       debug("ClearInternal success");
277       self._db.clearRevisions(aRevisionStore,
278         function() {
279           debug("Revisions cleared");
281           self.addRevision(aRevisionStore, null, REVISION_VOID,
282             function() {
283               debug("ClearInternal - revisionId increased");
284               aResolve();
285             }
286           );
287         }
288       );
289     };
290   },
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);
300     };
301   },
303   addRevision: function(aRevisionStore, aId, aType, aSuccessCb) {
304     let self = this;
305     this._db.addRevision(aRevisionStore, aId, aType,
306       function(aRevisionId) {
307         self._revisionId = aRevisionId;
308         self.sendNotification(aId, aType, aRevisionId);
309         aSuccessCb();
310       }
311     );
312   },
314   retrieveRevisionId: function(aSuccessCb) {
315     let self = this;
316     this._db.revisionTxn(
317       'readonly',
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;
324           if (cursor) {
325             self._revisionId = cursor.value.revisionId;
326           }
328           aSuccessCb(self._revisionId);
329         };
330       }
331     );
332   },
334   sendNotification: function(aId, aOperation, aRevisionId) {
335     debug("SendNotification");
336     if (aOperation == REVISION_VOID) {
337       aOperation = "cleared";
338     }
340     cpmm.sendAsyncMessage("DataStore:Changed",
341                           { store: this.name, owner: this._owner,
342                             message: { revisionId: aRevisionId, id: aId,
343                                        operation: aOperation, owner: this._owner } },
344                           null,
345                           this._window.document.nodePrincipal);
346   },
348   receiveMessage: function(aMessage) {
349     debug("receiveMessage");
351     if (aMessage.name != "DataStore:Changed:Return:OK") {
352       debug("Wrong message: " + aMessage.name);
353       return;
354     }
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) {
359       return;
360     }
362     let self = this;
364     this.retrieveRevisionId(
365       function() {
366         // If the window has been destroyed we don't emit the events.
367         if (self._shuttingdown) {
368           return;
369         }
371         // If we have an active cursor we don't emit events.
372         if (self._cursor) {
373           return;
374         }
376         let event = new self._window.DataStoreChangeEvent('change',
377                                                           aMessage.data.message);
378         self._eventTarget.dispatchEvent(event);
379       }
380     );
381   },
383   get exposedObject() {
384     debug("get exposedObject");
385     return this._exposedObject;
386   },
388   set exposedObject(aObject) {
389     debug("set exposedObject");
390     this._exposedObject = aObject;
391   },
393   syncTerminated: function(aCursor) {
394     // This checks is to avoid that an invalid cursor stops a sync.
395     if (this._cursor == aCursor) {
396       this._cursor = null;
397     }
398   },
400   // Public interface :
402   get name() {
403     return this._name;
404   },
406   get owner() {
407     return this._owner;
408   },
410   get readOnly() {
411     return this._readOnly;
412   },
414   get: function() {
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);
419       }
420     }
422     if (ids.length == 0) {
423       return this._window.Promise.resolve(new this._window.Array());
424     }
426     let self = this;
428     // Promise<Object>
429     return this.newDBPromise("readonly",
430       function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
431                self.getInternal(aStore, ids,
432                                 function(aResults) {
433           aResolve(ids.length > 1 ? aResults : aResults[0]);
434         });
435       }
436     );
437   },
439   put: function(aObj, aId, aRevisionId) {
440     if (!validateId(aId)) {
441       return throwInvalidArg(this._window);
442     }
444     if (this._readOnly) {
445       return throwReadOnly(this._window);
446     }
448     let self = this;
450     // Promise<void>
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);
455         });
456       }
457     );
458   },
460   add: function(aObj, aId, aRevisionId) {
461     if (aId) {
462       if (!validateId(aId)) {
463         return throwInvalidArg(this._window);
464       }
465     }
467     if (this._readOnly) {
468       return throwReadOnly(this._window);
469     }
471     let self = this;
473     // Promise<int>
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);
478         });
479       }
480     );
481   },
483   remove: function(aId, aRevisionId) {
484     if (!validateId(aId)) {
485       return throwInvalidArg(this._window);
486     }
488     if (this._readOnly) {
489       return throwReadOnly(this._window);
490     }
492     let self = this;
494     // Promise<void>
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);
499         });
500       }
501     );
502   },
504   clear: function(aRevisionId) {
505     if (this._readOnly) {
506       return throwReadOnly(this._window);
507     }
509     let self = this;
511     // Promise<void>
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);
516         });
517       }
518     );
519   },
521   get revisionId() {
522     return this._revisionId;
523   },
525   getLength: function() {
526     let self = this;
528     // Promise<int>
529     return this.newDBPromise("readonly",
530       function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
531         self.getLengthInternal(aResolve, aStore);
532       }
533     );
534   },
536   sync: function(aRevisionId) {
537     debug("Sync");
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;
546   }
549 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DataStore]);