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/. */
9 this.EXPORTED_SYMBOLS = ['DataStoreCursor'];
12 //dump('DEBUG DataStoreCursor: ' + s + '\n');
15 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
18 const STATE_REVISION_INIT = 1;
19 const STATE_REVISION_CHECK = 2;
20 const STATE_SEND_ALL = 3;
21 const STATE_REVISION_SEND = 4;
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');
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
40 * State: init: do you have RID ?
41 * YES: state->initRevision; loop
42 * NO: get R; X=0; state->sendAll; send a 'clear'
44 * State: initRevision. Get R from RID. Done?
45 * YES: state->revisionCheck; loop
46 * NO: RID = null; state->init; loop
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'
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'
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?
63 * YES: send 'add' with ID and object
64 * YES and L[0] == 'updated': R=L[0]; get the object; found?
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
71 * State: done: send a 'done' with R
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]),
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;
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;
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) {
134 switch (this._state) {
136 this.stateMachineInit(aStore, aRevisionStore, aResolve, aReject);
139 case STATE_REVISION_INIT:
140 this.stateMachineRevisionInit(aStore, aRevisionStore, aResolve, aReject);
143 case STATE_REVISION_CHECK:
144 this.stateMachineRevisionCheck(aStore, aRevisionStore, aResolve, aReject);
148 this.stateMachineSendAll(aStore, aRevisionStore, aResolve, aReject);
151 case STATE_REVISION_SEND:
152 this.stateMachineRevisionSend(aStore, aRevisionStore, aResolve, aReject);
156 this.stateMachineDone(aStore, aRevisionStore, aResolve, aReject);
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);
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"));
179 self._revision = aEvent.target.result.value;
181 self._state = STATE_SEND_ALL;
182 aResolve(self.createTask('clear', null, '', null));
186 stateMachineRevisionInit: function(aStore, aRevisionStore, aResolve, aReject) {
187 debug('StateMachineRevisionInit');
190 let request = this._dataStore._db.getInternalRevisionId(
193 function(aInternalRevisionId) {
194 // This revision doesn't exist.
195 if (aInternalRevisionId == undefined) {
196 self._revisionId = null;
198 self._state = STATE_INIT;
199 self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
203 self._revision = { revisionId: self._revisionId,
204 internalRevisionId: aInternalRevisionId };
205 self._state = STATE_REVISION_CHECK;
206 self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
211 stateMachineRevisionCheck: function(aStore, aRevisionStore, aResolve, aReject) {
212 debug('StateMachineRevisionCheck');
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) {
231 changes.addedIds[data.objectId] = data.internalRevisionId;
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
238 if (!(data.objectId in changes.addedIds) &&
239 !(data.objectId in changes.updatedIds)) {
240 changes.updatedIds[data.objectId] = data.internalRevisionId;
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];
252 changes.removedIds[id] = data.internalRevisionId;
255 if (id in changes.updatedIds) {
256 delete changes.updatedIds[id];
262 dump('Internal error: Revision "' + REVISION_VOID + '" should not be found!!!\n');
266 self._revisionId = null;
268 self._state = STATE_INIT;
269 self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
274 // From changes to a map of internalRevisionId.
276 function addRevisions(obj) {
277 for (let key in obj) {
278 revisions[obj[key]] = true;
282 addRevisions(changes.addedIds);
283 addRevisions(changes.updatedIds);
284 addRevisions(changes.removedIds);
286 // Create the list of revisions.
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;
300 if (list.length == 0) {
301 self._state = STATE_DONE;
302 self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
306 // Some revision has to be sent.
307 self._revisionsList = list;
308 self._state = STATE_REVISION_SEND;
309 self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
313 stateMachineSendAll: function(aStore, aRevisionStore, aResolve, aReject) {
314 debug('StateMachineSendAll');
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;
322 aResolve(self.createTask('clear', null, '', null));
326 let request = aStore.openCursor(self._window.IDBKeyRange.lowerBound(self._objectId, true));
327 request.onsuccess = function(aEvent) {
328 let cursor = aEvent.target.result;
330 self._state = STATE_REVISION_CHECK;
331 self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
335 self._objectId = cursor.key;
336 aResolve(self.createTask('add', self._objectId, '', cursor.value));
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);
350 this._revision = this._revisionsList.shift();
352 switch (this._revision.operation) {
353 case REVISION_REMOVED:
354 aResolve(this.createTask('remove', this._revision.objectId, '', null));
357 case REVISION_ADDED: {
358 let request = aStore.get(this._revision.objectId);
360 request.onsuccess = function(aEvent) {
361 if (aEvent.target.result == undefined) {
362 self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
366 aResolve(self.createTask('add', self._revision.objectId, '',
367 aEvent.target.result));
372 case REVISION_UPDATED: {
373 let request = aStore.get(this._revision.objectId);
375 request.onsuccess = function(aEvent) {
376 if (aEvent.target.result == undefined) {
377 self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
381 if (aEvent.target.result.revisionId > self._revision.internalRevisionId) {
382 self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
386 aResolve(self.createTask('update', self._revision.objectId, '',
387 aEvent.target.result));
394 dump('Internal error: Revision "' + REVISION_VOID + '" should not be found!!!\n');
398 // This revision contains data that has already been sent by another one.
399 this.stateMachine(aStore, aRevisionStore, aResolve, aReject);
404 stateMachineDone: function(aStore, aRevisionStore, aResolve, aReject) {
406 aResolve(this.createTask('done', null, this._revision.revisionId, null));
412 return this._dataStore.exposedObject;
418 // If the window has been destroyed we cannot create the Promise object.
419 if (this._shuttingdown) {
420 throw Cr.NS_ERROR_FAILURE;
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);
430 aReject(createDOMError(self._window, aEvent));
437 this._dataStore.syncTerminated(this);
440 createTask: function(aOperation, aId, aRevisionId, aData) {
441 return Cu.cloneInto({ operation: aOperation, id: aId,
442 revisionId: aRevisionId, data: aData }, this._window);