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/. */
11 * This module provides Promise-based wrappers around ordinarily
12 * IDBRequest-based IndexedDB methods and classes.
15 /* exported IndexedDB */
16 var EXPORTED_SYMBOLS = ["IndexedDB"];
18 Cu.importGlobalProperties(["indexedDB"]);
21 * Wraps the given request object, and returns a Promise which resolves when
22 * the requests succeeds or rejects when it fails.
24 * @param {IDBRequest} request
25 * An IndexedDB request object to wrap.
28 function wrapRequest(request) {
29 return new Promise((resolve, reject) => {
30 request.onsuccess = () => {
31 resolve(request.result);
33 request.onerror = () => {
34 reject(request.error);
40 * Forwards a set of getter properties from a wrapper class to the wrapped
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.
51 function forwardGetters(cls, target, props) {
52 for (let prop of props) {
53 Object.defineProperty(cls.prototype, prop, {
55 return this[target][prop];
62 * Forwards a set of getter and setter properties from a wrapper class to the
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.
73 function forwardProps(cls, target, props) {
74 for (let prop of props) {
75 Object.defineProperty(cls.prototype, prop, {
77 return this[target][prop];
80 this[target][prop] = value;
87 * Wraps a set of IDBRequest-based methods via {@link wrapRequest} and
88 * forwards them to the equivalent methods on the wrapped object.
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.
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));
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.
117 function forwardMethods(cls, target, methods) {
118 for (let method of methods) {
119 cls.prototype[method] = function(...args) {
120 return this[target][method](...args);
126 constructor(cursorRequest, source) {
127 this.cursorRequest = cursorRequest;
128 this.source = source;
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);
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.
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);
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"]);
177 constructor(cursed) {
178 this.cursed = cursed;
181 openCursor(...args) {
182 const cursor = new CursorWithValue(this.cursed.openCursor(...args), this);
183 return cursor.awaitRequest();
186 openKeyCursor(...args) {
187 const cursor = new Cursor(this.cursed.openKeyCursor(...args), this);
188 return cursor.awaitRequest();
192 wrapMethods(Cursed, "cursed",
193 ["count", "get", "getAll", "getAllKeys", "getKey"]);
195 class Index extends Cursed {
196 constructor(index, objectStore) {
199 this.objectStore = objectStore;
204 forwardGetters(Index, "index",
205 ["isAutoLocale", "keyPath", "locale", "multiEntry", "name", "unique"]);
207 class ObjectStore extends Cursed {
214 createIndex(...args) {
215 return new Index(this.store.createIndex(...args),
220 return new Index(this.store.index(...args),
225 wrapMethods(ObjectStore, "store",
226 ["add", "clear", "delete", "put"]);
228 forwardMethods(ObjectStore, "store", ["deleteIndex"]);
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);
243 return new ObjectStore(this.transaction.objectStore(name));
247 * Returns a Promise which resolves when the transaction completes, or
248 * rejects when a transaction error occurs.
253 return this._completionPromise;
257 forwardGetters(Transaction, "transaction",
258 ["db", "mode", "error", "objectStoreNames"]);
260 forwardMethods(Transaction, "transaction", ["abort"]);
264 * Opens the database with the given name, and returns a Promise which
265 * resolves to an IndexedDB instance when the operation completes.
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.
281 * @returns {Promise<IndexedDB>}
283 static open(dbName, options, onupgradeneeded = null) {
284 let request = indexedDB.open(dbName, options);
285 return this._wrapOpenRequest(request, onupgradeneeded);
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.
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.
308 * @returns {Promise<IndexedDB>}
310 static openForPrincipal(principal, dbName, options, onupgradeneeded = null) {
311 const request = indexedDB.openForPrincipal(principal, dbName, options);
312 return this._wrapOpenRequest(request, onupgradeneeded);
315 static _wrapOpenRequest(request, onupgradeneeded = null) {
316 request.onupgradeneeded = event => {
317 let db = new this(request.result);
318 if (onupgradeneeded) {
319 onupgradeneeded(db, event);
321 db.onupgradeneeded(event);
325 return wrapRequest(request).then(db => new this(db));
335 * Opens a transaction for the given object stores.
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
346 * @returns {Transaction|Promise}
348 transaction(storeNames, mode, callback = null) {
349 let transaction = new Transaction(this.db.transaction(storeNames, mode));
352 let result = new Promise(resolve => {
353 resolve(callback(transaction));
355 return transaction.promiseComplete().then(() => result);
362 * Opens a transaction for a single object store, and returns that object
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
374 * @returns {ObjectStore|Promise}
376 objectStore(storeName, mode, callback = null) {
377 let transaction = this.transaction([storeName], mode);
378 let objectStore = transaction.objectStore(storeName);
381 let result = new Promise(resolve => {
382 resolve(callback(objectStore));
384 return transaction.promiseComplete().then(() => result);
390 createObjectStore(...args) {
391 return new ObjectStore(this.db.createObjectStore(...args));
395 for (let method of ["cmp", "deleteDatabase"]) {
396 IndexedDB[method] = function(...args) {
397 return indexedDB[method](...args);
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"]);