123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251 |
- /*
- Copyright 2018 Google LLC
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- import '../_version.js';
- /**
- * A class that wraps common IndexedDB functionality in a promise-based API.
- * It exposes all the underlying power and functionality of IndexedDB, but
- * wraps the most commonly used features in a way that's much simpler to use.
- *
- * @private
- */
- export class DBWrapper {
- /**
- * @param {string} name
- * @param {number} version
- * @param {Object=} [callback]
- * @param {!Function} [callbacks.onupgradeneeded]
- * @param {!Function} [callbacks.onversionchange] Defaults to
- * DBWrapper.prototype._onversionchange when not specified.
- * @private
- */
- constructor(name, version, { onupgradeneeded, onversionchange, } = {}) {
- this._db = null;
- this._name = name;
- this._version = version;
- this._onupgradeneeded = onupgradeneeded;
- this._onversionchange = onversionchange || (() => this.close());
- }
- /**
- * Returns the IDBDatabase instance (not normally needed).
- * @return {IDBDatabase|undefined}
- *
- * @private
- */
- get db() {
- return this._db;
- }
- /**
- * Opens a connected to an IDBDatabase, invokes any onupgradedneeded
- * callback, and added an onversionchange callback to the database.
- *
- * @return {IDBDatabase}
- * @private
- */
- async open() {
- if (this._db)
- return;
- this._db = await new Promise((resolve, reject) => {
- // This flag is flipped to true if the timeout callback runs prior
- // to the request failing or succeeding. Note: we use a timeout instead
- // of an onblocked handler since there are cases where onblocked will
- // never never run. A timeout better handles all possible scenarios:
- // https://github.com/w3c/IndexedDB/issues/223
- let openRequestTimedOut = false;
- setTimeout(() => {
- openRequestTimedOut = true;
- reject(new Error('The open request was blocked and timed out'));
- }, this.OPEN_TIMEOUT);
- const openRequest = indexedDB.open(this._name, this._version);
- openRequest.onerror = () => reject(openRequest.error);
- openRequest.onupgradeneeded = (evt) => {
- if (openRequestTimedOut) {
- openRequest.transaction.abort();
- openRequest.result.close();
- }
- else if (typeof this._onupgradeneeded === 'function') {
- this._onupgradeneeded(evt);
- }
- };
- openRequest.onsuccess = () => {
- const db = openRequest.result;
- if (openRequestTimedOut) {
- db.close();
- }
- else {
- db.onversionchange = this._onversionchange.bind(this);
- resolve(db);
- }
- };
- });
- return this;
- }
- /**
- * Polyfills the native `getKey()` method. Note, this is overridden at
- * runtime if the browser supports the native method.
- *
- * @param {string} storeName
- * @param {*} query
- * @return {Array}
- * @private
- */
- async getKey(storeName, query) {
- return (await this.getAllKeys(storeName, query, 1))[0];
- }
- /**
- * Polyfills the native `getAll()` method. Note, this is overridden at
- * runtime if the browser supports the native method.
- *
- * @param {string} storeName
- * @param {*} query
- * @param {number} count
- * @return {Array}
- * @private
- */
- async getAll(storeName, query, count) {
- return await this.getAllMatching(storeName, { query, count });
- }
- /**
- * Polyfills the native `getAllKeys()` method. Note, this is overridden at
- * runtime if the browser supports the native method.
- *
- * @param {string} storeName
- * @param {*} query
- * @param {number} count
- * @return {Array}
- * @private
- */
- async getAllKeys(storeName, query, count) {
- const entries = await this.getAllMatching(storeName, { query, count, includeKeys: true });
- return entries.map((entry) => entry.key);
- }
- /**
- * Supports flexible lookup in an object store by specifying an index,
- * query, direction, and count. This method returns an array of objects
- * with the signature .
- *
- * @param {string} storeName
- * @param {Object} [opts]
- * @param {string} [opts.index] The index to use (if specified).
- * @param {*} [opts.query]
- * @param {IDBCursorDirection} [opts.direction]
- * @param {number} [opts.count] The max number of results to return.
- * @param {boolean} [opts.includeKeys] When true, the structure of the
- * returned objects is changed from an array of values to an array of
- * objects in the form {key, primaryKey, value}.
- * @return {Array}
- * @private
- */
- async getAllMatching(storeName, { index, query = null, // IE/Edge errors if query === `undefined`.
- direction = 'next', count, includeKeys = false, } = {}) {
- return await this.transaction([storeName], 'readonly', (txn, done) => {
- const store = txn.objectStore(storeName);
- const target = index ? store.index(index) : store;
- const results = [];
- const request = target.openCursor(query, direction);
- request.onsuccess = () => {
- const cursor = request.result;
- if (cursor) {
- results.push(includeKeys ? cursor : cursor.value);
- if (count && results.length >= count) {
- done(results);
- }
- else {
- cursor.continue();
- }
- }
- else {
- done(results);
- }
- };
- });
- }
- /**
- * Accepts a list of stores, a transaction type, and a callback and
- * performs a transaction. A promise is returned that resolves to whatever
- * value the callback chooses. The callback holds all the transaction logic
- * and is invoked with two arguments:
- * 1. The IDBTransaction object
- * 2. A `done` function, that's used to resolve the promise when
- * when the transaction is done, if passed a value, the promise is
- * resolved to that value.
- *
- * @param {Array<string>} storeNames An array of object store names
- * involved in the transaction.
- * @param {string} type Can be `readonly` or `readwrite`.
- * @param {!Function} callback
- * @return {*} The result of the transaction ran by the callback.
- * @private
- */
- async transaction(storeNames, type, callback) {
- await this.open();
- return await new Promise((resolve, reject) => {
- const txn = this._db.transaction(storeNames, type);
- txn.onabort = () => reject(txn.error);
- txn.oncomplete = () => resolve();
- callback(txn, (value) => resolve(value));
- });
- }
- /**
- * Delegates async to a native IDBObjectStore method.
- *
- * @param {string} method The method name.
- * @param {string} storeName The object store name.
- * @param {string} type Can be `readonly` or `readwrite`.
- * @param {...*} args The list of args to pass to the native method.
- * @return {*} The result of the transaction.
- * @private
- */
- async _call(method, storeName, type, ...args) {
- const callback = (txn, done) => {
- const objStore = txn.objectStore(storeName);
- // TODO(philipwalton): Fix this underlying TS2684 error.
- // @ts-ignore
- const request = objStore[method].apply(objStore, args);
- request.onsuccess = () => done(request.result);
- };
- return await this.transaction([storeName], type, callback);
- }
- /**
- * Closes the connection opened by `DBWrapper.open()`. Generally this method
- * doesn't need to be called since:
- * 1. It's usually better to keep a connection open since opening
- * a new connection is somewhat slow.
- * 2. Connections are automatically closed when the reference is
- * garbage collected.
- * The primary use case for needing to close a connection is when another
- * reference (typically in another tab) needs to upgrade it and would be
- * blocked by the current, open connection.
- *
- * @private
- */
- close() {
- if (this._db) {
- this._db.close();
- this._db = null;
- }
- }
- }
- // Exposed on the prototype to let users modify the default timeout on a
- // per-instance or global basis.
- DBWrapper.prototype.OPEN_TIMEOUT = 2000;
- // Wrap native IDBObjectStore methods according to their mode.
- const methodsToWrap = {
- readonly: ['get', 'count', 'getKey', 'getAll', 'getAllKeys'],
- readwrite: ['add', 'put', 'clear', 'delete'],
- };
- for (const [mode, methods] of Object.entries(methodsToWrap)) {
- for (const method of methods) {
- if (method in IDBObjectStore.prototype) {
- // Don't use arrow functions here since we're outside of the class.
- DBWrapper.prototype[method] =
- async function (storeName, ...args) {
- return await this._call(method, storeName, mode, ...args);
- };
- }
- }
- }
|