CacheTimestampsModel.js 6.1 KB

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