CacheTimestampsModel.mjs 5.6 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 {DBWrapper} from 'workbox-core/_private/DBWrapper.mjs';
  8. import {deleteDatabase} from 'workbox-core/_private/deleteDatabase.mjs';
  9. import '../_version.mjs';
  10. const DB_NAME = 'workbox-expiration';
  11. const OBJECT_STORE_NAME = 'cache-entries';
  12. const normalizeURL = (unNormalizedUrl) => {
  13. const url = new URL(unNormalizedUrl, location);
  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. await this._db.put(OBJECT_STORE_NAME, {
  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. }
  76. /**
  77. * Returns the timestamp stored for a given URL.
  78. *
  79. * @param {string} url
  80. * @return {number}
  81. *
  82. * @private
  83. */
  84. async getTimestamp(url) {
  85. const entry = await this._db.get(OBJECT_STORE_NAME, this._getId(url));
  86. return entry.timestamp;
  87. }
  88. /**
  89. * Iterates through all the entries in the object store (from newest to
  90. * oldest) and removes entries once either `maxCount` is reached or the
  91. * entry's timestamp is less than `minTimestamp`.
  92. *
  93. * @param {number} minTimestamp
  94. * @param {number} maxCount
  95. *
  96. * @private
  97. */
  98. async expireEntries(minTimestamp, maxCount) {
  99. const entriesToDelete = await this._db.transaction(
  100. OBJECT_STORE_NAME, 'readwrite', (txn, done) => {
  101. const store = txn.objectStore(OBJECT_STORE_NAME);
  102. const entriesToDelete = [];
  103. let entriesNotDeletedCount = 0;
  104. store.index('timestamp')
  105. .openCursor(null, 'prev')
  106. .onsuccess = ({target}) => {
  107. const cursor = target.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. } else {
  127. entriesNotDeletedCount++;
  128. }
  129. }
  130. cursor.continue();
  131. } else {
  132. done(entriesToDelete);
  133. }
  134. };
  135. });
  136. // TODO(philipwalton): once the Safari bug in the following issue is fixed,
  137. // we should be able to remove this loop and do the entry deletion in the
  138. // cursor loop above:
  139. // https://github.com/GoogleChrome/workbox/issues/1978
  140. const urlsDeleted = [];
  141. for (const entry of entriesToDelete) {
  142. await this._db.delete(OBJECT_STORE_NAME, entry.id);
  143. urlsDeleted.push(entry.url);
  144. }
  145. return urlsDeleted;
  146. }
  147. /**
  148. * Takes a URL and returns an ID that will be unique in the object store.
  149. *
  150. * @param {string} url
  151. * @return {string}
  152. *
  153. * @private
  154. */
  155. _getId(url) {
  156. // Creating an ID from the URL and cache name won't be necessary once
  157. // Edge switches to Chromium and all browsers we support work with
  158. // array keyPaths.
  159. return this._cacheName + '|' + normalizeURL(url);
  160. }
  161. }
  162. export {CacheTimestampsModel};