dom/datastore/DataStoreCursorImpl.jsm

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:75087ccebcfb
1 /* -*- Mode: js2; js2-basic-offset: 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/. */
6
7 'use strict'
8
9 this.EXPORTED_SYMBOLS = ['DataStoreCursor'];
10
11 function debug(s) {
12 //dump('DEBUG DataStoreCursor: ' + s + '\n');
13 }
14
15 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
16
17 const STATE_INIT = 0;
18 const STATE_REVISION_INIT = 1;
19 const STATE_REVISION_CHECK = 2;
20 const STATE_SEND_ALL = 3;
21 const STATE_REVISION_SEND = 4;
22 const STATE_DONE = 5;
23
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'
29
30 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
31
32 /**
33 * legend:
34 * - RID = revision ID
35 * - R = revision object (with the internalRevisionId that is a number)
36 * - X = current object ID.
37 * - L = the list of revisions that we have to send
38 *
39 * State: init: do you have RID ?
40 * YES: state->initRevision; loop
41 * NO: get R; X=0; state->sendAll; send a 'clear'
42 *
43 * State: initRevision. Get R from RID. Done?
44 * YES: state->revisionCheck; loop
45 * NO: RID = null; state->init; loop
46 *
47 * State: revisionCheck: get all the revisions between R and NOW. Done?
48 * YES and R == NOW: state->done; loop
49 * YES and R != NOW: Store this revisions in L; state->revisionSend; loop
50 * NO: R = NOW; X=0; state->sendAll; send a 'clear'
51 *
52 * State: sendAll: is R still the last revision?
53 * YES get the first object with id > X. Done?
54 * YES: X = object.id; send 'add'
55 * NO: state->revisionCheck; loop
56 * NO: R = NOW; X=0; send a 'clear'
57 *
58 * State: revisionSend: do you have something from L to send?
59 * YES and L[0] == 'removed': R=L[0]; send 'remove' with ID
60 * YES and L[0] == 'added': R=L[0]; get the object; found?
61 * NO: loop
62 * YES: send 'add' with ID and object
63 * YES and L[0] == 'updated': R=L[0]; get the object; found?
64 * NO: loop
65 * YES and object.R > R: continue
66 * YES and object.R <= R: send 'update' with ID and object
67 * YES L[0] == 'void': R=L[0]; state->init; loop
68 * NO: state->revisionCheck; loop
69 *
70 * State: done: send a 'done' with R
71 */
72
73 /* Helper functions */
74 function createDOMError(aWindow, aEvent) {
75 return new aWindow.DOMError(aEvent.target.error.name);
76 }
77
78 /* DataStoreCursor object */
79 this.DataStoreCursor = function(aWindow, aDataStore, aRevisionId) {
80 debug("DataStoreCursor created");
81 this.init(aWindow, aDataStore, aRevisionId);
82 }
83
84 this.DataStoreCursor.prototype = {
85 classDescription: 'DataStoreCursor XPCOM Component',
86 classID: Components.ID('{b6d14349-1eab-46b8-8513-584a7328a26b}'),
87 contractID: '@mozilla.org/dom/datastore-cursor-impl;1',
88 QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsISupports]),
89
90 _window: null,
91 _dataStore: null,
92 _revisionId: null,
93 _revision: null,
94 _revisionsList: null,
95 _objectId: 0,
96
97 _state: STATE_INIT,
98
99 init: function(aWindow, aDataStore, aRevisionId) {
100 debug('DataStoreCursor init');
101
102 this._window = aWindow;
103 this._dataStore = aDataStore;
104 this._revisionId = aRevisionId;
105 },
106
107 // This is the implementation of the state machine.
108 // Read the comments at the top of this file in order to follow what it does.
109 stateMachine: function(aStore, aRevisionStore, aResolve, aReject) {
110 debug('StateMachine: ' + this._state);
111
112 switch (this._state) {
113 case STATE_INIT:
114 this.stateMachineInit(aStore, aRevisionStore, aResolve, aReject);
115 break;
116
117 case STATE_REVISION_INIT:
118 this.stateMachineRevisionInit(aStore, aRevisionStore, aResolve, aReject);
119 break;
120
121 case STATE_REVISION_CHECK:
122 this.stateMachineRevisionCheck(aStore, aRevisionStore, aResolve, aReject);
123 break;
124
125 case STATE_SEND_ALL:
126 this.stateMachineSendAll(aStore, aRevisionStore, aResolve, aReject);
127 break;
128
129 case STATE_REVISION_SEND:
130 this.stateMachineRevisionSend(aStore, aRevisionStore, aResolve, aReject);
131 break;
132
133 case STATE_DONE:
134 this.stateMachineDone(aStore, aRevisionStore, aResolve, aReject);
135 break;
136 }
137 },
138
139 stateMachineInit: function(aStore, aRevisionStore, aResolve, aReject) {
140 debug('StateMachineInit');
141
142 if (this._revisionId) {
143 this._state = STATE_REVISION_INIT;
144 this.stateMachine(aStore, aRevisionStore, aResolve, aReject);
145 return;
146 }
147
148 let self = this;
149 let request = aRevisionStore.openCursor(null, 'prev');
150 request.onsuccess = function(aEvent) {
151 self._revision = aEvent.target.result.value;
152 self._objectId = 0;
153 self._state = STATE_SEND_ALL;
154 aResolve(Cu.cloneInto({ operation: 'clear' }, self._window));
155 }
156 },
157
158 stateMachineRevisionInit: function(aStore, aRevisionStore, aResolve, aReject) {
159 debug('StateMachineRevisionInit');
160
161 let self = this;
162 let request = this._dataStore._db.getInternalRevisionId(
163 self._revisionId,
164 aRevisionStore,
165 function(aInternalRevisionId) {
166 // This revision doesn't exist.
167 if (aInternalRevisionId == undefined) {
168 self._revisionId = null;
169 self._objectId = 0;
170 self._state = STATE_INIT;
171 self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
172 return;
173 }
174
175 self._revision = { revisionId: self._revisionId,
176 internalRevisionId: aInternalRevisionId };
177 self._state = STATE_REVISION_CHECK;
178 self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
179 }
180 );
181 },
182
183 stateMachineRevisionCheck: function(aStore, aRevisionStore, aResolve, aReject) {
184 debug('StateMachineRevisionCheck');
185
186 let changes = {
187 addedIds: {},
188 updatedIds: {},
189 removedIds: {}
190 };
191
192 let self = this;
193 let request = aRevisionStore.mozGetAll(
194 self._window.IDBKeyRange.lowerBound(this._revision.internalRevisionId, true));
195 request.onsuccess = function(aEvent) {
196
197 // Optimize the operations.
198 for (let i = 0; i < aEvent.target.result.length; ++i) {
199 let data = aEvent.target.result[i];
200
201 switch (data.operation) {
202 case REVISION_ADDED:
203 changes.addedIds[data.objectId] = data.internalRevisionId;
204 break;
205
206 case REVISION_UPDATED:
207 // We don't consider an update if this object has been added
208 // or if it has been already modified by a previous
209 // operation.
210 if (!(data.objectId in changes.addedIds) &&
211 !(data.objectId in changes.updatedIds)) {
212 changes.updatedIds[data.objectId] = data.internalRevisionId;
213 }
214 break;
215
216 case REVISION_REMOVED:
217 let id = data.objectId;
218
219 // If the object has been added in this range of revisions
220 // we can ignore it and remove it from the list.
221 if (id in changes.addedIds) {
222 delete changes.addedIds[id];
223 } else {
224 changes.removedIds[id] = data.internalRevisionId;
225 }
226
227 if (id in changes.updatedIds) {
228 delete changes.updatedIds[id];
229 }
230 break;
231
232 case REVISION_VOID:
233 if (i != 0) {
234 dump('Internal error: Revision "' + REVISION_VOID + '" should not be found!!!\n');
235 return;
236 }
237
238 self._revisionId = null;
239 self._objectId = 0;
240 self._state = STATE_INIT;
241 self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
242 return;
243 }
244 }
245
246 // From changes to a map of internalRevisionId.
247 let revisions = {};
248 function addRevisions(obj) {
249 for (let key in obj) {
250 revisions[obj[key]] = true;
251 }
252 }
253
254 addRevisions(changes.addedIds);
255 addRevisions(changes.updatedIds);
256 addRevisions(changes.removedIds);
257
258 // Create the list of revisions.
259 let list = [];
260 for (let i = 0; i < aEvent.target.result.length; ++i) {
261 let data = aEvent.target.result[i];
262
263 // If this revision doesn't contain useful data, we still need to keep
264 // it in the list because we need to update the internal revision ID.
265 if (!(data.internalRevisionId in revisions)) {
266 data.operation = REVISION_SKIP;
267 }
268
269 list.push(data);
270 }
271
272 if (list.length == 0) {
273 self._state = STATE_DONE;
274 self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
275 return;
276 }
277
278 // Some revision has to be sent.
279 self._revisionsList = list;
280 self._state = STATE_REVISION_SEND;
281 self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
282 };
283 },
284
285 stateMachineSendAll: function(aStore, aRevisionStore, aResolve, aReject) {
286 debug('StateMachineSendAll');
287
288 let self = this;
289 let request = aRevisionStore.openCursor(null, 'prev');
290 request.onsuccess = function(aEvent) {
291 if (self._revision.revisionId != aEvent.target.result.value.revisionId) {
292 self._revision = aEvent.target.result.value;
293 self._objectId = 0;
294 aResolve(Cu.cloneInto({ operation: 'clear' }, self._window));
295 return;
296 }
297
298 let request = aStore.openCursor(self._window.IDBKeyRange.lowerBound(self._objectId, true));
299 request.onsuccess = function(aEvent) {
300 let cursor = aEvent.target.result;
301 if (!cursor) {
302 self._state = STATE_REVISION_CHECK;
303 self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
304 return;
305 }
306
307 self._objectId = cursor.key;
308 aResolve(Cu.cloneInto({ operation: 'add', id: self._objectId,
309 data: cursor.value }, self._window));
310 };
311 };
312 },
313
314 stateMachineRevisionSend: function(aStore, aRevisionStore, aResolve, aReject) {
315 debug('StateMachineRevisionSend');
316
317 if (!this._revisionsList.length) {
318 this._state = STATE_REVISION_CHECK;
319 this.stateMachine(aStore, aRevisionStore, aResolve, aReject);
320 return;
321 }
322
323 this._revision = this._revisionsList.shift();
324
325 switch (this._revision.operation) {
326 case REVISION_REMOVED:
327 aResolve(Cu.cloneInto({ operation: 'remove', id: this._revision.objectId },
328 this._window));
329 break;
330
331 case REVISION_ADDED: {
332 let request = aStore.get(this._revision.objectId);
333 let self = this;
334 request.onsuccess = function(aEvent) {
335 if (aEvent.target.result == undefined) {
336 self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
337 return;
338 }
339
340 aResolve(Cu.cloneInto({ operation: 'add', id: self._revision.objectId,
341 data: aEvent.target.result }, self._window));
342 }
343 break;
344 }
345
346 case REVISION_UPDATED: {
347 let request = aStore.get(this._revision.objectId);
348 let self = this;
349 request.onsuccess = function(aEvent) {
350 if (aEvent.target.result == undefined) {
351 self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
352 return;
353 }
354
355 if (aEvent.target.result.revisionId > self._revision.internalRevisionId) {
356 self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
357 return;
358 }
359
360 aResolve(Cu.cloneInto({ operation: 'update', id: self._revision.objectId,
361 data: aEvent.target.result }, self._window));
362 }
363 break;
364 }
365
366 case REVISION_VOID:
367 // Internal error!
368 dump('Internal error: Revision "' + REVISION_VOID + '" should not be found!!!\n');
369 break;
370
371 case REVISION_SKIP:
372 // This revision contains data that has already been sent by another one.
373 this.stateMachine(aStore, aRevisionStore, aResolve, aReject);
374 break;
375 }
376 },
377
378 stateMachineDone: function(aStore, aRevisionStore, aResolve, aReject) {
379 this.close();
380 aResolve(Cu.cloneInto({ revisionId: this._revision.revisionId,
381 operation: 'done' }, this._window));
382 },
383
384 // public interface
385
386 get store() {
387 return this._dataStore.exposedObject;
388 },
389
390 next: function() {
391 debug('Next');
392
393 let self = this;
394 return new this._window.Promise(function(aResolve, aReject) {
395 self._dataStore._db.cursorTxn(
396 function(aTxn, aStore, aRevisionStore) {
397 self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
398 },
399 function(aEvent) {
400 aReject(createDOMError(self._window, aEvent));
401 }
402 );
403 });
404 },
405
406 close: function() {
407 this._dataStore.syncTerminated(this);
408 }
409 };

mercurial