Bug 1892041 - Part 1: Update test262 features. r=spidermonkey-reviewers,dminor
[gecko.git] / toolkit / modules / IndexedDB.sys.mjs
blobb3b9f81b8d3675f7d118b9ee3f67b47045d7daea
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/. */
7 /**
8  * @file
9  *
10  * This module provides Promise-based wrappers around ordinarily
11  * IDBRequest-based IndexedDB methods and classes.
12  */
14 /**
15  * Wraps the given request object, and returns a Promise which resolves when
16  * the requests succeeds or rejects when it fails.
17  *
18  * @param {IDBRequest} request
19  *        An IndexedDB request object to wrap.
20  * @returns {Promise}
21  */
22 function wrapRequest(request) {
23   return new Promise((resolve, reject) => {
24     request.onsuccess = () => {
25       resolve(request.result);
26     };
27     request.onerror = () => {
28       reject(request.error);
29     };
30   });
33 /**
34  * Forwards a set of getter properties from a wrapper class to the wrapped
35  * object.
36  *
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.
44  */
45 function forwardGetters(cls, target, props) {
46   for (let prop of props) {
47     Object.defineProperty(cls.prototype, prop, {
48       get() {
49         return this[target][prop];
50       },
51     });
52   }
55 /**
56  * Forwards a set of getter and setter properties from a wrapper class to the
57  * wrapped object.
58  *
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.
66  */
67 function forwardProps(cls, target, props) {
68   for (let prop of props) {
69     Object.defineProperty(cls.prototype, prop, {
70       get() {
71         return this[target][prop];
72       },
73       set(value) {
74         this[target][prop] = value;
75       },
76     });
77   }
80 /**
81  * Wraps a set of IDBRequest-based methods via {@link wrapRequest} and
82  * forwards them to the equivalent methods on the wrapped object.
83  *
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.
91  */
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));
96     };
97   }
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.
110  */
111 function forwardMethods(cls, target, methods) {
112   for (let method of methods) {
113     cls.prototype[method] = function (...args) {
114       return this[target][method](...args);
115     };
116   }
119 class Cursor {
120   constructor(cursorRequest, source) {
121     this.cursorRequest = cursorRequest;
122     this.source = source;
123     this.cursor = null;
124   }
126   get done() {
127     return !this.cursor;
128   }
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);
136     return this;
137   }
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.
149  */
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);
155       await promise;
156     };
157   }
160 defineCursorUpdateMethods(Cursor, [
161   "advance",
162   "continue",
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"]);
173 class Cursed {
174   constructor(cursed) {
175     this.cursed = cursed;
176   }
178   openCursor(...args) {
179     const cursor = new CursorWithValue(this.cursed.openCursor(...args), this);
180     return cursor.awaitRequest();
181   }
183   openKeyCursor(...args) {
184     const cursor = new Cursor(this.cursed.openKeyCursor(...args), this);
185     return cursor.awaitRequest();
186   }
189 wrapMethods(Cursed, "cursed", [
190   "count",
191   "get",
192   "getAll",
193   "getAllKeys",
194   "getKey",
197 class Index extends Cursed {
198   constructor(index, objectStore) {
199     super(index);
201     this.objectStore = objectStore;
202     this.index = index;
203   }
206 forwardGetters(Index, "index", [
207   "isAutoLocale",
208   "keyPath",
209   "locale",
210   "multiEntry",
211   "name",
212   "unique",
215 class ObjectStore extends Cursed {
216   constructor(store) {
217     super(store);
219     this.store = store;
220   }
222   createIndex(...args) {
223     return new Index(this.store.createIndex(...args), this);
224   }
226   index(...args) {
227     return new Index(this.store.index(...args), this);
228   }
231 wrapMethods(ObjectStore, "store", ["add", "clear", "delete", "put"]);
233 forwardMethods(ObjectStore, "store", ["deleteIndex"]);
235 class Transaction {
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);
243       };
244       transaction.onabort = () => {
245         const error =
246           transaction.error ||
247           new DOMException("The operation has been aborted", "AbortError");
248         reject(error);
249       };
250     });
251   }
253   objectStore(name) {
254     return new ObjectStore(this.transaction.objectStore(name));
255   }
257   /**
258    * Returns a Promise which resolves when the transaction completes, or
259    * rejects when a transaction error or abort occurs.
260    *
261    * @returns {Promise}
262    */
263   promiseComplete() {
264     return this._completionPromise;
265   }
268 forwardGetters(Transaction, "transaction", [
269   "db",
270   "mode",
271   "error",
272   "objectStoreNames",
275 forwardMethods(Transaction, "transaction", ["abort"]);
277 export class IndexedDB {
278   /**
279    * Opens the database with the given name, and returns a Promise which
280    * resolves to an IndexedDB instance when the operation completes.
281    *
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.
295    *
296    * @returns {Promise<IndexedDB>}
297    */
298   static open(dbName, options, onupgradeneeded = null) {
299     let request = indexedDB.open(dbName, options);
300     return this._wrapOpenRequest(request, onupgradeneeded);
301   }
303   /**
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.
306    *
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.
322    *
323    * @returns {Promise<IndexedDB>}
324    */
325   static openForPrincipal(principal, dbName, options, onupgradeneeded = null) {
326     const request = indexedDB.openForPrincipal(principal, dbName, options);
327     return this._wrapOpenRequest(request, onupgradeneeded);
328   }
330   static _wrapOpenRequest(request, onupgradeneeded = null) {
331     request.onupgradeneeded = event => {
332       let db = new this(request.result);
333       if (onupgradeneeded) {
334         onupgradeneeded(db, event);
335       } else {
336         db.onupgradeneeded(event);
337       }
338     };
340     return wrapRequest(request).then(db => new this(db));
341   }
343   constructor(db) {
344     this.db = db;
345   }
347   onupgradeneeded() {}
349   /**
350    * Opens a transaction for the given object stores.
351    *
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
360    *        completes.
361    * @returns {Transaction|Promise}
362    */
363   transaction(storeNames, mode, callback = null) {
364     let transaction = new Transaction(this.db.transaction(storeNames, mode));
366     if (callback) {
367       let result = new Promise(resolve => {
368         resolve(callback(transaction));
369       });
370       return transaction.promiseComplete().then(() => result);
371     }
373     return transaction;
374   }
376   /**
377    * Opens a transaction for a single object store, and returns that object
378    * store.
379    *
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
388    *        completes.
389    * @returns {ObjectStore|Promise}
390    */
391   objectStore(storeName, mode, callback = null) {
392     let transaction = this.transaction([storeName], mode);
393     let objectStore = transaction.objectStore(storeName);
395     if (callback) {
396       let result = new Promise(resolve => {
397         resolve(callback(objectStore));
398       });
399       return transaction.promiseComplete().then(() => result);
400     }
402     return objectStore;
403   }
405   createObjectStore(...args) {
406     return new ObjectStore(this.db.createObjectStore(...args));
407   }
410 for (let method of ["cmp", "deleteDatabase"]) {
411   IndexedDB[method] = function (...args) {
412     return indexedDB[method](...args);
413   };
416 forwardMethods(IndexedDB, "db", [
417   "addEventListener",
418   "close",
419   "deleteObjectStore",
420   "hasEventListener",
421   "removeEventListener",
424 forwardGetters(IndexedDB, "db", ["name", "objectStoreNames", "version"]);
426 forwardProps(IndexedDB, "db", [
427   "onabort",
428   "onclose",
429   "onerror",
430   "onversionchange",