|
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 }; |