1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/dom/datastore/DataStoreCursorImpl.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,409 @@ 1.4 +/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */ 1.5 +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +'use strict' 1.11 + 1.12 +this.EXPORTED_SYMBOLS = ['DataStoreCursor']; 1.13 + 1.14 +function debug(s) { 1.15 + //dump('DEBUG DataStoreCursor: ' + s + '\n'); 1.16 +} 1.17 + 1.18 +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; 1.19 + 1.20 +const STATE_INIT = 0; 1.21 +const STATE_REVISION_INIT = 1; 1.22 +const STATE_REVISION_CHECK = 2; 1.23 +const STATE_SEND_ALL = 3; 1.24 +const STATE_REVISION_SEND = 4; 1.25 +const STATE_DONE = 5; 1.26 + 1.27 +const REVISION_ADDED = 'added'; 1.28 +const REVISION_UPDATED = 'updated'; 1.29 +const REVISION_REMOVED = 'removed'; 1.30 +const REVISION_VOID = 'void'; 1.31 +const REVISION_SKIP = 'skip' 1.32 + 1.33 +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); 1.34 + 1.35 +/** 1.36 + * legend: 1.37 + * - RID = revision ID 1.38 + * - R = revision object (with the internalRevisionId that is a number) 1.39 + * - X = current object ID. 1.40 + * - L = the list of revisions that we have to send 1.41 + * 1.42 + * State: init: do you have RID ? 1.43 + * YES: state->initRevision; loop 1.44 + * NO: get R; X=0; state->sendAll; send a 'clear' 1.45 + * 1.46 + * State: initRevision. Get R from RID. Done? 1.47 + * YES: state->revisionCheck; loop 1.48 + * NO: RID = null; state->init; loop 1.49 + * 1.50 + * State: revisionCheck: get all the revisions between R and NOW. Done? 1.51 + * YES and R == NOW: state->done; loop 1.52 + * YES and R != NOW: Store this revisions in L; state->revisionSend; loop 1.53 + * NO: R = NOW; X=0; state->sendAll; send a 'clear' 1.54 + * 1.55 + * State: sendAll: is R still the last revision? 1.56 + * YES get the first object with id > X. Done? 1.57 + * YES: X = object.id; send 'add' 1.58 + * NO: state->revisionCheck; loop 1.59 + * NO: R = NOW; X=0; send a 'clear' 1.60 + * 1.61 + * State: revisionSend: do you have something from L to send? 1.62 + * YES and L[0] == 'removed': R=L[0]; send 'remove' with ID 1.63 + * YES and L[0] == 'added': R=L[0]; get the object; found? 1.64 + * NO: loop 1.65 + * YES: send 'add' with ID and object 1.66 + * YES and L[0] == 'updated': R=L[0]; get the object; found? 1.67 + * NO: loop 1.68 + * YES and object.R > R: continue 1.69 + * YES and object.R <= R: send 'update' with ID and object 1.70 + * YES L[0] == 'void': R=L[0]; state->init; loop 1.71 + * NO: state->revisionCheck; loop 1.72 + * 1.73 + * State: done: send a 'done' with R 1.74 + */ 1.75 + 1.76 +/* Helper functions */ 1.77 +function createDOMError(aWindow, aEvent) { 1.78 + return new aWindow.DOMError(aEvent.target.error.name); 1.79 +} 1.80 + 1.81 +/* DataStoreCursor object */ 1.82 +this.DataStoreCursor = function(aWindow, aDataStore, aRevisionId) { 1.83 + debug("DataStoreCursor created"); 1.84 + this.init(aWindow, aDataStore, aRevisionId); 1.85 +} 1.86 + 1.87 +this.DataStoreCursor.prototype = { 1.88 + classDescription: 'DataStoreCursor XPCOM Component', 1.89 + classID: Components.ID('{b6d14349-1eab-46b8-8513-584a7328a26b}'), 1.90 + contractID: '@mozilla.org/dom/datastore-cursor-impl;1', 1.91 + QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsISupports]), 1.92 + 1.93 + _window: null, 1.94 + _dataStore: null, 1.95 + _revisionId: null, 1.96 + _revision: null, 1.97 + _revisionsList: null, 1.98 + _objectId: 0, 1.99 + 1.100 + _state: STATE_INIT, 1.101 + 1.102 + init: function(aWindow, aDataStore, aRevisionId) { 1.103 + debug('DataStoreCursor init'); 1.104 + 1.105 + this._window = aWindow; 1.106 + this._dataStore = aDataStore; 1.107 + this._revisionId = aRevisionId; 1.108 + }, 1.109 + 1.110 + // This is the implementation of the state machine. 1.111 + // Read the comments at the top of this file in order to follow what it does. 1.112 + stateMachine: function(aStore, aRevisionStore, aResolve, aReject) { 1.113 + debug('StateMachine: ' + this._state); 1.114 + 1.115 + switch (this._state) { 1.116 + case STATE_INIT: 1.117 + this.stateMachineInit(aStore, aRevisionStore, aResolve, aReject); 1.118 + break; 1.119 + 1.120 + case STATE_REVISION_INIT: 1.121 + this.stateMachineRevisionInit(aStore, aRevisionStore, aResolve, aReject); 1.122 + break; 1.123 + 1.124 + case STATE_REVISION_CHECK: 1.125 + this.stateMachineRevisionCheck(aStore, aRevisionStore, aResolve, aReject); 1.126 + break; 1.127 + 1.128 + case STATE_SEND_ALL: 1.129 + this.stateMachineSendAll(aStore, aRevisionStore, aResolve, aReject); 1.130 + break; 1.131 + 1.132 + case STATE_REVISION_SEND: 1.133 + this.stateMachineRevisionSend(aStore, aRevisionStore, aResolve, aReject); 1.134 + break; 1.135 + 1.136 + case STATE_DONE: 1.137 + this.stateMachineDone(aStore, aRevisionStore, aResolve, aReject); 1.138 + break; 1.139 + } 1.140 + }, 1.141 + 1.142 + stateMachineInit: function(aStore, aRevisionStore, aResolve, aReject) { 1.143 + debug('StateMachineInit'); 1.144 + 1.145 + if (this._revisionId) { 1.146 + this._state = STATE_REVISION_INIT; 1.147 + this.stateMachine(aStore, aRevisionStore, aResolve, aReject); 1.148 + return; 1.149 + } 1.150 + 1.151 + let self = this; 1.152 + let request = aRevisionStore.openCursor(null, 'prev'); 1.153 + request.onsuccess = function(aEvent) { 1.154 + self._revision = aEvent.target.result.value; 1.155 + self._objectId = 0; 1.156 + self._state = STATE_SEND_ALL; 1.157 + aResolve(Cu.cloneInto({ operation: 'clear' }, self._window)); 1.158 + } 1.159 + }, 1.160 + 1.161 + stateMachineRevisionInit: function(aStore, aRevisionStore, aResolve, aReject) { 1.162 + debug('StateMachineRevisionInit'); 1.163 + 1.164 + let self = this; 1.165 + let request = this._dataStore._db.getInternalRevisionId( 1.166 + self._revisionId, 1.167 + aRevisionStore, 1.168 + function(aInternalRevisionId) { 1.169 + // This revision doesn't exist. 1.170 + if (aInternalRevisionId == undefined) { 1.171 + self._revisionId = null; 1.172 + self._objectId = 0; 1.173 + self._state = STATE_INIT; 1.174 + self.stateMachine(aStore, aRevisionStore, aResolve, aReject); 1.175 + return; 1.176 + } 1.177 + 1.178 + self._revision = { revisionId: self._revisionId, 1.179 + internalRevisionId: aInternalRevisionId }; 1.180 + self._state = STATE_REVISION_CHECK; 1.181 + self.stateMachine(aStore, aRevisionStore, aResolve, aReject); 1.182 + } 1.183 + ); 1.184 + }, 1.185 + 1.186 + stateMachineRevisionCheck: function(aStore, aRevisionStore, aResolve, aReject) { 1.187 + debug('StateMachineRevisionCheck'); 1.188 + 1.189 + let changes = { 1.190 + addedIds: {}, 1.191 + updatedIds: {}, 1.192 + removedIds: {} 1.193 + }; 1.194 + 1.195 + let self = this; 1.196 + let request = aRevisionStore.mozGetAll( 1.197 + self._window.IDBKeyRange.lowerBound(this._revision.internalRevisionId, true)); 1.198 + request.onsuccess = function(aEvent) { 1.199 + 1.200 + // Optimize the operations. 1.201 + for (let i = 0; i < aEvent.target.result.length; ++i) { 1.202 + let data = aEvent.target.result[i]; 1.203 + 1.204 + switch (data.operation) { 1.205 + case REVISION_ADDED: 1.206 + changes.addedIds[data.objectId] = data.internalRevisionId; 1.207 + break; 1.208 + 1.209 + case REVISION_UPDATED: 1.210 + // We don't consider an update if this object has been added 1.211 + // or if it has been already modified by a previous 1.212 + // operation. 1.213 + if (!(data.objectId in changes.addedIds) && 1.214 + !(data.objectId in changes.updatedIds)) { 1.215 + changes.updatedIds[data.objectId] = data.internalRevisionId; 1.216 + } 1.217 + break; 1.218 + 1.219 + case REVISION_REMOVED: 1.220 + let id = data.objectId; 1.221 + 1.222 + // If the object has been added in this range of revisions 1.223 + // we can ignore it and remove it from the list. 1.224 + if (id in changes.addedIds) { 1.225 + delete changes.addedIds[id]; 1.226 + } else { 1.227 + changes.removedIds[id] = data.internalRevisionId; 1.228 + } 1.229 + 1.230 + if (id in changes.updatedIds) { 1.231 + delete changes.updatedIds[id]; 1.232 + } 1.233 + break; 1.234 + 1.235 + case REVISION_VOID: 1.236 + if (i != 0) { 1.237 + dump('Internal error: Revision "' + REVISION_VOID + '" should not be found!!!\n'); 1.238 + return; 1.239 + } 1.240 + 1.241 + self._revisionId = null; 1.242 + self._objectId = 0; 1.243 + self._state = STATE_INIT; 1.244 + self.stateMachine(aStore, aRevisionStore, aResolve, aReject); 1.245 + return; 1.246 + } 1.247 + } 1.248 + 1.249 + // From changes to a map of internalRevisionId. 1.250 + let revisions = {}; 1.251 + function addRevisions(obj) { 1.252 + for (let key in obj) { 1.253 + revisions[obj[key]] = true; 1.254 + } 1.255 + } 1.256 + 1.257 + addRevisions(changes.addedIds); 1.258 + addRevisions(changes.updatedIds); 1.259 + addRevisions(changes.removedIds); 1.260 + 1.261 + // Create the list of revisions. 1.262 + let list = []; 1.263 + for (let i = 0; i < aEvent.target.result.length; ++i) { 1.264 + let data = aEvent.target.result[i]; 1.265 + 1.266 + // If this revision doesn't contain useful data, we still need to keep 1.267 + // it in the list because we need to update the internal revision ID. 1.268 + if (!(data.internalRevisionId in revisions)) { 1.269 + data.operation = REVISION_SKIP; 1.270 + } 1.271 + 1.272 + list.push(data); 1.273 + } 1.274 + 1.275 + if (list.length == 0) { 1.276 + self._state = STATE_DONE; 1.277 + self.stateMachine(aStore, aRevisionStore, aResolve, aReject); 1.278 + return; 1.279 + } 1.280 + 1.281 + // Some revision has to be sent. 1.282 + self._revisionsList = list; 1.283 + self._state = STATE_REVISION_SEND; 1.284 + self.stateMachine(aStore, aRevisionStore, aResolve, aReject); 1.285 + }; 1.286 + }, 1.287 + 1.288 + stateMachineSendAll: function(aStore, aRevisionStore, aResolve, aReject) { 1.289 + debug('StateMachineSendAll'); 1.290 + 1.291 + let self = this; 1.292 + let request = aRevisionStore.openCursor(null, 'prev'); 1.293 + request.onsuccess = function(aEvent) { 1.294 + if (self._revision.revisionId != aEvent.target.result.value.revisionId) { 1.295 + self._revision = aEvent.target.result.value; 1.296 + self._objectId = 0; 1.297 + aResolve(Cu.cloneInto({ operation: 'clear' }, self._window)); 1.298 + return; 1.299 + } 1.300 + 1.301 + let request = aStore.openCursor(self._window.IDBKeyRange.lowerBound(self._objectId, true)); 1.302 + request.onsuccess = function(aEvent) { 1.303 + let cursor = aEvent.target.result; 1.304 + if (!cursor) { 1.305 + self._state = STATE_REVISION_CHECK; 1.306 + self.stateMachine(aStore, aRevisionStore, aResolve, aReject); 1.307 + return; 1.308 + } 1.309 + 1.310 + self._objectId = cursor.key; 1.311 + aResolve(Cu.cloneInto({ operation: 'add', id: self._objectId, 1.312 + data: cursor.value }, self._window)); 1.313 + }; 1.314 + }; 1.315 + }, 1.316 + 1.317 + stateMachineRevisionSend: function(aStore, aRevisionStore, aResolve, aReject) { 1.318 + debug('StateMachineRevisionSend'); 1.319 + 1.320 + if (!this._revisionsList.length) { 1.321 + this._state = STATE_REVISION_CHECK; 1.322 + this.stateMachine(aStore, aRevisionStore, aResolve, aReject); 1.323 + return; 1.324 + } 1.325 + 1.326 + this._revision = this._revisionsList.shift(); 1.327 + 1.328 + switch (this._revision.operation) { 1.329 + case REVISION_REMOVED: 1.330 + aResolve(Cu.cloneInto({ operation: 'remove', id: this._revision.objectId }, 1.331 + this._window)); 1.332 + break; 1.333 + 1.334 + case REVISION_ADDED: { 1.335 + let request = aStore.get(this._revision.objectId); 1.336 + let self = this; 1.337 + request.onsuccess = function(aEvent) { 1.338 + if (aEvent.target.result == undefined) { 1.339 + self.stateMachine(aStore, aRevisionStore, aResolve, aReject); 1.340 + return; 1.341 + } 1.342 + 1.343 + aResolve(Cu.cloneInto({ operation: 'add', id: self._revision.objectId, 1.344 + data: aEvent.target.result }, self._window)); 1.345 + } 1.346 + break; 1.347 + } 1.348 + 1.349 + case REVISION_UPDATED: { 1.350 + let request = aStore.get(this._revision.objectId); 1.351 + let self = this; 1.352 + request.onsuccess = function(aEvent) { 1.353 + if (aEvent.target.result == undefined) { 1.354 + self.stateMachine(aStore, aRevisionStore, aResolve, aReject); 1.355 + return; 1.356 + } 1.357 + 1.358 + if (aEvent.target.result.revisionId > self._revision.internalRevisionId) { 1.359 + self.stateMachine(aStore, aRevisionStore, aResolve, aReject); 1.360 + return; 1.361 + } 1.362 + 1.363 + aResolve(Cu.cloneInto({ operation: 'update', id: self._revision.objectId, 1.364 + data: aEvent.target.result }, self._window)); 1.365 + } 1.366 + break; 1.367 + } 1.368 + 1.369 + case REVISION_VOID: 1.370 + // Internal error! 1.371 + dump('Internal error: Revision "' + REVISION_VOID + '" should not be found!!!\n'); 1.372 + break; 1.373 + 1.374 + case REVISION_SKIP: 1.375 + // This revision contains data that has already been sent by another one. 1.376 + this.stateMachine(aStore, aRevisionStore, aResolve, aReject); 1.377 + break; 1.378 + } 1.379 + }, 1.380 + 1.381 + stateMachineDone: function(aStore, aRevisionStore, aResolve, aReject) { 1.382 + this.close(); 1.383 + aResolve(Cu.cloneInto({ revisionId: this._revision.revisionId, 1.384 + operation: 'done' }, this._window)); 1.385 + }, 1.386 + 1.387 + // public interface 1.388 + 1.389 + get store() { 1.390 + return this._dataStore.exposedObject; 1.391 + }, 1.392 + 1.393 + next: function() { 1.394 + debug('Next'); 1.395 + 1.396 + let self = this; 1.397 + return new this._window.Promise(function(aResolve, aReject) { 1.398 + self._dataStore._db.cursorTxn( 1.399 + function(aTxn, aStore, aRevisionStore) { 1.400 + self.stateMachine(aStore, aRevisionStore, aResolve, aReject); 1.401 + }, 1.402 + function(aEvent) { 1.403 + aReject(createDOMError(self._window, aEvent)); 1.404 + } 1.405 + ); 1.406 + }); 1.407 + }, 1.408 + 1.409 + close: function() { 1.410 + this._dataStore.syncTerminated(this); 1.411 + } 1.412 +};