From b3c08d1df385fb7a27d6420c7d93ba20c598b84f Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Fri, 22 Mar 2024 11:19:33 -0700 Subject: [PATCH] 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> --- package-lock.json | 8 ++--- package.json | 2 +- src/sw.ts | 77 ++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 68 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index f463a203..b3dc99e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@helia/verified-fetch": "^1.2.1", + "@helia/verified-fetch": "^1.3.0", "@libp2p/logger": "^4.0.6", "@multiformats/dns": "^1.0.5", "@sgtpooki/file-type": "^1.0.1", @@ -3016,9 +3016,9 @@ "integrity": "sha512-HzdtdBwxsIkzpeXzhQ5mAhhuxcHbjEHH+JQoxt7hG/2HGFjjwyolLo7hbaexcnhoEuV4e0TNJ8kkpMjiEYY4VQ==" }, "node_modules/@helia/verified-fetch": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@helia/verified-fetch/-/verified-fetch-1.2.1.tgz", - "integrity": "sha512-l1DJT0qkbLrGv80OgwfEwg4J0H2xpROyNMBkCEyYl1z557zRLsgMsSIuoL4dXhryYlNWZuPIAstHCvxN8YdLjw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@helia/verified-fetch/-/verified-fetch-1.3.0.tgz", + "integrity": "sha512-8mEmh+A6PMnEVOwY1QT+mBMpABfhBE8qd8MbwO6zC/qrsDop9UrU1D/4Lcm9G1cjqGItPxB8u4Y1d2sUoXeHPg==", "dependencies": { "@helia/block-brokers": "^2.0.2", "@helia/car": "^3.1.0", diff --git a/package.json b/package.json index 66f4bd6c..74126cb8 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ } }, "dependencies": { - "@helia/verified-fetch": "^1.2.1", + "@helia/verified-fetch": "^1.3.0", "@libp2p/logger": "^4.0.6", "@multiformats/dns": "^1.0.5", "@sgtpooki/file-type": "^1.0.1", diff --git a/src/sw.ts b/src/sw.ts index a2420b49..86305a29 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -26,8 +26,9 @@ interface AggregateError extends Error { interface FetchHandlerArg { path: string request: Request - + event: FetchEvent } + interface GetVerifiedFetchUrlOptions { protocol?: string | null id?: string | null @@ -38,6 +39,7 @@ interface StoreReponseInCacheOptions { response: Response cacheKey: string isMutable: boolean + event: FetchEvent } /** @@ -80,6 +82,7 @@ const CURRENT_CACHES = Object.freeze({ }) let verifiedFetch: VerifiedFetch const channel = new HeliaServiceWorkerCommsChannel('SW') +const timeoutAbortEventType = 'verified-fetch-timeout' const ONE_HOUR_IN_SECONDS = 3600 const urlInterceptRegex = [new RegExp(`${self.location.origin}/ip(n|f)s/`)] const updateVerifiedFetch = async (): Promise => { @@ -309,19 +312,17 @@ function getCacheKey (event: FetchEvent): string { } async function fetchAndUpdateCache (event: FetchEvent, url: URL, cacheKey: string): Promise { - const response = await fetchHandler({ path: url.pathname, request: event.request }) + const response = await fetchHandler({ path: url.pathname, request: event.request, event }) // log all of the headers: response.headers.forEach((value, key) => { log.trace('helia-sw: response headers: %s: %s', key, value) }) - log('helia-sw: response range header value: "%s"', response.headers.get('content-range')) - log('helia-sw: response status: %s', response.status) try { - await storeReponseInCache({ response, isMutable: true, cacheKey }) + await storeReponseInCache({ response, isMutable: true, cacheKey, event }) trace('helia-ws: updated cache for %s', cacheKey) } catch (err) { error('helia-ws: failed updating response in cache for %s', cacheKey, err) @@ -356,10 +357,25 @@ async function getResponseFromCacheOrFetch (event: FetchEvent): Promise { - // 👇 only cache successful responses - if (!response.ok || invalidOkResponseCodesForCache.some(code => code === response.status)) { +function shouldCacheResponse ({ event, response }: { event: FetchEvent, response: Response }): boolean { + if (!response.ok) { + return false + } + const statusCodesToNotCache = [206] + if (statusCodesToNotCache.some(code => code === response.status)) { + log('helia-sw: not caching response with status %s', response.status) + return false + } + if (event.request.headers.get('pragma') === 'no-cache' || event.request.headers.get('cache-control') === 'no-cache') { + log('helia-sw: request indicated no-cache, not caching') + return false + } + + return true +} + +async function storeReponseInCache ({ response, isMutable, cacheKey, event }: StoreReponseInCacheOptions): Promise { + if (!shouldCacheResponse({ event, response })) { return } trace('helia-ws: updating cache for %s in the background', cacheKey) @@ -378,10 +394,11 @@ async function storeReponseInCache ({ response, isMutable, cacheKey }: StoreRepo } log('helia-ws: storing response for key %s in cache', cacheKey) - await cache.put(cacheKey, respToCache) + // do not await this.. large responses will delay [TTFB](https://web.dev/articles/ttfb) and [TTI](https://web.dev/articles/tti) + void cache.put(cacheKey, respToCache) } -async function fetchHandler ({ path, request }: FetchHandlerArg): Promise { +async function fetchHandler ({ path, request, event }: FetchHandlerArg): Promise { // test and enforce origin isolation before anything else is executed const originLocation = await findOriginIsolationRedirect(new URL(request.url)) if (originLocation !== null) { @@ -407,8 +424,29 @@ async function fetchHandler ({ path, request }: FetchHandlerArg): Promise): void => { + clearTimeout(signalAbortTimeout) + if (event?.type === timeoutAbortEventType) { + log.trace('helia-sw: timeout waiting for response from @helia/verified-fetch') + abortController.abort('timeout') + } else { + log.trace('helia-sw: request signal aborted') + abortController.abort('request signal aborted') + } + } + /** + * five minute delay to get the initial response. + * + * @todo reduce to 2 minutes? + */ + const signalAbortTimeout = setTimeout(() => { + abortFn({ type: timeoutAbortEventType }) + }, 5 * 60 * 1000) + // if the fetch event is aborted, we need to abort the signal we give to @helia/verified-fetch + event.request.signal.addEventListener('abort', abortFn) + try { const { id, protocol } = getSubdomainParts(request.url) const verifiedFetchUrl = getVerifiedFetchUrl({ id, protocol, path }) @@ -419,7 +457,7 @@ async function fetchHandler ({ path, request }: FetchHandlerArg): Promise