From 6756630b8cff7c771784ec1a4350575bfc2520a8 Mon Sep 17 00:00:00 2001 From: Daniel N <2color@users.noreply.github.com> Date: Mon, 11 Mar 2024 13:18:13 +0100 Subject: [PATCH 01/16] feat: naive cache implmentation --- src/sw.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/sw.ts b/src/sw.ts index a997bd7f..6965cb2d 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -37,6 +37,7 @@ interface GetVerifiedFetchUrlOptions { declare let self: ServiceWorkerGlobalScope let verifiedFetch: VerifiedFetch const channel = new HeliaServiceWorkerCommsChannel('SW') +const IPFS_CACHE = 'IPFS_CACHE' const urlInterceptRegex = [new RegExp(`${self.location.origin}/ip(n|f)s/`)] const updateVerifiedFetch = async (): Promise => { verifiedFetch = await getVerifiedFetch() @@ -94,7 +95,27 @@ self.addEventListener('fetch', (event) => { // intercept and do our own stuff... event.respondWith(fetchHandler({ path: url.pathname, request })) } else if (isSubdomainRequest(event)) { - event.respondWith(fetchHandler({ path: url.pathname, request })) + // const { id, protocol } = getSubdomainParts(request.url) + // const cacheKey = `${protocol}://${id}${url.pathname}` + // log(`cache key:`, cacheKey) + event.respondWith( + caches.open(IPFS_CACHE).then((cache) => { + return cache.match(event.request).then(async (response) => { + if (response) { + // If there is an entry in the cache for event.request, + // then response will be defined and we can just return it. + // Note that in this example, only font resources are cached. + log('helia-ws: found response in cache:', response) + return response + } + // const verifiedFetchUrl = getVerifiedFetchUrl({ id, protocol, path }) + response = await fetchHandler({ path: url.pathname, request }) + + cache.put(event.request, response.clone()) + return response + }) + }), + ) } }) From 3cc5b9882dbbf347a8c5fd51df311c8705dc6fc8 Mon Sep 17 00:00:00 2001 From: Daniel N <2color@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:12:49 +0100 Subject: [PATCH 02/16] fix: only cache successful responses --- src/sw.ts | 47 ++++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/src/sw.ts b/src/sw.ts index 6965cb2d..f53fc527 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -95,27 +95,32 @@ self.addEventListener('fetch', (event) => { // intercept and do our own stuff... event.respondWith(fetchHandler({ path: url.pathname, request })) } else if (isSubdomainRequest(event)) { - // const { id, protocol } = getSubdomainParts(request.url) - // const cacheKey = `${protocol}://${id}${url.pathname}` - // log(`cache key:`, cacheKey) - event.respondWith( - caches.open(IPFS_CACHE).then((cache) => { - return cache.match(event.request).then(async (response) => { - if (response) { - // If there is an entry in the cache for event.request, - // then response will be defined and we can just return it. - // Note that in this example, only font resources are cached. - log('helia-ws: found response in cache:', response) - return response - } - // const verifiedFetchUrl = getVerifiedFetchUrl({ id, protocol, path }) - response = await fetchHandler({ path: url.pathname, request }) - - cache.put(event.request, response.clone()) - return response - }) - }), - ) + const cacheKey = event.request.url + log(`helia-sw: cache key:`, cacheKey.toString()) + + event.respondWith((async () => { + const cache = await caches.open(IPFS_CACHE) + const cachedResponse = await cache.match(cacheKey) + + if (cachedResponse) { + // If there is an entry in the cache for event.request, + // then response will be defined and we can just return it. + // Note that in this example, only font resources are cached. + log('helia-ws: cached response for %s found in cache: %o', cacheKey, cachedResponse) + return cachedResponse + } + + // 👇 fetch because no cached response was found + const response = await fetchHandler({ path: url.pathname, request }) + + if(response.ok) { + // 👇 only cache successful responses + log('helia-ws: storing cache key %s in cache', cacheKey) + // Clone the response since streams can only be consumed once. + cache.put(cacheKey, response.clone()) + } + return response + })()) } }) From 34a396b6dcf55bc95d3129f3b22de8a9655a1461 Mon Sep 17 00:00:00 2001 From: Daniel N <2color@users.noreply.github.com> Date: Tue, 12 Mar 2024 17:14:34 +0100 Subject: [PATCH 03/16] feat: improve cache mechanism --- src/sw.ts | 68 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/src/sw.ts b/src/sw.ts index a2283ca6..21956fc1 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -3,7 +3,7 @@ import { createVerifiedFetch, type VerifiedFetch } from '@helia/verified-fetch' import { HeliaServiceWorkerCommsChannel, type ChannelMessage } from './lib/channel.ts' import { getConfig } from './lib/config-db.ts' import { contentTypeParser } from './lib/content-type-parser.ts' -import { getSubdomainParts } from './lib/get-subdomain-parts.ts' +import { getSubdomainParts, type UrlParts } from './lib/get-subdomain-parts.ts' import { isConfigPage } from './lib/is-config-page.ts' import { error, log, trace } from './lib/logger.ts' import { findOriginIsolationRedirect } from './lib/path-or-subdomain.ts' @@ -40,7 +40,8 @@ interface GetVerifiedFetchUrlOptions { declare let self: ServiceWorkerGlobalScope let verifiedFetch: VerifiedFetch const channel = new HeliaServiceWorkerCommsChannel('SW') -const IPFS_CACHE = 'IPFS_CACHE' +const IMMUTABLE_CACHE = 'IMMUTABLE_CACHE' +const MUTABLE_CACHE = 'MUTABLE_CACHE' const urlInterceptRegex = [new RegExp(`${self.location.origin}/ip(n|f)s/`)] const updateVerifiedFetch = async (): Promise => { verifiedFetch = await getVerifiedFetch() @@ -86,6 +87,8 @@ self.addEventListener('fetch', (event) => { const request = event.request const urlString = request.url const url = new URL(urlString) + const { protocol, id } = getSubdomainParts(event.request.url) + log('helia-sw: incoming request url: %s:', event.request.url, protocol, id) if (isConfigPageRequest(url) || isSwAssetRequest(event)) { // get the assets from the server @@ -102,30 +105,44 @@ self.addEventListener('fetch', (event) => { // intercept and do our own stuff... event.respondWith(fetchHandler({ path: url.pathname, request })) } else if (isSubdomainRequest(event)) { - const cacheKey = event.request.url - log(`helia-sw: cache key:`, cacheKey.toString()) + const isMutable = protocol === 'ipns' + const cacheKey = `${event.request.url}-${event.request.headers.get('Accept') ?? ''}` + const cacheName = isMutable ? MUTABLE_CACHE : IMMUTABLE_CACHE + log('helia-sw: cache name: %s | key: %s', cacheName, cacheKey) event.respondWith((async () => { - const cache = await caches.open(IPFS_CACHE) + const cache = await caches.open(cacheName) const cachedResponse = await cache.match(cacheKey) - if (cachedResponse) { + if ((cachedResponse != null) && !hasExpired(cachedResponse)) { // If there is an entry in the cache for event.request, // then response will be defined and we can just return it. - // Note that in this example, only font resources are cached. - log('helia-ws: cached response for %s found in cache: %o', cacheKey, cachedResponse) + log('helia-ws: cached response HIT for %s (expires: %s) %o', cacheKey, cachedResponse.headers.get('Expires'), cachedResponse) + + trace('helia-ws: updating cache for %s in the background', cacheKey) + // 👇 update cache in the background wihtout awaiting + fetchHandler({ path: url.pathname, request }) + return cachedResponse } // 👇 fetch because no cached response was found const response = await fetchHandler({ path: url.pathname, request }) - if(response.ok) { + if (response.ok) { // 👇 only cache successful responses log('helia-ws: storing cache key %s in cache', cacheKey) // Clone the response since streams can only be consumed once. - cache.put(cacheKey, response.clone()) + const respToCache = response.clone() + + if (isMutable) { + // 👇 Set expires header to an hour from now for mutable (ipns://) resources + setExpiryHeader(respToCache, 3600) + } + + cache.put(cacheKey, respToCache) } + return response })()) } @@ -203,6 +220,37 @@ function isSwAssetRequest (event: FetchEvent): boolean { return isActualSwAsset } +/** + * Set the expires header with a timestamp base + */ +function setExpiryHeader (response: Response, ttlSeconds: number = 3600): Response { + const expirationTime = new Date(Date.now() + ttlSeconds * 1000) + + response.headers.set('Expires', expirationTime.toUTCString()) + return response +} + +/** + * Checks whether a cached response object has expired by looking at the expires header + * Note that this ignores the Cache-Control header since the expires header is set by us + */ +function hasExpired (response: Response): boolean { + const expiresHeader = response.headers.get('Expires') + + if (!expiresHeader) { + return false + } + + const expires = new Date(expiresHeader) + const now = new Date() + + if (expires < now) { + return true + } + + return false +} + async function fetchHandler ({ path, request }: FetchHandlerArg): Promise { /** * > Any global variables you set will be lost if the service worker shuts down. From 9ff73146917f5ae0cf34d7711ccec953412cca1f Mon Sep 17 00:00:00 2001 From: Daniel N <2color@users.noreply.github.com> Date: Wed, 13 Mar 2024 12:50:29 +0100 Subject: [PATCH 04/16] fix: linting errors --- src/sw.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/sw.ts b/src/sw.ts index 21956fc1..e82c19d1 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -3,7 +3,7 @@ import { createVerifiedFetch, type VerifiedFetch } from '@helia/verified-fetch' import { HeliaServiceWorkerCommsChannel, type ChannelMessage } from './lib/channel.ts' import { getConfig } from './lib/config-db.ts' import { contentTypeParser } from './lib/content-type-parser.ts' -import { getSubdomainParts, type UrlParts } from './lib/get-subdomain-parts.ts' +import { getSubdomainParts } from './lib/get-subdomain-parts.ts' import { isConfigPage } from './lib/is-config-page.ts' import { error, log, trace } from './lib/logger.ts' import { findOriginIsolationRedirect } from './lib/path-or-subdomain.ts' @@ -120,8 +120,8 @@ self.addEventListener('fetch', (event) => { log('helia-ws: cached response HIT for %s (expires: %s) %o', cacheKey, cachedResponse.headers.get('Expires'), cachedResponse) trace('helia-ws: updating cache for %s in the background', cacheKey) - // 👇 update cache in the background wihtout awaiting - fetchHandler({ path: url.pathname, request }) + // 👇 update cache in the background (don't await) + void fetchHandler({ path: url.pathname, request }) return cachedResponse } @@ -131,16 +131,18 @@ self.addEventListener('fetch', (event) => { if (response.ok) { // 👇 only cache successful responses - log('helia-ws: storing cache key %s in cache', cacheKey) // Clone the response since streams can only be consumed once. const respToCache = response.clone() if (isMutable) { + trace('helia-ws: setting expires header on response key %s before storing in cache', cacheKey) + // 👇 Set expires header to an hour from now for mutable (ipns://) resources - setExpiryHeader(respToCache, 3600) + setExpiresHeader(respToCache, 3600) } - cache.put(cacheKey, respToCache) + log('helia-ws: storing cache key %s in cache', cacheKey) + void cache.put(cacheKey, respToCache) } return response @@ -221,13 +223,12 @@ function isSwAssetRequest (event: FetchEvent): boolean { } /** - * Set the expires header with a timestamp base + * Set the expires header on a response object to a timestamp based on the passed ttl interval */ -function setExpiryHeader (response: Response, ttlSeconds: number = 3600): Response { +function setExpiresHeader (response: Response, ttlSeconds: number = 3600): void { const expirationTime = new Date(Date.now() + ttlSeconds * 1000) response.headers.set('Expires', expirationTime.toUTCString()) - return response } /** @@ -237,7 +238,7 @@ function setExpiryHeader (response: Response, ttlSeconds: number = 3600): Respon function hasExpired (response: Response): boolean { const expiresHeader = response.headers.get('Expires') - if (!expiresHeader) { + if (expiresHeader == null) { return false } From f56d25ec6f15a79ad3c2ddb0354a5b762c2b99bf Mon Sep 17 00:00:00 2001 From: Daniel N <2color@users.noreply.github.com> Date: Wed, 13 Mar 2024 13:41:14 +0100 Subject: [PATCH 05/16] refactor: move caching logic to separate functions also make sure that we actually store reponse in cache when fetching it in the background --- src/sw.ts | 110 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 65 insertions(+), 45 deletions(-) diff --git a/src/sw.ts b/src/sw.ts index e82c19d1..945acb81 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -32,6 +32,13 @@ interface GetVerifiedFetchUrlOptions { path: string } +interface StoreReponseInCacheOptions { + response: Response + cacheKey: string + isMutable: boolean + cache: Cache +} + /** ****************************************************** * "globals" @@ -87,8 +94,7 @@ self.addEventListener('fetch', (event) => { const request = event.request const urlString = request.url const url = new URL(urlString) - const { protocol, id } = getSubdomainParts(event.request.url) - log('helia-sw: incoming request url: %s:', event.request.url, protocol, id) + log('helia-sw: incoming request url: %s:', event.request.url) if (isConfigPageRequest(url) || isSwAssetRequest(event)) { // get the assets from the server @@ -102,51 +108,9 @@ self.addEventListener('fetch', (event) => { } if (isRootRequestForContent(event)) { - // intercept and do our own stuff... event.respondWith(fetchHandler({ path: url.pathname, request })) } else if (isSubdomainRequest(event)) { - const isMutable = protocol === 'ipns' - const cacheKey = `${event.request.url}-${event.request.headers.get('Accept') ?? ''}` - const cacheName = isMutable ? MUTABLE_CACHE : IMMUTABLE_CACHE - log('helia-sw: cache name: %s | key: %s', cacheName, cacheKey) - - event.respondWith((async () => { - const cache = await caches.open(cacheName) - const cachedResponse = await cache.match(cacheKey) - - if ((cachedResponse != null) && !hasExpired(cachedResponse)) { - // If there is an entry in the cache for event.request, - // then response will be defined and we can just return it. - log('helia-ws: cached response HIT for %s (expires: %s) %o', cacheKey, cachedResponse.headers.get('Expires'), cachedResponse) - - trace('helia-ws: updating cache for %s in the background', cacheKey) - // 👇 update cache in the background (don't await) - void fetchHandler({ path: url.pathname, request }) - - return cachedResponse - } - - // 👇 fetch because no cached response was found - const response = await fetchHandler({ path: url.pathname, request }) - - if (response.ok) { - // 👇 only cache successful responses - // Clone the response since streams can only be consumed once. - const respToCache = response.clone() - - if (isMutable) { - trace('helia-ws: setting expires header on response key %s before storing in cache', cacheKey) - - // 👇 Set expires header to an hour from now for mutable (ipns://) resources - setExpiresHeader(respToCache, 3600) - } - - log('helia-ws: storing cache key %s in cache', cacheKey) - void cache.put(cacheKey, respToCache) - } - - return response - })()) + event.respondWith(getResponseFromCacheOrFetch(event)) } }) @@ -252,6 +216,62 @@ function hasExpired (response: Response): boolean { return false } +async function getResponseFromCacheOrFetch (event: FetchEvent): Promise { + const { protocol } = getSubdomainParts(event.request.url) + const url = new URL(event.request.url) + const isMutable = protocol === 'ipns' + const cacheKey = `${event.request.url}-${event.request.headers.get('Accept') ?? ''}` + log('helia-sw: cache key: %s', cacheKey) + const cache = await caches.open(isMutable ? MUTABLE_CACHE : IMMUTABLE_CACHE) + const cachedResponse = await cache.match(cacheKey) + + if ((cachedResponse != null) && !hasExpired(cachedResponse)) { + // If there is an entry in the cache for event.request, + // then response will be defined and we can just return it. + log('helia-ws: cached response HIT for %s (expires: %s) %o', cacheKey, cachedResponse.headers.get('Expires'), cachedResponse) + + trace('helia-ws: updating cache for %s in the background', cacheKey) + // 👇 update cache in the background (don't await) + fetchHandler({ path: url.pathname, request: event.request }) + .then(async response => storeReponseInCache({ response, isMutable, cache, cacheKey })) + .catch(err => { + err('helia-ws: failed updating response in cache for %s in the background', cacheKey, err) + }) + + return cachedResponse + } + + // 👇 fetch because no cached response was found + const response = await fetchHandler({ path: url.pathname, request: event.request }) + + void storeReponseInCache({ response, isMutable, cache, cacheKey }).catch(err => { + err('helia-ws: failed storing response in cache for %s', cacheKey, err) + }) + + return response +} + +async function storeReponseInCache ({ response, isMutable, cache, cacheKey }: StoreReponseInCacheOptions): Promise { + // 👇 only cache successful responses + if (!response.ok) { + return + } + + // Clone the response since streams can only be consumed once. + const respToCache = response.clone() + + if (isMutable) { + trace('helia-ws: setting expires header on response key %s before storing in cache', cacheKey) + // 👇 Set expires header to an hour from now for mutable (ipns://) resources + // Note that this technically breaks HTTP semantics, whereby the cache-control max-age takes precendence + // Seting this header is only used by the service worker asd a mechanism similar to stale-while-revalidate + setExpiresHeader(respToCache, 3600) + } + + log('helia-ws: storing cache key %s in cache', cacheKey) + void cache.put(cacheKey, respToCache) +} + async function fetchHandler ({ path, request }: FetchHandlerArg): Promise { /** * > Any global variables you set will be lost if the service worker shuts down. From 0095181a9f3246cf8535c54c2e6f211a8de8fb94 Mon Sep 17 00:00:00 2001 From: Daniel N <2color@users.noreply.github.com> Date: Wed, 13 Mar 2024 13:42:07 +0100 Subject: [PATCH 06/16] fix: small optimisation --- src/sw.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/sw.ts b/src/sw.ts index 945acb81..e39929ac 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -273,12 +273,6 @@ async function storeReponseInCache ({ response, isMutable, cache, cacheKey }: St } async function fetchHandler ({ path, request }: FetchHandlerArg): Promise { - /** - * > Any global variables you set will be lost if the service worker shuts down. - * - * @see https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/lifecycle - */ - verifiedFetch = verifiedFetch ?? await getVerifiedFetch() // test and enforce origin isolation before anything else is executed const originLocation = await findOriginIsolationRedirect(new URL(request.url)) if (originLocation !== null) { @@ -292,6 +286,13 @@ async function fetchHandler ({ path, request }: FetchHandlerArg): Promise Any global variables you set will be lost if the service worker shuts down. + * + * @see https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/lifecycle + */ + verifiedFetch = verifiedFetch ?? await getVerifiedFetch() + /** * Note that there are existing bugs regarding service worker signal handling: * * https://bugs.chromium.org/p/chromium/issues/detail?id=823697 From 555fbcccb7c17a3321d7a7122f000df5625938ce Mon Sep 17 00:00:00 2001 From: Daniel N <2color@users.noreply.github.com> Date: Wed, 13 Mar 2024 14:33:09 +0100 Subject: [PATCH 07/16] chore: change log to trace --- src/sw.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sw.ts b/src/sw.ts index e39929ac..967397c1 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -221,7 +221,7 @@ async function getResponseFromCacheOrFetch (event: FetchEvent): Promise Date: Wed, 13 Mar 2024 16:46:48 -0700 Subject: [PATCH 08/16] chore: PR comments and minor improvements --- src/sw.ts | 50 ++++++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/src/sw.ts b/src/sw.ts index 967397c1..c466b208 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -36,7 +36,6 @@ interface StoreReponseInCacheOptions { response: Response cacheKey: string isMutable: boolean - cache: Cache } /** @@ -195,6 +194,10 @@ function setExpiresHeader (response: Response, ttlSeconds: number = 3600): void response.headers.set('Expires', expirationTime.toUTCString()) } +function isValidCacheResponse (cachedResponse?: Response): cachedResponse is Response { + return cachedResponse != null && !hasExpired(cachedResponse) +} + /** * Checks whether a cached response object has expired by looking at the expires header * Note that this ignores the Cache-Control header since the expires header is set by us @@ -216,46 +219,49 @@ function hasExpired (response: Response): boolean { return false } +function getCacheKey (event: FetchEvent): string { + return `${event.request.url}-${event.request.headers.get('Accept') ?? ''}` +} + async function getResponseFromCacheOrFetch (event: FetchEvent): Promise { const { protocol } = getSubdomainParts(event.request.url) const url = new URL(event.request.url) const isMutable = protocol === 'ipns' - const cacheKey = `${event.request.url}-${event.request.headers.get('Accept') ?? ''}` + const cacheKey = getCacheKey(event) trace('helia-sw: cache key: %s', cacheKey) const cache = await caches.open(isMutable ? MUTABLE_CACHE : IMMUTABLE_CACHE) const cachedResponse = await cache.match(cacheKey) - if ((cachedResponse != null) && !hasExpired(cachedResponse)) { - // If there is an entry in the cache for event.request, - // then response will be defined and we can just return it. + /** + * If there is an entry in the cache for event.request, then cachedResponse + * will be defined and we will return it. There is no need to + * update the cache entry in the background. + */ + if (isValidCacheResponse(cachedResponse)) { log('helia-ws: cached response HIT for %s (expires: %s) %o', cacheKey, cachedResponse.headers.get('Expires'), cachedResponse) - - trace('helia-ws: updating cache for %s in the background', cacheKey) - // 👇 update cache in the background (don't await) - fetchHandler({ path: url.pathname, request: event.request }) - .then(async response => storeReponseInCache({ response, isMutable, cache, cacheKey })) - .catch(err => { - err('helia-ws: failed updating response in cache for %s in the background', cacheKey, err) - }) - return cachedResponse } - // 👇 fetch because no cached response was found - const response = await fetchHandler({ path: url.pathname, request: event.request }) + // 👇 fetch always + const response = fetchHandler({ path: url.pathname, request: event.request }) - void storeReponseInCache({ response, isMutable, cache, cacheKey }).catch(err => { - err('helia-ws: failed storing response in cache for %s', cacheKey, err) - }) + void response + .then(async response => storeReponseInCache({ response, isMutable, cacheKey })) + .catch(err => { + error('helia-ws: failed updating response in cache for %s in the background', cacheKey, err) + }) return response } -async function storeReponseInCache ({ response, isMutable, cache, cacheKey }: StoreReponseInCacheOptions): Promise { +async function storeReponseInCache ({ response, isMutable, cacheKey }: StoreReponseInCacheOptions): Promise { // 👇 only cache successful responses if (!response.ok) { return } + trace('helia-ws: updating cache for %s in the background', cacheKey) + + const cache = await caches.open(isMutable ? MUTABLE_CACHE : IMMUTABLE_CACHE) // Clone the response since streams can only be consumed once. const respToCache = response.clone() @@ -264,12 +270,12 @@ async function storeReponseInCache ({ response, isMutable, cache, cacheKey }: St trace('helia-ws: setting expires header on response key %s before storing in cache', cacheKey) // 👇 Set expires header to an hour from now for mutable (ipns://) resources // Note that this technically breaks HTTP semantics, whereby the cache-control max-age takes precendence - // Seting this header is only used by the service worker asd a mechanism similar to stale-while-revalidate + // Setting this header is only used by the service worker using a mechanism similar to stale-while-revalidate setExpiresHeader(respToCache, 3600) } log('helia-ws: storing cache key %s in cache', cacheKey) - void cache.put(cacheKey, respToCache) + await cache.put(cacheKey, respToCache) } async function fetchHandler ({ path, request }: FetchHandlerArg): Promise { From 69f4d4c145418150573458885402ee60747d5b17 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Wed, 13 Mar 2024 17:13:00 -0700 Subject: [PATCH 09/16] fix: use sw-cache-expires --- src/sw.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sw.ts b/src/sw.ts index c466b208..f2625bae 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -191,7 +191,7 @@ function isSwAssetRequest (event: FetchEvent): boolean { function setExpiresHeader (response: Response, ttlSeconds: number = 3600): void { const expirationTime = new Date(Date.now() + ttlSeconds * 1000) - response.headers.set('Expires', expirationTime.toUTCString()) + response.headers.set('sw-cache-expires', expirationTime.toUTCString()) } function isValidCacheResponse (cachedResponse?: Response): cachedResponse is Response { @@ -203,7 +203,7 @@ function isValidCacheResponse (cachedResponse?: Response): cachedResponse is Res * Note that this ignores the Cache-Control header since the expires header is set by us */ function hasExpired (response: Response): boolean { - const expiresHeader = response.headers.get('Expires') + const expiresHeader = response.headers.get('sw-cache-expires') if (expiresHeader == null) { return false @@ -238,7 +238,7 @@ async function getResponseFromCacheOrFetch (event: FetchEvent): Promise Date: Wed, 13 Mar 2024 17:13:24 -0700 Subject: [PATCH 10/16] chore: cleanup hasExpired return --- src/sw.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/sw.ts b/src/sw.ts index f2625bae..b5862efc 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -212,11 +212,7 @@ function hasExpired (response: Response): boolean { const expires = new Date(expiresHeader) const now = new Date() - if (expires < now) { - return true - } - - return false + return expires < now } function getCacheKey (event: FetchEvent): string { From a0c8dcb399ac25cd87b2ef202c6e42bc35abeb27 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Wed, 13 Mar 2024 17:13:34 -0700 Subject: [PATCH 11/16] chore: log on cache miss --- src/sw.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sw.ts b/src/sw.ts index b5862efc..088d2dee 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -236,6 +236,8 @@ async function getResponseFromCacheOrFetch (event: FetchEvent): Promise Date: Wed, 13 Mar 2024 17:14:09 -0700 Subject: [PATCH 12/16] chore: wait for response so we can cache it --- src/sw.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sw.ts b/src/sw.ts index 088d2dee..7ee9d271 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -243,7 +243,7 @@ async function getResponseFromCacheOrFetch (event: FetchEvent): Promise storeReponseInCache({ response, isMutable, cacheKey })) .catch(err => { error('helia-ws: failed updating response in cache for %s in the background', cacheKey, err) From 0bc73ed5cef346bcce6197bcd8172bd6379a4393 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Wed, 13 Mar 2024 17:22:09 -0700 Subject: [PATCH 13/16] chore: minor cleanup --- src/sw.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/sw.ts b/src/sw.ts index 7ee9d271..36152fd3 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -236,11 +236,9 @@ async function getResponseFromCacheOrFetch (event: FetchEvent): Promise Date: Wed, 13 Mar 2024 17:32:36 -0700 Subject: [PATCH 14/16] chore: minor cleanup --- src/sw.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/sw.ts b/src/sw.ts index 36152fd3..d8f7b50e 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -194,10 +194,6 @@ function setExpiresHeader (response: Response, ttlSeconds: number = 3600): void response.headers.set('sw-cache-expires', expirationTime.toUTCString()) } -function isValidCacheResponse (cachedResponse?: Response): cachedResponse is Response { - return cachedResponse != null && !hasExpired(cachedResponse) -} - /** * Checks whether a cached response object has expired by looking at the expires header * Note that this ignores the Cache-Control header since the expires header is set by us @@ -227,18 +223,19 @@ async function getResponseFromCacheOrFetch (event: FetchEvent): Promise Date: Wed, 13 Mar 2024 17:44:40 -0700 Subject: [PATCH 15/16] fix: stale-while-revalidate for ipns --- src/sw.ts | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/sw.ts b/src/sw.ts index d8f7b50e..b0eb6c29 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -215,6 +215,17 @@ function getCacheKey (event: FetchEvent): string { return `${event.request.url}-${event.request.headers.get('Accept') ?? ''}` } +async function fetchAndUpdateCache (event: FetchEvent, url: URL, cacheKey: string): Promise { + const response = await fetchHandler({ path: url.pathname, request: event.request }) + try { + await storeReponseInCache({ response, isMutable: true, cacheKey }) + trace('helia-ws: updated cache for %s', cacheKey) + } catch (err) { + error('helia-ws: failed updating response in cache for %s', cacheKey, err) + } + return response +} + async function getResponseFromCacheOrFetch (event: FetchEvent): Promise { const { protocol } = getSubdomainParts(event.request.url) const url = new URL(event.request.url) @@ -223,28 +234,22 @@ async function getResponseFromCacheOrFetch (event: FetchEvent): Promise storeReponseInCache({ response, isMutable, cacheKey })) - .catch(err => { - error('helia-ws: failed updating response in cache for %s in the background', cacheKey, err) - }) + log('helia-ws: cached response MISS for %s', cacheKey) - return response + return fetchAndUpdateCache(event, url, cacheKey) } async function storeReponseInCache ({ response, isMutable, cacheKey }: StoreReponseInCacheOptions): Promise { @@ -267,7 +272,7 @@ async function storeReponseInCache ({ response, isMutable, cacheKey }: StoreRepo setExpiresHeader(respToCache, 3600) } - log('helia-ws: storing cache key %s in cache', cacheKey) + log('helia-ws: storing response for key %s in cache', cacheKey) await cache.put(cacheKey, respToCache) } From e8a41ea752f7b5613183a3beae917f7d1ada3723 Mon Sep 17 00:00:00 2001 From: Daniel N <2color@users.noreply.github.com> Date: Thu, 14 Mar 2024 13:12:59 +0100 Subject: [PATCH 16/16] chore: move magic number to constant --- src/sw.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/sw.ts b/src/sw.ts index b0eb6c29..1d4722bc 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -48,6 +48,7 @@ let verifiedFetch: VerifiedFetch const channel = new HeliaServiceWorkerCommsChannel('SW') const IMMUTABLE_CACHE = 'IMMUTABLE_CACHE' const MUTABLE_CACHE = 'MUTABLE_CACHE' +const ONE_HOUR_IN_SECONDS = 3600 const urlInterceptRegex = [new RegExp(`${self.location.origin}/ip(n|f)s/`)] const updateVerifiedFetch = async (): Promise => { verifiedFetch = await getVerifiedFetch() @@ -187,8 +188,9 @@ function isSwAssetRequest (event: FetchEvent): boolean { /** * Set the expires header on a response object to a timestamp based on the passed ttl interval + * Defaults to */ -function setExpiresHeader (response: Response, ttlSeconds: number = 3600): void { +function setExpiresHeader (response: Response, ttlSeconds: number = ONE_HOUR_IN_SECONDS): void { const expirationTime = new Date(Date.now() + ttlSeconds * 1000) response.headers.set('sw-cache-expires', expirationTime.toUTCString()) @@ -269,7 +271,7 @@ async function storeReponseInCache ({ response, isMutable, cacheKey }: StoreRepo // 👇 Set expires header to an hour from now for mutable (ipns://) resources // Note that this technically breaks HTTP semantics, whereby the cache-control max-age takes precendence // Setting this header is only used by the service worker using a mechanism similar to stale-while-revalidate - setExpiresHeader(respToCache, 3600) + setExpiresHeader(respToCache, ONE_HOUR_IN_SECONDS) } log('helia-ws: storing response for key %s in cache', cacheKey)