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: add caching to service worker #92

Merged
merged 20 commits into from
Mar 14, 2024
Merged

feat: add caching to service worker #92

merged 20 commits into from
Mar 14, 2024

Conversation

2color
Copy link
Member

@2color 2color commented Mar 7, 2024

Description

This PR adds caching for responses using the Cache API.

Fixes #73

Notes & open questions

TODO

  • Double check how the caching and invalidation works for IPNS paths

Change checklist

  • I have performed a self-review of my own code
  • I have made corresponding changes to the documentation if necessary (this includes comments as well)
  • I have added tests that prove my fix is effective or that my feature works

@SgtPooki
Copy link
Member

SgtPooki commented Mar 8, 2024

Right now the cache key is based on the event.request. This seems reasonable but maybe it's not optimal?

We should probably use the verifiedFetchUrl you have in the other PR

@2color
Copy link
Member Author

2color commented Mar 11, 2024

Right now the cache key is based on the event.request. This seems reasonable but maybe it's not optimal?

We should probably use the verifiedFetchUrl you have in the other PR

The Cache API does not support URLs with the ipfs:// protocol scheme:
Screenshot 2024-03-11 at 1 45 39 pm

@2color 2color marked this pull request as ready for review March 11, 2024 13:15
src/sw.ts Outdated Show resolved Hide resolved
src/sw.ts Outdated Show resolved Hide resolved
src/sw.ts Outdated Show resolved Hide resolved
2color added 2 commits March 12, 2024 12:34
* origin/main:
  feat: allow loading config page from subdomain after sw registration (#96)
  chore!: dist files optimizations (sw asset name changes) (#97)
  Update src/components/CidRenderer.tsx
  chore: remove in-page rendering
@SgtPooki
Copy link
Member

The Cache API does not support URLs with the ipfs:// protocol scheme:

you're right.. I forgot service worker cache requires inputs to be a request object

Copy link
Member

@SgtPooki SgtPooki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks mostly good, we need to figure out ipfs/helia-verified-fetch#17 tho

src/sw.ts Outdated Show resolved Hide resolved
src/sw.ts Outdated Show resolved Hide resolved
src/sw.ts Outdated Show resolved Hide resolved
@2color
Copy link
Member Author

2color commented Mar 13, 2024

we need to figure out ipfs/helia-verified-fetch#17 tho

Yeah.

That can be done in parallel, i.e. no need to block this PR, since the Cache API takes precedence over the browser HTTP cache..

@2color
Copy link
Member Author

2color commented Mar 13, 2024

Pretty obvious but worth noting here that depending on whether the origin has ipns or ipfs, every resource handled by the service worker will either be mutable or immutable.

For example, when opening vitalik-eth.ipns.inbrowser.link all of the resources requested as a path on the origin are considered mutable.

On the other hand, all resources bafybeiawq7pbt4krnopfmcvymvp2uz4ohibd5p7ugskkybvdmwa2v7evpy.ipfs.inbrowser.link are immutable.

src/sw.ts Outdated
function setExpiresHeader (response: Response, ttlSeconds: number = 3600): void {
const expirationTime = new Date(Date.now() + ttlSeconds * 1000)

response.headers.set('Expires', expirationTime.toUTCString())
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be sensible to make this cache header explicitly SW specific (originally suggested in https://phyks.me/2019/01/manage-expiration-of-cached-assets-with-service-worker-caching.html)

Suggested change
response.headers.set('Expires', expirationTime.toUTCString())
response.headers.set('sw-cache-expires', expirationTime.toUTCString())

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that makes sense

Copy link
Member

@lidel lidel Mar 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Long term, if we can avoid inventing things specific to service worker, and reuse HTTP semantics, that is better for verified-fetch being useful in more contexts, and easier to test (if these are set upstream, places like https://github.com/ipfs/helia-http-gateway get these headers for free, and we also increase surface of things that can be tested there).

My initial thought here was that if we reuse semantically meaningful Expires and set it only if not set yet, in the future it can be set inside verified-fetch, and not only we don't need to invent special header here, but also improve the upstream library and the code here will be smaller.

But fine to go with custom header here for now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lidel Because the SW Cache API always takes precedence over browser HTTP cache and and verified-fetch will be returning the Cache-Control header, the Expires header would be: no-op.

Even if we were to use the Expires header, by respecting it when there's a Cache-Control header, we'd be breaking from HTTP semantics. This is why I think it makes sense to make it clear that this header is purely for SW purposes.

@SgtPooki
Copy link
Member

For example, when opening vitalik-eth.ipns.inbrowser.link all of the resources requested as a path on the origin are considered mutable.

Yea, it would be nice if we could map the ipns name to a CID and re-route requests to ipfs://cid so that they're cached.

Copy link
Member

@SgtPooki SgtPooki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leaving some comments.. im going to check the code out locally and play with it

src/sw.ts Show resolved Hide resolved
src/sw.ts Outdated
* 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')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const expiresHeader = response.headers.get('Expires')
const expiresHeader = response.headers.get('sw-cache-expires')

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved in #109

src/sw.ts Outdated
function setExpiresHeader (response: Response, ttlSeconds: number = 3600): void {
const expirationTime = new Date(Date.now() + ttlSeconds * 1000)

response.headers.set('Expires', expirationTime.toUTCString())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that makes sense

src/sw.ts Outdated Show resolved Hide resolved
src/sw.ts Show resolved Hide resolved
src/sw.ts Outdated
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved in #109

src/sw.ts Show resolved Hide resolved
src/sw.ts Outdated
}

log('helia-ws: storing cache key %s in cache', cacheKey)
void cache.put(cacheKey, respToCache)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
void cache.put(cacheKey, respToCache)
await cache.put(cacheKey, respToCache)

control flow should be done in higher level fetchHandler or getResponseFromCacheOrFetch

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved in #109

src/sw.ts Outdated
Comment on lines 229 to 230
// If there is an entry in the cache for event.request,
// then response will be defined and we can just return it.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spacing here seems off

Suggested change
// 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 response will be defined and we can just return it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved in #109

src/sw.ts Outdated
Comment on lines 235 to 251
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
Copy link
Member

@SgtPooki SgtPooki Mar 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we could combine these so we don't have two potential places where cache storing happens and could fail

Copy link
Member

@SgtPooki SgtPooki Mar 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking at the code more locally, we don't need to fetch from the network at all on cache-hit. Since we're currently fighting rate limits from the trustless gateway, we could just not do network requests. The likelihood of something updating on IPFS is very low, whereas stale-while-revalidate is good for frequently updated content that you want a quick response for.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved in #109

src/sw.ts Outdated
Comment on lines 235 to 239
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)
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to call fetchHandler and update the cache if we already have a cached response?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved in #109. stale-while-revalidate only for ipns cache HIT

src/sw.ts Outdated
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
err('helia-ws: failed updating response in cache for %s in the background', cacheKey, err)
error('helia-ws: failed updating response in cache for %s in the background', cacheKey, err)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved in #109

@SgtPooki SgtPooki merged commit 4674dd0 into main Mar 14, 2024
21 checks passed
@SgtPooki SgtPooki deleted the add-cache branch March 14, 2024 20:39
@lidel lidel mentioned this pull request Mar 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Cache final Responses
3 participants