CacheTimestampsModel.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  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 { openDB, deleteDB } from 'idb';
  8. import '../_version.js';
  9. const DB_NAME = 'workbox-expiration';
  10. const CACHE_OBJECT_STORE = 'cache-entries';
  11. const normalizeURL = (unNormalizedUrl) => {
  12. const url = new URL(unNormalizedUrl, location.href);
  13. url.hash = '';
  14. return url.href;
  15. };
  16. /**
  17. * Returns the timestamp model.
  18. *
  19. * @private
  20. */
  21. class CacheTimestampsModel {
  22. /**
  23. *
  24. * @param {string} cacheName
  25. *
  26. * @private
  27. */
  28. constructor(cacheName) {
  29. this._db = null;
  30. this._cacheName = cacheName;
  31. }
  32. /**
  33. * Performs an upgrade of indexedDB.
  34. *
  35. * @param {IDBPDatabase<CacheDbSchema>} db
  36. *
  37. * @private
  38. */
  39. _upgradeDb(db) {
  40. // TODO(philipwalton): EdgeHTML doesn't support arrays as a keyPath, so we
  41. // have to use the `id` keyPath here and create our own values (a
  42. // concatenation of `url + cacheName`) instead of simply using
  43. // `keyPath: ['url', 'cacheName']`, which is supported in other browsers.
  44. const objStore = db.createObjectStore(CACHE_OBJECT_STORE, { keyPath: 'id' });
  45. // TODO(philipwalton): once we don't have to support EdgeHTML, we can
  46. // create a single index with the keyPath `['cacheName', 'timestamp']`
  47. // instead of doing both these indexes.
  48. objStore.createIndex('cacheName', 'cacheName', { unique: false });
  49. objStore.createIndex('timestamp', 'timestamp', { unique: false });
  50. }
  51. /**
  52. * Performs an upgrade of indexedDB and deletes deprecated DBs.
  53. *
  54. * @param {IDBPDatabase<CacheDbSchema>} db
  55. *
  56. * @private
  57. */
  58. _upgradeDbAndDeleteOldDbs(db) {
  59. this._upgradeDb(db);
  60. if (this._cacheName) {
  61. void deleteDB(this._cacheName);
  62. }
  63. }
  64. /**
  65. * @param {string} url
  66. * @param {number} timestamp
  67. *
  68. * @private
  69. */
  70. async setTimestamp(url, timestamp) {
  71. url = normalizeURL(url);
  72. const entry = {
  73. url,
  74. timestamp,
  75. cacheName: this._cacheName,
  76. // Creating an ID from the URL and cache name won't be necessary once
  77. // Edge switches to Chromium and all browsers we support work with
  78. // array keyPaths.
  79. id: this._getId(url),
  80. };
  81. const db = await this.getDb();
  82. const tx = db.transaction(CACHE_OBJECT_STORE, 'readwrite', {
  83. durability: 'relaxed',
  84. });
  85. await tx.store.put(entry);
  86. await tx.done;
  87. }
  88. /**
  89. * Returns the timestamp stored for a given URL.
  90. *
  91. * @param {string} url
  92. * @return {number | undefined}
  93. *
  94. * @private
  95. */
  96. async getTimestamp(url) {
  97. const db = await this.getDb();
  98. const entry = await db.get(CACHE_OBJECT_STORE, this._getId(url));
  99. return entry === null || entry === void 0 ? void 0 : entry.timestamp;
  100. }
  101. /**
  102. * Iterates through all the entries in the object store (from newest to
  103. * oldest) and removes entries once either `maxCount` is reached or the
  104. * entry's timestamp is less than `minTimestamp`.
  105. *
  106. * @param {number} minTimestamp
  107. * @param {number} maxCount
  108. * @return {Array<string>}
  109. *
  110. * @private
  111. */
  112. async expireEntries(minTimestamp, maxCount) {
  113. const db = await this.getDb();
  114. let cursor = await db
  115. .transaction(CACHE_OBJECT_STORE)
  116. .store.index('timestamp')
  117. .openCursor(null, 'prev');
  118. const entriesToDelete = [];
  119. let entriesNotDeletedCount = 0;
  120. while (cursor) {
  121. const result = cursor.value;
  122. // TODO(philipwalton): once we can use a multi-key index, we
  123. // won't have to check `cacheName` here.
  124. if (result.cacheName === this._cacheName) {
  125. // Delete an entry if it's older than the max age or
  126. // if we already have the max number allowed.
  127. if ((minTimestamp && result.timestamp < minTimestamp) ||
  128. (maxCount && entriesNotDeletedCount >= maxCount)) {
  129. // TODO(philipwalton): we should be able to delete the
  130. // entry right here, but doing so causes an iteration
  131. // bug in Safari stable (fixed in TP). Instead we can
  132. // store the keys of the entries to delete, and then
  133. // delete the separate transactions.
  134. // https://github.com/GoogleChrome/workbox/issues/1978
  135. // cursor.delete();
  136. // We only need to return the URL, not the whole entry.
  137. entriesToDelete.push(cursor.value);
  138. }
  139. else {
  140. entriesNotDeletedCount++;
  141. }
  142. }
  143. cursor = await cursor.continue();
  144. }
  145. // TODO(philipwalton): once the Safari bug in the following issue is fixed,
  146. // we should be able to remove this loop and do the entry deletion in the
  147. // cursor loop above:
  148. // https://github.com/GoogleChrome/workbox/issues/1978
  149. const urlsDeleted = [];
  150. for (const entry of entriesToDelete) {
  151. await db.delete(CACHE_OBJECT_STORE, entry.id);
  152. urlsDeleted.push(entry.url);
  153. }
  154. return urlsDeleted;
  155. }
  156. /**
  157. * Takes a URL and returns an ID that will be unique in the object store.
  158. *
  159. * @param {string} url
  160. * @return {string}
  161. *
  162. * @private
  163. */
  164. _getId(url) {
  165. // Creating an ID from the URL and cache name won't be necessary once
  166. // Edge switches to Chromium and all browsers we support work with
  167. // array keyPaths.
  168. return this._cacheName + '|' + normalizeURL(url);
  169. }
  170. /**
  171. * Returns an open connection to the database.
  172. *
  173. * @private
  174. */
  175. async getDb() {
  176. if (!this._db) {
  177. this._db = await openDB(DB_NAME, 1, {
  178. upgrade: this._upgradeDbAndDeleteOldDbs.bind(this),
  179. });
  180. }
  181. return this._db;
  182. }
  183. }
  184. export { CacheTimestampsModel };