feat: add presets stale-while-revalidate & delete-old-caches

This commit is contained in:
Ayo Ayco 2025-04-07 23:22:56 +01:00
parent 00e382dc70
commit 8d7e5df515
12 changed files with 206 additions and 5 deletions

5
demo/tsconfig.json Normal file
View file

@ -0,0 +1,5 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}

View file

@ -8,8 +8,18 @@
"url": "https://git.sr.ht/~ayoayco/astro-sw"
},
"exports": {
".": "./dist/astro-sw.js",
"./globals": "./dist/globals.js"
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./globals": {
"types": "./dist/eslint/globals.d.ts",
"default": "./dist/eslint/globals.js"
},
"./presets/**": {
"types": "./dist/presets/**/index.d.ts",
"default": "./dist/presets/**/index.js"
}
},
"files": [
"dist"
@ -20,7 +30,7 @@
"node": ">=18.0.0"
},
"scripts": {
"build": "tsup src/** --format esm --dts --clean",
"build": "tsup src/**/index.ts src/eslint/globals.ts --format esm --dts --clean",
"test": "vitest run",
"version:patch": "npm version patch",
"version:minor": "npm version minor",

View file

@ -18,6 +18,7 @@ const ASTROSW = '@ayco/astro-sw'
*/
export default function serviceWorker(options: Config): AstroIntegration {
const {
presets,
assetCachePrefix = ASTROSW,
assetCacheVersionID = '0',
path: serviceWorkerPath = undefined,
@ -37,6 +38,9 @@ export default function serviceWorker(options: Config): AstroIntegration {
afterRegistration: afterRegistrationFn = () => {},
} = registrationHooks
// TODO use presets
console.log(presets)
/**
* @type {Array<string>}
*/

View file

@ -0,0 +1,16 @@
import { ServiceWorkerPreset } from '../../types'
export const activateFn: ServiceWorkerPreset['activate'] = async ({
cacheName,
}) => {
const allowCacheNames = [cacheName]
const allCaches = await caches.keys()
allCaches.forEach((key) => {
if (!allowCacheNames.includes(key)) {
console.info('Deleting old cache', key)
caches.delete(key)
}
})
}
export default activateFn

View file

@ -0,0 +1,8 @@
import { ServiceWorkerPreset } from '../../types'
import activate from './activate'
const deleteOldCaches: ServiceWorkerPreset = {
activate,
}
export default deleteOldCaches

View file

@ -0,0 +1,7 @@
import { ServiceWorkerPreset } from '../../types'
export const activateFn: ServiceWorkerPreset['activate'] = ({ event }) => {
console.info('activating service worker...', event)
}
export default activateFn

View file

@ -0,0 +1,90 @@
import { ServiceWorkerPreset } from '../../types'
export const fetchFn: ServiceWorkerPreset['fetch'] = ({ event, cacheName }) => {
console.info('fetch happened', { data: event })
event.respondWith(
cacheAndRevalidate(
{
request: event.request,
fallbackUrl: './',
},
cacheName
)
)
}
export default fetchFn
// @ts-expect-error TODO fix types
const putInCache = async (request, response, cacheName) => {
const cache = await caches.open(cacheName)
if (response.ok) {
console.info('adding one response to cache...', request.url)
// if exists, replace
cache.keys().then((keys) => {
if (keys.includes(request)) {
cache.delete(request)
}
})
cache.put(request, response)
}
}
const cacheAndRevalidate = async (
// @ts-expect-error TODO fix types
{ request, fallbackUrl },
cacheName: string
) => {
const cache = await caches.open(cacheName)
// Try get the resource from the cache
const responseFromCache = await cache.match(request)
if (responseFromCache) {
console.info('using cached response...', responseFromCache.url)
// get network response for revalidation of cached assets
fetch(request.clone())
.then((responseFromNetwork) => {
if (responseFromNetwork) {
console.info('fetched updated resource...', responseFromNetwork.url)
putInCache(request, responseFromNetwork.clone(), cacheName)
}
})
.catch((error) => {
console.info('failed to fetch updated resource', error)
})
return responseFromCache
}
try {
// Try to get the resource from the network for 5 seconds
const responseFromNetwork = await fetch(request.clone())
// response may be used only once
// we need to save clone to put one copy in cache
// and serve second one
putInCache(request, responseFromNetwork.clone(), cacheName)
console.info('using network response', responseFromNetwork.url)
return responseFromNetwork
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
// Try the fallback
const fallbackResponse = await cache.match(fallbackUrl)
if (fallbackResponse) {
console.info('using fallback cached response...', fallbackResponse.url)
return fallbackResponse
}
// when even the fallback response is not available,
// there is nothing we can do, but we must always
// return a Response object
return new Response('Network error happened', {
status: 408,
headers: { 'Content-Type': 'text/plain' },
})
}
}

View file

@ -0,0 +1,16 @@
/**
* preset for stale-while-revalidate caching strategy
*/
import { ServiceWorkerPreset } from '../../types'
import activate from './activate'
import install from './install'
import fetch from './fetch'
export const presetStaleWhileRevalidate: ServiceWorkerPreset = {
activate,
install,
fetch,
}
export default presetStaleWhileRevalidate

View file

@ -0,0 +1,33 @@
import { ServiceWorkerPreset } from '../../types'
declare const self: ServiceWorkerGlobalScope
export const installFn: ServiceWorkerPreset['install'] = ({
event,
routes,
cacheName,
}) => {
console.info('installing service worker...')
self.skipWaiting() // go straight to activate
event.waitUntil(addResourcesToCache(routes ?? [], cacheName))
}
// @ts-expect-error TODO fix types
const addResourcesToCache = async (resources, cacheName: string) => {
const cache = await caches.open(cacheName)
console.info('adding resources to cache...', resources)
try {
await cache.addAll(resources)
} catch (error) {
console.error(
'failed to add resources to cache; make sure requests exists and that there are no duplicates',
{
resources,
error,
}
)
}
}
export default installFn

View file

@ -1,7 +1,18 @@
import type { BuildOptions } from 'esbuild'
export type ServiceWorkerPreset = {
activate?: (options: { event: ExtendableEvent; cacheName: string }) => void
install?: (options: {
event: ExtendableEvent
routes: string[]
cacheName: string
}) => void
fetch?: (options: { event: FetchEvent; cacheName: string }) => void
}
export type Config = {
path: string
path?: string
presets?: ServiceWorkerPreset[]
assetCachePrefix?: string
assetCacheVersionID?: string
customRoutes?: string[]

View file

@ -5,7 +5,8 @@
"moduleResolution": "bundler" /* Specify how TypeScript looks up a file from a given module specifier. */,
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
"strict": true /* Enable all strict type-checking options. */
"strict": true /* Enable all strict type-checking options. */,
"lib": ["WebWorker", "ES2021.String"]
},
"exclude": ["./dist/**/*"]
}