mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
8edc010a10
* fix(fetch-cache): dedupe identical render fetches Uncached and no-store fetches currently bypass the persistent fetch cache by calling the original fetch directly. That diverges from Next.js, where the patched fetch wraps the original fetch in a request-scoped dedupe layer before persistent cache policy runs, so repeated GET and HEAD fetches during a render share one network response. Add request-scoped fetch dedupe state to the fetch cache context and route network misses and cache-bypass fetches through it. The dedupe key follows the Next.js method, header, mode, redirect, credentials, referrer, referrerPolicy, and integrity semantics while still opting out for abort signals, keepalive, and side-effecting methods. Update fetch-cache coverage for uncached render dedupe, independent response bodies, request-scope isolation, trace header exclusion, and the changed no-store/no-cache interaction. * fix(fetch-cache): give each cloned dedupe response its own Headers Pass `new Headers(response.headers)` per clone instead of sharing the same Headers object across both branches and the dedupe entry. Matches Next.js' `cloneResponse` and avoids a future-correctness trap if any code path mutates response headers post-fetch. Also drops two redundant `runWithFetchDedupe` calls inside `dispatchAppPage` (the ISR revalidation render path and the intercept stream render path) — both sites are inline anonymous functions that already inherit the dedupe scope from the outer `dispatchAppPage` / `runAppPageRevalidationContext` wrap, so the inner calls were no-ops and just obscured that inheritance. * test(fetch-cache): cover Request input dedupe + always clone bodyless responses Always construct fresh Response objects in cloneDedupeResponse, including the bodyless path that previously returned the same `response` reference for both tuple slots. Mirrors Next.js' cloneResponse and avoids any "disturbed response" surprise if a runtime tracks consumption state on the shared reference. Factor the per-clone construction into buildDedupeClone() so bodied and bodyless clones share the headers-copy + url-restore + finalizer-register treatment. Adds a comment near entry.response assignment noting that the unconsumed tee branch is bounded by the render scope: when runWithFetchDedupe exits, the dedupe map becomes unreachable and the FinalizationRegistry cancels any still-unconsumed branch. Also adds two tests covering the Request-object input path of createFetchDedupeCandidate — one verifying dedupe applies for identical Request inputs, one verifying differing non-trace headers on Request inputs prevent dedupe. * fix(fetch-cache): drop failed dedupe entries so later callers can retry When the original fetch rejects, the dedupe entry stayed in the map with response: null. Subsequent callers within the same render scope chained on the rejected promise — propagation was correct, but they could never retry, because the failed entry blocked fresh attempts for the rest of the scope. React.cache() (which Next.js uses) avoids this because each call site naturally retries on failure. Splice the entry out of the URL bucket on rejection so a later fetch to the same URL within the same render scope creates a fresh entry and re-issues the upstream request. Also align the fetch-dedupe-metadata fixture with fetch-dedupe-isr-metadata by replacing the `as CountBody` cast with a runtime assertCountBody check. * docs(fetch-cache): document scope inheritance + harden ISR test stabilization Rename `dispatchAppPageWithDedupe` to `dispatchAppPageInner` so the name reflects that it runs *inside* the dedupe scope rather than activating it. Add comments at the four `runWithFetchDedupe` / `renderToReadableStream` sites explaining how each one relates to the surrounding dedupe scope: - app-page-render and app-page-boundary: defensive wrap, no-op under dispatch; standalone callers must keep an outer scope alive across async stream consumption since `runWithFetchDedupe` of a synchronous fn only covers the synchronous portion. - ISR revalidation render and intercept render in dispatch: explicitly note why no inner wrap is needed (the outer revalidation context / dispatch wrapper already activated dedupe). - runWithFetchDedupe doc: document the ALS-scope-vs-async-consumption caveat that ties this together. Replace the fixed 200ms post-condition sleep in the ISR background dedupe test with a 500ms count-stabilization poll. A stray third upstream fetch (which would betray dedupe leaking across the metadata + page render boundary) is now caught regardless of when it fires. --------- Co-authored-by: James <james@eli.cx>