|
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 = ["DataStore"]; |
|
10 |
|
11 function debug(s) { |
|
12 //dump('DEBUG DataStore: ' + s + '\n'); |
|
13 } |
|
14 |
|
15 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; |
|
16 |
|
17 const REVISION_ADDED = "added"; |
|
18 const REVISION_UPDATED = "updated"; |
|
19 const REVISION_REMOVED = "removed"; |
|
20 const REVISION_VOID = "void"; |
|
21 |
|
22 // This value has to be tuned a bit. Currently it's just a guess |
|
23 // and yet we don't know if it's too low or too high. |
|
24 const MAX_REQUESTS = 25; |
|
25 |
|
26 Cu.import("resource://gre/modules/DataStoreCursorImpl.jsm"); |
|
27 Cu.import("resource://gre/modules/DataStoreDB.jsm"); |
|
28 Cu.import('resource://gre/modules/Services.jsm'); |
|
29 Cu.import('resource://gre/modules/XPCOMUtils.jsm'); |
|
30 Cu.importGlobalProperties(["indexedDB"]); |
|
31 |
|
32 XPCOMUtils.defineLazyServiceGetter(this, "cpmm", |
|
33 "@mozilla.org/childprocessmessagemanager;1", |
|
34 "nsIMessageSender"); |
|
35 |
|
36 /* Helper functions */ |
|
37 function createDOMError(aWindow, aEvent) { |
|
38 return new aWindow.DOMError(aEvent); |
|
39 } |
|
40 |
|
41 function throwInvalidArg(aWindow) { |
|
42 return aWindow.Promise.reject( |
|
43 new aWindow.DOMError("SyntaxError", "Non-numeric or invalid id")); |
|
44 } |
|
45 |
|
46 function throwReadOnly(aWindow) { |
|
47 return aWindow.Promise.reject( |
|
48 new aWindow.DOMError("ReadOnlyError", "DataStore in readonly mode")); |
|
49 } |
|
50 |
|
51 function validateId(aId) { |
|
52 // If string, it cannot be empty. |
|
53 if (typeof(aId) == 'string') { |
|
54 return aId.length; |
|
55 } |
|
56 |
|
57 aId = parseInt(aId); |
|
58 return (!isNaN(aId) && aId > 0); |
|
59 } |
|
60 |
|
61 /* DataStore object */ |
|
62 this.DataStore = function(aWindow, aName, aOwner, aReadOnly) { |
|
63 debug("DataStore created"); |
|
64 this.init(aWindow, aName, aOwner, aReadOnly); |
|
65 } |
|
66 |
|
67 this.DataStore.prototype = { |
|
68 classDescription: "DataStore XPCOM Component", |
|
69 classID: Components.ID("{db5c9602-030f-4bff-a3de-881a8de370f2}"), |
|
70 contractID: "@mozilla.org/dom/datastore-impl;1", |
|
71 QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsISupports, |
|
72 Components.interfaces.nsIObserver]), |
|
73 |
|
74 callbacks: [], |
|
75 |
|
76 _window: null, |
|
77 _name: null, |
|
78 _owner: null, |
|
79 _readOnly: null, |
|
80 _revisionId: null, |
|
81 _exposedObject: null, |
|
82 _cursor: null, |
|
83 _shuttingdown: false, |
|
84 _eventTarget: null, |
|
85 |
|
86 init: function(aWindow, aName, aOwner, aReadOnly) { |
|
87 debug("DataStore init"); |
|
88 |
|
89 this._window = aWindow; |
|
90 this._name = aName; |
|
91 this._owner = aOwner; |
|
92 this._readOnly = aReadOnly; |
|
93 |
|
94 this._db = new DataStoreDB(); |
|
95 this._db.init(aOwner, aName); |
|
96 |
|
97 Services.obs.addObserver(this, "inner-window-destroyed", false); |
|
98 |
|
99 let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) |
|
100 .getInterface(Ci.nsIDOMWindowUtils); |
|
101 this._innerWindowID = util.currentInnerWindowID; |
|
102 |
|
103 cpmm.addMessageListener("DataStore:Changed:Return:OK", this); |
|
104 cpmm.sendAsyncMessage("DataStore:RegisterForMessages", |
|
105 { store: this._name, owner: this._owner }); |
|
106 }, |
|
107 |
|
108 observe: function(aSubject, aTopic, aData) { |
|
109 let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data; |
|
110 if (wId == this._innerWindowID) { |
|
111 Services.obs.removeObserver(this, "inner-window-destroyed"); |
|
112 |
|
113 cpmm.removeMessageListener("DataStore:Changed:Return:OK", this); |
|
114 cpmm.sendAsyncMessage("DataStore:UnregisterForMessages"); |
|
115 this._shuttingdown = true; |
|
116 this._db.close(); |
|
117 } |
|
118 }, |
|
119 |
|
120 setEventTarget: function(aEventTarget) { |
|
121 this._eventTarget = aEventTarget; |
|
122 }, |
|
123 |
|
124 newDBPromise: function(aTxnType, aFunction) { |
|
125 let self = this; |
|
126 return new this._window.Promise(function(aResolve, aReject) { |
|
127 debug("DBPromise started"); |
|
128 self._db.txn( |
|
129 aTxnType, |
|
130 function(aTxn, aStore, aRevisionStore) { |
|
131 debug("DBPromise success"); |
|
132 aFunction(aResolve, aReject, aTxn, aStore, aRevisionStore); |
|
133 }, |
|
134 function(aEvent) { |
|
135 debug("DBPromise error"); |
|
136 aReject(createDOMError(self._window, aEvent)); |
|
137 } |
|
138 ); |
|
139 }); |
|
140 }, |
|
141 |
|
142 checkRevision: function(aReject, aRevisionStore, aRevisionId, aCallback) { |
|
143 if (!aRevisionId) { |
|
144 aCallback(); |
|
145 return; |
|
146 } |
|
147 |
|
148 let self = this; |
|
149 |
|
150 let request = aRevisionStore.openCursor(null, 'prev'); |
|
151 request.onsuccess = function(aEvent) { |
|
152 let cursor = aEvent.target.result; |
|
153 if (!cursor) { |
|
154 dump("This cannot really happen."); |
|
155 return; |
|
156 } |
|
157 |
|
158 if (cursor.value.revisionId != aRevisionId) { |
|
159 aReject(new self._window.DOMError("ConstraintError", |
|
160 "RevisionId is not up-to-date")); |
|
161 return; |
|
162 } |
|
163 |
|
164 aCallback(); |
|
165 } |
|
166 }, |
|
167 |
|
168 getInternal: function(aStore, aIds, aCallback) { |
|
169 debug("GetInternal: " + aIds.toSource()); |
|
170 |
|
171 // Creation of the results array. |
|
172 let results = new Array(aIds.length); |
|
173 |
|
174 // We're going to create this amount of requests. |
|
175 let pendingIds = aIds.length; |
|
176 let indexPos = 0; |
|
177 |
|
178 let self = this; |
|
179 |
|
180 function getInternalSuccess(aEvent, aPos) { |
|
181 debug("GetInternal success. Record: " + aEvent.target.result); |
|
182 results[aPos] = Cu.cloneInto(aEvent.target.result, self._window); |
|
183 if (!--pendingIds) { |
|
184 aCallback(results); |
|
185 return; |
|
186 } |
|
187 |
|
188 if (indexPos < aIds.length) { |
|
189 // Just MAX_REQUESTS requests at the same time. |
|
190 let count = 0; |
|
191 while (indexPos < aIds.length && ++count < MAX_REQUESTS) { |
|
192 getInternalRequest(); |
|
193 } |
|
194 } |
|
195 } |
|
196 |
|
197 function getInternalRequest() { |
|
198 let currentPos = indexPos++; |
|
199 let request = aStore.get(aIds[currentPos]); |
|
200 request.onsuccess = function(aEvent) { |
|
201 getInternalSuccess(aEvent, currentPos); |
|
202 } |
|
203 } |
|
204 |
|
205 getInternalRequest(); |
|
206 }, |
|
207 |
|
208 putInternal: function(aResolve, aStore, aRevisionStore, aObj, aId) { |
|
209 debug("putInternal " + aId); |
|
210 |
|
211 let self = this; |
|
212 let request = aStore.put(aObj, aId); |
|
213 request.onsuccess = function(aEvent) { |
|
214 debug("putInternal success"); |
|
215 |
|
216 self.addRevision(aRevisionStore, aId, REVISION_UPDATED, |
|
217 function() { |
|
218 debug("putInternal - revisionId increased"); |
|
219 // No wrap here because the result is always a int. |
|
220 aResolve(aEvent.target.result); |
|
221 } |
|
222 ); |
|
223 }; |
|
224 }, |
|
225 |
|
226 addInternal: function(aResolve, aStore, aRevisionStore, aObj, aId) { |
|
227 debug("AddInternal"); |
|
228 |
|
229 let self = this; |
|
230 let request = aStore.add(aObj, aId); |
|
231 request.onsuccess = function(aEvent) { |
|
232 debug("Request successful. Id: " + aEvent.target.result); |
|
233 self.addRevision(aRevisionStore, aEvent.target.result, REVISION_ADDED, |
|
234 function() { |
|
235 debug("AddInternal - revisionId increased"); |
|
236 // No wrap here because the result is always a int. |
|
237 aResolve(aEvent.target.result); |
|
238 } |
|
239 ); |
|
240 }; |
|
241 }, |
|
242 |
|
243 removeInternal: function(aResolve, aStore, aRevisionStore, aId) { |
|
244 debug("RemoveInternal"); |
|
245 |
|
246 let self = this; |
|
247 let request = aStore.get(aId); |
|
248 request.onsuccess = function(aEvent) { |
|
249 debug("RemoveInternal success. Record: " + aEvent.target.result); |
|
250 if (aEvent.target.result === undefined) { |
|
251 aResolve(false); |
|
252 return; |
|
253 } |
|
254 |
|
255 let deleteRequest = aStore.delete(aId); |
|
256 deleteRequest.onsuccess = function() { |
|
257 debug("RemoveInternal success"); |
|
258 self.addRevision(aRevisionStore, aId, REVISION_REMOVED, |
|
259 function() { |
|
260 aResolve(true); |
|
261 } |
|
262 ); |
|
263 }; |
|
264 }; |
|
265 }, |
|
266 |
|
267 clearInternal: function(aResolve, aStore, aRevisionStore) { |
|
268 debug("ClearInternal"); |
|
269 |
|
270 let self = this; |
|
271 let request = aStore.clear(); |
|
272 request.onsuccess = function() { |
|
273 debug("ClearInternal success"); |
|
274 self._db.clearRevisions(aRevisionStore, |
|
275 function() { |
|
276 debug("Revisions cleared"); |
|
277 |
|
278 self.addRevision(aRevisionStore, 0, REVISION_VOID, |
|
279 function() { |
|
280 debug("ClearInternal - revisionId increased"); |
|
281 aResolve(); |
|
282 } |
|
283 ); |
|
284 } |
|
285 ); |
|
286 }; |
|
287 }, |
|
288 |
|
289 getLengthInternal: function(aResolve, aStore) { |
|
290 debug("GetLengthInternal"); |
|
291 |
|
292 let request = aStore.count(); |
|
293 request.onsuccess = function(aEvent) { |
|
294 debug("GetLengthInternal success: " + aEvent.target.result); |
|
295 // No wrap here because the result is always a int. |
|
296 aResolve(aEvent.target.result); |
|
297 }; |
|
298 }, |
|
299 |
|
300 addRevision: function(aRevisionStore, aId, aType, aSuccessCb) { |
|
301 let self = this; |
|
302 this._db.addRevision(aRevisionStore, aId, aType, |
|
303 function(aRevisionId) { |
|
304 self._revisionId = aRevisionId; |
|
305 self.sendNotification(aId, aType, aRevisionId); |
|
306 aSuccessCb(); |
|
307 } |
|
308 ); |
|
309 }, |
|
310 |
|
311 retrieveRevisionId: function(aSuccessCb) { |
|
312 let self = this; |
|
313 this._db.revisionTxn( |
|
314 'readonly', |
|
315 function(aTxn, aRevisionStore) { |
|
316 debug("RetrieveRevisionId transaction success"); |
|
317 |
|
318 let request = aRevisionStore.openCursor(null, 'prev'); |
|
319 request.onsuccess = function(aEvent) { |
|
320 let cursor = aEvent.target.result; |
|
321 if (cursor) { |
|
322 self._revisionId = cursor.value.revisionId; |
|
323 } |
|
324 |
|
325 aSuccessCb(self._revisionId); |
|
326 }; |
|
327 } |
|
328 ); |
|
329 }, |
|
330 |
|
331 sendNotification: function(aId, aOperation, aRevisionId) { |
|
332 debug("SendNotification"); |
|
333 if (aOperation == REVISION_VOID) { |
|
334 aOperation = "cleared"; |
|
335 } |
|
336 |
|
337 cpmm.sendAsyncMessage("DataStore:Changed", |
|
338 { store: this.name, owner: this._owner, |
|
339 message: { revisionId: aRevisionId, id: aId, |
|
340 operation: aOperation, owner: this._owner } } ); |
|
341 }, |
|
342 |
|
343 receiveMessage: function(aMessage) { |
|
344 debug("receiveMessage"); |
|
345 |
|
346 if (aMessage.name != "DataStore:Changed:Return:OK") { |
|
347 debug("Wrong message: " + aMessage.name); |
|
348 return; |
|
349 } |
|
350 |
|
351 // If this message is not for this DataStore, let's ignore it. |
|
352 if (aMessage.data.owner != this._owner || |
|
353 aMessage.data.store != this._name) { |
|
354 return; |
|
355 } |
|
356 |
|
357 let self = this; |
|
358 |
|
359 this.retrieveRevisionId( |
|
360 function() { |
|
361 // If the window has been destroyed we don't emit the events. |
|
362 if (self._shuttingdown) { |
|
363 return; |
|
364 } |
|
365 |
|
366 // If we have an active cursor we don't emit events. |
|
367 if (self._cursor) { |
|
368 return; |
|
369 } |
|
370 |
|
371 let event = new self._window.DataStoreChangeEvent('change', |
|
372 aMessage.data.message); |
|
373 self._eventTarget.dispatchEvent(event); |
|
374 } |
|
375 ); |
|
376 }, |
|
377 |
|
378 get exposedObject() { |
|
379 debug("get exposedObject"); |
|
380 return this._exposedObject; |
|
381 }, |
|
382 |
|
383 set exposedObject(aObject) { |
|
384 debug("set exposedObject"); |
|
385 this._exposedObject = aObject; |
|
386 }, |
|
387 |
|
388 syncTerminated: function(aCursor) { |
|
389 // This checks is to avoid that an invalid cursor stops a sync. |
|
390 if (this._cursor == aCursor) { |
|
391 this._cursor = null; |
|
392 } |
|
393 }, |
|
394 |
|
395 // Public interface : |
|
396 |
|
397 get name() { |
|
398 return this._name; |
|
399 }, |
|
400 |
|
401 get owner() { |
|
402 return this._owner; |
|
403 }, |
|
404 |
|
405 get readOnly() { |
|
406 return this._readOnly; |
|
407 }, |
|
408 |
|
409 get: function() { |
|
410 let ids = Array.prototype.slice.call(arguments); |
|
411 for (let i = 0; i < ids.length; ++i) { |
|
412 if (!validateId(ids[i])) { |
|
413 return throwInvalidArg(this._window); |
|
414 } |
|
415 } |
|
416 |
|
417 let self = this; |
|
418 |
|
419 // Promise<Object> |
|
420 return this.newDBPromise("readonly", |
|
421 function(aResolve, aReject, aTxn, aStore, aRevisionStore) { |
|
422 self.getInternal(aStore, ids, |
|
423 function(aResults) { |
|
424 aResolve(ids.length > 1 ? aResults : aResults[0]); |
|
425 }); |
|
426 } |
|
427 ); |
|
428 }, |
|
429 |
|
430 put: function(aObj, aId, aRevisionId) { |
|
431 if (!validateId(aId)) { |
|
432 return throwInvalidArg(this._window); |
|
433 } |
|
434 |
|
435 if (this._readOnly) { |
|
436 return throwReadOnly(this._window); |
|
437 } |
|
438 |
|
439 let self = this; |
|
440 |
|
441 // Promise<void> |
|
442 return this.newDBPromise("readwrite", |
|
443 function(aResolve, aReject, aTxn, aStore, aRevisionStore) { |
|
444 self.checkRevision(aReject, aRevisionStore, aRevisionId, function() { |
|
445 self.putInternal(aResolve, aStore, aRevisionStore, aObj, aId); |
|
446 }); |
|
447 } |
|
448 ); |
|
449 }, |
|
450 |
|
451 add: function(aObj, aId, aRevisionId) { |
|
452 if (aId) { |
|
453 if (!validateId(aId)) { |
|
454 return throwInvalidArg(this._window); |
|
455 } |
|
456 } |
|
457 |
|
458 if (this._readOnly) { |
|
459 return throwReadOnly(this._window); |
|
460 } |
|
461 |
|
462 let self = this; |
|
463 |
|
464 // Promise<int> |
|
465 return this.newDBPromise("readwrite", |
|
466 function(aResolve, aReject, aTxn, aStore, aRevisionStore) { |
|
467 self.checkRevision(aReject, aRevisionStore, aRevisionId, function() { |
|
468 self.addInternal(aResolve, aStore, aRevisionStore, aObj, aId); |
|
469 }); |
|
470 } |
|
471 ); |
|
472 }, |
|
473 |
|
474 remove: function(aId, aRevisionId) { |
|
475 if (!validateId(aId)) { |
|
476 return throwInvalidArg(this._window); |
|
477 } |
|
478 |
|
479 if (this._readOnly) { |
|
480 return throwReadOnly(this._window); |
|
481 } |
|
482 |
|
483 let self = this; |
|
484 |
|
485 // Promise<void> |
|
486 return this.newDBPromise("readwrite", |
|
487 function(aResolve, aReject, aTxn, aStore, aRevisionStore) { |
|
488 self.checkRevision(aReject, aRevisionStore, aRevisionId, function() { |
|
489 self.removeInternal(aResolve, aStore, aRevisionStore, aId); |
|
490 }); |
|
491 } |
|
492 ); |
|
493 }, |
|
494 |
|
495 clear: function(aRevisionId) { |
|
496 if (this._readOnly) { |
|
497 return throwReadOnly(this._window); |
|
498 } |
|
499 |
|
500 let self = this; |
|
501 |
|
502 // Promise<void> |
|
503 return this.newDBPromise("readwrite", |
|
504 function(aResolve, aReject, aTxn, aStore, aRevisionStore) { |
|
505 self.checkRevision(aReject, aRevisionStore, aRevisionId, function() { |
|
506 self.clearInternal(aResolve, aStore, aRevisionStore); |
|
507 }); |
|
508 } |
|
509 ); |
|
510 }, |
|
511 |
|
512 get revisionId() { |
|
513 return this._revisionId; |
|
514 }, |
|
515 |
|
516 getLength: function() { |
|
517 let self = this; |
|
518 |
|
519 // Promise<int> |
|
520 return this.newDBPromise("readonly", |
|
521 function(aResolve, aReject, aTxn, aStore, aRevisionStore) { |
|
522 self.getLengthInternal(aResolve, aStore); |
|
523 } |
|
524 ); |
|
525 }, |
|
526 |
|
527 sync: function(aRevisionId) { |
|
528 debug("Sync"); |
|
529 this._cursor = new DataStoreCursor(this._window, this, aRevisionId); |
|
530 |
|
531 let cursorImpl = this._window.DataStoreCursorImpl. |
|
532 _create(this._window, this._cursor); |
|
533 |
|
534 let exposedCursor = new this._window.DataStoreCursor(); |
|
535 exposedCursor.setDataStoreCursorImpl(cursorImpl); |
|
536 return exposedCursor; |
|
537 } |
|
538 }; |