Skip to content

Commit b3c08d1

Browse files
SgtPooki2color
andauthored
feat: quick TTFB & TTI for large content (#138)
* feat: quick TTFB * tmp: clear timeout before awaiting response * fix: TTI avg < 10s on big-buck-bunny from new kubo node * chore: minor cleanup * chore: log prefix change * chore: PR suggestion var name Co-authored-by: Daniel Norman <1992255+2color@users.noreply.github.com> * chore: use statusCodesToNotCache * chore: use non-magic string for timeoutAbortEvent type * chore: abortFn check for truth first --------- Co-authored-by: Daniel Norman <1992255+2color@users.noreply.github.com>
1 parent b064d20 commit b3c08d1

File tree

3 files changed

+68
-19
lines changed

3 files changed

+68
-19
lines changed

package-lock.json

+4-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
}
4141
},
4242
"dependencies": {
43-
"@helia/verified-fetch": "^1.2.1",
43+
"@helia/verified-fetch": "^1.3.0",
4444
"@libp2p/logger": "^4.0.6",
4545
"@multiformats/dns": "^1.0.5",
4646
"@sgtpooki/file-type": "^1.0.1",

src/sw.ts

+63-14
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ interface AggregateError extends Error {
2626
interface FetchHandlerArg {
2727
path: string
2828
request: Request
29-
29+
event: FetchEvent
3030
}
31+
3132
interface GetVerifiedFetchUrlOptions {
3233
protocol?: string | null
3334
id?: string | null
@@ -38,6 +39,7 @@ interface StoreReponseInCacheOptions {
3839
response: Response
3940
cacheKey: string
4041
isMutable: boolean
42+
event: FetchEvent
4143
}
4244

4345
/**
@@ -80,6 +82,7 @@ const CURRENT_CACHES = Object.freeze({
8082
})
8183
let verifiedFetch: VerifiedFetch
8284
const channel = new HeliaServiceWorkerCommsChannel('SW')
85+
const timeoutAbortEventType = 'verified-fetch-timeout'
8386
const ONE_HOUR_IN_SECONDS = 3600
8487
const urlInterceptRegex = [new RegExp(`${self.location.origin}/ip(n|f)s/`)]
8588
const updateVerifiedFetch = async (): Promise<void> => {
@@ -309,19 +312,17 @@ function getCacheKey (event: FetchEvent): string {
309312
}
310313

311314
async function fetchAndUpdateCache (event: FetchEvent, url: URL, cacheKey: string): Promise<Response> {
312-
const response = await fetchHandler({ path: url.pathname, request: event.request })
315+
const response = await fetchHandler({ path: url.pathname, request: event.request, event })
313316

314317
// log all of the headers:
315318
response.headers.forEach((value, key) => {
316319
log.trace('helia-sw: response headers: %s: %s', key, value)
317320
})
318321

319-
log('helia-sw: response range header value: "%s"', response.headers.get('content-range'))
320-
321322
log('helia-sw: response status: %s', response.status)
322323

323324
try {
324-
await storeReponseInCache({ response, isMutable: true, cacheKey })
325+
await storeReponseInCache({ response, isMutable: true, cacheKey, event })
325326
trace('helia-ws: updated cache for %s', cacheKey)
326327
} catch (err) {
327328
error('helia-ws: failed updating response in cache for %s', cacheKey, err)
@@ -356,10 +357,25 @@ async function getResponseFromCacheOrFetch (event: FetchEvent): Promise<Response
356357
return fetchAndUpdateCache(event, url, cacheKey)
357358
}
358359

359-
const invalidOkResponseCodesForCache = [206]
360-
async function storeReponseInCache ({ response, isMutable, cacheKey }: StoreReponseInCacheOptions): Promise<void> {
361-
// 👇 only cache successful responses
362-
if (!response.ok || invalidOkResponseCodesForCache.some(code => code === response.status)) {
360+
function shouldCacheResponse ({ event, response }: { event: FetchEvent, response: Response }): boolean {
361+
if (!response.ok) {
362+
return false
363+
}
364+
const statusCodesToNotCache = [206]
365+
if (statusCodesToNotCache.some(code => code === response.status)) {
366+
log('helia-sw: not caching response with status %s', response.status)
367+
return false
368+
}
369+
if (event.request.headers.get('pragma') === 'no-cache' || event.request.headers.get('cache-control') === 'no-cache') {
370+
log('helia-sw: request indicated no-cache, not caching')
371+
return false
372+
}
373+
374+
return true
375+
}
376+
377+
async function storeReponseInCache ({ response, isMutable, cacheKey, event }: StoreReponseInCacheOptions): Promise<void> {
378+
if (!shouldCacheResponse({ event, response })) {
363379
return
364380
}
365381
trace('helia-ws: updating cache for %s in the background', cacheKey)
@@ -378,10 +394,11 @@ async function storeReponseInCache ({ response, isMutable, cacheKey }: StoreRepo
378394
}
379395

380396
log('helia-ws: storing response for key %s in cache', cacheKey)
381-
await cache.put(cacheKey, respToCache)
397+
// do not await this.. large responses will delay [TTFB](https://web.dev/articles/ttfb) and [TTI](https://web.dev/articles/tti)
398+
void cache.put(cacheKey, respToCache)
382399
}
383400

384-
async function fetchHandler ({ path, request }: FetchHandlerArg): Promise<Response> {
401+
async function fetchHandler ({ path, request, event }: FetchHandlerArg): Promise<Response> {
385402
// test and enforce origin isolation before anything else is executed
386403
const originLocation = await findOriginIsolationRedirect(new URL(request.url))
387404
if (originLocation !== null) {
@@ -407,8 +424,29 @@ async function fetchHandler ({ path, request }: FetchHandlerArg): Promise<Respon
407424
* * https://bugs.chromium.org/p/chromium/issues/detail?id=823697
408425
* * https://bugzilla.mozilla.org/show_bug.cgi?id=1394102
409426
*/
410-
// 5 minute timeout
411-
const signal = AbortSignal.timeout(5 * 60 * 1000)
427+
const abortController = new AbortController()
428+
const signal = abortController.signal
429+
const abortFn = (event: Pick<AbortSignalEventMap['abort'], 'type'>): void => {
430+
clearTimeout(signalAbortTimeout)
431+
if (event?.type === timeoutAbortEventType) {
432+
log.trace('helia-sw: timeout waiting for response from @helia/verified-fetch')
433+
abortController.abort('timeout')
434+
} else {
435+
log.trace('helia-sw: request signal aborted')
436+
abortController.abort('request signal aborted')
437+
}
438+
}
439+
/**
440+
* five minute delay to get the initial response.
441+
*
442+
* @todo reduce to 2 minutes?
443+
*/
444+
const signalAbortTimeout = setTimeout(() => {
445+
abortFn({ type: timeoutAbortEventType })
446+
}, 5 * 60 * 1000)
447+
// if the fetch event is aborted, we need to abort the signal we give to @helia/verified-fetch
448+
event.request.signal.addEventListener('abort', abortFn)
449+
412450
try {
413451
const { id, protocol } = getSubdomainParts(request.url)
414452
const verifiedFetchUrl = getVerifiedFetchUrl({ id, protocol, path })
@@ -419,14 +457,25 @@ async function fetchHandler ({ path, request }: FetchHandlerArg): Promise<Respon
419457
log.trace('fetchHandler: request headers: %s: %s', key, value)
420458
})
421459

422-
return await verifiedFetch(verifiedFetchUrl, {
460+
const response = await verifiedFetch(verifiedFetchUrl, {
423461
signal,
424462
headers,
425463
// TODO redirect: 'manual', // enable when http urls are supported by verified-fetch: https://github.com/ipfs-shipyard/helia-service-worker-gateway/issues/62#issuecomment-1977661456
426464
onProgress: (e) => {
427465
trace(`${e.type}: `, e.detail)
428466
}
429467
})
468+
/**
469+
* Now that we've got a response back from Helia, don't abort the promise since any additional networking calls
470+
* that may performed by Helia would be dropped.
471+
*
472+
* If `event.request.signal` is aborted, that would cancel any underlying network requests.
473+
*
474+
* Note: we haven't awaited the arrayBuffer, blob, json, etc. `await verifiedFetch` only awaits the construction of
475+
* the response object, regardless of it's inner content
476+
*/
477+
clearTimeout(signalAbortTimeout)
478+
return response
430479
} catch (err: unknown) {
431480
const errorMessages: string[] = []
432481
if (isAggregateError(err)) {

0 commit comments

Comments
 (0)