Bumping manifests a=b2g-bump
[gecko.git] / dom / datastore / DataStoreCursorImpl.jsm
blob0efa74f44d9eba3d8d3b5168847b329e787e5ba4
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 this.EXPORTED_SYMBOLS = ['DataStoreCursor'];
11 function debug(s) {
12   //dump('DEBUG DataStoreCursor: ' + s + '\n');
15 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
17 const STATE_INIT = 0;
18 const STATE_REVISION_INIT = 1;
19 const STATE_REVISION_CHECK = 2;
20 const STATE_SEND_ALL = 3;
21 const STATE_REVISION_SEND = 4;
22 const STATE_DONE = 5;
24 const REVISION_ADDED = 'added';
25 const REVISION_UPDATED = 'updated';
26 const REVISION_REMOVED = 'removed';
27 const REVISION_VOID = 'void';
28 const REVISION_SKIP = 'skip'
30 Cu.import('resource://gre/modules/Services.jsm');
31 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
33 /**
34  * legend:
35  * - RID = revision ID
36  * - R = revision object (with the internalRevisionId that is a number)
37  * - X = current object ID.
38  * - L = the list of revisions that we have to send
39  *
40  * State: init: do you have RID ?
41  *   YES: state->initRevision; loop
42  *   NO: get R; X=0; state->sendAll; send a 'clear'
43  *
44  * State: initRevision. Get R from RID. Done?
45  *   YES: state->revisionCheck; loop
46  *   NO: RID = null; state->init; loop
47  *
48  * State: revisionCheck: get all the revisions between R and NOW. Done?
49  *   YES and R == NOW: state->done; loop
50  *   YES and R != NOW: Store this revisions in L; state->revisionSend; loop
51  *   NO: R = NOW; X=0; state->sendAll; send a 'clear'
52  *
53  * State: sendAll: is R still the last revision?
54  *   YES get the first object with id > X. Done?
55  *     YES: X = object.id; send 'add'
56  *     NO: state->revisionCheck; loop
57  *   NO: R = NOW; X=0; send a 'clear'
58  *
59  * State: revisionSend: do you have something from L to send?
60  *   YES and L[0] == 'removed': R=L[0]; send 'remove' with ID
61  *   YES and L[0] == 'added': R=L[0]; get the object; found?
62  *     NO: loop
63  *     YES: send 'add' with ID and object
64  *   YES and L[0] == 'updated': R=L[0]; get the object; found?
65  *     NO: loop
66  *     YES and object.R > R: continue
67  *     YES and object.R <= R: send 'update' with ID and object
68  *   YES L[0] == 'void': R=L[0]; state->init; loop
69  *   NO: state->revisionCheck; loop
70  *
71  * State: done: send a 'done' with R
72  */
74 /* Helper functions */
75 function createDOMError(aWindow, aEvent) {
76   return new aWindow.DOMError(aEvent);
79 /* DataStoreCursor object */
80 this.DataStoreCursor = function(aWindow, aDataStore, aRevisionId) {
81   debug("DataStoreCursor created");
82   this.init(aWindow, aDataStore, aRevisionId);
85 this.DataStoreCursor.prototype = {
86   classDescription: 'DataStoreCursor XPCOM Component',
87   classID: Components.ID('{b6d14349-1eab-46b8-8513-584a7328a26b}'),
88   contractID: '@mozilla.org/dom/datastore-cursor-impl;1',
89   QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsISupports]),
91   _shuttingdown: false,
93   _window: null,
94   _dataStore: null,
95   _revisionId: null,
96   _revision: null,
97   _revisionsList: null,
98   _objectId: 0,
100   _state: STATE_INIT,
102   init: function(aWindow, aDataStore, aRevisionId) {
103     debug('DataStoreCursor init');
105     this._window = aWindow;
106     this._dataStore = aDataStore;
107     this._revisionId = aRevisionId;
109     Services.obs.addObserver(this, "inner-window-destroyed", false);
111     let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
112                       .getInterface(Ci.nsIDOMWindowUtils);
113     this._innerWindowID = util.currentInnerWindowID;
114   },
116   observe: function(aSubject, aTopic, aData) {
117     let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
118     if (wId == this._innerWindowID) {
119       Services.obs.removeObserver(this, "inner-window-destroyed");
120       this._shuttingdown = true;
121     }
122   },
124   // This is the implementation of the state machine.
125   // Read the comments at the top of this file in order to follow what it does.
126   stateMachine: function(aStore, aRevisionStore, aResolve, aReject) {
127     debug('StateMachine: ' + this._state);
129     // If the window has been destroyed we cannot create the Promise object.
130     if (this._shuttingdown) {
131       return;
132     }
134     switch (this._state) {
135       case STATE_INIT:
136         this.stateMachineInit(aStore, aRevisionStore, aResolve, aReject);
137         break;
139       case STATE_REVISION_INIT:
140         this.stateMachineRevisionInit(aStore, aRevisionStore, aResolve, aReject);
141         break;
143       case STATE_REVISION_CHECK:
144         this.stateMachineRevisionCheck(aStore, aRevisionStore, aResolve, aReject);
145         break;
147       case STATE_SEND_ALL:
148         this.stateMachineSendAll(aStore, aRevisionStore, aResolve, aReject);
149         break;
151       case STATE_REVISION_SEND:
152         this.stateMachineRevisionSend(aStore, aRevisionStore, aResolve, aReject);
153         break;
155       case STATE_DONE:
156         this.stateMachineDone(aStore, aRevisionStore, aResolve, aReject);
157         break;
158     }
159   },
161   stateMachineInit: function(aStore, aRevisionStore, aResolve, aReject) {
162     debug('StateMachineInit');
164     if (this._revisionId) {
165       this._state = STATE_REVISION_INIT;
166       this.stateMachine(aStore, aRevisionStore, aResolve, aReject);
167       return;
168     }
170     let self = this;
171     let request = aRevisionStore.openCursor(null, 'prev');
172     request.onsuccess = function(aEvent) {
173       if (aEvent.target.result === undefined) {
174         aReject(self._window.DOMError("InvalidRevision",
175                                       "The DataStore is corrupted"));
176         return;
177       }
179       self._revision = aEvent.target.result.value;
180       self._objectId = 0;
181       self._state = STATE_SEND_ALL;
182       aResolve(self.createTask('clear', null, '', null));
183     }
184   },
186   stateMachineRevisionInit: function(aStore, aRevisionStore, aResolve, aReject) {
187     debug('StateMachineRevisionInit');
189     let self = this;
190     let request = this._dataStore._db.getInternalRevisionId(
191       self._revisionId,
192       aRevisionStore,
193       function(aInternalRevisionId) {
194         // This revision doesn't exist.
195         if (aInternalRevisionId == undefined) {
196           self._revisionId = null;
197           self._objectId = 0;
198           self._state = STATE_INIT;
199           self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
200           return;
201         }
203         self._revision = { revisionId: self._revisionId,
204                            internalRevisionId: aInternalRevisionId };
205         self._state = STATE_REVISION_CHECK;
206         self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
207       }
208     );
209   },
211   stateMachineRevisionCheck: function(aStore, aRevisionStore, aResolve, aReject) {
212     debug('StateMachineRevisionCheck');
214     let changes = {
215       addedIds: {},
216       updatedIds: {},
217       removedIds: {}
218     };
220     let self = this;
221     let request = aRevisionStore.mozGetAll(
222       self._window.IDBKeyRange.lowerBound(this._revision.internalRevisionId, true));
223     request.onsuccess = function(aEvent) {
225       // Optimize the operations.
226       for (let i = 0; i < aEvent.target.result.length; ++i) {
227         let data = aEvent.target.result[i];
229         switch (data.operation) {
230           case REVISION_ADDED:
231             changes.addedIds[data.objectId] = data.internalRevisionId;
232             break;
234           case REVISION_UPDATED:
235             // We don't consider an update if this object has been added
236             // or if it has been already modified by a previous
237             // operation.
238             if (!(data.objectId in changes.addedIds) &&
239                 !(data.objectId in changes.updatedIds)) {
240               changes.updatedIds[data.objectId] = data.internalRevisionId;
241             }
242             break;
244           case REVISION_REMOVED:
245             let id = data.objectId;
247             // If the object has been added in this range of revisions
248             // we can ignore it and remove it from the list.
249             if (id in changes.addedIds) {
250               delete changes.addedIds[id];
251             } else {
252               changes.removedIds[id] = data.internalRevisionId;
253             }
255             if (id in changes.updatedIds) {
256               delete changes.updatedIds[id];
257             }
258             break;
260           case REVISION_VOID:
261             if (i != 0) {
262               dump('Internal error: Revision "' + REVISION_VOID + '" should not be found!!!\n');
263               return;
264             }
266             self._revisionId = null;
267             self._objectId = 0;
268             self._state = STATE_INIT;
269             self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
270             return;
271         }
272       }
274       // From changes to a map of internalRevisionId.
275       let revisions = {};
276       function addRevisions(obj) {
277         for (let key in obj) {
278           revisions[obj[key]] = true;
279         }
280       }
282       addRevisions(changes.addedIds);
283       addRevisions(changes.updatedIds);
284       addRevisions(changes.removedIds);
286       // Create the list of revisions.
287       let list = [];
288       for (let i = 0; i < aEvent.target.result.length; ++i) {
289         let data = aEvent.target.result[i];
291         // If this revision doesn't contain useful data, we still need to keep
292         // it in the list because we need to update the internal revision ID.
293         if (!(data.internalRevisionId in revisions)) {
294           data.operation = REVISION_SKIP;
295         }
297         list.push(data);
298       }
300       if (list.length == 0) {
301         self._state = STATE_DONE;
302         self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
303         return;
304       }
306       // Some revision has to be sent.
307       self._revisionsList = list;
308       self._state = STATE_REVISION_SEND;
309       self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
310     };
311   },
313   stateMachineSendAll: function(aStore, aRevisionStore, aResolve, aReject) {
314     debug('StateMachineSendAll');
316     let self = this;
317     let request = aRevisionStore.openCursor(null, 'prev');
318     request.onsuccess = function(aEvent) {
319       if (self._revision.revisionId != aEvent.target.result.value.revisionId) {
320         self._revision = aEvent.target.result.value;
321         self._objectId = 0;
322         aResolve(self.createTask('clear', null, '', null));
323         return;
324       }
326       let request = aStore.openCursor(self._window.IDBKeyRange.lowerBound(self._objectId, true));
327       request.onsuccess = function(aEvent) {
328         let cursor = aEvent.target.result;
329         if (!cursor) {
330           self._state = STATE_REVISION_CHECK;
331           self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
332           return;
333         }
335         self._objectId = cursor.key;
336         aResolve(self.createTask('add', self._objectId, '', cursor.value));
337       };
338     };
339   },
341   stateMachineRevisionSend: function(aStore, aRevisionStore, aResolve, aReject) {
342     debug('StateMachineRevisionSend');
344     if (!this._revisionsList.length) {
345       this._state = STATE_REVISION_CHECK;
346       this.stateMachine(aStore, aRevisionStore, aResolve, aReject);
347       return;
348     }
350     this._revision = this._revisionsList.shift();
352     switch (this._revision.operation) {
353       case REVISION_REMOVED:
354         aResolve(this.createTask('remove', this._revision.objectId, '', null));
355         break;
357       case REVISION_ADDED: {
358         let request = aStore.get(this._revision.objectId);
359         let self = this;
360         request.onsuccess = function(aEvent) {
361           if (aEvent.target.result == undefined) {
362             self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
363             return;
364           }
366           aResolve(self.createTask('add', self._revision.objectId, '',
367                                    aEvent.target.result));
368         }
369         break;
370       }
372       case REVISION_UPDATED: {
373         let request = aStore.get(this._revision.objectId);
374         let self = this;
375         request.onsuccess = function(aEvent) {
376           if (aEvent.target.result == undefined) {
377             self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
378             return;
379           }
381           if (aEvent.target.result.revisionId >  self._revision.internalRevisionId) {
382             self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
383             return;
384           }
386           aResolve(self.createTask('update', self._revision.objectId, '',
387                                    aEvent.target.result));
388         }
389         break;
390       }
392       case REVISION_VOID:
393         // Internal error!
394         dump('Internal error: Revision "' + REVISION_VOID + '" should not be found!!!\n');
395         break;
397       case REVISION_SKIP:
398         // This revision contains data that has already been sent by another one.
399         this.stateMachine(aStore, aRevisionStore, aResolve, aReject);
400         break;
401     }
402   },
404   stateMachineDone: function(aStore, aRevisionStore, aResolve, aReject) {
405     this.close();
406     aResolve(this.createTask('done', null, this._revision.revisionId, null));
407   },
409   // public interface
411   get store() {
412     return this._dataStore.exposedObject;
413   },
415   next: function() {
416     debug('Next');
418     // If the window has been destroyed we cannot create the Promise object.
419     if (this._shuttingdown) {
420       throw Cr.NS_ERROR_FAILURE;
421     }
423     let self = this;
424     return new this._window.Promise(function(aResolve, aReject) {
425       self._dataStore._db.cursorTxn(
426         function(aTxn, aStore, aRevisionStore) {
427           self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
428         },
429         function(aEvent) {
430           aReject(createDOMError(self._window, aEvent));
431         }
432       );
433     });
434   },
436   close: function() {
437     this._dataStore.syncTerminated(this);
438   },
440   createTask: function(aOperation, aId, aRevisionId, aData) {
441     return Cu.cloneInto({ operation: aOperation, id: aId,
442                           revisionId: aRevisionId, data: aData }, this._window);
443   }