Source code

Revision control

Other Tools

1
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
2
/* vim: set sts=2 sw=2 et 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
"use strict";
7
8
/**
9
* @file
10
*
11
* This module provides Promise-based wrappers around ordinarily
12
* IDBRequest-based IndexedDB methods and classes.
13
*/
14
15
/* exported IndexedDB */
16
var EXPORTED_SYMBOLS = ["IndexedDB"];
17
18
Cu.importGlobalProperties(["indexedDB"]);
19
20
/**
21
* Wraps the given request object, and returns a Promise which resolves when
22
* the requests succeeds or rejects when it fails.
23
*
24
* @param {IDBRequest} request
25
* An IndexedDB request object to wrap.
26
* @returns {Promise}
27
*/
28
function wrapRequest(request) {
29
return new Promise((resolve, reject) => {
30
request.onsuccess = () => {
31
resolve(request.result);
32
};
33
request.onerror = () => {
34
reject(request.error);
35
};
36
});
37
}
38
39
/**
40
* Forwards a set of getter properties from a wrapper class to the wrapped
41
* object.
42
*
43
* @param {function} cls
44
* The class constructor for which to forward the getters.
45
* @param {string} target
46
* The name of the property which contains the wrapped object to which
47
* to forward the getters.
48
* @param {Array<string>} props
49
* A list of property names to forward.
50
*/
51
function forwardGetters(cls, target, props) {
52
for (let prop of props) {
53
Object.defineProperty(cls.prototype, prop, {
54
get() {
55
return this[target][prop];
56
},
57
});
58
}
59
}
60
61
/**
62
* Forwards a set of getter and setter properties from a wrapper class to the
63
* wrapped object.
64
*
65
* @param {function} cls
66
* The class constructor for which to forward the properties.
67
* @param {string} target
68
* The name of the property which contains the wrapped object to which
69
* to forward the properties.
70
* @param {Array<string>} props
71
* A list of property names to forward.
72
*/
73
function forwardProps(cls, target, props) {
74
for (let prop of props) {
75
Object.defineProperty(cls.prototype, prop, {
76
get() {
77
return this[target][prop];
78
},
79
set(value) {
80
this[target][prop] = value;
81
},
82
});
83
}
84
}
85
86
/**
87
* Wraps a set of IDBRequest-based methods via {@link wrapRequest} and
88
* forwards them to the equivalent methods on the wrapped object.
89
*
90
* @param {function} cls
91
* The class constructor for which to forward the methods.
92
* @param {string} target
93
* The name of the property which contains the wrapped object to which
94
* to forward the methods.
95
* @param {Array<string>} methods
96
* A list of method names to forward.
97
*/
98
function wrapMethods(cls, target, methods) {
99
for (let method of methods) {
100
cls.prototype[method] = function(...args) {
101
return wrapRequest(this[target][method](...args));
102
};
103
}
104
}
105
106
/**
107
* Forwards a set of methods from a wrapper class to the wrapped object.
108
*
109
* @param {function} cls
110
* The class constructor for which to forward the getters.
111
* @param {string} target
112
* The name of the property which contains the wrapped object to which
113
* to forward the methods.
114
* @param {Array<string>} methods
115
* A list of method names to forward.
116
*/
117
function forwardMethods(cls, target, methods) {
118
for (let method of methods) {
119
cls.prototype[method] = function(...args) {
120
return this[target][method](...args);
121
};
122
}
123
}
124
125
class Cursor {
126
constructor(cursorRequest, source) {
127
this.cursorRequest = cursorRequest;
128
this.source = source;
129
this.cursor = null;
130
}
131
132
get done() {
133
return !this.cursor;
134
}
135
136
// This method is used internally to wait the cursor's IDBRequest to have been
137
// completed and the internal cursor has been updated (used when we initially
138
// create the cursor from Cursed.openCursor/openKeyCursor, and in the method
139
// of this class defined by defineCursorUpdateMethods).
140
async awaitRequest() {
141
this.cursor = await wrapRequest(this.cursorRequest);
142
return this;
143
}
144
}
145
146
/**
147
* Define the Cursor class methods that update the cursor (continue, continuePrimaryKey
148
* and advance) as async functions that call the related IDBCursor methods and
149
* await the cursor's IDBRequest to be completed.
150
*
151
* @param {function} cls
152
* The class constructor for which to define the cursor update methods.
153
* @param {Array<string>} methods
154
* A list of "cursor update" method names to define.
155
*/
156
function defineCursorUpdateMethods(cls, methods) {
157
for (let method of methods) {
158
cls.prototype[method] = async function(...args) {
159
const promise = this.awaitRequest();
160
this.cursor[method](...args);
161
await promise;
162
};
163
}
164
}
165
166
defineCursorUpdateMethods(Cursor, [
167
"advance",
168
"continue",
169
"continuePrimaryKey",
170
]);
171
172
forwardGetters(Cursor, "cursor", ["direction", "key", "primaryKey"]);
173
wrapMethods(Cursor, "cursor", ["delete", "update"]);
174
175
class CursorWithValue extends Cursor {}
176
177
forwardGetters(CursorWithValue, "cursor", ["value"]);
178
179
class Cursed {
180
constructor(cursed) {
181
this.cursed = cursed;
182
}
183
184
openCursor(...args) {
185
const cursor = new CursorWithValue(this.cursed.openCursor(...args), this);
186
return cursor.awaitRequest();
187
}
188
189
openKeyCursor(...args) {
190
const cursor = new Cursor(this.cursed.openKeyCursor(...args), this);
191
return cursor.awaitRequest();
192
}
193
}
194
195
wrapMethods(Cursed, "cursed", [
196
"count",
197
"get",
198
"getAll",
199
"getAllKeys",
200
"getKey",
201
]);
202
203
class Index extends Cursed {
204
constructor(index, objectStore) {
205
super(index);
206
207
this.objectStore = objectStore;
208
this.index = index;
209
}
210
}
211
212
forwardGetters(Index, "index", [
213
"isAutoLocale",
214
"keyPath",
215
"locale",
216
"multiEntry",
217
"name",
218
"unique",
219
]);
220
221
class ObjectStore extends Cursed {
222
constructor(store) {
223
super(store);
224
225
this.store = store;
226
}
227
228
createIndex(...args) {
229
return new Index(this.store.createIndex(...args), this);
230
}
231
232
index(...args) {
233
return new Index(this.store.index(...args), this);
234
}
235
}
236
237
wrapMethods(ObjectStore, "store", ["add", "clear", "delete", "put"]);
238
239
forwardMethods(ObjectStore, "store", ["deleteIndex"]);
240
241
class Transaction {
242
constructor(transaction) {
243
this.transaction = transaction;
244
245
this._completionPromise = new Promise((resolve, reject) => {
246
transaction.oncomplete = resolve;
247
transaction.onerror = () => {
248
reject(transaction.error);
249
};
250
transaction.onabort = () => {
251
const error =
252
transaction.error ||
253
new DOMException("The operation has been aborted", "AbortError");
254
reject(error);
255
};
256
});
257
}
258
259
objectStore(name) {
260
return new ObjectStore(this.transaction.objectStore(name));
261
}
262
263
/**
264
* Returns a Promise which resolves when the transaction completes, or
265
* rejects when a transaction error or abort occurs.
266
*
267
* @returns {Promise}
268
*/
269
promiseComplete() {
270
return this._completionPromise;
271
}
272
}
273
274
forwardGetters(Transaction, "transaction", [
275
"db",
276
"mode",
277
"error",
278
"objectStoreNames",
279
]);
280
281
forwardMethods(Transaction, "transaction", ["abort"]);
282
283
class IndexedDB {
284
/**
285
* Opens the database with the given name, and returns a Promise which
286
* resolves to an IndexedDB instance when the operation completes.
287
*
288
* @param {string} dbName
289
* The name of the database to open.
290
* @param {object} options
291
* The options with which to open the database.
292
* @param {integer} options.version
293
* The schema version with which the database needs to be opened. If
294
* the database does not exist, or its current schema version does
295
* not match, the `onupgradeneeded` function will be called.
296
* @param {function} [onupgradeneeded]
297
* A function which will be called with an IndexedDB object as its
298
* first parameter when the database needs to be created, or its
299
* schema needs to be upgraded. If this function is not provided, the
300
* {@link #onupgradeneeded} method will be called instead.
301
*
302
* @returns {Promise<IndexedDB>}
303
*/
304
static open(dbName, options, onupgradeneeded = null) {
305
let request = indexedDB.open(dbName, options);
306
return this._wrapOpenRequest(request, onupgradeneeded);
307
}
308
309
/**
310
* Opens the database for a given principal and with the given name, returns
311
* a Promise which resolves to an IndexedDB instance when the operation completes.
312
*
313
* @param {nsIPrincipal} principal
314
* The principal to open the database for.
315
* @param {string} dbName
316
* The name of the database to open.
317
* @param {object} options
318
* The options with which to open the database.
319
* @param {integer} options.version
320
* The schema version with which the database needs to be opened. If
321
* the database does not exist, or its current schema version does
322
* not match, the `onupgradeneeded` function will be called.
323
* @param {function} [onupgradeneeded]
324
* A function which will be called with an IndexedDB object as its
325
* first parameter when the database needs to be created, or its
326
* schema needs to be upgraded. If this function is not provided, the
327
* {@link #onupgradeneeded} method will be called instead.
328
*
329
* @returns {Promise<IndexedDB>}
330
*/
331
static openForPrincipal(principal, dbName, options, onupgradeneeded = null) {
332
const request = indexedDB.openForPrincipal(principal, dbName, options);
333
return this._wrapOpenRequest(request, onupgradeneeded);
334
}
335
336
static _wrapOpenRequest(request, onupgradeneeded = null) {
337
request.onupgradeneeded = event => {
338
let db = new this(request.result);
339
if (onupgradeneeded) {
340
onupgradeneeded(db, event);
341
} else {
342
db.onupgradeneeded(event);
343
}
344
};
345
346
return wrapRequest(request).then(db => new this(db));
347
}
348
349
constructor(db) {
350
this.db = db;
351
}
352
353
onupgradeneeded() {}
354
355
/**
356
* Opens a transaction for the given object stores.
357
*
358
* @param {Array<string>} storeNames
359
* The names of the object stores for which to open a transaction.
360
* @param {string} [mode = "readonly"]
361
* The mode in which to open the transaction.
362
* @param {function} [callback]
363
* An optional callback function. If provided, the function will be
364
* called with the Transaction, and a Promise will be returned, which
365
* will resolve to the callback's return value when the transaction
366
* completes.
367
* @returns {Transaction|Promise}
368
*/
369
transaction(storeNames, mode, callback = null) {
370
let transaction = new Transaction(this.db.transaction(storeNames, mode));
371
372
if (callback) {
373
let result = new Promise(resolve => {
374
resolve(callback(transaction));
375
});
376
return transaction.promiseComplete().then(() => result);
377
}
378
379
return transaction;
380
}
381
382
/**
383
* Opens a transaction for a single object store, and returns that object
384
* store.
385
*
386
* @param {string} storeName
387
* The name of the object store to open.
388
* @param {string} [mode = "readonly"]
389
* The mode in which to open the transaction.
390
* @param {function} [callback]
391
* An optional callback function. If provided, the function will be
392
* called with the ObjectStore, and a Promise will be returned, which
393
* will resolve to the callback's return value when the transaction
394
* completes.
395
* @returns {ObjectStore|Promise}
396
*/
397
objectStore(storeName, mode, callback = null) {
398
let transaction = this.transaction([storeName], mode);
399
let objectStore = transaction.objectStore(storeName);
400
401
if (callback) {
402
let result = new Promise(resolve => {
403
resolve(callback(objectStore));
404
});
405
return transaction.promiseComplete().then(() => result);
406
}
407
408
return objectStore;
409
}
410
411
createObjectStore(...args) {
412
return new ObjectStore(this.db.createObjectStore(...args));
413
}
414
}
415
416
for (let method of ["cmp", "deleteDatabase"]) {
417
IndexedDB[method] = function(...args) {
418
return indexedDB[method](...args);
419
};
420
}
421
422
forwardMethods(IndexedDB, "db", [
423
"addEventListener",
424
"close",
425
"deleteObjectStore",
426
"hasEventListener",
427
"removeEventListener",
428
]);
429
430
forwardGetters(IndexedDB, "db", ["name", "objectStoreNames", "version"]);
431
432
forwardProps(IndexedDB, "db", [
433
"onabort",
434
"onclose",
435
"onerror",
436
"onversionchange",
437
]);