@@ -32,6 +32,12 @@ interface GetVerifiedFetchUrlOptions {
32
32
path : string
33
33
}
34
34
35
+ interface StoreReponseInCacheOptions {
36
+ response : Response
37
+ cacheKey : string
38
+ isMutable : boolean
39
+ }
40
+
35
41
/**
36
42
******************************************************
37
43
* "globals"
@@ -40,6 +46,9 @@ interface GetVerifiedFetchUrlOptions {
40
46
declare let self : ServiceWorkerGlobalScope
41
47
let verifiedFetch : VerifiedFetch
42
48
const channel = new HeliaServiceWorkerCommsChannel ( 'SW' )
49
+ const IMMUTABLE_CACHE = 'IMMUTABLE_CACHE'
50
+ const MUTABLE_CACHE = 'MUTABLE_CACHE'
51
+ const ONE_HOUR_IN_SECONDS = 3600
43
52
const urlInterceptRegex = [ new RegExp ( `${ self . location . origin } /ip(n|f)s/` ) ]
44
53
const updateVerifiedFetch = async ( ) : Promise < void > => {
45
54
verifiedFetch = await getVerifiedFetch ( )
@@ -85,6 +94,7 @@ self.addEventListener('fetch', (event) => {
85
94
const request = event . request
86
95
const urlString = request . url
87
96
const url = new URL ( urlString )
97
+ log ( 'helia-sw: incoming request url: %s:' , event . request . url )
88
98
89
99
if ( isConfigPageRequest ( url ) || isSwAssetRequest ( event ) ) {
90
100
// get the assets from the server
@@ -98,10 +108,9 @@ self.addEventListener('fetch', (event) => {
98
108
}
99
109
100
110
if ( isRootRequestForContent ( event ) ) {
101
- // intercept and do our own stuff...
102
111
event . respondWith ( fetchHandler ( { path : url . pathname , request } ) )
103
112
} else if ( isSubdomainRequest ( event ) ) {
104
- event . respondWith ( fetchHandler ( { path : url . pathname , request } ) )
113
+ event . respondWith ( getResponseFromCacheOrFetch ( event ) )
105
114
}
106
115
} )
107
116
@@ -177,13 +186,99 @@ function isSwAssetRequest (event: FetchEvent): boolean {
177
186
return isActualSwAsset
178
187
}
179
188
189
+ /**
190
+ * Set the expires header on a response object to a timestamp based on the passed ttl interval
191
+ * Defaults to
192
+ */
193
+ function setExpiresHeader ( response : Response , ttlSeconds : number = ONE_HOUR_IN_SECONDS ) : void {
194
+ const expirationTime = new Date ( Date . now ( ) + ttlSeconds * 1000 )
195
+
196
+ response . headers . set ( 'sw-cache-expires' , expirationTime . toUTCString ( ) )
197
+ }
198
+
199
+ /**
200
+ * Checks whether a cached response object has expired by looking at the expires header
201
+ * Note that this ignores the Cache-Control header since the expires header is set by us
202
+ */
203
+ function hasExpired ( response : Response ) : boolean {
204
+ const expiresHeader = response . headers . get ( 'sw-cache-expires' )
205
+
206
+ if ( expiresHeader == null ) {
207
+ return false
208
+ }
209
+
210
+ const expires = new Date ( expiresHeader )
211
+ const now = new Date ( )
212
+
213
+ return expires < now
214
+ }
215
+
216
+ function getCacheKey ( event : FetchEvent ) : string {
217
+ return `${ event . request . url } -${ event . request . headers . get ( 'Accept' ) ?? '' } `
218
+ }
219
+
220
+ async function fetchAndUpdateCache ( event : FetchEvent , url : URL , cacheKey : string ) : Promise < Response > {
221
+ const response = await fetchHandler ( { path : url . pathname , request : event . request } )
222
+ try {
223
+ await storeReponseInCache ( { response, isMutable : true , cacheKey } )
224
+ trace ( 'helia-ws: updated cache for %s' , cacheKey )
225
+ } catch ( err ) {
226
+ error ( 'helia-ws: failed updating response in cache for %s' , cacheKey , err )
227
+ }
228
+ return response
229
+ }
230
+
231
+ async function getResponseFromCacheOrFetch ( event : FetchEvent ) : Promise < Response > {
232
+ const { protocol } = getSubdomainParts ( event . request . url )
233
+ const url = new URL ( event . request . url )
234
+ const isMutable = protocol === 'ipns'
235
+ const cacheKey = getCacheKey ( event )
236
+ trace ( 'helia-sw: cache key: %s' , cacheKey )
237
+ const cache = await caches . open ( isMutable ? MUTABLE_CACHE : IMMUTABLE_CACHE )
238
+ const cachedResponse = await cache . match ( cacheKey )
239
+ const validCacheHit = cachedResponse != null && ! hasExpired ( cachedResponse )
240
+
241
+ if ( validCacheHit ) {
242
+ log ( 'helia-ws: cached response HIT for %s (expires: %s) %o' , cacheKey , cachedResponse . headers . get ( 'sw-cache-expires' ) , cachedResponse )
243
+
244
+ if ( isMutable ) {
245
+ // If the response is mutable, update the cache in the background.
246
+ void fetchAndUpdateCache ( event , url , cacheKey )
247
+ }
248
+
249
+ return cachedResponse
250
+ }
251
+
252
+ log ( 'helia-ws: cached response MISS for %s' , cacheKey )
253
+
254
+ return fetchAndUpdateCache ( event , url , cacheKey )
255
+ }
256
+
257
+ async function storeReponseInCache ( { response, isMutable, cacheKey } : StoreReponseInCacheOptions ) : Promise < void > {
258
+ // 👇 only cache successful responses
259
+ if ( ! response . ok ) {
260
+ return
261
+ }
262
+ trace ( 'helia-ws: updating cache for %s in the background' , cacheKey )
263
+
264
+ const cache = await caches . open ( isMutable ? MUTABLE_CACHE : IMMUTABLE_CACHE )
265
+
266
+ // Clone the response since streams can only be consumed once.
267
+ const respToCache = response . clone ( )
268
+
269
+ if ( isMutable ) {
270
+ trace ( 'helia-ws: setting expires header on response key %s before storing in cache' , cacheKey )
271
+ // 👇 Set expires header to an hour from now for mutable (ipns://) resources
272
+ // Note that this technically breaks HTTP semantics, whereby the cache-control max-age takes precendence
273
+ // Setting this header is only used by the service worker using a mechanism similar to stale-while-revalidate
274
+ setExpiresHeader ( respToCache , ONE_HOUR_IN_SECONDS )
275
+ }
276
+
277
+ log ( 'helia-ws: storing response for key %s in cache' , cacheKey )
278
+ await cache . put ( cacheKey , respToCache )
279
+ }
280
+
180
281
async function fetchHandler ( { path, request } : FetchHandlerArg ) : Promise < Response > {
181
- /**
182
- * > Any global variables you set will be lost if the service worker shuts down.
183
- *
184
- * @see https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/lifecycle
185
- */
186
- verifiedFetch = verifiedFetch ?? await getVerifiedFetch ( )
187
282
// test and enforce origin isolation before anything else is executed
188
283
const originLocation = await findOriginIsolationRedirect ( new URL ( request . url ) )
189
284
if ( originLocation !== null ) {
@@ -197,6 +292,13 @@ async function fetchHandler ({ path, request }: FetchHandlerArg): Promise<Respon
197
292
} )
198
293
}
199
294
295
+ /**
296
+ * > Any global variables you set will be lost if the service worker shuts down.
297
+ *
298
+ * @see https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/lifecycle
299
+ */
300
+ verifiedFetch = verifiedFetch ?? await getVerifiedFetch ( )
301
+
200
302
/**
201
303
* Note that there are existing bugs regarding service worker signal handling:
202
304
* * https://bugs.chromium.org/p/chromium/issues/detail?id=823697
0 commit comments