DBWrapper.mjs 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  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.mjs';
  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, {
  26. onupgradeneeded,
  27. onversionchange = this._onversionchange,
  28. } = {}) {
  29. this._name = name;
  30. this._version = version;
  31. this._onupgradeneeded = onupgradeneeded;
  32. this._onversionchange = onversionchange;
  33. // If this is null, it means the database isn't open.
  34. this._db = null;
  35. }
  36. /**
  37. * Returns the IDBDatabase instance (not normally needed).
  38. *
  39. * @private
  40. */
  41. get db() {
  42. return this._db;
  43. }
  44. /**
  45. * Opens a connected to an IDBDatabase, invokes any onupgradedneeded
  46. * callback, and added an onversionchange callback to the database.
  47. *
  48. * @return {IDBDatabase}
  49. * @private
  50. */
  51. async open() {
  52. if (this._db) return;
  53. this._db = await new Promise((resolve, reject) => {
  54. // This flag is flipped to true if the timeout callback runs prior
  55. // to the request failing or succeeding. Note: we use a timeout instead
  56. // of an onblocked handler since there are cases where onblocked will
  57. // never never run. A timeout better handles all possible scenarios:
  58. // https://github.com/w3c/IndexedDB/issues/223
  59. let openRequestTimedOut = false;
  60. setTimeout(() => {
  61. openRequestTimedOut = true;
  62. reject(new Error('The open request was blocked and timed out'));
  63. }, this.OPEN_TIMEOUT);
  64. const openRequest = indexedDB.open(this._name, this._version);
  65. openRequest.onerror = () => reject(openRequest.error);
  66. openRequest.onupgradeneeded = (evt) => {
  67. if (openRequestTimedOut) {
  68. openRequest.transaction.abort();
  69. evt.target.result.close();
  70. } else if (this._onupgradeneeded) {
  71. this._onupgradeneeded(evt);
  72. }
  73. };
  74. openRequest.onsuccess = ({target}) => {
  75. const db = target.result;
  76. if (openRequestTimedOut) {
  77. db.close();
  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. return (await this.getAllMatching(
  123. storeName, {query, count, includeKeys: true})).map(({key}) => 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, {
  143. index,
  144. query = null, // IE errors if query === `undefined`.
  145. direction = 'next',
  146. count,
  147. includeKeys,
  148. } = {}) {
  149. return await this.transaction([storeName], 'readonly', (txn, done) => {
  150. const store = txn.objectStore(storeName);
  151. const target = index ? store.index(index) : store;
  152. const results = [];
  153. target.openCursor(query, direction).onsuccess = ({target}) => {
  154. const cursor = target.result;
  155. if (cursor) {
  156. const {primaryKey, key, value} = cursor;
  157. results.push(includeKeys ? {primaryKey, key, value} : value);
  158. if (count && results.length >= count) {
  159. done(results);
  160. } else {
  161. cursor.continue();
  162. }
  163. } else {
  164. done(results);
  165. }
  166. };
  167. });
  168. }
  169. /**
  170. * Accepts a list of stores, a transaction type, and a callback and
  171. * performs a transaction. A promise is returned that resolves to whatever
  172. * value the callback chooses. The callback holds all the transaction logic
  173. * and is invoked with two arguments:
  174. * 1. The IDBTransaction object
  175. * 2. A `done` function, that's used to resolve the promise when
  176. * when the transaction is done, if passed a value, the promise is
  177. * resolved to that value.
  178. *
  179. * @param {Array<string>} storeNames An array of object store names
  180. * involved in the transaction.
  181. * @param {string} type Can be `readonly` or `readwrite`.
  182. * @param {!Function} callback
  183. * @return {*} The result of the transaction ran by the callback.
  184. * @private
  185. */
  186. async transaction(storeNames, type, callback) {
  187. await this.open();
  188. return await new Promise((resolve, reject) => {
  189. const txn = this._db.transaction(storeNames, type);
  190. txn.onabort = ({target}) => reject(target.error);
  191. txn.oncomplete = () => resolve();
  192. callback(txn, (value) => resolve(value));
  193. });
  194. }
  195. /**
  196. * Delegates async to a native IDBObjectStore method.
  197. *
  198. * @param {string} method The method name.
  199. * @param {string} storeName The object store name.
  200. * @param {string} type Can be `readonly` or `readwrite`.
  201. * @param {...*} args The list of args to pass to the native method.
  202. * @return {*} The result of the transaction.
  203. * @private
  204. */
  205. async _call(method, storeName, type, ...args) {
  206. const callback = (txn, done) => {
  207. txn.objectStore(storeName)[method](...args).onsuccess = ({target}) => {
  208. done(target.result);
  209. };
  210. };
  211. return await this.transaction([storeName], type, callback);
  212. }
  213. /**
  214. * The default onversionchange handler, which closes the database so other
  215. * connections can open without being blocked.
  216. *
  217. * @private
  218. */
  219. _onversionchange() {
  220. this.close();
  221. }
  222. /**
  223. * Closes the connection opened by `DBWrapper.open()`. Generally this method
  224. * doesn't need to be called since:
  225. * 1. It's usually better to keep a connection open since opening
  226. * a new connection is somewhat slow.
  227. * 2. Connections are automatically closed when the reference is
  228. * garbage collected.
  229. * The primary use case for needing to close a connection is when another
  230. * reference (typically in another tab) needs to upgrade it and would be
  231. * blocked by the current, open connection.
  232. *
  233. * @private
  234. */
  235. close() {
  236. if (this._db) {
  237. this._db.close();
  238. this._db = null;
  239. }
  240. }
  241. }
  242. // Exposed to let users modify the default timeout on a per-instance
  243. // or global basis.
  244. DBWrapper.prototype.OPEN_TIMEOUT = 2000;
  245. // Wrap native IDBObjectStore methods according to their mode.
  246. const methodsToWrap = {
  247. 'readonly': ['get', 'count', 'getKey', 'getAll', 'getAllKeys'],
  248. 'readwrite': ['add', 'put', 'clear', 'delete'],
  249. };
  250. for (const [mode, methods] of Object.entries(methodsToWrap)) {
  251. for (const method of methods) {
  252. if (method in IDBObjectStore.prototype) {
  253. // Don't use arrow functions here since we're outside of the class.
  254. DBWrapper.prototype[method] = async function(storeName, ...args) {
  255. return await this._call(method, storeName, mode, ...args);
  256. };
  257. }
  258. }
  259. }