Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: some updates to #92 #109

Merged
merged 8 commits into from
Mar 14, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 34 additions & 30 deletions src/sw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ interface StoreReponseInCacheOptions {
response: Response
cacheKey: string
isMutable: boolean
cache: Cache
}

/**
Expand Down Expand Up @@ -192,15 +191,15 @@ 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())
}

/**
* 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')
const expiresHeader = response.headers.get('sw-cache-expires')

if (expiresHeader == null) {
return false
Expand All @@ -209,53 +208,58 @@ function hasExpired (response: Response): boolean {
const expires = new Date(expiresHeader)
const now = new Date()

if (expires < now) {
return true
}
return expires < now
}

function getCacheKey (event: FetchEvent): string {
return `${event.request.url}-${event.request.headers.get('Accept') ?? ''}`
}

return false
async function fetchAndUpdateCache (event: FetchEvent, url: URL, cacheKey: string): Promise<Response> {
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<Response> {
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)
const validCacheHit = cachedResponse != null && !hasExpired(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.
log('helia-ws: cached response HIT for %s (expires: %s) %o', cacheKey, cachedResponse.headers.get('Expires'), cachedResponse)
if (validCacheHit) {
log('helia-ws: cached response HIT for %s (expires: %s) %o', cacheKey, cachedResponse.headers.get('sw-cache-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)
})
if (isMutable) {
// If the response is mutable, update the cache in the background.
void fetchAndUpdateCache(event, url, cacheKey)
}

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)
})
log('helia-ws: cached response MISS for %s', cacheKey)

return response
return fetchAndUpdateCache(event, url, cacheKey)
}

async function storeReponseInCache ({ response, isMutable, cache, cacheKey }: StoreReponseInCacheOptions): Promise<void> {
async function storeReponseInCache ({ response, isMutable, cacheKey }: StoreReponseInCacheOptions): Promise<void> {
// 👇 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()
Expand All @@ -264,12 +268,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)
log('helia-ws: storing response for key %s in cache', cacheKey)
await cache.put(cacheKey, respToCache)
}

async function fetchHandler ({ path, request }: FetchHandlerArg): Promise<Response> {
Expand Down
Loading