| // Copyright (c) 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| class DatabaseUtils { |
| /** Wrap an IndexedDB request into a Promise. |
| * |
| * This should not be used for open(). |
| * |
| * @param {IDBRequest} request the request to be wrapped |
| * @returns {Promise<Object>} promise that resolves with the request's result, |
| * or rejects with an error |
| */ |
| static async promiseForRequest(request) { |
| return new Promise((resolve, reject) => { |
| request.onsuccess = event => { resolve(event.target.result); }; |
| request.onerror = event => { reject(event.target.error); }; |
| request.onblocked = event => { |
| reject(event.target.error || |
| (new Error("blocked by other database connections"))); |
| }; |
| request.onupgradeneeded = event => { |
| reject(event.target.error || |
| (new Error("unexpected upgradeneeded event"))); |
| }; |
| }); |
| } |
| |
| /** Wrap an IndexedDB database open request into a Promise. |
| * |
| * This is intended to be used by open(). |
| * |
| * @param {IDBOpenDBRequest} request the request to be wrapped |
| * @returns {Promise<{database: idbDatabase, transaction: IDBTransaction?}>} |
| * promise that resolves with an object whose "database" property is the |
| * newly opened database; if an upgradeneeded event is received, the |
| * "transaction" property holds the upgrade transaction |
| */ |
| static async promiseForOpenRequest(request) { |
| return new Promise((resolve, reject) => { |
| request.onsuccess = event => { |
| resolve({ database: event.target.result, transaction: null }); |
| }; |
| request.onerror = event => { reject(event.target.error); }; |
| request.onblocked = event => { |
| reject(event.target.error || |
| (new Error("blocked by other database connections"))); |
| }; |
| request.onupgradeneeded = event => { |
| resolve({ |
| database: event.target.result, |
| transaction: event.target.transaction |
| }); |
| }; |
| }); |
| } |
| |
| /** Wrap an IndexedDB transaction into a Promise. |
| * |
| * @param {IDBTransaction} transaction the transaction to be wrapped |
| * @returns {Promise<Object>} promise that resolves with undefined when the |
| * transaction is completed, or rejects with an error if the transaction |
| * is aborted or errors out |
| */ |
| static async promiseForTransaction(transaction) { |
| return new Promise((resolve, reject) => { |
| transaction.oncomplete = () => { resolve(); }; |
| transaction.onabort = event => { reject(event.target.error); }; |
| transaction.onerror = event => { reject(event.target.error); }; |
| }); |
| } |
| } |
| |
| class Database { |
| /** Open a database. |
| * |
| * @param {string} dbName the name of the database to be opened |
| * @return {Promise<Database>} promise that resolves with a new Database |
| * instance for the open database |
| */ |
| static async open(dbName) { |
| const request = indexedDB.open(dbName, 1); |
| const result = await DatabaseUtils.promiseForOpenRequest(request); |
| |
| if (result.transaction !== null) { |
| fail("expected db to exist"); |
| return; |
| } |
| |
| return new Database(dbName, result.database); |
| } |
| |
| /** Do not instantiate directly. Use Database.open() instead. |
| * |
| * @param {string} dbName the database's name |
| * @param {IDBDatabase} idbDatabase the IndexedDB instance wrapped by this |
| */ |
| constructor(dbName, idbDatabase) { |
| this._dbName = dbName; |
| this._idbDatabase = idbDatabase; |
| } |
| |
| /** Closes the underlying database. All future operations will fail. */ |
| close() { |
| this._idbDatabase.close(); |
| } |
| |
| /** Reads from a store by iterating a cursor. |
| * |
| * @param {string} storeName the name of the store being read |
| * @param {{index?: string, range?: IDBKeyRange}} query narrows down the data |
| * being read |
| * @param {function(IDBCursor): boolean} cursorCallback called for each cursor |
| * yielded by the iteration; must return a truthy value to continue |
| * iteration, or a falsey value to stop iterating |
| */ |
| async iterateCursor(storeName, query, cursorCallback) { |
| const transaction = this._idbDatabase.transaction([storeName], 'readonly'); |
| const transactionPromise = DatabaseUtils.promiseForTransaction(transaction); |
| |
| const objectStore = transaction.objectStore(storeName); |
| const dataSource = ('index' in query) ? objectStore.index(query.index) |
| : objectStore; |
| const request = ('range' in query) ? dataSource.openCursor(query.range) |
| : dataSource.openCursor(); |
| while (true) { |
| const cursor = await DatabaseUtils.promiseForRequest(request); |
| if (!cursor) |
| break; // The iteration completed. |
| |
| const willContinue = cursorCallback(cursor); |
| if (!willContinue) |
| break; |
| cursor.continue(); |
| } |
| |
| await transactionPromise; |
| return true; |
| } |
| |
| async read() { |
| const status = document.getElementById('status'); |
| status.textContent = 'Started reading items'; |
| |
| let i = 0; |
| const result = await this.iterateCursor('store', {}, cursor => { |
| i += 1; |
| if (cursor.primaryKey !== i) { |
| status.textContent = |
| `Incorrect primaryKey - wanted ${i} got ${cursor.primaryKey}`; |
| return false; |
| } |
| if (cursor.key !== i) { |
| status.textContent = `Incorrect key - wanted ${i} got ${cursor.key}`; |
| return false; |
| } |
| if (cursor.value.id !== i) { |
| status.textContent = |
| `Incorrect value.id - wanted ${i} got ${cursor.key}`; |
| return false; |
| } |
| return true; |
| }); |
| if (!result) { |
| status.textContent = `Failed to read items`; |
| return false; |
| } |
| status.textContent = `Done reading ${i} items`; |
| return true; |
| } |
| }; |