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/. */
10 * This module provides Promise-based wrappers around ordinarily
11 * IDBRequest-based IndexedDB methods and classes.
15 * Wraps the given request object, and returns a Promise which resolves when
16 * the requests succeeds or rejects when it fails.
18 * @param {IDBRequest} request
19 * An IndexedDB request object to wrap.
22 function wrapRequest(request) {
23 return new Promise((resolve, reject) => {
24 request.onsuccess = () => {
25 resolve(request.result);
27 request.onerror = () => {
28 reject(request.error);
34 * Forwards a set of getter properties from a wrapper class to the wrapped
37 * @param {function} cls
38 * The class constructor for which to forward the getters.
39 * @param {string} target
40 * The name of the property which contains the wrapped object to which
41 * to forward the getters.
42 * @param {Array<string>} props
43 * A list of property names to forward.
45 function forwardGetters(cls, target, props) {
46 for (let prop of props) {
47 Object.defineProperty(cls.prototype, prop, {
49 return this[target][prop];
56 * Forwards a set of getter and setter properties from a wrapper class to the
59 * @param {function} cls
60 * The class constructor for which to forward the properties.
61 * @param {string} target
62 * The name of the property which contains the wrapped object to which
63 * to forward the properties.
64 * @param {Array<string>} props
65 * A list of property names to forward.
67 function forwardProps(cls, target, props) {
68 for (let prop of props) {
69 Object.defineProperty(cls.prototype, prop, {
71 return this[target][prop];
74 this[target][prop] = value;
81 * Wraps a set of IDBRequest-based methods via {@link wrapRequest} and
82 * forwards them to the equivalent methods on the wrapped object.
84 * @param {function} cls
85 * The class constructor for which to forward the methods.
86 * @param {string} target
87 * The name of the property which contains the wrapped object to which
88 * to forward the methods.
89 * @param {Array<string>} methods
90 * A list of method names to forward.
92 function wrapMethods(cls, target, methods) {
93 for (let method of methods) {
94 cls.prototype[method] = function (...args) {
95 return wrapRequest(this[target][method](...args));
101 * Forwards a set of methods from a wrapper class to the wrapped object.
103 * @param {function} cls
104 * The class constructor for which to forward the getters.
105 * @param {string} target
106 * The name of the property which contains the wrapped object to which
107 * to forward the methods.
108 * @param {Array<string>} methods
109 * A list of method names to forward.
111 function forwardMethods(cls, target, methods) {
112 for (let method of methods) {
113 cls.prototype[method] = function (...args) {
114 return this[target][method](...args);
120 constructor(cursorRequest, source) {
121 this.cursorRequest = cursorRequest;
122 this.source = source;
130 // This method is used internally to wait the cursor's IDBRequest to have been
131 // completed and the internal cursor has been updated (used when we initially
132 // create the cursor from Cursed.openCursor/openKeyCursor, and in the method
133 // of this class defined by defineCursorUpdateMethods).
134 async awaitRequest() {
135 this.cursor = await wrapRequest(this.cursorRequest);
141 * Define the Cursor class methods that update the cursor (continue, continuePrimaryKey
142 * and advance) as async functions that call the related IDBCursor methods and
143 * await the cursor's IDBRequest to be completed.
145 * @param {function} cls
146 * The class constructor for which to define the cursor update methods.
147 * @param {Array<string>} methods
148 * A list of "cursor update" method names to define.
150 function defineCursorUpdateMethods(cls, methods) {
151 for (let method of methods) {
152 cls.prototype[method] = async function (...args) {
153 const promise = this.awaitRequest();
154 this.cursor[method](...args);
160 defineCursorUpdateMethods(Cursor, [
163 "continuePrimaryKey",
166 forwardGetters(Cursor, "cursor", ["direction", "key", "primaryKey"]);
167 wrapMethods(Cursor, "cursor", ["delete", "update"]);
169 class CursorWithValue extends Cursor {}
171 forwardGetters(CursorWithValue, "cursor", ["value"]);
174 constructor(cursed) {
175 this.cursed = cursed;
178 openCursor(...args) {
179 const cursor = new CursorWithValue(this.cursed.openCursor(...args), this);
180 return cursor.awaitRequest();
183 openKeyCursor(...args) {
184 const cursor = new Cursor(this.cursed.openKeyCursor(...args), this);
185 return cursor.awaitRequest();
189 wrapMethods(Cursed, "cursed", [
197 class Index extends Cursed {
198 constructor(index, objectStore) {
201 this.objectStore = objectStore;
206 forwardGetters(Index, "index", [
215 class ObjectStore extends Cursed {
222 createIndex(...args) {
223 return new Index(this.store.createIndex(...args), this);
227 return new Index(this.store.index(...args), this);
231 wrapMethods(ObjectStore, "store", ["add", "clear", "delete", "put"]);
233 forwardMethods(ObjectStore, "store", ["deleteIndex"]);
236 constructor(transaction) {
237 this.transaction = transaction;
239 this._completionPromise = new Promise((resolve, reject) => {
240 transaction.oncomplete = resolve;
241 transaction.onerror = () => {
242 reject(transaction.error);
244 transaction.onabort = () => {
247 new DOMException("The operation has been aborted", "AbortError");
254 return new ObjectStore(this.transaction.objectStore(name));
258 * Returns a Promise which resolves when the transaction completes, or
259 * rejects when a transaction error or abort occurs.
264 return this._completionPromise;
268 forwardGetters(Transaction, "transaction", [
275 forwardMethods(Transaction, "transaction", ["abort"]);
277 export class IndexedDB {
279 * Opens the database with the given name, and returns a Promise which
280 * resolves to an IndexedDB instance when the operation completes.
282 * @param {string} dbName
283 * The name of the database to open.
284 * @param {object} options
285 * The options with which to open the database.
286 * @param {integer} options.version
287 * The schema version with which the database needs to be opened. If
288 * the database does not exist, or its current schema version does
289 * not match, the `onupgradeneeded` function will be called.
290 * @param {function} [onupgradeneeded]
291 * A function which will be called with an IndexedDB object as its
292 * first parameter when the database needs to be created, or its
293 * schema needs to be upgraded. If this function is not provided, the
294 * {@link #onupgradeneeded} method will be called instead.
296 * @returns {Promise<IndexedDB>}
298 static open(dbName, options, onupgradeneeded = null) {
299 let request = indexedDB.open(dbName, options);
300 return this._wrapOpenRequest(request, onupgradeneeded);
304 * Opens the database for a given principal and with the given name, returns
305 * a Promise which resolves to an IndexedDB instance when the operation completes.
307 * @param {nsIPrincipal} principal
308 * The principal to open the database for.
309 * @param {string} dbName
310 * The name of the database to open.
311 * @param {object} options
312 * The options with which to open the database.
313 * @param {integer} options.version
314 * The schema version with which the database needs to be opened. If
315 * the database does not exist, or its current schema version does
316 * not match, the `onupgradeneeded` function will be called.
317 * @param {function} [onupgradeneeded]
318 * A function which will be called with an IndexedDB object as its
319 * first parameter when the database needs to be created, or its
320 * schema needs to be upgraded. If this function is not provided, the
321 * {@link #onupgradeneeded} method will be called instead.
323 * @returns {Promise<IndexedDB>}
325 static openForPrincipal(principal, dbName, options, onupgradeneeded = null) {
326 const request = indexedDB.openForPrincipal(principal, dbName, options);
327 return this._wrapOpenRequest(request, onupgradeneeded);
330 static _wrapOpenRequest(request, onupgradeneeded = null) {
331 request.onupgradeneeded = event => {
332 let db = new this(request.result);
333 if (onupgradeneeded) {
334 onupgradeneeded(db, event);
336 db.onupgradeneeded(event);
340 return wrapRequest(request).then(db => new this(db));
350 * Opens a transaction for the given object stores.
352 * @param {Array<string>} storeNames
353 * The names of the object stores for which to open a transaction.
354 * @param {string} [mode = "readonly"]
355 * The mode in which to open the transaction.
356 * @param {function} [callback]
357 * An optional callback function. If provided, the function will be
358 * called with the Transaction, and a Promise will be returned, which
359 * will resolve to the callback's return value when the transaction
361 * @returns {Transaction|Promise}
363 transaction(storeNames, mode, callback = null) {
364 let transaction = new Transaction(this.db.transaction(storeNames, mode));
367 let result = new Promise(resolve => {
368 resolve(callback(transaction));
370 return transaction.promiseComplete().then(() => result);
377 * Opens a transaction for a single object store, and returns that object
380 * @param {string} storeName
381 * The name of the object store to open.
382 * @param {string} [mode = "readonly"]
383 * The mode in which to open the transaction.
384 * @param {function} [callback]
385 * An optional callback function. If provided, the function will be
386 * called with the ObjectStore, and a Promise will be returned, which
387 * will resolve to the callback's return value when the transaction
389 * @returns {ObjectStore|Promise}
391 objectStore(storeName, mode, callback = null) {
392 let transaction = this.transaction([storeName], mode);
393 let objectStore = transaction.objectStore(storeName);
396 let result = new Promise(resolve => {
397 resolve(callback(objectStore));
399 return transaction.promiseComplete().then(() => result);
405 createObjectStore(...args) {
406 return new ObjectStore(this.db.createObjectStore(...args));
410 for (let method of ["cmp", "deleteDatabase"]) {
411 IndexedDB[method] = function (...args) {
412 return indexedDB[method](...args);
416 forwardMethods(IndexedDB, "db", [
421 "removeEventListener",
424 forwardGetters(IndexedDB, "db", ["name", "objectStoreNames", "version"]);
426 forwardProps(IndexedDB, "db", [