Bug 1608150 [wpt PR 21112] - Add missing space in `./wpt lint` command line docs...
[gecko.git] / toolkit / modules / IndexedDB.jsm
blobc0f11635445f5ec9517998356dd0c00daec8ab73
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";
8 /**
9  * @file
10  *
11  * This module provides Promise-based wrappers around ordinarily
12  * IDBRequest-based IndexedDB methods and classes.
13  */
15 /* exported IndexedDB */
16 var EXPORTED_SYMBOLS = ["IndexedDB"];
18 Cu.importGlobalProperties(["indexedDB"]);
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   });
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   }
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   }
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   }
107  * Forwards a set of methods from a wrapper class to the wrapped object.
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   }
125 class Cursor {
126   constructor(cursorRequest, source) {
127     this.cursorRequest = cursorRequest;
128     this.source = source;
129     this.cursor = null;
130   }
132   get done() {
133     return !this.cursor;
134   }
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   }
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.
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   }
166 defineCursorUpdateMethods(Cursor, [
167   "advance",
168   "continue",
169   "continuePrimaryKey",
172 forwardGetters(Cursor, "cursor", ["direction", "key", "primaryKey"]);
173 wrapMethods(Cursor, "cursor", ["delete", "update"]);
175 class CursorWithValue extends Cursor {}
177 forwardGetters(CursorWithValue, "cursor", ["value"]);
179 class Cursed {
180   constructor(cursed) {
181     this.cursed = cursed;
182   }
184   openCursor(...args) {
185     const cursor = new CursorWithValue(this.cursed.openCursor(...args), this);
186     return cursor.awaitRequest();
187   }
189   openKeyCursor(...args) {
190     const cursor = new Cursor(this.cursed.openKeyCursor(...args), this);
191     return cursor.awaitRequest();
192   }
195 wrapMethods(Cursed, "cursed", [
196   "count",
197   "get",
198   "getAll",
199   "getAllKeys",
200   "getKey",
203 class Index extends Cursed {
204   constructor(index, objectStore) {
205     super(index);
207     this.objectStore = objectStore;
208     this.index = index;
209   }
212 forwardGetters(Index, "index", [
213   "isAutoLocale",
214   "keyPath",
215   "locale",
216   "multiEntry",
217   "name",
218   "unique",
221 class ObjectStore extends Cursed {
222   constructor(store) {
223     super(store);
225     this.store = store;
226   }
228   createIndex(...args) {
229     return new Index(this.store.createIndex(...args), this);
230   }
232   index(...args) {
233     return new Index(this.store.index(...args), this);
234   }
237 wrapMethods(ObjectStore, "store", ["add", "clear", "delete", "put"]);
239 forwardMethods(ObjectStore, "store", ["deleteIndex"]);
241 class Transaction {
242   constructor(transaction) {
243     this.transaction = transaction;
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   }
259   objectStore(name) {
260     return new ObjectStore(this.transaction.objectStore(name));
261   }
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   }
274 forwardGetters(Transaction, "transaction", [
275   "db",
276   "mode",
277   "error",
278   "objectStoreNames",
281 forwardMethods(Transaction, "transaction", ["abort"]);
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   }
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   }
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     };
346     return wrapRequest(request).then(db => new this(db));
347   }
349   constructor(db) {
350     this.db = db;
351   }
353   onupgradeneeded() {}
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));
372     if (callback) {
373       let result = new Promise(resolve => {
374         resolve(callback(transaction));
375       });
376       return transaction.promiseComplete().then(() => result);
377     }
379     return transaction;
380   }
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);
401     if (callback) {
402       let result = new Promise(resolve => {
403         resolve(callback(objectStore));
404       });
405       return transaction.promiseComplete().then(() => result);
406     }
408     return objectStore;
409   }
411   createObjectStore(...args) {
412     return new ObjectStore(this.db.createObjectStore(...args));
413   }
416 for (let method of ["cmp", "deleteDatabase"]) {
417   IndexedDB[method] = function(...args) {
418     return indexedDB[method](...args);
419   };
422 forwardMethods(IndexedDB, "db", [
423   "addEventListener",
424   "close",
425   "deleteObjectStore",
426   "hasEventListener",
427   "removeEventListener",
430 forwardGetters(IndexedDB, "db", ["name", "objectStoreNames", "version"]);
432 forwardProps(IndexedDB, "db", [
433   "onabort",
434   "onclose",
435   "onerror",
436   "onversionchange",