@serwist/expiration 9.0.0-preview.0 → 9.0.0-preview.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +24 -228
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -9,38 +9,16 @@ const normalizeURL = (unNormalizedUrl)=>{
9
9
  url.hash = "";
10
10
  return url.href;
11
11
  };
12
- /**
13
- * Returns the timestamp model.
14
- *
15
- * @private
16
- */ class CacheTimestampsModel {
12
+ class CacheTimestampsModel {
17
13
  _cacheName;
18
14
  _db = null;
19
- /**
20
- *
21
- * @param cacheName
22
- *
23
- * @private
24
- */ constructor(cacheName){
15
+ constructor(cacheName){
25
16
  this._cacheName = cacheName;
26
17
  }
27
- /**
28
- * Performs an upgrade of indexedDB.
29
- *
30
- * @param db
31
- *
32
- * @private
33
- */ _upgradeDb(db) {
34
- // TODO(philipwalton): EdgeHTML doesn't support arrays as a keyPath, so we
35
- // have to use the `id` keyPath here and create our own values (a
36
- // concatenation of `url + cacheName`) instead of simply using
37
- // `keyPath: ['url', 'cacheName']`, which is supported in other browsers.
18
+ _upgradeDb(db) {
38
19
  const objStore = db.createObjectStore(CACHE_OBJECT_STORE, {
39
20
  keyPath: "id"
40
21
  });
41
- // TODO(philipwalton): once we don't have to support EdgeHTML, we can
42
- // create a single index with the keyPath `['cacheName', 'timestamp']`
43
- // instead of doing both these indexes.
44
22
  objStore.createIndex("cacheName", "cacheName", {
45
23
  unique: false
46
24
  });
@@ -48,32 +26,18 @@ const normalizeURL = (unNormalizedUrl)=>{
48
26
  unique: false
49
27
  });
50
28
  }
51
- /**
52
- * Performs an upgrade of indexedDB and deletes deprecated DBs.
53
- *
54
- * @param db
55
- *
56
- * @private
57
- */ _upgradeDbAndDeleteOldDbs(db) {
29
+ _upgradeDbAndDeleteOldDbs(db) {
58
30
  this._upgradeDb(db);
59
31
  if (this._cacheName) {
60
32
  void deleteDB(this._cacheName);
61
33
  }
62
34
  }
63
- /**
64
- * @param url
65
- * @param timestamp
66
- *
67
- * @private
68
- */ async setTimestamp(url, timestamp) {
35
+ async setTimestamp(url, timestamp) {
69
36
  url = normalizeURL(url);
70
37
  const entry = {
71
38
  url,
72
39
  timestamp,
73
40
  cacheName: this._cacheName,
74
- // Creating an ID from the URL and cache name won't be necessary once
75
- // Edge switches to Chromium and all browsers we support work with
76
- // array keyPaths.
77
41
  id: this._getId(url)
78
42
  };
79
43
  const db = await this.getDb();
@@ -83,47 +47,20 @@ const normalizeURL = (unNormalizedUrl)=>{
83
47
  await tx.store.put(entry);
84
48
  await tx.done;
85
49
  }
86
- /**
87
- * Returns the timestamp stored for a given URL.
88
- *
89
- * @param url
90
- * @returns
91
- * @private
92
- */ async getTimestamp(url) {
50
+ async getTimestamp(url) {
93
51
  const db = await this.getDb();
94
52
  const entry = await db.get(CACHE_OBJECT_STORE, this._getId(url));
95
53
  return entry?.timestamp;
96
54
  }
97
- /**
98
- * Iterates through all the entries in the object store (from newest to
99
- * oldest) and removes entries once either `maxCount` is reached or the
100
- * entry's timestamp is less than `minTimestamp`.
101
- *
102
- * @param minTimestamp
103
- * @param maxCount
104
- * @returns
105
- * @private
106
- */ async expireEntries(minTimestamp, maxCount) {
55
+ async expireEntries(minTimestamp, maxCount) {
107
56
  const db = await this.getDb();
108
57
  let cursor = await db.transaction(CACHE_OBJECT_STORE).store.index("timestamp").openCursor(null, "prev");
109
58
  const entriesToDelete = [];
110
59
  let entriesNotDeletedCount = 0;
111
60
  while(cursor){
112
61
  const result = cursor.value;
113
- // TODO(philipwalton): once we can use a multi-key index, we
114
- // won't have to check `cacheName` here.
115
62
  if (result.cacheName === this._cacheName) {
116
- // Delete an entry if it's older than the max age or
117
- // if we already have the max number allowed.
118
63
  if (minTimestamp && result.timestamp < minTimestamp || maxCount && entriesNotDeletedCount >= maxCount) {
119
- // TODO(philipwalton): we should be able to delete the
120
- // entry right here, but doing so causes an iteration
121
- // bug in Safari stable (fixed in TP). Instead we can
122
- // store the keys of the entries to delete, and then
123
- // delete the separate transactions.
124
- // https://github.com/GoogleChrome/workbox/issues/1978
125
- // cursor.delete();
126
- // We only need to return the URL, not the whole entry.
127
64
  entriesToDelete.push(cursor.value);
128
65
  } else {
129
66
  entriesNotDeletedCount++;
@@ -131,10 +68,6 @@ const normalizeURL = (unNormalizedUrl)=>{
131
68
  }
132
69
  cursor = await cursor.continue();
133
70
  }
134
- // TODO(philipwalton): once the Safari bug in the following issue is fixed,
135
- // we should be able to remove this loop and do the entry deletion in the
136
- // cursor loop above:
137
- // https://github.com/GoogleChrome/workbox/issues/1978
138
71
  const urlsDeleted = [];
139
72
  for (const entry of entriesToDelete){
140
73
  await db.delete(CACHE_OBJECT_STORE, entry.id);
@@ -142,23 +75,10 @@ const normalizeURL = (unNormalizedUrl)=>{
142
75
  }
143
76
  return urlsDeleted;
144
77
  }
145
- /**
146
- * Takes a URL and returns an ID that will be unique in the object store.
147
- *
148
- * @param url
149
- * @returns
150
- * @private
151
- */ _getId(url) {
152
- // Creating an ID from the URL and cache name won't be necessary once
153
- // Edge switches to Chromium and all browsers we support work with
154
- // array keyPaths.
78
+ _getId(url) {
155
79
  return `${this._cacheName}|${normalizeURL(url)}`;
156
80
  }
157
- /**
158
- * Returns an open connection to the database.
159
- *
160
- * @private
161
- */ async getDb() {
81
+ async getDb() {
162
82
  if (!this._db) {
163
83
  this._db = await openDB(DB_NAME, 1, {
164
84
  upgrade: this._upgradeDbAndDeleteOldDbs.bind(this)
@@ -168,11 +88,7 @@ const normalizeURL = (unNormalizedUrl)=>{
168
88
  }
169
89
  }
170
90
 
171
- /**
172
- * The `CacheExpiration` class allows you define an expiration and / or
173
- * limit on the number of responses stored in a
174
- * [`Cache`](https://developer.mozilla.org/en-US/docs/Web/API/Cache).
175
- */ class CacheExpiration {
91
+ class CacheExpiration {
176
92
  _isRunning = false;
177
93
  _rerunRequested = false;
178
94
  _maxEntries;
@@ -180,13 +96,7 @@ const normalizeURL = (unNormalizedUrl)=>{
180
96
  _matchOptions;
181
97
  _cacheName;
182
98
  _timestampModel;
183
- /**
184
- * To construct a new CacheExpiration instance you must provide at least
185
- * one of the `config` properties.
186
- *
187
- * @param cacheName Name of the cache to apply restrictions to.
188
- * @param config
189
- */ constructor(cacheName, config = {}){
99
+ constructor(cacheName, config = {}){
190
100
  if (process.env.NODE_ENV !== "production") {
191
101
  assert.isType(cacheName, "string", {
192
102
  moduleName: "@serwist/expiration",
@@ -224,9 +134,7 @@ const normalizeURL = (unNormalizedUrl)=>{
224
134
  this._cacheName = cacheName;
225
135
  this._timestampModel = new CacheTimestampsModel(cacheName);
226
136
  }
227
- /**
228
- * Expires entries for the given cache and given criteria.
229
- */ async expireEntries() {
137
+ async expireEntries() {
230
138
  if (this._isRunning) {
231
139
  this._rerunRequested = true;
232
140
  return;
@@ -234,7 +142,6 @@ const normalizeURL = (unNormalizedUrl)=>{
234
142
  this._isRunning = true;
235
143
  const minTimestamp = this._maxAgeSeconds ? Date.now() - this._maxAgeSeconds * 1000 : 0;
236
144
  const urlsExpired = await this._timestampModel.expireEntries(minTimestamp, this._maxEntries);
237
- // Delete URLs from the cache
238
145
  const cache = await self.caches.open(this._cacheName);
239
146
  for (const url of urlsExpired){
240
147
  await cache.delete(url, this._matchOptions);
@@ -257,13 +164,7 @@ const normalizeURL = (unNormalizedUrl)=>{
257
164
  dontWaitFor(this.expireEntries());
258
165
  }
259
166
  }
260
- /**
261
- * Update the timestamp for the given URL. This ensures the when
262
- * removing entries based on maximum entries, most recently used
263
- * is accurate or when expiring, the timestamp is up-to-date.
264
- *
265
- * @param url
266
- */ async updateTimestamp(url) {
167
+ async updateTimestamp(url) {
267
168
  if (process.env.NODE_ENV !== "production") {
268
169
  assert.isType(url, "string", {
269
170
  moduleName: "@serwist/expiration",
@@ -274,17 +175,7 @@ const normalizeURL = (unNormalizedUrl)=>{
274
175
  }
275
176
  await this._timestampModel.setTimestamp(url, Date.now());
276
177
  }
277
- /**
278
- * Can be used to check if a URL has expired or not before it's used.
279
- *
280
- * This requires a look up from IndexedDB, so can be slow.
281
- *
282
- * Note: This method will not remove the cached entry, call
283
- * `expireEntries()` to remove indexedDB and Cache entries.
284
- *
285
- * @param url
286
- * @returns
287
- */ async isURLExpired(url) {
178
+ async isURLExpired(url) {
288
179
  if (!this._maxAgeSeconds) {
289
180
  if (process.env.NODE_ENV !== "production") {
290
181
  throw new SerwistError("expired-test-without-max-age", {
@@ -298,44 +189,17 @@ const normalizeURL = (unNormalizedUrl)=>{
298
189
  const expireOlderThan = Date.now() - this._maxAgeSeconds * 1000;
299
190
  return timestamp !== undefined ? timestamp < expireOlderThan : true;
300
191
  }
301
- /**
302
- * Removes the IndexedDB object store used to keep track of cache expiration
303
- * metadata.
304
- */ async delete() {
305
- // Make sure we don't attempt another rerun if we're called in the middle of
306
- // a cache expiration.
192
+ async delete() {
307
193
  this._rerunRequested = false;
308
- await this._timestampModel.expireEntries(Infinity); // Expires all.
194
+ await this._timestampModel.expireEntries(Infinity);
309
195
  }
310
196
  }
311
197
 
312
- /**
313
- * This plugin can be used in a `@serwist/strategies` Strategy to regularly enforce a
314
- * limit on the age and / or the number of cached requests.
315
- *
316
- * It can only be used with Strategy instances that have a
317
- * [custom `cacheName` property set](/web/tools/workbox/guides/configure-workbox#custom_cache_names_in_strategies).
318
- * In other words, it can't be used to expire entries in strategy that uses the
319
- * default runtime cache name.
320
- *
321
- * Whenever a cached response is used or updated, this plugin will look
322
- * at the associated cache and remove any old or extra responses.
323
- *
324
- * When using `maxAgeSeconds`, responses may be used *once* after expiring
325
- * because the expiration clean up will not have occurred until *after* the
326
- * cached response has been used. If the response has a "Date" header, then
327
- * a light weight expiration check is performed and the response will not be
328
- * used immediately.
329
- *
330
- * When using `maxEntries`, the entry least-recently requested will be removed
331
- * from the cache first.
332
- */ class ExpirationPlugin {
198
+ class ExpirationPlugin {
333
199
  _config;
334
200
  _maxAgeSeconds;
335
201
  _cacheExpirations;
336
- /**
337
- * @param config
338
- */ constructor(config = {}){
202
+ constructor(config = {}){
339
203
  if (process.env.NODE_ENV !== "production") {
340
204
  if (!(config.maxEntries || config.maxAgeSeconds)) {
341
205
  throw new SerwistError("max-entries-or-age-required", {
@@ -368,14 +232,7 @@ const normalizeURL = (unNormalizedUrl)=>{
368
232
  registerQuotaErrorCallback(()=>this.deleteCacheAndMetadata());
369
233
  }
370
234
  }
371
- /**
372
- * A simple helper method to return a CacheExpiration instance for a given
373
- * cache name.
374
- *
375
- * @param cacheName
376
- * @returns
377
- * @private
378
- */ _getCacheExpiration(cacheName) {
235
+ _getCacheExpiration(cacheName) {
379
236
  if (cacheName === privateCacheNames.getRuntimeName()) {
380
237
  throw new SerwistError("expire-custom-caches-only");
381
238
  }
@@ -386,36 +243,19 @@ const normalizeURL = (unNormalizedUrl)=>{
386
243
  }
387
244
  return cacheExpiration;
388
245
  }
389
- /**
390
- * A "lifecycle" callback that will be triggered automatically by the
391
- * `@serwist/strategies` handlers when a `Response` is about to be returned
392
- * from a [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) to
393
- * the handler. It allows the `Response` to be inspected for freshness and
394
- * prevents it from being used if the `Response`'s `Date` header value is
395
- * older than the configured `maxAgeSeconds`.
396
- *
397
- * @param options
398
- * @returns Either the `cachedResponse`, if it's fresh, or `null` if the `Response`
399
- * is older than `maxAgeSeconds`.
400
- * @private
401
- */ cachedResponseWillBeUsed = async ({ event, request, cacheName, cachedResponse })=>{
246
+ cachedResponseWillBeUsed = async ({ event, request, cacheName, cachedResponse })=>{
402
247
  if (!cachedResponse) {
403
248
  return null;
404
249
  }
405
250
  const isFresh = this._isResponseDateFresh(cachedResponse);
406
- // Expire entries to ensure that even if the expiration date has
407
- // expired, it'll only be used once.
408
251
  const cacheExpiration = this._getCacheExpiration(cacheName);
409
252
  dontWaitFor(cacheExpiration.expireEntries());
410
- // Update the metadata for the request URL to the current timestamp,
411
- // but don't `await` it as we don't want to block the response.
412
253
  const updateTimestampDone = cacheExpiration.updateTimestamp(request.url);
413
254
  if (event) {
414
255
  try {
415
256
  event.waitUntil(updateTimestampDone);
416
257
  } catch (error) {
417
258
  if (process.env.NODE_ENV !== "production") {
418
- // The event may not be a fetch event; only log the URL if it is.
419
259
  if ("request" in event) {
420
260
  logger.warn(`Unable to ensure service worker stays alive when updating cache entry for '${getFriendlyURL(event.request.url)}'.`);
421
261
  }
@@ -424,56 +264,30 @@ const normalizeURL = (unNormalizedUrl)=>{
424
264
  }
425
265
  return isFresh ? cachedResponse : null;
426
266
  };
427
- /**
428
- * @param cachedResponse
429
- * @returns
430
- * @private
431
- */ _isResponseDateFresh(cachedResponse) {
267
+ _isResponseDateFresh(cachedResponse) {
432
268
  if (!this._maxAgeSeconds) {
433
- // We aren't expiring by age, so return true, it's fresh
434
269
  return true;
435
270
  }
436
- // Check if the 'date' header will suffice a quick expiration check.
437
- // See https://github.com/GoogleChromeLabs/sw-toolbox/issues/164 for
438
- // discussion.
439
271
  const dateHeaderTimestamp = this._getDateHeaderTimestamp(cachedResponse);
440
272
  if (dateHeaderTimestamp === null) {
441
- // Unable to parse date, so assume it's fresh.
442
273
  return true;
443
274
  }
444
- // If we have a valid headerTime, then our response is fresh iff the
445
- // headerTime plus maxAgeSeconds is greater than the current time.
446
275
  const now = Date.now();
447
276
  return dateHeaderTimestamp >= now - this._maxAgeSeconds * 1000;
448
277
  }
449
- /**
450
- * This method will extract the data header and parse it into a useful
451
- * value.
452
- *
453
- * @param cachedResponse
454
- * @returns
455
- * @private
456
- */ _getDateHeaderTimestamp(cachedResponse) {
278
+ _getDateHeaderTimestamp(cachedResponse) {
457
279
  if (!cachedResponse.headers.has("date")) {
458
280
  return null;
459
281
  }
460
282
  const dateHeader = cachedResponse.headers.get("date");
461
283
  const parsedDate = new Date(dateHeader);
462
284
  const headerTime = parsedDate.getTime();
463
- // If the Date header was invalid for some reason, parsedDate.getTime()
464
- // will return NaN.
465
285
  if (Number.isNaN(headerTime)) {
466
286
  return null;
467
287
  }
468
288
  return headerTime;
469
289
  }
470
- /**
471
- * A "lifecycle" callback that will be triggered automatically by the
472
- * `@serwist/strategies` handlers when an entry is added to a cache.
473
- *
474
- * @param options
475
- * @private
476
- */ cacheDidUpdate = async ({ cacheName, request })=>{
290
+ cacheDidUpdate = async ({ cacheName, request })=>{
477
291
  if (process.env.NODE_ENV !== "production") {
478
292
  assert.isType(cacheName, "string", {
479
293
  moduleName: "@serwist/expiration",
@@ -492,29 +306,11 @@ const normalizeURL = (unNormalizedUrl)=>{
492
306
  await cacheExpiration.updateTimestamp(request.url);
493
307
  await cacheExpiration.expireEntries();
494
308
  };
495
- /**
496
- * This is a helper method that performs two operations:
497
- *
498
- * - Deletes *all* the underlying Cache instances associated with this plugin
499
- * instance, by calling caches.delete() on your behalf.
500
- * - Deletes the metadata from IndexedDB used to keep track of expiration
501
- * details for each Cache instance.
502
- *
503
- * When using cache expiration, calling this method is preferable to calling
504
- * `caches.delete()` directly, since this will ensure that the IndexedDB
505
- * metadata is also cleanly removed and open IndexedDB instances are deleted.
506
- *
507
- * Note that if you're *not* using cache expiration for a given cache, calling
508
- * `caches.delete()` and passing in the cache's name should be sufficient.
509
- * There is no Serwist-specific method needed for cleanup in that case.
510
- */ async deleteCacheAndMetadata() {
511
- // Do this one at a time instead of all at once via `Promise.all()` to
512
- // reduce the chance of inconsistency if a promise rejects.
309
+ async deleteCacheAndMetadata() {
513
310
  for (const [cacheName, cacheExpiration] of this._cacheExpirations){
514
311
  await self.caches.delete(cacheName);
515
312
  await cacheExpiration.delete();
516
313
  }
517
- // Reset this._cacheExpirations to its initial state.
518
314
  this._cacheExpirations = new Map();
519
315
  }
520
316
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serwist/expiration",
3
- "version": "9.0.0-preview.0",
3
+ "version": "9.0.0-preview.1",
4
4
  "type": "module",
5
5
  "description": "A service worker helper library that expires cached responses based on age or maximum number of entries.",
6
6
  "files": [
@@ -30,12 +30,12 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "idb": "8.0.0",
33
- "@serwist/core": "9.0.0-preview.0"
33
+ "@serwist/core": "9.0.0-preview.1"
34
34
  },
35
35
  "devDependencies": {
36
36
  "rollup": "4.9.6",
37
37
  "typescript": "5.4.0-dev.20240203",
38
- "@serwist/constants": "9.0.0-preview.0"
38
+ "@serwist/constants": "9.0.0-preview.1"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "typescript": ">=5.0.0"