michael@0: /* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */ michael@0: /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: 'use strict' michael@0: michael@0: this.EXPORTED_SYMBOLS = ['DataStoreCursor']; michael@0: michael@0: function debug(s) { michael@0: //dump('DEBUG DataStoreCursor: ' + s + '\n'); michael@0: } michael@0: michael@0: const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; michael@0: michael@0: const STATE_INIT = 0; michael@0: const STATE_REVISION_INIT = 1; michael@0: const STATE_REVISION_CHECK = 2; michael@0: const STATE_SEND_ALL = 3; michael@0: const STATE_REVISION_SEND = 4; michael@0: const STATE_DONE = 5; michael@0: michael@0: const REVISION_ADDED = 'added'; michael@0: const REVISION_UPDATED = 'updated'; michael@0: const REVISION_REMOVED = 'removed'; michael@0: const REVISION_VOID = 'void'; michael@0: const REVISION_SKIP = 'skip' michael@0: michael@0: Cu.import('resource://gre/modules/XPCOMUtils.jsm'); michael@0: michael@0: /** michael@0: * legend: michael@0: * - RID = revision ID michael@0: * - R = revision object (with the internalRevisionId that is a number) michael@0: * - X = current object ID. michael@0: * - L = the list of revisions that we have to send michael@0: * michael@0: * State: init: do you have RID ? michael@0: * YES: state->initRevision; loop michael@0: * NO: get R; X=0; state->sendAll; send a 'clear' michael@0: * michael@0: * State: initRevision. Get R from RID. Done? michael@0: * YES: state->revisionCheck; loop michael@0: * NO: RID = null; state->init; loop michael@0: * michael@0: * State: revisionCheck: get all the revisions between R and NOW. Done? michael@0: * YES and R == NOW: state->done; loop michael@0: * YES and R != NOW: Store this revisions in L; state->revisionSend; loop michael@0: * NO: R = NOW; X=0; state->sendAll; send a 'clear' michael@0: * michael@0: * State: sendAll: is R still the last revision? michael@0: * YES get the first object with id > X. Done? michael@0: * YES: X = object.id; send 'add' michael@0: * NO: state->revisionCheck; loop michael@0: * NO: R = NOW; X=0; send a 'clear' michael@0: * michael@0: * State: revisionSend: do you have something from L to send? michael@0: * YES and L[0] == 'removed': R=L[0]; send 'remove' with ID michael@0: * YES and L[0] == 'added': R=L[0]; get the object; found? michael@0: * NO: loop michael@0: * YES: send 'add' with ID and object michael@0: * YES and L[0] == 'updated': R=L[0]; get the object; found? michael@0: * NO: loop michael@0: * YES and object.R > R: continue michael@0: * YES and object.R <= R: send 'update' with ID and object michael@0: * YES L[0] == 'void': R=L[0]; state->init; loop michael@0: * NO: state->revisionCheck; loop michael@0: * michael@0: * State: done: send a 'done' with R michael@0: */ michael@0: michael@0: /* Helper functions */ michael@0: function createDOMError(aWindow, aEvent) { michael@0: return new aWindow.DOMError(aEvent.target.error.name); michael@0: } michael@0: michael@0: /* DataStoreCursor object */ michael@0: this.DataStoreCursor = function(aWindow, aDataStore, aRevisionId) { michael@0: debug("DataStoreCursor created"); michael@0: this.init(aWindow, aDataStore, aRevisionId); michael@0: } michael@0: michael@0: this.DataStoreCursor.prototype = { michael@0: classDescription: 'DataStoreCursor XPCOM Component', michael@0: classID: Components.ID('{b6d14349-1eab-46b8-8513-584a7328a26b}'), michael@0: contractID: '@mozilla.org/dom/datastore-cursor-impl;1', michael@0: QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsISupports]), michael@0: michael@0: _window: null, michael@0: _dataStore: null, michael@0: _revisionId: null, michael@0: _revision: null, michael@0: _revisionsList: null, michael@0: _objectId: 0, michael@0: michael@0: _state: STATE_INIT, michael@0: michael@0: init: function(aWindow, aDataStore, aRevisionId) { michael@0: debug('DataStoreCursor init'); michael@0: michael@0: this._window = aWindow; michael@0: this._dataStore = aDataStore; michael@0: this._revisionId = aRevisionId; michael@0: }, michael@0: michael@0: // This is the implementation of the state machine. michael@0: // Read the comments at the top of this file in order to follow what it does. michael@0: stateMachine: function(aStore, aRevisionStore, aResolve, aReject) { michael@0: debug('StateMachine: ' + this._state); michael@0: michael@0: switch (this._state) { michael@0: case STATE_INIT: michael@0: this.stateMachineInit(aStore, aRevisionStore, aResolve, aReject); michael@0: break; michael@0: michael@0: case STATE_REVISION_INIT: michael@0: this.stateMachineRevisionInit(aStore, aRevisionStore, aResolve, aReject); michael@0: break; michael@0: michael@0: case STATE_REVISION_CHECK: michael@0: this.stateMachineRevisionCheck(aStore, aRevisionStore, aResolve, aReject); michael@0: break; michael@0: michael@0: case STATE_SEND_ALL: michael@0: this.stateMachineSendAll(aStore, aRevisionStore, aResolve, aReject); michael@0: break; michael@0: michael@0: case STATE_REVISION_SEND: michael@0: this.stateMachineRevisionSend(aStore, aRevisionStore, aResolve, aReject); michael@0: break; michael@0: michael@0: case STATE_DONE: michael@0: this.stateMachineDone(aStore, aRevisionStore, aResolve, aReject); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: stateMachineInit: function(aStore, aRevisionStore, aResolve, aReject) { michael@0: debug('StateMachineInit'); michael@0: michael@0: if (this._revisionId) { michael@0: this._state = STATE_REVISION_INIT; michael@0: this.stateMachine(aStore, aRevisionStore, aResolve, aReject); michael@0: return; michael@0: } michael@0: michael@0: let self = this; michael@0: let request = aRevisionStore.openCursor(null, 'prev'); michael@0: request.onsuccess = function(aEvent) { michael@0: self._revision = aEvent.target.result.value; michael@0: self._objectId = 0; michael@0: self._state = STATE_SEND_ALL; michael@0: aResolve(Cu.cloneInto({ operation: 'clear' }, self._window)); michael@0: } michael@0: }, michael@0: michael@0: stateMachineRevisionInit: function(aStore, aRevisionStore, aResolve, aReject) { michael@0: debug('StateMachineRevisionInit'); michael@0: michael@0: let self = this; michael@0: let request = this._dataStore._db.getInternalRevisionId( michael@0: self._revisionId, michael@0: aRevisionStore, michael@0: function(aInternalRevisionId) { michael@0: // This revision doesn't exist. michael@0: if (aInternalRevisionId == undefined) { michael@0: self._revisionId = null; michael@0: self._objectId = 0; michael@0: self._state = STATE_INIT; michael@0: self.stateMachine(aStore, aRevisionStore, aResolve, aReject); michael@0: return; michael@0: } michael@0: michael@0: self._revision = { revisionId: self._revisionId, michael@0: internalRevisionId: aInternalRevisionId }; michael@0: self._state = STATE_REVISION_CHECK; michael@0: self.stateMachine(aStore, aRevisionStore, aResolve, aReject); michael@0: } michael@0: ); michael@0: }, michael@0: michael@0: stateMachineRevisionCheck: function(aStore, aRevisionStore, aResolve, aReject) { michael@0: debug('StateMachineRevisionCheck'); michael@0: michael@0: let changes = { michael@0: addedIds: {}, michael@0: updatedIds: {}, michael@0: removedIds: {} michael@0: }; michael@0: michael@0: let self = this; michael@0: let request = aRevisionStore.mozGetAll( michael@0: self._window.IDBKeyRange.lowerBound(this._revision.internalRevisionId, true)); michael@0: request.onsuccess = function(aEvent) { michael@0: michael@0: // Optimize the operations. michael@0: for (let i = 0; i < aEvent.target.result.length; ++i) { michael@0: let data = aEvent.target.result[i]; michael@0: michael@0: switch (data.operation) { michael@0: case REVISION_ADDED: michael@0: changes.addedIds[data.objectId] = data.internalRevisionId; michael@0: break; michael@0: michael@0: case REVISION_UPDATED: michael@0: // We don't consider an update if this object has been added michael@0: // or if it has been already modified by a previous michael@0: // operation. michael@0: if (!(data.objectId in changes.addedIds) && michael@0: !(data.objectId in changes.updatedIds)) { michael@0: changes.updatedIds[data.objectId] = data.internalRevisionId; michael@0: } michael@0: break; michael@0: michael@0: case REVISION_REMOVED: michael@0: let id = data.objectId; michael@0: michael@0: // If the object has been added in this range of revisions michael@0: // we can ignore it and remove it from the list. michael@0: if (id in changes.addedIds) { michael@0: delete changes.addedIds[id]; michael@0: } else { michael@0: changes.removedIds[id] = data.internalRevisionId; michael@0: } michael@0: michael@0: if (id in changes.updatedIds) { michael@0: delete changes.updatedIds[id]; michael@0: } michael@0: break; michael@0: michael@0: case REVISION_VOID: michael@0: if (i != 0) { michael@0: dump('Internal error: Revision "' + REVISION_VOID + '" should not be found!!!\n'); michael@0: return; michael@0: } michael@0: michael@0: self._revisionId = null; michael@0: self._objectId = 0; michael@0: self._state = STATE_INIT; michael@0: self.stateMachine(aStore, aRevisionStore, aResolve, aReject); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // From changes to a map of internalRevisionId. michael@0: let revisions = {}; michael@0: function addRevisions(obj) { michael@0: for (let key in obj) { michael@0: revisions[obj[key]] = true; michael@0: } michael@0: } michael@0: michael@0: addRevisions(changes.addedIds); michael@0: addRevisions(changes.updatedIds); michael@0: addRevisions(changes.removedIds); michael@0: michael@0: // Create the list of revisions. michael@0: let list = []; michael@0: for (let i = 0; i < aEvent.target.result.length; ++i) { michael@0: let data = aEvent.target.result[i]; michael@0: michael@0: // If this revision doesn't contain useful data, we still need to keep michael@0: // it in the list because we need to update the internal revision ID. michael@0: if (!(data.internalRevisionId in revisions)) { michael@0: data.operation = REVISION_SKIP; michael@0: } michael@0: michael@0: list.push(data); michael@0: } michael@0: michael@0: if (list.length == 0) { michael@0: self._state = STATE_DONE; michael@0: self.stateMachine(aStore, aRevisionStore, aResolve, aReject); michael@0: return; michael@0: } michael@0: michael@0: // Some revision has to be sent. michael@0: self._revisionsList = list; michael@0: self._state = STATE_REVISION_SEND; michael@0: self.stateMachine(aStore, aRevisionStore, aResolve, aReject); michael@0: }; michael@0: }, michael@0: michael@0: stateMachineSendAll: function(aStore, aRevisionStore, aResolve, aReject) { michael@0: debug('StateMachineSendAll'); michael@0: michael@0: let self = this; michael@0: let request = aRevisionStore.openCursor(null, 'prev'); michael@0: request.onsuccess = function(aEvent) { michael@0: if (self._revision.revisionId != aEvent.target.result.value.revisionId) { michael@0: self._revision = aEvent.target.result.value; michael@0: self._objectId = 0; michael@0: aResolve(Cu.cloneInto({ operation: 'clear' }, self._window)); michael@0: return; michael@0: } michael@0: michael@0: let request = aStore.openCursor(self._window.IDBKeyRange.lowerBound(self._objectId, true)); michael@0: request.onsuccess = function(aEvent) { michael@0: let cursor = aEvent.target.result; michael@0: if (!cursor) { michael@0: self._state = STATE_REVISION_CHECK; michael@0: self.stateMachine(aStore, aRevisionStore, aResolve, aReject); michael@0: return; michael@0: } michael@0: michael@0: self._objectId = cursor.key; michael@0: aResolve(Cu.cloneInto({ operation: 'add', id: self._objectId, michael@0: data: cursor.value }, self._window)); michael@0: }; michael@0: }; michael@0: }, michael@0: michael@0: stateMachineRevisionSend: function(aStore, aRevisionStore, aResolve, aReject) { michael@0: debug('StateMachineRevisionSend'); michael@0: michael@0: if (!this._revisionsList.length) { michael@0: this._state = STATE_REVISION_CHECK; michael@0: this.stateMachine(aStore, aRevisionStore, aResolve, aReject); michael@0: return; michael@0: } michael@0: michael@0: this._revision = this._revisionsList.shift(); michael@0: michael@0: switch (this._revision.operation) { michael@0: case REVISION_REMOVED: michael@0: aResolve(Cu.cloneInto({ operation: 'remove', id: this._revision.objectId }, michael@0: this._window)); michael@0: break; michael@0: michael@0: case REVISION_ADDED: { michael@0: let request = aStore.get(this._revision.objectId); michael@0: let self = this; michael@0: request.onsuccess = function(aEvent) { michael@0: if (aEvent.target.result == undefined) { michael@0: self.stateMachine(aStore, aRevisionStore, aResolve, aReject); michael@0: return; michael@0: } michael@0: michael@0: aResolve(Cu.cloneInto({ operation: 'add', id: self._revision.objectId, michael@0: data: aEvent.target.result }, self._window)); michael@0: } michael@0: break; michael@0: } michael@0: michael@0: case REVISION_UPDATED: { michael@0: let request = aStore.get(this._revision.objectId); michael@0: let self = this; michael@0: request.onsuccess = function(aEvent) { michael@0: if (aEvent.target.result == undefined) { michael@0: self.stateMachine(aStore, aRevisionStore, aResolve, aReject); michael@0: return; michael@0: } michael@0: michael@0: if (aEvent.target.result.revisionId > self._revision.internalRevisionId) { michael@0: self.stateMachine(aStore, aRevisionStore, aResolve, aReject); michael@0: return; michael@0: } michael@0: michael@0: aResolve(Cu.cloneInto({ operation: 'update', id: self._revision.objectId, michael@0: data: aEvent.target.result }, self._window)); michael@0: } michael@0: break; michael@0: } michael@0: michael@0: case REVISION_VOID: michael@0: // Internal error! michael@0: dump('Internal error: Revision "' + REVISION_VOID + '" should not be found!!!\n'); michael@0: break; michael@0: michael@0: case REVISION_SKIP: michael@0: // This revision contains data that has already been sent by another one. michael@0: this.stateMachine(aStore, aRevisionStore, aResolve, aReject); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: stateMachineDone: function(aStore, aRevisionStore, aResolve, aReject) { michael@0: this.close(); michael@0: aResolve(Cu.cloneInto({ revisionId: this._revision.revisionId, michael@0: operation: 'done' }, this._window)); michael@0: }, michael@0: michael@0: // public interface michael@0: michael@0: get store() { michael@0: return this._dataStore.exposedObject; michael@0: }, michael@0: michael@0: next: function() { michael@0: debug('Next'); michael@0: michael@0: let self = this; michael@0: return new this._window.Promise(function(aResolve, aReject) { michael@0: self._dataStore._db.cursorTxn( michael@0: function(aTxn, aStore, aRevisionStore) { michael@0: self.stateMachine(aStore, aRevisionStore, aResolve, aReject); michael@0: }, michael@0: function(aEvent) { michael@0: aReject(createDOMError(self._window, aEvent)); michael@0: } michael@0: ); michael@0: }); michael@0: }, michael@0: michael@0: close: function() { michael@0: this._dataStore.syncTerminated(this); michael@0: } michael@0: };