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