Bug 1518618 - Add custom classes to the selectors for matches, attributes and pseudoc...
[gecko.git] / toolkit / modules / IndexedDB.jsm
blob9114de6651b61fa7535049c319ec41143f540f55
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, ["advance", "continue", "continuePrimaryKey"]);
168 forwardGetters(Cursor, "cursor",
169                ["direction", "key", "primaryKey"]);
170 wrapMethods(Cursor, "cursor", ["delete", "update"]);
172 class CursorWithValue extends Cursor {}
174 forwardGetters(CursorWithValue, "cursor", ["value"]);
176 class Cursed {
177   constructor(cursed) {
178     this.cursed = cursed;
179   }
181   openCursor(...args) {
182     const cursor = new CursorWithValue(this.cursed.openCursor(...args), this);
183     return cursor.awaitRequest();
184   }
186   openKeyCursor(...args) {
187     const cursor = new Cursor(this.cursed.openKeyCursor(...args), this);
188     return cursor.awaitRequest();
189   }
192 wrapMethods(Cursed, "cursed",
193             ["count", "get", "getAll", "getAllKeys", "getKey"]);
195 class Index extends Cursed {
196   constructor(index, objectStore) {
197     super(index);
199     this.objectStore = objectStore;
200     this.index = index;
201   }
204 forwardGetters(Index, "index",
205                ["isAutoLocale", "keyPath", "locale", "multiEntry", "name", "unique"]);
207 class ObjectStore extends Cursed {
208   constructor(store) {
209     super(store);
211     this.store = store;
212   }
214   createIndex(...args) {
215     return new Index(this.store.createIndex(...args),
216                      this);
217   }
219   index(...args) {
220     return new Index(this.store.index(...args),
221                      this);
222   }
225 wrapMethods(ObjectStore, "store",
226             ["add", "clear", "delete", "put"]);
228 forwardMethods(ObjectStore, "store", ["deleteIndex"]);
230 class Transaction {
231   constructor(transaction) {
232     this.transaction = transaction;
234     this._completionPromise = new Promise((resolve, reject) => {
235       transaction.oncomplete = resolve;
236       transaction.onerror = () => {
237         reject(transaction.error);
238       };
239     });
240   }
242   objectStore(name) {
243     return new ObjectStore(this.transaction.objectStore(name));
244   }
246   /**
247    * Returns a Promise which resolves when the transaction completes, or
248    * rejects when a transaction error occurs.
249    *
250    * @returns {Promise}
251    */
252   promiseComplete() {
253     return this._completionPromise;
254   }
257 forwardGetters(Transaction, "transaction",
258                ["db", "mode", "error", "objectStoreNames"]);
260 forwardMethods(Transaction, "transaction", ["abort"]);
262 class IndexedDB {
263   /**
264    * Opens the database with the given name, and returns a Promise which
265    * resolves to an IndexedDB instance when the operation completes.
266    *
267    * @param {string} dbName
268    *        The name of the database to open.
269    * @param {object} options
270    *        The options with which to open the database.
271    * @param {integer} options.version
272    *        The schema version with which the database needs to be opened. If
273    *        the database does not exist, or its current schema version does
274    *        not match, the `onupgradeneeded` function will be called.
275    * @param {function} [onupgradeneeded]
276    *        A function which will be called with an IndexedDB object as its
277    *        first parameter when the database needs to be created, or its
278    *        schema needs to be upgraded. If this function is not provided, the
279    *        {@link #onupgradeneeded} method will be called instead.
280    *
281    * @returns {Promise<IndexedDB>}
282    */
283   static open(dbName, options, onupgradeneeded = null) {
284     let request = indexedDB.open(dbName, options);
285     return this._wrapOpenRequest(request, onupgradeneeded);
286   }
288   /**
289    * Opens the database for a given principal and with the given name, returns
290    * a Promise which resolves to an IndexedDB instance when the operation completes.
291    *
292    * @param {nsIPrincipal} principal
293    *        The principal to open the database for.
294    * @param {string} dbName
295    *        The name of the database to open.
296    * @param {object} options
297    *        The options with which to open the database.
298    * @param {integer} options.version
299    *        The schema version with which the database needs to be opened. If
300    *        the database does not exist, or its current schema version does
301    *        not match, the `onupgradeneeded` function will be called.
302    * @param {function} [onupgradeneeded]
303    *        A function which will be called with an IndexedDB object as its
304    *        first parameter when the database needs to be created, or its
305    *        schema needs to be upgraded. If this function is not provided, the
306    *        {@link #onupgradeneeded} method will be called instead.
307    *
308    * @returns {Promise<IndexedDB>}
309    */
310   static openForPrincipal(principal, dbName, options, onupgradeneeded = null) {
311     const request = indexedDB.openForPrincipal(principal, dbName, options);
312     return this._wrapOpenRequest(request, onupgradeneeded);
313   }
315   static _wrapOpenRequest(request, onupgradeneeded = null) {
316     request.onupgradeneeded = event => {
317       let db = new this(request.result);
318       if (onupgradeneeded) {
319         onupgradeneeded(db, event);
320       } else {
321         db.onupgradeneeded(event);
322       }
323     };
325     return wrapRequest(request).then(db => new this(db));
326   }
328   constructor(db) {
329     this.db = db;
330   }
332   onupgradeneeded() {}
334   /**
335    * Opens a transaction for the given object stores.
336    *
337    * @param {Array<string>} storeNames
338    *        The names of the object stores for which to open a transaction.
339    * @param {string} [mode = "readonly"]
340    *        The mode in which to open the transaction.
341    * @param {function} [callback]
342    *        An optional callback function. If provided, the function will be
343    *        called with the Transaction, and a Promise will be returned, which
344    *        will resolve to the callback's return value when the transaction
345    *        completes.
346    * @returns {Transaction|Promise}
347    */
348   transaction(storeNames, mode, callback = null) {
349     let transaction = new Transaction(this.db.transaction(storeNames, mode));
351     if (callback) {
352       let result = new Promise(resolve => {
353         resolve(callback(transaction));
354       });
355       return transaction.promiseComplete().then(() => result);
356     }
358     return transaction;
359   }
361   /**
362    * Opens a transaction for a single object store, and returns that object
363    * store.
364    *
365    * @param {string} storeName
366    *        The name of the object store to open.
367    * @param {string} [mode = "readonly"]
368    *        The mode in which to open the transaction.
369    * @param {function} [callback]
370    *        An optional callback function. If provided, the function will be
371    *        called with the ObjectStore, and a Promise will be returned, which
372    *        will resolve to the callback's return value when the transaction
373    *        completes.
374    * @returns {ObjectStore|Promise}
375    */
376   objectStore(storeName, mode, callback = null) {
377     let transaction = this.transaction([storeName], mode);
378     let objectStore = transaction.objectStore(storeName);
380     if (callback) {
381       let result = new Promise(resolve => {
382         resolve(callback(objectStore));
383       });
384       return transaction.promiseComplete().then(() => result);
385     }
387     return objectStore;
388   }
390   createObjectStore(...args) {
391     return new ObjectStore(this.db.createObjectStore(...args));
392   }
395 for (let method of ["cmp", "deleteDatabase"]) {
396   IndexedDB[method] = function(...args) {
397     return indexedDB[method](...args);
398   };
401 forwardMethods(IndexedDB, "db",
402                ["addEventListener", "close", "deleteObjectStore", "hasEventListener", "removeEventListener"]);
404 forwardGetters(IndexedDB, "db",
405                ["name", "objectStoreNames", "version"]);
407 forwardProps(IndexedDB, "db",
408              ["onabort", "onclose", "onerror", "onversionchange"]);