@rangojs/router 0.0.0-experimental.eb0645d3 → 0.0.0-experimental.f1468e3c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -0
- package/README.md +126 -16
- package/dist/bin/rango.js +319 -95
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +2724 -1053
- package/package.json +68 -14
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +64 -2
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +224 -32
- package/skills/caching/SKILL.md +279 -17
- package/skills/composability/SKILL.md +27 -3
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +11 -9
- package/skills/hooks/SKILL.md +243 -29
- package/skills/host-router/SKILL.md +83 -23
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +68 -19
- package/skills/layout/SKILL.md +13 -9
- package/skills/links/SKILL.md +190 -23
- package/skills/loader/SKILL.md +235 -9
- package/skills/middleware/SKILL.md +18 -10
- package/skills/migrate-nextjs/SKILL.md +43 -19
- package/skills/migrate-react-router/SKILL.md +8 -2
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +18 -7
- package/skills/prerender/SKILL.md +65 -60
- package/skills/rango/SKILL.md +251 -24
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +115 -48
- package/skills/route/SKILL.md +46 -5
- package/skills/router-setup/SKILL.md +30 -8
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +130 -0
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +122 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +123 -0
- package/skills/typesafety/SKILL.md +322 -29
- package/skills/use-cache/SKILL.md +57 -14
- package/skills/view-transitions/SKILL.md +337 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +192 -150
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +94 -25
- package/src/browser/navigation-client.ts +121 -84
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +115 -67
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +147 -128
- package/src/browser/prefetch/cache.ts +107 -56
- package/src/browser/prefetch/fetch.ts +204 -34
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +30 -7
- package/src/browser/react/NavigationProvider.tsx +283 -118
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/deferred-handle-resolution.ts +75 -0
- package/src/browser/react/filter-segment-order.ts +66 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +178 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +17 -14
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +10 -5
- package/src/browser/react/use-params.ts +11 -11
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +25 -3
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +99 -8
- package/src/browser/rsc-router.tsx +91 -24
- package/src/browser/scroll-restoration.ts +30 -17
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +214 -55
- package/src/browser/types.ts +80 -9
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +117 -14
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +16 -5
- package/src/build/route-types/include-resolution.ts +117 -23
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +22 -6
- package/src/build/route-types/router-processing.ts +55 -28
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +29 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +224 -41
- package/src/cache/cache-scope.ts +188 -82
- package/src/cache/cache-tag.ts +103 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +1989 -378
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +89 -21
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +10 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +56 -12
- package/src/cache/segment-codec.ts +9 -17
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +37 -100
- package/src/client.rsc.tsx +44 -21
- package/src/client.tsx +36 -61
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +18 -6
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +30 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +31 -23
- package/src/handles/MetaTags.tsx +62 -19
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +37 -8
- package/src/handles/is-thenable.ts +19 -0
- package/src/handles/meta.ts +51 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +96 -12
- package/src/index.ts +94 -14
- package/src/internal-debug.ts +11 -10
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +61 -6
- package/src/redirect-origin.ts +100 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +34 -0
- package/src/reverse.ts +65 -40
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +19 -77
- package/src/route-definition/dsl-helpers.ts +304 -309
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +82 -55
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/redirect.ts +44 -11
- package/src/route-definition/resolve-handler-use.ts +12 -1
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +19 -46
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +73 -25
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +27 -43
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +39 -20
- package/src/router/lazy-includes.ts +10 -47
- package/src/router/loader-resolution.ts +155 -72
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +18 -29
- package/src/router/match-api.ts +9 -24
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +40 -24
- package/src/router/match-middleware/cache-lookup.ts +159 -285
- package/src/router/match-middleware/cache-store.ts +64 -52
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +44 -74
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +7 -134
- package/src/router/middleware.ts +247 -166
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +85 -94
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +104 -65
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +28 -62
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +3 -28
- package/src/router/router-interfaces.ts +83 -35
- package/src/router/router-options.ts +136 -5
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +97 -84
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +76 -39
- package/src/router/segment-resolution/revalidation.ts +272 -320
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +56 -0
- package/src/router/segment-resolution.ts +5 -1
- package/src/router/segment-wrappers.ts +6 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +161 -199
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +206 -0
- package/src/router/trie-matching.ts +162 -64
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +110 -55
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +264 -220
- package/src/rsc/helpers.ts +100 -6
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +114 -38
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +117 -11
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +88 -188
- package/src/rsc/rsc-rendering.ts +98 -76
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +281 -117
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +23 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-loader-promise.ts +31 -4
- package/src/segment-system.tsx +254 -143
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +163 -51
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +21 -38
- package/src/server/loader-registry.ts +33 -42
- package/src/server/request-context.ts +287 -178
- package/src/ssr/index.tsx +21 -16
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +701 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +257 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +371 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +357 -0
- package/src/testing/render-route.tsx +581 -0
- package/src/testing/run-loader.ts +385 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/run-transition-when.ts +164 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +20 -58
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +110 -62
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +11 -9
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +135 -14
- package/src/urls/include-helper.ts +9 -56
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +29 -12
- package/src/urls/path-helper.ts +17 -106
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -19
- package/src/use-loader.tsx +292 -107
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/discover-routers.ts +126 -85
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +96 -68
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +44 -0
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +2 -0
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +126 -8
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
- package/src/vite/plugins/expose-action-id.ts +48 -95
- package/src/vite/plugins/expose-id-utils.ts +88 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
- package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
- package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
- package/src/vite/plugins/expose-internal-ids.ts +505 -486
- package/src/vite/plugins/performance-tracks.ts +26 -25
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/version-injector.ts +40 -29
- package/src/vite/plugins/version-plugin.ts +37 -40
- package/src/vite/plugins/virtual-entries.ts +39 -25
- package/src/vite/rango.ts +109 -118
- package/src/vite/router-discovery.ts +718 -119
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +20 -52
- package/src/vite/utils/prerender-utils.ts +54 -39
- package/src/vite/utils/shared-utils.ts +90 -41
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Prefetch Cache
|
|
3
3
|
*
|
|
4
|
-
* In-memory cache storing
|
|
5
|
-
* on subsequent navigation.
|
|
4
|
+
* In-memory cache storing eagerly-decoded prefetch payloads for instant,
|
|
5
|
+
* already-warm cache hits on subsequent navigation. A prefetch fetches the
|
|
6
|
+
* RSC partial AND decodes it (createFromFetch) up front — decoding the Flight
|
|
7
|
+
* stream resolves the route's client references, so the route's JS chunks are
|
|
8
|
+
* imported during prefetch rather than on click. The decoded payload is reused
|
|
9
|
+
* verbatim by navigation, so a prefetched click loads no new code. Two key
|
|
10
|
+
* scopes are in play:
|
|
6
11
|
* - Wildcard (default): built by `buildPrefetchKey(rangoState, target)` —
|
|
7
12
|
* shape `rangoState\0/target?...`. Shared across all source pages and
|
|
8
13
|
* invalidated automatically when Rango state bumps (deploy or
|
|
@@ -17,8 +22,8 @@
|
|
|
17
22
|
* from other pages.
|
|
18
23
|
*
|
|
19
24
|
* Also tracks in-flight prefetch promises. Each promise resolves to the
|
|
20
|
-
*
|
|
21
|
-
* still-downloading prefetch without
|
|
25
|
+
* decoded prefetch entry (or null), letting navigation adopt a
|
|
26
|
+
* still-downloading prefetch without issuing a duplicate request. A
|
|
22
27
|
* single promise can be registered under multiple alias keys (see
|
|
23
28
|
* `setInflightPromiseWithAliases`) so same-source navigations adopt via
|
|
24
29
|
* their source key while cross-source ones fall through to the wildcard
|
|
@@ -26,14 +31,50 @@
|
|
|
26
31
|
*
|
|
27
32
|
* Replaces the previous browser HTTP cache approach which was unreliable
|
|
28
33
|
* due to response draining race conditions and browser inconsistencies.
|
|
34
|
+
*
|
|
35
|
+
* State here lives in module-level singletons (cache, inflight, generation,
|
|
36
|
+
* cacheTTL, etc.) rather than a per-instance factory. This is correct because
|
|
37
|
+
* exactly one router is live per document — an SPA navigation crossing a
|
|
38
|
+
* host-router boundary forces a full document reload — so the singletons are
|
|
39
|
+
* effectively per-document. Unit tests reset them via clearPrefetchCache().
|
|
29
40
|
*/
|
|
30
41
|
|
|
31
42
|
import { abortAllPrefetches } from "./queue.js";
|
|
32
43
|
import { invalidateRangoState } from "../rango-state.js";
|
|
44
|
+
import type { RscPayload } from "../types.js";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A prefetch that has been fetched AND eagerly decoded. Storing the decoded
|
|
48
|
+
* payload (not the raw Response) is what makes a prefetched navigation "warm":
|
|
49
|
+
* decoding the Flight stream during prefetch pulls the route's client chunks,
|
|
50
|
+
* so the click reuses ready elements and loads no new JS.
|
|
51
|
+
*/
|
|
52
|
+
export interface DecodedPrefetch {
|
|
53
|
+
/** The eagerly-decoded RSC payload. Reused verbatim by navigation. */
|
|
54
|
+
payload: Promise<RscPayload>;
|
|
55
|
+
/**
|
|
56
|
+
* Resolves when the underlying RSC stream finishes draining. Navigation
|
|
57
|
+
* forwards this as its streamComplete so scroll/revalidation gating is
|
|
58
|
+
* unchanged from the fresh-fetch path.
|
|
59
|
+
*/
|
|
60
|
+
streamComplete: Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Prefetch scope as tagged by the server via `X-RSC-Prefetch-Scope`.
|
|
63
|
+
* `"source"` means the response is source-page-sensitive and must not be
|
|
64
|
+
* reused by a navigation from a different page — navigation enforces this
|
|
65
|
+
* when it adopted an inflight entry through the wildcard key.
|
|
66
|
+
*/
|
|
67
|
+
scope: "source" | "wildcard";
|
|
68
|
+
/**
|
|
69
|
+
* Synchronously-readable flag, flipped to true when `streamComplete` resolves
|
|
70
|
+
* (the entire RSC stream has drained). Navigation reads this at click time to
|
|
71
|
+
* tell a FULLY warmed prefetch (payload commits without suspending → safe to
|
|
72
|
+
* commit in a startTransition, no fallback flash) from a partially-warmed one
|
|
73
|
+
* (still streaming → stream its fallbacks like a cold load). Starts false.
|
|
74
|
+
*/
|
|
75
|
+
complete: boolean;
|
|
76
|
+
}
|
|
33
77
|
|
|
34
|
-
// Default TTL: 5 minutes. Overridden by initPrefetchCache() with
|
|
35
|
-
// the server-configured prefetchCacheTTL from router options.
|
|
36
|
-
// 0 disables the in-memory cache entirely.
|
|
37
78
|
let cacheTTL = 300_000;
|
|
38
79
|
|
|
39
80
|
/**
|
|
@@ -55,31 +96,19 @@ export function isPrefetchCacheDisabled(): boolean {
|
|
|
55
96
|
const MAX_PREFETCH_CACHE_SIZE = 50;
|
|
56
97
|
|
|
57
98
|
interface PrefetchCacheEntry {
|
|
58
|
-
|
|
99
|
+
entry: DecodedPrefetch;
|
|
59
100
|
timestamp: number;
|
|
60
101
|
}
|
|
61
102
|
|
|
62
103
|
const cache = new Map<string, PrefetchCacheEntry>();
|
|
63
104
|
const inflight = new Set<string>();
|
|
64
105
|
|
|
65
|
-
|
|
66
|
-
* In-flight promise map. When a prefetch fetch is in progress, its
|
|
67
|
-
* Promise<Response | null> is stored here so navigation can await
|
|
68
|
-
* it instead of starting a duplicate request.
|
|
69
|
-
*/
|
|
70
|
-
const inflightPromises = new Map<string, Promise<Response | null>>();
|
|
106
|
+
const inflightPromises = new Map<string, Promise<DecodedPrefetch | null>>();
|
|
71
107
|
|
|
72
|
-
/**
|
|
73
|
-
* Alias map for in-flight promises registered under multiple keys (see
|
|
74
|
-
* dual inflight in prefetch/fetch.ts). Records each key's sibling set so
|
|
75
|
-
* that consuming or clearing any one key atomically removes every alias —
|
|
76
|
-
* guaranteeing a single consumer for the shared Response stream.
|
|
77
|
-
*/
|
|
78
108
|
const inflightAliases = new Map<string, string[]>();
|
|
79
109
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// response (the data may be stale due to a server action invalidation).
|
|
110
|
+
const adoptedKeys = new Set<string>();
|
|
111
|
+
|
|
83
112
|
let generation = 0;
|
|
84
113
|
|
|
85
114
|
/**
|
|
@@ -152,14 +181,14 @@ export function hasPrefetch(key: string): boolean {
|
|
|
152
181
|
}
|
|
153
182
|
|
|
154
183
|
/**
|
|
155
|
-
* Consume a cached prefetch
|
|
156
|
-
* One-time consumption: the entry is deleted after retrieval.
|
|
184
|
+
* Consume a cached, eagerly-decoded prefetch. Returns null if not found or
|
|
185
|
+
* expired. One-time consumption: the entry is deleted after retrieval.
|
|
157
186
|
* Returns null when caching is disabled (TTL <= 0).
|
|
158
187
|
*
|
|
159
188
|
* Does NOT check in-flight prefetches — use consumeInflightPrefetch()
|
|
160
|
-
* for that (returns a Promise instead of a
|
|
189
|
+
* for that (returns a Promise instead of a resolved entry).
|
|
161
190
|
*/
|
|
162
|
-
export function consumePrefetch(key: string):
|
|
191
|
+
export function consumePrefetch(key: string): DecodedPrefetch | null {
|
|
163
192
|
if (cacheTTL <= 0) return null;
|
|
164
193
|
const entry = cache.get(key);
|
|
165
194
|
if (!entry) return null;
|
|
@@ -168,13 +197,14 @@ export function consumePrefetch(key: string): Response | null {
|
|
|
168
197
|
return null;
|
|
169
198
|
}
|
|
170
199
|
cache.delete(key);
|
|
171
|
-
return entry.
|
|
200
|
+
return entry.entry;
|
|
172
201
|
}
|
|
173
202
|
|
|
174
203
|
/**
|
|
175
204
|
* Consume an in-flight prefetch promise. Returns null if no prefetch is
|
|
176
|
-
* in-flight for this key. The returned Promise resolves to the
|
|
177
|
-
*
|
|
205
|
+
* in-flight for this key. The returned Promise resolves to the decoded
|
|
206
|
+
* prefetch entry (or null if the fetch failed/was aborted, or carried a
|
|
207
|
+
* control header the navigation must re-fetch to honor).
|
|
178
208
|
*
|
|
179
209
|
* One-time consumption: the promise entry is removed (along with any
|
|
180
210
|
* sibling aliases registered via `setInflightPromiseWithAliases`) so a
|
|
@@ -188,38 +218,51 @@ export function consumePrefetch(key: string): Response | null {
|
|
|
188
218
|
*/
|
|
189
219
|
export function consumeInflightPrefetch(
|
|
190
220
|
key: string,
|
|
191
|
-
): Promise<
|
|
221
|
+
): Promise<DecodedPrefetch | null> | null {
|
|
192
222
|
const promise = inflightPromises.get(key);
|
|
193
223
|
if (!promise) return null;
|
|
194
224
|
// Remove the promise under every alias so a second consumer cannot
|
|
195
|
-
// adopt the same stream and race on the body
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
|
|
225
|
+
// adopt the same stream and race on the body, and mark every alias as
|
|
226
|
+
// adopted so the pending `storePrefetch` (which resolves later, after this
|
|
227
|
+
// adoption) does not leave the now-owned, single-use entry in the cache map.
|
|
228
|
+
// `inflightAliases` is intentionally preserved — `clearPrefetchInflight()` in
|
|
229
|
+
// the fetch's `.finally()` still needs it to clear every inflight flag and
|
|
230
|
+
// adopted marker; deleting here would strand the sibling's flag forever.
|
|
231
|
+
forEachAlias(key, (k) => {
|
|
232
|
+
inflightPromises.delete(k);
|
|
233
|
+
adoptedKeys.add(k);
|
|
234
|
+
});
|
|
200
235
|
return promise;
|
|
201
236
|
}
|
|
202
237
|
|
|
203
238
|
/**
|
|
204
|
-
* Store
|
|
205
|
-
* The response should be a clone() of the original so the caller can
|
|
206
|
-
* still consume the body. The clone's body streams independently.
|
|
239
|
+
* Store an eagerly-decoded prefetch in the in-memory cache.
|
|
207
240
|
*
|
|
208
241
|
* Skips storage if the generation has changed since the fetch started
|
|
209
242
|
* (a server action invalidated the cache mid-flight).
|
|
210
243
|
*/
|
|
211
244
|
export function storePrefetch(
|
|
212
245
|
key: string,
|
|
213
|
-
|
|
246
|
+
entry: DecodedPrefetch,
|
|
214
247
|
fetchGeneration: number,
|
|
215
248
|
): void {
|
|
216
249
|
if (cacheTTL <= 0) return;
|
|
217
250
|
if (fetchGeneration !== generation) return;
|
|
218
251
|
|
|
252
|
+
// If a navigation already adopted this prefetch's in-flight promise, it owns
|
|
253
|
+
// the single-use entry (and has drained its handle generator). Do NOT also
|
|
254
|
+
// publish it to the cache map, or a later navigation would be served the
|
|
255
|
+
// exhausted entry and lose that route's handles. Clear the marker (under all
|
|
256
|
+
// aliases) now that the decision is made.
|
|
257
|
+
if (adoptedKeys.has(key)) {
|
|
258
|
+
forEachAlias(key, (k) => adoptedKeys.delete(k));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
219
262
|
// Evict expired entries
|
|
220
263
|
const now = Date.now();
|
|
221
|
-
for (const [k,
|
|
222
|
-
if (now -
|
|
264
|
+
for (const [k, cached] of cache) {
|
|
265
|
+
if (now - cached.timestamp > cacheTTL) {
|
|
223
266
|
cache.delete(k);
|
|
224
267
|
}
|
|
225
268
|
}
|
|
@@ -230,7 +273,24 @@ export function storePrefetch(
|
|
|
230
273
|
if (oldest) cache.delete(oldest);
|
|
231
274
|
}
|
|
232
275
|
|
|
233
|
-
cache.set(key, {
|
|
276
|
+
cache.set(key, { entry, timestamp: now });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Remove a single stored prefetch entry. Used to evict an entry whose body
|
|
281
|
+
* stream stalled after headers arrived (its payload / streamComplete never
|
|
282
|
+
* settle), so future prefetches and navigation refetch instead of dedupe-ing
|
|
283
|
+
* against — and awaiting — a stuck entry.
|
|
284
|
+
*
|
|
285
|
+
* Identity-guarded: only evicts when the entry CURRENTLY stored under `key` is
|
|
286
|
+
* the exact `entry` we published. A generation check alone is insufficient —
|
|
287
|
+
* after this entry is consumed (consumePrefetch) and a fresh prefetch
|
|
288
|
+
* republishes under the SAME key in the SAME generation, a gen-only guard would
|
|
289
|
+
* delete that valid newer entry. Reference identity drops only our own stalled
|
|
290
|
+
* entry.
|
|
291
|
+
*/
|
|
292
|
+
export function removePrefetch(key: string, entry: DecodedPrefetch): void {
|
|
293
|
+
if (cache.get(key)?.entry === entry) cache.delete(key);
|
|
234
294
|
}
|
|
235
295
|
|
|
236
296
|
/**
|
|
@@ -245,12 +305,9 @@ export function markPrefetchInflight(key: string): void {
|
|
|
245
305
|
inflight.add(key);
|
|
246
306
|
}
|
|
247
307
|
|
|
248
|
-
/**
|
|
249
|
-
* Store the in-flight Promise for a prefetch so navigation can reuse it.
|
|
250
|
-
*/
|
|
251
308
|
export function setInflightPromise(
|
|
252
309
|
key: string,
|
|
253
|
-
promise: Promise<
|
|
310
|
+
promise: Promise<DecodedPrefetch | null>,
|
|
254
311
|
): void {
|
|
255
312
|
inflightPromises.set(key, promise);
|
|
256
313
|
}
|
|
@@ -263,7 +320,7 @@ export function setInflightPromise(
|
|
|
263
320
|
*/
|
|
264
321
|
export function setInflightPromiseWithAliases(
|
|
265
322
|
keys: string[],
|
|
266
|
-
promise: Promise<
|
|
323
|
+
promise: Promise<DecodedPrefetch | null>,
|
|
267
324
|
): void {
|
|
268
325
|
for (const k of keys) {
|
|
269
326
|
inflightPromises.set(k, promise);
|
|
@@ -276,22 +333,16 @@ export function clearPrefetchInflight(key: string): void {
|
|
|
276
333
|
inflight.delete(k);
|
|
277
334
|
inflightPromises.delete(k);
|
|
278
335
|
inflightAliases.delete(k);
|
|
336
|
+
adoptedKeys.delete(k);
|
|
279
337
|
});
|
|
280
338
|
}
|
|
281
339
|
|
|
282
|
-
/**
|
|
283
|
-
* Invalidate all prefetch state. Called when server actions mutate data.
|
|
284
|
-
* Clears the in-memory cache, cancels in-flight prefetches, and rotates
|
|
285
|
-
* the Rango state key so CDN-cached responses are also invalidated.
|
|
286
|
-
*
|
|
287
|
-
* Uses abortAllPrefetches (hard cancel) because in-flight responses
|
|
288
|
-
* may contain stale data after a mutation.
|
|
289
|
-
*/
|
|
290
340
|
export function clearPrefetchCache(): void {
|
|
291
341
|
generation++;
|
|
292
342
|
inflight.clear();
|
|
293
343
|
inflightPromises.clear();
|
|
294
344
|
inflightAliases.clear();
|
|
345
|
+
adoptedKeys.clear();
|
|
295
346
|
cache.clear();
|
|
296
347
|
abortAllPrefetches();
|
|
297
348
|
invalidateRangoState();
|
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Fetch-based prefetch logic used by Link (hover/viewport/render strategies)
|
|
5
5
|
* and useRouter().prefetch(). Sends the same headers and segment IDs as a
|
|
6
|
-
* real navigation so the server returns a proper diff. The
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* real navigation so the server returns a proper diff. The response is fetched
|
|
7
|
+
* AND eagerly decoded (createFromFetch) up front: decoding the Flight stream
|
|
8
|
+
* resolves the route's client references, so the route's JS chunks are imported
|
|
9
|
+
* during prefetch rather than on click. The decoded payload is stored in an
|
|
10
|
+
* in-memory cache and reused verbatim by navigation, so a prefetched click
|
|
11
|
+
* loads no new code.
|
|
9
12
|
*
|
|
10
13
|
* In-flight promises are tracked in the cache so that navigation can reuse
|
|
11
|
-
* a prefetch that is still downloading instead of starting a
|
|
14
|
+
* a prefetch that is still downloading/decoding instead of starting a
|
|
15
|
+
* duplicate request.
|
|
12
16
|
*/
|
|
13
17
|
|
|
14
18
|
import {
|
|
@@ -18,13 +22,51 @@ import {
|
|
|
18
22
|
markPrefetchInflight,
|
|
19
23
|
setInflightPromiseWithAliases,
|
|
20
24
|
storePrefetch,
|
|
25
|
+
removePrefetch,
|
|
21
26
|
clearPrefetchInflight,
|
|
22
27
|
currentGeneration,
|
|
28
|
+
type DecodedPrefetch,
|
|
23
29
|
} from "./cache.js";
|
|
24
30
|
import { getRangoState } from "../rango-state.js";
|
|
31
|
+
import { isActionFenceActive } from "../action-fence.js";
|
|
25
32
|
import { enqueuePrefetch } from "./queue.js";
|
|
26
33
|
import { shouldPrefetch } from "./policy.js";
|
|
27
34
|
import { debugLog } from "../logging.js";
|
|
35
|
+
import { teeWithCompletion, isForeignRouterId } from "../response-adapter.js";
|
|
36
|
+
import type { RscPayload } from "../types.js";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Decoder injected at app startup (see setPrefetchDecoder). This is
|
|
40
|
+
* `deps.createFromFetch` — decoupled from the RSC runtime exactly like the
|
|
41
|
+
* navigation client. Prefetch decodes through it so the route's client chunks
|
|
42
|
+
* are pulled during the prefetch, not on click.
|
|
43
|
+
*/
|
|
44
|
+
type PrefetchDecoder = (response: Promise<Response>) => Promise<RscPayload>;
|
|
45
|
+
|
|
46
|
+
let decoder: PrefetchDecoder | null = null;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Hard ceiling for ANY prefetch fetch (hover/direct AND queue-driven). A server
|
|
50
|
+
* that stalls leaves the fetch pending forever — its `.finally()` never runs,
|
|
51
|
+
* `clearPrefetchInflight` never fires, and `hasPrefetch(key)` stays true,
|
|
52
|
+
* permanently deduping every future prefetch of that URL. The hover path passes
|
|
53
|
+
* no signal; the queue passes its own AbortController signal that only aborts on
|
|
54
|
+
* navigation, never on a stall — so the timeout is layered on BOTH (combined
|
|
55
|
+
* with any caller signal via AbortSignal.any) and aborts the stalled fetch so it
|
|
56
|
+
* settles (rejects) and the inflight key is always released. Generous so a
|
|
57
|
+
* slow-but-live response is never cut short.
|
|
58
|
+
*/
|
|
59
|
+
const PREFETCH_FETCH_TIMEOUT_MS = 30_000;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Wire the RSC decoder used to eagerly decode prefetched responses. Called
|
|
63
|
+
* once from initBrowserApp with the same createFromFetch the navigation client
|
|
64
|
+
* uses. Until set, prefetch warming is inert (prefetches are skipped) — the
|
|
65
|
+
* browser app always sets it before any Link can fire.
|
|
66
|
+
*/
|
|
67
|
+
export function setPrefetchDecoder(fn: PrefetchDecoder): void {
|
|
68
|
+
decoder = fn;
|
|
69
|
+
}
|
|
28
70
|
|
|
29
71
|
/**
|
|
30
72
|
* Check if a URL resolves to the current page (same pathname + search).
|
|
@@ -78,41 +120,99 @@ function buildPrefetchUrl(
|
|
|
78
120
|
}
|
|
79
121
|
|
|
80
122
|
/**
|
|
81
|
-
* Core prefetch fetch logic. Fetches the response,
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* reuse an in-flight prefetch via
|
|
123
|
+
* Core prefetch fetch logic. Fetches the response, eagerly decodes it, and
|
|
124
|
+
* stores the decoded payload in the in-memory cache. The returned Promise
|
|
125
|
+
* resolves to the decoded entry (or null on failure / control header) so
|
|
126
|
+
* navigation can safely reuse an in-flight prefetch via
|
|
127
|
+
* consumeInflightPrefetch().
|
|
128
|
+
*
|
|
129
|
+
* Eager decode is the warming step: createFromFetch parses the Flight stream,
|
|
130
|
+
* which resolves the route's client references and imports its JS chunks. The
|
|
131
|
+
* stored payload is reused as-is by navigation, so the click loads no new code.
|
|
132
|
+
*
|
|
133
|
+
* Control headers are NOT acted on here. A speculative prefetch must never
|
|
134
|
+
* reload the page or throw a redirect — if the response carries X-RSC-Reload
|
|
135
|
+
* or X-RSC-Redirect, we drop it (resolve null) and let the real navigation
|
|
136
|
+
* re-fetch and honor it.
|
|
85
137
|
*
|
|
86
138
|
* Inflight + storage key selection:
|
|
87
139
|
*
|
|
88
140
|
* - `forceSourceScope` (Link opted in with `prefetchKey=":source"`): single
|
|
89
|
-
* inflight registration under `sourceKey`;
|
|
90
|
-
*
|
|
141
|
+
* inflight registration under `sourceKey`; entry stored under `sourceKey`.
|
|
142
|
+
* No wildcard leak is possible.
|
|
91
143
|
*
|
|
92
144
|
* - Otherwise: dual inflight registration under both `wildcardKey` and
|
|
93
145
|
* `sourceKey` so same-source navigations adopt directly via their own
|
|
94
146
|
* source key. Storage key is chosen at response time from the
|
|
95
147
|
* `X-RSC-Prefetch-Scope` header — `"source"` → `sourceKey` (intercept
|
|
96
|
-
* modals etc.), anything else → `wildcardKey`.
|
|
97
|
-
* that adopted via `wildcardKey`
|
|
98
|
-
*
|
|
148
|
+
* modals etc.), anything else → `wildcardKey`. The entry records its scope
|
|
149
|
+
* so cross-source navigations that adopted via `wildcardKey` can bail out
|
|
150
|
+
* in `navigation-client.ts` when the adopted entry turns out source-scoped.
|
|
99
151
|
*/
|
|
100
152
|
function executePrefetchFetch(
|
|
101
153
|
wildcardKey: string,
|
|
102
154
|
sourceKey: string,
|
|
103
155
|
fetchUrl: string,
|
|
104
156
|
forceSourceScope: boolean,
|
|
157
|
+
expectedRouterId?: string,
|
|
105
158
|
signal?: AbortSignal,
|
|
106
|
-
): Promise<
|
|
159
|
+
): Promise<DecodedPrefetch | null> {
|
|
107
160
|
const gen = currentGeneration();
|
|
108
161
|
const inflightKeys = forceSourceScope
|
|
109
162
|
? [sourceKey]
|
|
110
163
|
: [wildcardKey, sourceKey];
|
|
111
164
|
for (const k of inflightKeys) markPrefetchInflight(k);
|
|
112
165
|
|
|
113
|
-
|
|
166
|
+
// Always layer a stall timeout. It covers BOTH "no response ever arrives"
|
|
167
|
+
// (strands the inflight key) AND "the body stalls after headers" (leaves a
|
|
168
|
+
// published entry whose payload/streamComplete never settle, that future
|
|
169
|
+
// prefetches dedupe against and navigation awaits forever). Applies to the
|
|
170
|
+
// hover/direct path (no caller signal) and the queue-driven path (whose caller
|
|
171
|
+
// signal only aborts on navigation, never on a stall). On fire it aborts the
|
|
172
|
+
// fetch/stream and evicts the published entry if one exists; it is NOT cleared
|
|
173
|
+
// when headers arrive (see below) — it is cleared when the stream completes, or
|
|
174
|
+
// in `.finally()` for paths that publish no streaming entry.
|
|
175
|
+
let publishedKey: string | undefined;
|
|
176
|
+
let publishedEntry: DecodedPrefetch | undefined;
|
|
177
|
+
const timeoutController = new AbortController();
|
|
178
|
+
const timeoutId: ReturnType<typeof setTimeout> = setTimeout(() => {
|
|
179
|
+
timeoutController.abort();
|
|
180
|
+
// Body stalled after headers: evict the published-but-never-settling entry
|
|
181
|
+
// so future prefetches/navigation refetch. Identity-guarded (pass the exact
|
|
182
|
+
// entry) so a fresh entry republished under the same key — after this one was
|
|
183
|
+
// consumed — is NOT dropped. The abort cancels the tee (its finally resolves
|
|
184
|
+
// streamComplete) and rejects the eager decode.
|
|
185
|
+
if (publishedKey !== undefined && publishedEntry !== undefined) {
|
|
186
|
+
removePrefetch(publishedKey, publishedEntry);
|
|
187
|
+
}
|
|
188
|
+
}, PREFETCH_FETCH_TIMEOUT_MS);
|
|
189
|
+
let effectiveSignal: AbortSignal;
|
|
190
|
+
if (!signal) {
|
|
191
|
+
effectiveSignal = timeoutController.signal;
|
|
192
|
+
} else if (typeof AbortSignal.any === "function") {
|
|
193
|
+
// Combine the caller's signal (navigation-abort) with the timeout so either
|
|
194
|
+
// can settle the fetch.
|
|
195
|
+
effectiveSignal = AbortSignal.any([signal, timeoutController.signal]);
|
|
196
|
+
} else {
|
|
197
|
+
// Legacy runtime without AbortSignal.any: forward the caller's abort onto the
|
|
198
|
+
// timeout controller so a single signal carries both reasons.
|
|
199
|
+
effectiveSignal = timeoutController.signal;
|
|
200
|
+
if (signal.aborted) timeoutController.abort();
|
|
201
|
+
else
|
|
202
|
+
signal.addEventListener("abort", () => timeoutController.abort(), {
|
|
203
|
+
once: true,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const promise: Promise<DecodedPrefetch | null> = fetch(fetchUrl, {
|
|
114
208
|
priority: "low" as RequestPriority,
|
|
115
|
-
|
|
209
|
+
// During an action's flight the state is not rotated, so the old
|
|
210
|
+
// X-Rango-State still matches the Vary-keyed HTTP-cache entry; bypass it so
|
|
211
|
+
// a prefetch fetches fresh rather than warming the map with stale bytes (the
|
|
212
|
+
// fence's HTTP-cache-bypass requirement applies to prefetch as well as
|
|
213
|
+
// navigation fetches).
|
|
214
|
+
...(isActionFenceActive() && { cache: "no-store" as RequestCache }),
|
|
215
|
+
signal: effectiveSignal,
|
|
116
216
|
headers: {
|
|
117
217
|
"X-Rango-State": getRangoState(),
|
|
118
218
|
"X-RSC-Router-Client-Path": window.location.href,
|
|
@@ -120,29 +220,97 @@ function executePrefetchFetch(
|
|
|
120
220
|
},
|
|
121
221
|
})
|
|
122
222
|
.then((response) => {
|
|
123
|
-
if (!response.ok) return null;
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const scope = response.headers.get("x-rsc-prefetch-scope");
|
|
138
|
-
storageKey = scope === "source" ? sourceKey : wildcardKey;
|
|
223
|
+
if (!response.ok || !decoder) return null;
|
|
224
|
+
// Control headers mean this response is stale (reload) or redirecting.
|
|
225
|
+
// Don't warm it — drop so navigation re-fetches and acts on the header.
|
|
226
|
+
if (
|
|
227
|
+
response.headers.has("X-RSC-Reload") ||
|
|
228
|
+
response.headers.has("X-RSC-Redirect")
|
|
229
|
+
) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
// Integrity check: never warm (or decode/import the chunks of) a foreign
|
|
233
|
+
// app's payload. A speculative prefetch must never reload — just drop it;
|
|
234
|
+
// navigation re-fetches and the server steers it.
|
|
235
|
+
if (isForeignRouterId(response, expectedRouterId)) {
|
|
236
|
+
return null;
|
|
139
237
|
}
|
|
140
|
-
|
|
141
|
-
|
|
238
|
+
|
|
239
|
+
const scope: "source" | "wildcard" =
|
|
240
|
+
forceSourceScope ||
|
|
241
|
+
response.headers.get("x-rsc-prefetch-scope") === "source"
|
|
242
|
+
? "source"
|
|
243
|
+
: "wildcard";
|
|
244
|
+
const storageKey = scope === "source" ? sourceKey : wildcardKey;
|
|
245
|
+
|
|
246
|
+
// Track stream completion off a tee so navigation's scroll/revalidation
|
|
247
|
+
// gating matches the fresh-fetch path; decode the other branch. The
|
|
248
|
+
// completion callback reports whether the stream ended on a clean EOF
|
|
249
|
+
// (true) or was aborted/errored (false) — only a clean end can mark the
|
|
250
|
+
// entry complete (see below).
|
|
251
|
+
let resolveStreamComplete!: () => void;
|
|
252
|
+
let endedCleanly = false;
|
|
253
|
+
const streamComplete = new Promise<void>((resolve) => {
|
|
254
|
+
resolveStreamComplete = resolve;
|
|
255
|
+
});
|
|
256
|
+
const tracked = teeWithCompletion(
|
|
257
|
+
response,
|
|
258
|
+
(clean) => {
|
|
259
|
+
endedCleanly = clean;
|
|
260
|
+
resolveStreamComplete();
|
|
261
|
+
},
|
|
262
|
+
effectiveSignal,
|
|
263
|
+
// Speculative prefetch: a never-consumed/aborted stream error is benign.
|
|
264
|
+
true,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// Eager decode: parsing the Flight stream imports the route's client
|
|
268
|
+
// chunks now, not on click.
|
|
269
|
+
const payload = decoder(Promise.resolve(tracked));
|
|
270
|
+
// Mark handled so an unconsumed prefetch decode error stays quiet; the
|
|
271
|
+
// error is still surfaced to navigation if it consumes the entry.
|
|
272
|
+
payload.catch(() => {});
|
|
273
|
+
|
|
274
|
+
const entry: DecodedPrefetch = {
|
|
275
|
+
payload,
|
|
276
|
+
streamComplete,
|
|
277
|
+
scope,
|
|
278
|
+
complete: false,
|
|
279
|
+
};
|
|
280
|
+
storePrefetch(storageKey, entry, gen);
|
|
281
|
+
// The stall timeout now owns the body stream: arm eviction (publishedKey)
|
|
282
|
+
// and clear the timer once the stream completes. The tee's finally resolves
|
|
283
|
+
// streamComplete on normal completion AND on abort, so a healthy body pays
|
|
284
|
+
// no lingering timer while a stalled one is evicted when the timer fires.
|
|
285
|
+
publishedKey = storageKey;
|
|
286
|
+
publishedEntry = entry;
|
|
287
|
+
// Evict a broken prefetch IMMEDIATELY on the earliest failure signal — do not
|
|
288
|
+
// wait for both branches to settle. A decode that rejects while the tracking
|
|
289
|
+
// stream is still draining (or hung) would otherwise leave the rejected payload
|
|
290
|
+
// consumable (navigation reads entry.payload regardless of `complete`) until EOF
|
|
291
|
+
// or the stall timeout. removePrefetch is identity-guarded, so a fresh entry
|
|
292
|
+
// republished under the same key is never dropped, and a double call is a no-op.
|
|
293
|
+
payload.catch(() => removePrefetch(storageKey, entry));
|
|
294
|
+
streamComplete.then(() => {
|
|
295
|
+
if (!endedCleanly) removePrefetch(storageKey, entry);
|
|
296
|
+
});
|
|
297
|
+
// Mark complete ONLY on a fully-healthy prefetch (decode resolved AND clean EOF).
|
|
298
|
+
Promise.allSettled([payload, streamComplete]).then(([decode]) => {
|
|
299
|
+
if (decode.status === "fulfilled" && endedCleanly) {
|
|
300
|
+
entry.complete = true;
|
|
301
|
+
}
|
|
302
|
+
clearTimeout(timeoutId);
|
|
303
|
+
});
|
|
304
|
+
return entry;
|
|
142
305
|
})
|
|
143
306
|
.catch(() => null)
|
|
144
307
|
.finally(() => {
|
|
145
308
|
clearPrefetchInflight(inflightKeys[0]!);
|
|
309
|
+
// Clear the stall timer here ONLY for paths that published no streaming
|
|
310
|
+
// entry (null return / fetch error / abort): the operation is fully done.
|
|
311
|
+
// When an entry WAS published, the timer stays armed to bound the body
|
|
312
|
+
// stream and is cleared on streamComplete (above) or on fire (eviction).
|
|
313
|
+
if (publishedKey === undefined) clearTimeout(timeoutId);
|
|
146
314
|
});
|
|
147
315
|
|
|
148
316
|
setInflightPromiseWithAliases(inflightKeys, promise);
|
|
@@ -223,6 +391,7 @@ export function prefetchDirect(
|
|
|
223
391
|
sourceKey,
|
|
224
392
|
targetUrl.toString(),
|
|
225
393
|
forceSourceScope,
|
|
394
|
+
routerId,
|
|
226
395
|
);
|
|
227
396
|
}
|
|
228
397
|
|
|
@@ -275,6 +444,7 @@ export function prefetchQueued(
|
|
|
275
444
|
sourceKey,
|
|
276
445
|
fetchUrlStr,
|
|
277
446
|
forceSourceScope,
|
|
447
|
+
routerId,
|
|
278
448
|
signal,
|
|
279
449
|
).then(() => {});
|
|
280
450
|
});
|
|
@@ -71,10 +71,13 @@ function scheduleDrain(): void {
|
|
|
71
71
|
Promise.race([waitForViewportImages(), wait(IMAGE_WAIT_TIMEOUT)]),
|
|
72
72
|
)
|
|
73
73
|
.then(() => {
|
|
74
|
-
|
|
75
|
-
//
|
|
76
|
-
//
|
|
74
|
+
// Stale drain: a cancel/abort happened while we were waiting, and a fresh
|
|
75
|
+
// scheduleDrain may already own drainScheduled for the new generation.
|
|
76
|
+
// Bail WITHOUT clearing the flag so we don't clobber the live wait's
|
|
77
|
+
// single-in-flight-drain coalescing (clearing it here would let the next
|
|
78
|
+
// enqueue start a third overlapping wait).
|
|
77
79
|
if (gen !== drainGeneration) return;
|
|
80
|
+
drainScheduled = false;
|
|
78
81
|
if (queue.length > 0) drain();
|
|
79
82
|
});
|
|
80
83
|
}
|