gecko-dev/dom/datastore/DataStoreCursor.jsm
2013-10-02 16:39:54 -04:00

417 lines
13 KiB
JavaScript

/* -*- 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/ObjectWrapper.jsm');
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. Default value is 0
* - MX = max known 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; get MX; 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; get MX; state->sendAll; send a 'clear';
*
* State: sendAll: get the first object with id > X. Done?
* YES and object.id > MX: state->revisionCheck; loop
* YES and object.id <= MX: X = object.id; send 'add'
* NO: state->revisionCheck; loop
*
* 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) {
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;1',
QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsISupports]),
_window: null,
_dataStore: null,
_revisionId: null,
_revision: null,
_revisionsList: null,
_objectId: 0,
_maxObjectId: 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.getMaxObjectId(aStore,
function() {
self._state = STATE_SEND_ALL;
aResolve(ObjectWrapper.wrap({ 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._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.getMaxObjectId(aStore,
function() {
self._revisionId = null;
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 = aStore.openCursor(self._window.IDBKeyRange.lowerBound(this._objectId, true));
request.onsuccess = function(aEvent) {
let cursor = aEvent.target.result;
if (!cursor || cursor.key > self._maxObjectId) {
self._state = STATE_REVISION_CHECK;
self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
return;
}
self._objectId = cursor.key;
aResolve(ObjectWrapper.wrap({ 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(ObjectWrapper.wrap({ 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(ObjectWrapper.wrap({ 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(ObjectWrapper.wrap({ 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(ObjectWrapper.wrap({ revisionId: this._revision.revisionId,
operation: 'done' }, this._window));
},
getMaxObjectId: function(aStore, aCallback) {
let self = this;
let request = aStore.openCursor(null, 'prev');
request.onsuccess = function(aEvent) {
if (aEvent.target.result) {
self._maxObjectId = aEvent.target.result.key;
}
aCallback();
}
},
// 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);
}
};