forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			431 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			431 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 | |
| /* vim: set sts=2 sw=2 et tw=80: */
 | |
| /* This Source Code Form is subject to the terms of the Mozilla Public
 | |
|  * License, v. 2.0. If a copy of the MPL was not distributed with this
 | |
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | |
| 
 | |
| /**
 | |
|  * @file
 | |
|  *
 | |
|  * This module provides Promise-based wrappers around ordinarily
 | |
|  * IDBRequest-based IndexedDB methods and classes.
 | |
|  */
 | |
| 
 | |
| /**
 | |
|  * Wraps the given request object, and returns a Promise which resolves when
 | |
|  * the requests succeeds or rejects when it fails.
 | |
|  *
 | |
|  * @param {IDBRequest} request
 | |
|  *        An IndexedDB request object to wrap.
 | |
|  * @returns {Promise}
 | |
|  */
 | |
| function wrapRequest(request) {
 | |
|   return new Promise((resolve, reject) => {
 | |
|     request.onsuccess = () => {
 | |
|       resolve(request.result);
 | |
|     };
 | |
|     request.onerror = () => {
 | |
|       reject(request.error);
 | |
|     };
 | |
|   });
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Forwards a set of getter properties from a wrapper class to the wrapped
 | |
|  * object.
 | |
|  *
 | |
|  * @param {function} cls
 | |
|  *        The class constructor for which to forward the getters.
 | |
|  * @param {string} target
 | |
|  *        The name of the property which contains the wrapped object to which
 | |
|  *        to forward the getters.
 | |
|  * @param {Array<string>} props
 | |
|  *        A list of property names to forward.
 | |
|  */
 | |
| function forwardGetters(cls, target, props) {
 | |
|   for (let prop of props) {
 | |
|     Object.defineProperty(cls.prototype, prop, {
 | |
|       get() {
 | |
|         return this[target][prop];
 | |
|       },
 | |
|     });
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Forwards a set of getter and setter properties from a wrapper class to the
 | |
|  * wrapped object.
 | |
|  *
 | |
|  * @param {function} cls
 | |
|  *        The class constructor for which to forward the properties.
 | |
|  * @param {string} target
 | |
|  *        The name of the property which contains the wrapped object to which
 | |
|  *        to forward the properties.
 | |
|  * @param {Array<string>} props
 | |
|  *        A list of property names to forward.
 | |
|  */
 | |
| function forwardProps(cls, target, props) {
 | |
|   for (let prop of props) {
 | |
|     Object.defineProperty(cls.prototype, prop, {
 | |
|       get() {
 | |
|         return this[target][prop];
 | |
|       },
 | |
|       set(value) {
 | |
|         this[target][prop] = value;
 | |
|       },
 | |
|     });
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Wraps a set of IDBRequest-based methods via {@link wrapRequest} and
 | |
|  * forwards them to the equivalent methods on the wrapped object.
 | |
|  *
 | |
|  * @param {function} cls
 | |
|  *        The class constructor for which to forward the methods.
 | |
|  * @param {string} target
 | |
|  *        The name of the property which contains the wrapped object to which
 | |
|  *        to forward the methods.
 | |
|  * @param {Array<string>} methods
 | |
|  *        A list of method names to forward.
 | |
|  */
 | |
| function wrapMethods(cls, target, methods) {
 | |
|   for (let method of methods) {
 | |
|     cls.prototype[method] = function (...args) {
 | |
|       return wrapRequest(this[target][method](...args));
 | |
|     };
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Forwards a set of methods from a wrapper class to the wrapped object.
 | |
|  *
 | |
|  * @param {function} cls
 | |
|  *        The class constructor for which to forward the getters.
 | |
|  * @param {string} target
 | |
|  *        The name of the property which contains the wrapped object to which
 | |
|  *        to forward the methods.
 | |
|  * @param {Array<string>} methods
 | |
|  *        A list of method names to forward.
 | |
|  */
 | |
| function forwardMethods(cls, target, methods) {
 | |
|   for (let method of methods) {
 | |
|     cls.prototype[method] = function (...args) {
 | |
|       return this[target][method](...args);
 | |
|     };
 | |
|   }
 | |
| }
 | |
| 
 | |
| class Cursor {
 | |
|   constructor(cursorRequest, source) {
 | |
|     this.cursorRequest = cursorRequest;
 | |
|     this.source = source;
 | |
|     this.cursor = null;
 | |
|   }
 | |
| 
 | |
|   get done() {
 | |
|     return !this.cursor;
 | |
|   }
 | |
| 
 | |
|   // This method is used internally to wait the cursor's IDBRequest to have been
 | |
|   // completed and the internal cursor has been updated (used when we initially
 | |
|   // create the cursor from Cursed.openCursor/openKeyCursor, and in the method
 | |
|   // of this class defined by defineCursorUpdateMethods).
 | |
|   async awaitRequest() {
 | |
|     this.cursor = await wrapRequest(this.cursorRequest);
 | |
|     return this;
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Define the Cursor class methods that update the cursor (continue, continuePrimaryKey
 | |
|  * and advance) as async functions that call the related IDBCursor methods and
 | |
|  * await the cursor's IDBRequest to be completed.
 | |
|  *
 | |
|  * @param {function} cls
 | |
|  *        The class constructor for which to define the cursor update methods.
 | |
|  * @param {Array<string>} methods
 | |
|  *        A list of "cursor update" method names to define.
 | |
|  */
 | |
| function defineCursorUpdateMethods(cls, methods) {
 | |
|   for (let method of methods) {
 | |
|     cls.prototype[method] = async function (...args) {
 | |
|       const promise = this.awaitRequest();
 | |
|       this.cursor[method](...args);
 | |
|       await promise;
 | |
|     };
 | |
|   }
 | |
| }
 | |
| 
 | |
| defineCursorUpdateMethods(Cursor, [
 | |
|   "advance",
 | |
|   "continue",
 | |
|   "continuePrimaryKey",
 | |
| ]);
 | |
| 
 | |
| forwardGetters(Cursor, "cursor", ["direction", "key", "primaryKey"]);
 | |
| wrapMethods(Cursor, "cursor", ["delete", "update"]);
 | |
| 
 | |
| class CursorWithValue extends Cursor {}
 | |
| 
 | |
| forwardGetters(CursorWithValue, "cursor", ["value"]);
 | |
| 
 | |
| class Cursed {
 | |
|   constructor(cursed) {
 | |
|     this.cursed = cursed;
 | |
|   }
 | |
| 
 | |
|   openCursor(...args) {
 | |
|     const cursor = new CursorWithValue(this.cursed.openCursor(...args), this);
 | |
|     return cursor.awaitRequest();
 | |
|   }
 | |
| 
 | |
|   openKeyCursor(...args) {
 | |
|     const cursor = new Cursor(this.cursed.openKeyCursor(...args), this);
 | |
|     return cursor.awaitRequest();
 | |
|   }
 | |
| }
 | |
| 
 | |
| wrapMethods(Cursed, "cursed", [
 | |
|   "count",
 | |
|   "get",
 | |
|   "getAll",
 | |
|   "getAllKeys",
 | |
|   "getKey",
 | |
| ]);
 | |
| 
 | |
| class Index extends Cursed {
 | |
|   constructor(index, objectStore) {
 | |
|     super(index);
 | |
| 
 | |
|     this.objectStore = objectStore;
 | |
|     this.index = index;
 | |
|   }
 | |
| }
 | |
| 
 | |
| forwardGetters(Index, "index", [
 | |
|   "isAutoLocale",
 | |
|   "keyPath",
 | |
|   "locale",
 | |
|   "multiEntry",
 | |
|   "name",
 | |
|   "unique",
 | |
| ]);
 | |
| 
 | |
| class ObjectStore extends Cursed {
 | |
|   constructor(store) {
 | |
|     super(store);
 | |
| 
 | |
|     this.store = store;
 | |
|   }
 | |
| 
 | |
|   createIndex(...args) {
 | |
|     return new Index(this.store.createIndex(...args), this);
 | |
|   }
 | |
| 
 | |
|   index(...args) {
 | |
|     return new Index(this.store.index(...args), this);
 | |
|   }
 | |
| }
 | |
| 
 | |
| wrapMethods(ObjectStore, "store", ["add", "clear", "delete", "put"]);
 | |
| 
 | |
| forwardMethods(ObjectStore, "store", ["deleteIndex"]);
 | |
| 
 | |
| class Transaction {
 | |
|   constructor(transaction) {
 | |
|     this.transaction = transaction;
 | |
| 
 | |
|     this._completionPromise = new Promise((resolve, reject) => {
 | |
|       transaction.oncomplete = resolve;
 | |
|       transaction.onerror = () => {
 | |
|         reject(transaction.error);
 | |
|       };
 | |
|       transaction.onabort = () => {
 | |
|         const error =
 | |
|           transaction.error ||
 | |
|           new DOMException("The operation has been aborted", "AbortError");
 | |
|         reject(error);
 | |
|       };
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   objectStore(name) {
 | |
|     return new ObjectStore(this.transaction.objectStore(name));
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns a Promise which resolves when the transaction completes, or
 | |
|    * rejects when a transaction error or abort occurs.
 | |
|    *
 | |
|    * @returns {Promise}
 | |
|    */
 | |
|   promiseComplete() {
 | |
|     return this._completionPromise;
 | |
|   }
 | |
| }
 | |
| 
 | |
| forwardGetters(Transaction, "transaction", [
 | |
|   "db",
 | |
|   "mode",
 | |
|   "error",
 | |
|   "objectStoreNames",
 | |
| ]);
 | |
| 
 | |
| forwardMethods(Transaction, "transaction", ["abort"]);
 | |
| 
 | |
| export class IndexedDB {
 | |
|   /**
 | |
|    * Opens the database with the given name, and returns a Promise which
 | |
|    * resolves to an IndexedDB instance when the operation completes.
 | |
|    *
 | |
|    * @param {string} dbName
 | |
|    *        The name of the database to open.
 | |
|    * @param {object} options
 | |
|    *        The options with which to open the database.
 | |
|    * @param {integer} options.version
 | |
|    *        The schema version with which the database needs to be opened. If
 | |
|    *        the database does not exist, or its current schema version does
 | |
|    *        not match, the `onupgradeneeded` function will be called.
 | |
|    * @param {function} [onupgradeneeded]
 | |
|    *        A function which will be called with an IndexedDB object as its
 | |
|    *        first parameter when the database needs to be created, or its
 | |
|    *        schema needs to be upgraded. If this function is not provided, the
 | |
|    *        {@link #onupgradeneeded} method will be called instead.
 | |
|    *
 | |
|    * @returns {Promise<IndexedDB>}
 | |
|    */
 | |
|   static open(dbName, options, onupgradeneeded = null) {
 | |
|     let request = indexedDB.open(dbName, options);
 | |
|     return this._wrapOpenRequest(request, onupgradeneeded);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Opens the database for a given principal and with the given name, returns
 | |
|    * a Promise which resolves to an IndexedDB instance when the operation completes.
 | |
|    *
 | |
|    * @param {nsIPrincipal} principal
 | |
|    *        The principal to open the database for.
 | |
|    * @param {string} dbName
 | |
|    *        The name of the database to open.
 | |
|    * @param {object} options
 | |
|    *        The options with which to open the database.
 | |
|    * @param {integer} options.version
 | |
|    *        The schema version with which the database needs to be opened. If
 | |
|    *        the database does not exist, or its current schema version does
 | |
|    *        not match, the `onupgradeneeded` function will be called.
 | |
|    * @param {function} [onupgradeneeded]
 | |
|    *        A function which will be called with an IndexedDB object as its
 | |
|    *        first parameter when the database needs to be created, or its
 | |
|    *        schema needs to be upgraded. If this function is not provided, the
 | |
|    *        {@link #onupgradeneeded} method will be called instead.
 | |
|    *
 | |
|    * @returns {Promise<IndexedDB>}
 | |
|    */
 | |
|   static openForPrincipal(principal, dbName, options, onupgradeneeded = null) {
 | |
|     const request = indexedDB.openForPrincipal(principal, dbName, options);
 | |
|     return this._wrapOpenRequest(request, onupgradeneeded);
 | |
|   }
 | |
| 
 | |
|   static _wrapOpenRequest(request, onupgradeneeded = null) {
 | |
|     request.onupgradeneeded = event => {
 | |
|       let db = new this(request.result);
 | |
|       if (onupgradeneeded) {
 | |
|         onupgradeneeded(db, event);
 | |
|       } else {
 | |
|         db.onupgradeneeded(event);
 | |
|       }
 | |
|     };
 | |
| 
 | |
|     return wrapRequest(request).then(db => new this(db));
 | |
|   }
 | |
| 
 | |
|   constructor(db) {
 | |
|     this.db = db;
 | |
|   }
 | |
| 
 | |
|   onupgradeneeded() {}
 | |
| 
 | |
|   /**
 | |
|    * Opens a transaction for the given object stores.
 | |
|    *
 | |
|    * @param {Array<string>} storeNames
 | |
|    *        The names of the object stores for which to open a transaction.
 | |
|    * @param {string} [mode = "readonly"]
 | |
|    *        The mode in which to open the transaction.
 | |
|    * @param {function} [callback]
 | |
|    *        An optional callback function. If provided, the function will be
 | |
|    *        called with the Transaction, and a Promise will be returned, which
 | |
|    *        will resolve to the callback's return value when the transaction
 | |
|    *        completes.
 | |
|    * @returns {Transaction|Promise}
 | |
|    */
 | |
|   transaction(storeNames, mode, callback = null) {
 | |
|     let transaction = new Transaction(this.db.transaction(storeNames, mode));
 | |
| 
 | |
|     if (callback) {
 | |
|       let result = new Promise(resolve => {
 | |
|         resolve(callback(transaction));
 | |
|       });
 | |
|       return transaction.promiseComplete().then(() => result);
 | |
|     }
 | |
| 
 | |
|     return transaction;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Opens a transaction for a single object store, and returns that object
 | |
|    * store.
 | |
|    *
 | |
|    * @param {string} storeName
 | |
|    *        The name of the object store to open.
 | |
|    * @param {string} [mode = "readonly"]
 | |
|    *        The mode in which to open the transaction.
 | |
|    * @param {function} [callback]
 | |
|    *        An optional callback function. If provided, the function will be
 | |
|    *        called with the ObjectStore, and a Promise will be returned, which
 | |
|    *        will resolve to the callback's return value when the transaction
 | |
|    *        completes.
 | |
|    * @returns {ObjectStore|Promise}
 | |
|    */
 | |
|   objectStore(storeName, mode, callback = null) {
 | |
|     let transaction = this.transaction([storeName], mode);
 | |
|     let objectStore = transaction.objectStore(storeName);
 | |
| 
 | |
|     if (callback) {
 | |
|       let result = new Promise(resolve => {
 | |
|         resolve(callback(objectStore));
 | |
|       });
 | |
|       return transaction.promiseComplete().then(() => result);
 | |
|     }
 | |
| 
 | |
|     return objectStore;
 | |
|   }
 | |
| 
 | |
|   createObjectStore(...args) {
 | |
|     return new ObjectStore(this.db.createObjectStore(...args));
 | |
|   }
 | |
| }
 | |
| 
 | |
| for (let method of ["cmp", "deleteDatabase"]) {
 | |
|   IndexedDB[method] = function (...args) {
 | |
|     return indexedDB[method](...args);
 | |
|   };
 | |
| }
 | |
| 
 | |
| forwardMethods(IndexedDB, "db", [
 | |
|   "addEventListener",
 | |
|   "close",
 | |
|   "deleteObjectStore",
 | |
|   "hasEventListener",
 | |
|   "removeEventListener",
 | |
| ]);
 | |
| 
 | |
| forwardGetters(IndexedDB, "db", ["name", "objectStoreNames", "version"]);
 | |
| 
 | |
| forwardProps(IndexedDB, "db", [
 | |
|   "onabort",
 | |
|   "onclose",
 | |
|   "onerror",
 | |
|   "onversionchange",
 | |
| ]);
 | 
