DBWrapper.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. /*
  2. Copyright 2018 Google LLC
  3. Use of this source code is governed by an MIT-style
  4. license that can be found in the LICENSE file or at
  5. https://opensource.org/licenses/MIT.
  6. */
  7. import '../_version.js';
  8. /**
  9. * A class that wraps common IndexedDB functionality in a promise-based API.
  10. * It exposes all the underlying power and functionality of IndexedDB, but
  11. * wraps the most commonly used features in a way that's much simpler to use.
  12. *
  13. * @private
  14. */
  15. export class DBWrapper {
  16. /**
  17. * @param {string} name
  18. * @param {number} version
  19. * @param {Object=} [callback]
  20. * @param {!Function} [callbacks.onupgradeneeded]
  21. * @param {!Function} [callbacks.onversionchange] Defaults to
  22. * DBWrapper.prototype._onversionchange when not specified.
  23. * @private
  24. */
  25. constructor(name, version, { onupgradeneeded, onversionchange, } = {}) {
  26. this._db = null;
  27. this._name = name;
  28. this._version = version;
  29. this._onupgradeneeded = onupgradeneeded;
  30. this._onversionchange = onversionchange || (() => this.close());
  31. }
  32. /**
  33. * Returns the IDBDatabase instance (not normally needed).
  34. * @return {IDBDatabase|undefined}
  35. *
  36. * @private
  37. */
  38. get db() {
  39. return this._db;
  40. }
  41. /**
  42. * Opens a connected to an IDBDatabase, invokes any onupgradedneeded
  43. * callback, and added an onversionchange callback to the database.
  44. *
  45. * @return {IDBDatabase}
  46. * @private
  47. */
  48. async open() {
  49. if (this._db)
  50. return;
  51. this._db = await new Promise((resolve, reject) => {
  52. // This flag is flipped to true if the timeout callback runs prior
  53. // to the request failing or succeeding. Note: we use a timeout instead
  54. // of an onblocked handler since there are cases where onblocked will
  55. // never never run. A timeout better handles all possible scenarios:
  56. // https://github.com/w3c/IndexedDB/issues/223
  57. let openRequestTimedOut = false;
  58. setTimeout(() => {
  59. openRequestTimedOut = true;
  60. reject(new Error('The open request was blocked and timed out'));
  61. }, this.OPEN_TIMEOUT);
  62. const openRequest = indexedDB.open(this._name, this._version);
  63. openRequest.onerror = () => reject(openRequest.error);
  64. openRequest.onupgradeneeded = (evt) => {
  65. if (openRequestTimedOut) {
  66. openRequest.transaction.abort();
  67. openRequest.result.close();
  68. }
  69. else if (typeof this._onupgradeneeded === 'function') {
  70. this._onupgradeneeded(evt);
  71. }
  72. };
  73. openRequest.onsuccess = () => {
  74. const db = openRequest.result;
  75. if (openRequestTimedOut) {
  76. db.close();
  77. }
  78. else {
  79. db.onversionchange = this._onversionchange.bind(this);
  80. resolve(db);
  81. }
  82. };
  83. });
  84. return this;
  85. }
  86. /**
  87. * Polyfills the native `getKey()` method. Note, this is overridden at
  88. * runtime if the browser supports the native method.
  89. *
  90. * @param {string} storeName
  91. * @param {*} query
  92. * @return {Array}
  93. * @private
  94. */
  95. async getKey(storeName, query) {
  96. return (await this.getAllKeys(storeName, query, 1))[0];
  97. }
  98. /**
  99. * Polyfills the native `getAll()` method. Note, this is overridden at
  100. * runtime if the browser supports the native method.
  101. *
  102. * @param {string} storeName
  103. * @param {*} query
  104. * @param {number} count
  105. * @return {Array}
  106. * @private
  107. */
  108. async getAll(storeName, query, count) {
  109. return await this.getAllMatching(storeName, { query, count });
  110. }
  111. /**
  112. * Polyfills the native `getAllKeys()` method. Note, this is overridden at
  113. * runtime if the browser supports the native method.
  114. *
  115. * @param {string} storeName
  116. * @param {*} query
  117. * @param {number} count
  118. * @return {Array}
  119. * @private
  120. */
  121. async getAllKeys(storeName, query, count) {
  122. const entries = await this.getAllMatching(storeName, { query, count, includeKeys: true });
  123. return entries.map((entry) => entry.key);
  124. }
  125. /**
  126. * Supports flexible lookup in an object store by specifying an index,
  127. * query, direction, and count. This method returns an array of objects
  128. * with the signature .
  129. *
  130. * @param {string} storeName
  131. * @param {Object} [opts]
  132. * @param {string} [opts.index] The index to use (if specified).
  133. * @param {*} [opts.query]
  134. * @param {IDBCursorDirection} [opts.direction]
  135. * @param {number} [opts.count] The max number of results to return.
  136. * @param {boolean} [opts.includeKeys] When true, the structure of the
  137. * returned objects is changed from an array of values to an array of
  138. * objects in the form {key, primaryKey, value}.
  139. * @return {Array}
  140. * @private
  141. */
  142. async getAllMatching(storeName, { index, query = null, // IE/Edge errors if query === `undefined`.
  143. direction = 'next', count, includeKeys = false, } = {}) {
  144. return await this.transaction([storeName], 'readonly', (txn, done) => {
  145. const store = txn.objectStore(storeName);
  146. const target = index ? store.index(index) : store;
  147. const results = [];
  148. const request = target.openCursor(query, direction);
  149. request.onsuccess = () => {
  150. const cursor = request.result;
  151. if (cursor) {
  152. results.push(includeKeys ? cursor : cursor.value);
  153. if (count && results.length >= count) {
  154. done(results);
  155. }
  156. else {
  157. cursor.continue();
  158. }
  159. }
  160. else {
  161. done(results);
  162. }
  163. };
  164. });
  165. }
  166. /**
  167. * Accepts a list of stores, a transaction type, and a callback and
  168. * performs a transaction. A promise is returned that resolves to whatever
  169. * value the callback chooses. The callback holds all the transaction logic
  170. * and is invoked with two arguments:
  171. * 1. The IDBTransaction object
  172. * 2. A `done` function, that's used to resolve the promise when
  173. * when the transaction is done, if passed a value, the promise is
  174. * resolved to that value.
  175. *
  176. * @param {Array<string>} storeNames An array of object store names
  177. * involved in the transaction.
  178. * @param {string} type Can be `readonly` or `readwrite`.
  179. * @param {!Function} callback
  180. * @return {*} The result of the transaction ran by the callback.
  181. * @private
  182. */
  183. async transaction(storeNames, type, callback) {
  184. await this.open();
  185. return await new Promise((resolve, reject) => {
  186. const txn = this._db.transaction(storeNames, type);
  187. txn.onabort = () => reject(txn.error);
  188. txn.oncomplete = () => resolve();
  189. callback(txn, (value) => resolve(value));
  190. });
  191. }
  192. /**
  193. * Delegates async to a native IDBObjectStore method.
  194. *
  195. * @param {string} method The method name.
  196. * @param {string} storeName The object store name.
  197. * @param {string} type Can be `readonly` or `readwrite`.
  198. * @param {...*} args The list of args to pass to the native method.
  199. * @return {*} The result of the transaction.
  200. * @private
  201. */
  202. async _call(method, storeName, type, ...args) {
  203. const callback = (txn, done) => {
  204. const objStore = txn.objectStore(storeName);
  205. // TODO(philipwalton): Fix this underlying TS2684 error.
  206. // @ts-ignore
  207. const request = objStore[method].apply(objStore, args);
  208. request.onsuccess = () => done(request.result);
  209. };
  210. return await this.transaction([storeName], type, callback);
  211. }
  212. /**
  213. * Closes the connection opened by `DBWrapper.open()`. Generally this method
  214. * doesn't need to be called since:
  215. * 1. It's usually better to keep a connection open since opening
  216. * a new connection is somewhat slow.
  217. * 2. Connections are automatically closed when the reference is
  218. * garbage collected.
  219. * The primary use case for needing to close a connection is when another
  220. * reference (typically in another tab) needs to upgrade it and would be
  221. * blocked by the current, open connection.
  222. *
  223. * @private
  224. */
  225. close() {
  226. if (this._db) {
  227. this._db.close();
  228. this._db = null;
  229. }
  230. }
  231. }
  232. // Exposed on the prototype to let users modify the default timeout on a
  233. // per-instance or global basis.
  234. DBWrapper.prototype.OPEN_TIMEOUT = 2000;
  235. // Wrap native IDBObjectStore methods according to their mode.
  236. const methodsToWrap = {
  237. readonly: ['get', 'count', 'getKey', 'getAll', 'getAllKeys'],
  238. readwrite: ['add', 'put', 'clear', 'delete'],
  239. };
  240. for (const [mode, methods] of Object.entries(methodsToWrap)) {
  241. for (const method of methods) {
  242. if (method in IDBObjectStore.prototype) {
  243. // Don't use arrow functions here since we're outside of the class.
  244. DBWrapper.prototype[method] =
  245. async function (storeName, ...args) {
  246. return await this._call(method, storeName, mode, ...args);
  247. };
  248. }
  249. }
  250. }