@serwist/expiration
Advanced tools
Comparing version 9.0.0-preview.17 to 9.0.0-preview.18
@@ -1,6 +0,3 @@ | ||
import { CacheExpiration } from "./CacheExpiration.js"; | ||
import type { ExpirationPluginOptions } from "./ExpirationPlugin.js"; | ||
import { ExpirationPlugin } from "./ExpirationPlugin.js"; | ||
export { CacheExpiration, ExpirationPlugin }; | ||
export type { ExpirationPluginOptions }; | ||
export { CacheExpiration, ExpirationPlugin } from "@serwist/sw/plugins"; | ||
export type { ExpirationPluginOptions } from "@serwist/sw/plugins"; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -1,330 +0,1 @@ | ||
import { assert, SerwistError, logger, privateCacheNames, getFriendlyURL } from '@serwist/core/internal'; | ||
import { deleteDB, openDB } from 'idb'; | ||
import { registerQuotaErrorCallback } from '@serwist/core'; | ||
const DB_NAME = "serwist-expiration"; | ||
const CACHE_OBJECT_STORE = "cache-entries"; | ||
const normalizeURL = (unNormalizedUrl)=>{ | ||
const url = new URL(unNormalizedUrl, location.href); | ||
url.hash = ""; | ||
return url.href; | ||
}; | ||
class CacheTimestampsModel { | ||
_cacheName; | ||
_db = null; | ||
constructor(cacheName){ | ||
this._cacheName = cacheName; | ||
} | ||
_getId(url) { | ||
return `${this._cacheName}|${normalizeURL(url)}`; | ||
} | ||
_upgradeDb(db) { | ||
const objStore = db.createObjectStore(CACHE_OBJECT_STORE, { | ||
keyPath: "id" | ||
}); | ||
objStore.createIndex("cacheName", "cacheName", { | ||
unique: false | ||
}); | ||
objStore.createIndex("timestamp", "timestamp", { | ||
unique: false | ||
}); | ||
} | ||
_upgradeDbAndDeleteOldDbs(db) { | ||
this._upgradeDb(db); | ||
if (this._cacheName) { | ||
void deleteDB(this._cacheName); | ||
} | ||
} | ||
async setTimestamp(url, timestamp) { | ||
url = normalizeURL(url); | ||
const entry = { | ||
id: this._getId(url), | ||
cacheName: this._cacheName, | ||
url, | ||
timestamp | ||
}; | ||
const db = await this.getDb(); | ||
const tx = db.transaction(CACHE_OBJECT_STORE, "readwrite", { | ||
durability: "relaxed" | ||
}); | ||
await tx.store.put(entry); | ||
await tx.done; | ||
} | ||
async getTimestamp(url) { | ||
const db = await this.getDb(); | ||
const entry = await db.get(CACHE_OBJECT_STORE, this._getId(url)); | ||
return entry?.timestamp; | ||
} | ||
async expireEntries(minTimestamp, maxCount) { | ||
const db = await this.getDb(); | ||
let cursor = await db.transaction(CACHE_OBJECT_STORE, "readwrite").store.index("timestamp").openCursor(null, "prev"); | ||
const urlsDeleted = []; | ||
let entriesNotDeletedCount = 0; | ||
while(cursor){ | ||
const result = cursor.value; | ||
if (result.cacheName === this._cacheName) { | ||
if (minTimestamp && result.timestamp < minTimestamp || maxCount && entriesNotDeletedCount >= maxCount) { | ||
cursor.delete(); | ||
urlsDeleted.push(result.url); | ||
} else { | ||
entriesNotDeletedCount++; | ||
} | ||
} | ||
cursor = await cursor.continue(); | ||
} | ||
return urlsDeleted; | ||
} | ||
async getDb() { | ||
if (!this._db) { | ||
this._db = await openDB(DB_NAME, 1, { | ||
upgrade: this._upgradeDbAndDeleteOldDbs.bind(this) | ||
}); | ||
} | ||
return this._db; | ||
} | ||
} | ||
class CacheExpiration { | ||
_isRunning = false; | ||
_rerunRequested = false; | ||
_maxEntries; | ||
_maxAgeSeconds; | ||
_matchOptions; | ||
_cacheName; | ||
_timestampModel; | ||
constructor(cacheName, config = {}){ | ||
if (process.env.NODE_ENV !== "production") { | ||
assert.isType(cacheName, "string", { | ||
moduleName: "@serwist/expiration", | ||
className: "CacheExpiration", | ||
funcName: "constructor", | ||
paramName: "cacheName" | ||
}); | ||
if (!(config.maxEntries || config.maxAgeSeconds)) { | ||
throw new SerwistError("max-entries-or-age-required", { | ||
moduleName: "@serwist/expiration", | ||
className: "CacheExpiration", | ||
funcName: "constructor" | ||
}); | ||
} | ||
if (config.maxEntries) { | ||
assert.isType(config.maxEntries, "number", { | ||
moduleName: "@serwist/expiration", | ||
className: "CacheExpiration", | ||
funcName: "constructor", | ||
paramName: "config.maxEntries" | ||
}); | ||
} | ||
if (config.maxAgeSeconds) { | ||
assert.isType(config.maxAgeSeconds, "number", { | ||
moduleName: "@serwist/expiration", | ||
className: "CacheExpiration", | ||
funcName: "constructor", | ||
paramName: "config.maxAgeSeconds" | ||
}); | ||
} | ||
} | ||
this._maxEntries = config.maxEntries; | ||
this._maxAgeSeconds = config.maxAgeSeconds; | ||
this._matchOptions = config.matchOptions; | ||
this._cacheName = cacheName; | ||
this._timestampModel = new CacheTimestampsModel(cacheName); | ||
} | ||
async expireEntries() { | ||
if (this._isRunning) { | ||
this._rerunRequested = true; | ||
return; | ||
} | ||
this._isRunning = true; | ||
const minTimestamp = this._maxAgeSeconds ? Date.now() - this._maxAgeSeconds * 1000 : 0; | ||
const urlsExpired = await this._timestampModel.expireEntries(minTimestamp, this._maxEntries); | ||
const cache = await self.caches.open(this._cacheName); | ||
for (const url of urlsExpired){ | ||
await cache.delete(url, this._matchOptions); | ||
} | ||
if (process.env.NODE_ENV !== "production") { | ||
if (urlsExpired.length > 0) { | ||
logger.groupCollapsed(`Expired ${urlsExpired.length} ` + `${urlsExpired.length === 1 ? "entry" : "entries"} and removed ` + `${urlsExpired.length === 1 ? "it" : "them"} from the ` + `'${this._cacheName}' cache.`); | ||
logger.log(`Expired the following ${urlsExpired.length === 1 ? "URL" : "URLs"}:`); | ||
for (const url of urlsExpired){ | ||
logger.log(` ${url}`); | ||
} | ||
logger.groupEnd(); | ||
} else { | ||
logger.debug("Cache expiration ran and found no entries to remove."); | ||
} | ||
} | ||
this._isRunning = false; | ||
if (this._rerunRequested) { | ||
this._rerunRequested = false; | ||
void this.expireEntries(); | ||
} | ||
} | ||
async updateTimestamp(url) { | ||
if (process.env.NODE_ENV !== "production") { | ||
assert.isType(url, "string", { | ||
moduleName: "@serwist/expiration", | ||
className: "CacheExpiration", | ||
funcName: "updateTimestamp", | ||
paramName: "url" | ||
}); | ||
} | ||
await this._timestampModel.setTimestamp(url, Date.now()); | ||
} | ||
async isURLExpired(url) { | ||
if (!this._maxAgeSeconds) { | ||
if (process.env.NODE_ENV !== "production") { | ||
throw new SerwistError("expired-test-without-max-age", { | ||
methodName: "isURLExpired", | ||
paramName: "maxAgeSeconds" | ||
}); | ||
} | ||
return false; | ||
} | ||
const timestamp = await this._timestampModel.getTimestamp(url); | ||
const expireOlderThan = Date.now() - this._maxAgeSeconds * 1000; | ||
return timestamp !== undefined ? timestamp < expireOlderThan : true; | ||
} | ||
async delete() { | ||
this._rerunRequested = false; | ||
await this._timestampModel.expireEntries(Number.POSITIVE_INFINITY); | ||
} | ||
} | ||
class ExpirationPlugin { | ||
_config; | ||
_cacheExpirations; | ||
constructor(config = {}){ | ||
if (process.env.NODE_ENV !== "production") { | ||
if (!(config.maxEntries || config.maxAgeSeconds)) { | ||
throw new SerwistError("max-entries-or-age-required", { | ||
moduleName: "@serwist/expiration", | ||
className: "ExpirationPlugin", | ||
funcName: "constructor" | ||
}); | ||
} | ||
if (config.maxEntries) { | ||
assert.isType(config.maxEntries, "number", { | ||
moduleName: "@serwist/expiration", | ||
className: "ExpirationPlugin", | ||
funcName: "constructor", | ||
paramName: "config.maxEntries" | ||
}); | ||
} | ||
if (config.maxAgeSeconds) { | ||
assert.isType(config.maxAgeSeconds, "number", { | ||
moduleName: "@serwist/expiration", | ||
className: "ExpirationPlugin", | ||
funcName: "constructor", | ||
paramName: "config.maxAgeSeconds" | ||
}); | ||
} | ||
if (config.maxAgeFrom) { | ||
assert.isType(config.maxAgeFrom, "string", { | ||
moduleName: "@serwist/expiration", | ||
className: "ExpirationPlugin", | ||
funcName: "constructor", | ||
paramName: "config.maxAgeFrom" | ||
}); | ||
} | ||
} | ||
this._config = config; | ||
this._cacheExpirations = new Map(); | ||
if (!this._config.maxAgeFrom) { | ||
this._config.maxAgeFrom = "last-fetched"; | ||
} | ||
if (this._config.purgeOnQuotaError) { | ||
registerQuotaErrorCallback(()=>this.deleteCacheAndMetadata()); | ||
} | ||
} | ||
_getCacheExpiration(cacheName) { | ||
if (cacheName === privateCacheNames.getRuntimeName()) { | ||
throw new SerwistError("expire-custom-caches-only"); | ||
} | ||
let cacheExpiration = this._cacheExpirations.get(cacheName); | ||
if (!cacheExpiration) { | ||
cacheExpiration = new CacheExpiration(cacheName, this._config); | ||
this._cacheExpirations.set(cacheName, cacheExpiration); | ||
} | ||
return cacheExpiration; | ||
} | ||
cachedResponseWillBeUsed({ event, cacheName, request, cachedResponse }) { | ||
if (!cachedResponse) { | ||
return null; | ||
} | ||
const isFresh = this._isResponseDateFresh(cachedResponse); | ||
const cacheExpiration = this._getCacheExpiration(cacheName); | ||
const isMaxAgeFromLastUsed = this._config.maxAgeFrom === "last-used"; | ||
const done = (async ()=>{ | ||
if (isMaxAgeFromLastUsed) { | ||
await cacheExpiration.updateTimestamp(request.url); | ||
} | ||
await cacheExpiration.expireEntries(); | ||
})(); | ||
try { | ||
event.waitUntil(done); | ||
} catch (error) { | ||
if (process.env.NODE_ENV !== "production") { | ||
if (event instanceof FetchEvent) { | ||
logger.warn(`Unable to ensure service worker stays alive when updating cache entry for '${getFriendlyURL(event.request.url)}'.`); | ||
} | ||
} | ||
} | ||
return isFresh ? cachedResponse : null; | ||
} | ||
_isResponseDateFresh(cachedResponse) { | ||
const isMaxAgeFromLastUsed = this._config.maxAgeFrom === "last-used"; | ||
if (isMaxAgeFromLastUsed) { | ||
return true; | ||
} | ||
const now = Date.now(); | ||
if (!this._config.maxAgeSeconds) { | ||
return true; | ||
} | ||
const dateHeaderTimestamp = this._getDateHeaderTimestamp(cachedResponse); | ||
if (dateHeaderTimestamp === null) { | ||
return true; | ||
} | ||
return dateHeaderTimestamp >= now - this._config.maxAgeSeconds * 1000; | ||
} | ||
_getDateHeaderTimestamp(cachedResponse) { | ||
if (!cachedResponse.headers.has("date")) { | ||
return null; | ||
} | ||
const dateHeader = cachedResponse.headers.get("date"); | ||
const parsedDate = new Date(dateHeader); | ||
const headerTime = parsedDate.getTime(); | ||
if (Number.isNaN(headerTime)) { | ||
return null; | ||
} | ||
return headerTime; | ||
} | ||
async cacheDidUpdate({ cacheName, request }) { | ||
if (process.env.NODE_ENV !== "production") { | ||
assert.isType(cacheName, "string", { | ||
moduleName: "@serwist/expiration", | ||
className: "Plugin", | ||
funcName: "cacheDidUpdate", | ||
paramName: "cacheName" | ||
}); | ||
assert.isInstance(request, Request, { | ||
moduleName: "@serwist/expiration", | ||
className: "Plugin", | ||
funcName: "cacheDidUpdate", | ||
paramName: "request" | ||
}); | ||
} | ||
const cacheExpiration = this._getCacheExpiration(cacheName); | ||
await cacheExpiration.updateTimestamp(request.url); | ||
await cacheExpiration.expireEntries(); | ||
} | ||
async deleteCacheAndMetadata() { | ||
for (const [cacheName, cacheExpiration] of this._cacheExpirations){ | ||
await self.caches.delete(cacheName); | ||
await cacheExpiration.delete(); | ||
} | ||
this._cacheExpirations = new Map(); | ||
} | ||
} | ||
export { CacheExpiration, ExpirationPlugin }; | ||
export { CacheExpiration, ExpirationPlugin } from '@serwist/sw/plugins'; |
{ | ||
"name": "@serwist/expiration", | ||
"version": "9.0.0-preview.17", | ||
"version": "9.0.0-preview.18", | ||
"type": "module", | ||
@@ -32,4 +32,3 @@ "description": "A module that expires cached responses based on age or maximum number of entries.", | ||
"dependencies": { | ||
"idb": "8.0.0", | ||
"@serwist/core": "9.0.0-preview.17" | ||
"@serwist/sw": "9.0.0-preview.18" | ||
}, | ||
@@ -39,3 +38,3 @@ "devDependencies": { | ||
"typescript": "5.5.0-dev.20240323", | ||
"@serwist/constants": "9.0.0-preview.17" | ||
"@serwist/constants": "9.0.0-preview.18" | ||
}, | ||
@@ -42,0 +41,0 @@ "peerDependencies": { |
@@ -1,15 +0,2 @@ | ||
/* | ||
Copyright 2018 Google LLC | ||
Use of this source code is governed by an MIT-style | ||
license that can be found in the LICENSE file or at | ||
https://opensource.org/licenses/MIT. | ||
*/ | ||
import { CacheExpiration } from "./CacheExpiration.js"; | ||
import type { ExpirationPluginOptions } from "./ExpirationPlugin.js"; | ||
import { ExpirationPlugin } from "./ExpirationPlugin.js"; | ||
export { CacheExpiration, ExpirationPlugin }; | ||
export type { ExpirationPluginOptions }; | ||
export { CacheExpiration, ExpirationPlugin } from "@serwist/sw/plugins"; | ||
export type { ExpirationPluginOptions } from "@serwist/sw/plugins"; |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Trivial Package
Supply chain riskPackages less than 10 lines of code are easily copied into your own project and may not warrant the additional supply chain risk of an external dependency.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Mixed license
License(Experimental) Package contains multiple licenses.
Found 1 instance in 1 package
Unidentified License
License(Experimental) Something that seems like a license was found, but its contents could not be matched with a known license.
Found 3 instances in 1 package
2
0
100
1
3154
7
5
2
+ Added@serwist/sw@9.0.0-preview.18
+ Added@serwist/core@9.0.0-preview.18(transitive)
+ Added@serwist/navigation-preload@9.0.0-preview.18(transitive)
+ Added@serwist/sw@9.0.0-preview.18(transitive)
- Removed@serwist/core@9.0.0-preview.17
- Removedidb@8.0.0
- Removed@serwist/core@9.0.0-preview.17(transitive)