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