diff --git a/demo/tsconfig.json b/demo/tsconfig.json new file mode 100644 index 0000000..8bf91d3 --- /dev/null +++ b/demo/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "astro/tsconfigs/strict", + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist"] +} diff --git a/package/package.json b/package/package.json index c865b5c..0693ded 100644 --- a/package/package.json +++ b/package/package.json @@ -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", diff --git a/package/src/globals.ts b/package/src/eslint/globals.ts similarity index 100% rename from package/src/globals.ts rename to package/src/eslint/globals.ts diff --git a/package/src/astro-sw.ts b/package/src/index.ts similarity index 99% rename from package/src/astro-sw.ts rename to package/src/index.ts index a6266b4..1023954 100644 --- a/package/src/astro-sw.ts +++ b/package/src/index.ts @@ -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} */ diff --git a/package/src/presets/delete-old-caches/activate.ts b/package/src/presets/delete-old-caches/activate.ts new file mode 100644 index 0000000..91780d9 --- /dev/null +++ b/package/src/presets/delete-old-caches/activate.ts @@ -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 diff --git a/package/src/presets/delete-old-caches/index.ts b/package/src/presets/delete-old-caches/index.ts new file mode 100644 index 0000000..a2fe7fb --- /dev/null +++ b/package/src/presets/delete-old-caches/index.ts @@ -0,0 +1,8 @@ +import { ServiceWorkerPreset } from '../../types' +import activate from './activate' + +const deleteOldCaches: ServiceWorkerPreset = { + activate, +} + +export default deleteOldCaches diff --git a/package/src/presets/stale-while-revalidate/activate.ts b/package/src/presets/stale-while-revalidate/activate.ts new file mode 100644 index 0000000..ccf7033 --- /dev/null +++ b/package/src/presets/stale-while-revalidate/activate.ts @@ -0,0 +1,7 @@ +import { ServiceWorkerPreset } from '../../types' + +export const activateFn: ServiceWorkerPreset['activate'] = ({ event }) => { + console.info('activating service worker...', event) +} + +export default activateFn diff --git a/package/src/presets/stale-while-revalidate/fetch.ts b/package/src/presets/stale-while-revalidate/fetch.ts new file mode 100644 index 0000000..b60881d --- /dev/null +++ b/package/src/presets/stale-while-revalidate/fetch.ts @@ -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' }, + }) + } +} diff --git a/package/src/presets/stale-while-revalidate/index.ts b/package/src/presets/stale-while-revalidate/index.ts new file mode 100644 index 0000000..388889c --- /dev/null +++ b/package/src/presets/stale-while-revalidate/index.ts @@ -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 diff --git a/package/src/presets/stale-while-revalidate/install.ts b/package/src/presets/stale-while-revalidate/install.ts new file mode 100644 index 0000000..1211080 --- /dev/null +++ b/package/src/presets/stale-while-revalidate/install.ts @@ -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 diff --git a/package/src/types.ts b/package/src/types.ts index 0892903..6dc2144 100644 --- a/package/src/types.ts +++ b/package/src/types.ts @@ -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[] diff --git a/tsconfig.json b/tsconfig.json index 1b61b1b..8727ff7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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/**/*"] }