@rangojs/router 0.0.0-experimental.9c9afef3 → 0.0.0-experimental.a014d2b7
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 +245 -49
- package/dist/bin/rango.js +440 -133
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3373 -1176
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- 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 +364 -0
- package/skills/hooks/SKILL.md +250 -30
- package/skills/host-router/SKILL.md +83 -23
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +87 -18
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +235 -9
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +771 -0
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +77 -7
- package/skills/prerender/SKILL.md +172 -125
- package/skills/rango/SKILL.md +251 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +123 -48
- package/skills/route/SKILL.md +70 -5
- package/skills/router-setup/SKILL.md +65 -8
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/streams-and-websockets/SKILL.md +283 -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 +1 -66
- 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/app-version.ts +14 -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 +131 -30
- package/src/browser/navigation-client.ts +186 -100
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +157 -74
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +165 -112
- package/src/browser/prefetch/cache.ts +205 -62
- package/src/browser/prefetch/fetch.ts +347 -39
- package/src/browser/prefetch/queue.ts +42 -8
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +102 -15
- package/src/browser/react/NavigationProvider.tsx +295 -119
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- 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 +23 -69
- 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 +32 -7
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +46 -11
- 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 +114 -24
- package/src/browser/scroll-restoration.ts +37 -22
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +222 -72
- package/src/browser/types.ts +102 -12
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +165 -36
- 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 +125 -24
- 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 +260 -94
- package/src/build/route-types/scan-filter.ts +9 -2
- 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 +119 -290
- 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 +70 -22
- 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 +99 -13
- package/src/index.ts +139 -19
- 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 +37 -41
- package/src/prerender.ts +198 -82
- 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 +62 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +19 -77
- package/src/route-definition/dsl-helpers.ts +461 -304
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +143 -69
- package/src/route-definition/index.ts +4 -2
- package/src/route-definition/redirect.ts +51 -10
- package/src/route-definition/resolve-handler-use.ts +160 -0
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +37 -46
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +164 -17
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +52 -31
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +48 -24
- package/src/router/lazy-includes.ts +15 -52
- package/src/router/loader-resolution.ts +268 -56
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +40 -42
- package/src/router/match-api.ts +124 -204
- 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 +170 -276
- 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 +45 -14
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +87 -39
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +7 -140
- package/src/router/middleware.ts +266 -169
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +132 -90
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +195 -56
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +3 -28
- package/src/router/router-interfaces.ts +115 -35
- package/src/router/router-options.ts +172 -15
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +162 -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 +351 -321
- 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 +163 -59
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +157 -54
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +655 -529
- package/src/rsc/helpers.ts +168 -46
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +122 -31
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +131 -14
- 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 +99 -189
- package/src/rsc/rsc-rendering.ts +109 -74
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +287 -115
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +29 -9
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +149 -0
- package/src/segment-system.tsx +236 -202
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +224 -52
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +40 -38
- package/src/server/loader-registry.ts +38 -46
- package/src/server/request-context.ts +401 -173
- package/src/ssr/index.tsx +24 -16
- package/src/static-handler.ts +27 -18
- 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 +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +125 -71
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +40 -11
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +12 -7
- package/src/types/segments.ts +136 -15
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +68 -18
- package/src/urls/path-helper.ts +57 -111
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -19
- package/src/use-loader.tsx +346 -89
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +36 -38
- package/src/vite/discovery/discover-routers.ts +130 -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 +214 -132
- 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 +57 -4
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +6 -0
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +178 -5
- 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-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
- package/src/vite/plugins/expose-action-id.ts +48 -95
- package/src/vite/plugins/expose-id-utils.ts +96 -51
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
- 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 +553 -317
- package/src/vite/plugins/performance-tracks.ts +64 -170
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- 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 +118 -114
- package/src/vite/router-discovery.ts +941 -142
- 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 +81 -34
- package/src/vite/utils/shared-utils.ts +92 -42
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/debug-channel.ts +0 -93
- 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
|
@@ -3,26 +3,87 @@
|
|
|
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 {
|
|
15
19
|
buildPrefetchKey,
|
|
20
|
+
buildSourceKey,
|
|
16
21
|
hasPrefetch,
|
|
17
22
|
markPrefetchInflight,
|
|
18
|
-
|
|
23
|
+
setInflightPromiseWithAliases,
|
|
19
24
|
storePrefetch,
|
|
25
|
+
removePrefetch,
|
|
20
26
|
clearPrefetchInflight,
|
|
21
27
|
currentGeneration,
|
|
28
|
+
type DecodedPrefetch,
|
|
22
29
|
} from "./cache.js";
|
|
23
30
|
import { getRangoState } from "../rango-state.js";
|
|
31
|
+
import { isActionFenceActive } from "../action-fence.js";
|
|
24
32
|
import { enqueuePrefetch } from "./queue.js";
|
|
25
33
|
import { shouldPrefetch } from "./policy.js";
|
|
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
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if a URL resolves to the current page (same pathname + search).
|
|
73
|
+
* Used to prevent same-page prefetching, which produces a trivial diff
|
|
74
|
+
* that would corrupt the (default wildcard) prefetch cache entry.
|
|
75
|
+
*/
|
|
76
|
+
function isSamePage(url: string): boolean {
|
|
77
|
+
try {
|
|
78
|
+
const target = new URL(url, window.location.origin);
|
|
79
|
+
return (
|
|
80
|
+
target.pathname + target.search ===
|
|
81
|
+
window.location.pathname + window.location.search
|
|
82
|
+
);
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
26
87
|
|
|
27
88
|
/**
|
|
28
89
|
* Build an RSC partial URL for prefetching.
|
|
@@ -34,6 +95,7 @@ function buildPrefetchUrl(
|
|
|
34
95
|
url: string,
|
|
35
96
|
segmentIds: string[],
|
|
36
97
|
version?: string,
|
|
98
|
+
routerId?: string,
|
|
37
99
|
): URL | null {
|
|
38
100
|
let targetUrl: URL;
|
|
39
101
|
try {
|
|
@@ -51,26 +113,106 @@ function buildPrefetchUrl(
|
|
|
51
113
|
if (version) {
|
|
52
114
|
targetUrl.searchParams.set("_rsc_v", version);
|
|
53
115
|
}
|
|
116
|
+
if (routerId) {
|
|
117
|
+
targetUrl.searchParams.set("_rsc_rid", routerId);
|
|
118
|
+
}
|
|
54
119
|
return targetUrl;
|
|
55
120
|
}
|
|
56
121
|
|
|
57
122
|
/**
|
|
58
|
-
* Core prefetch fetch logic. Fetches the response,
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* 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.
|
|
137
|
+
*
|
|
138
|
+
* Inflight + storage key selection:
|
|
139
|
+
*
|
|
140
|
+
* - `forceSourceScope` (Link opted in with `prefetchKey=":source"`): single
|
|
141
|
+
* inflight registration under `sourceKey`; entry stored under `sourceKey`.
|
|
142
|
+
* No wildcard leak is possible.
|
|
143
|
+
*
|
|
144
|
+
* - Otherwise: dual inflight registration under both `wildcardKey` and
|
|
145
|
+
* `sourceKey` so same-source navigations adopt directly via their own
|
|
146
|
+
* source key. Storage key is chosen at response time from the
|
|
147
|
+
* `X-RSC-Prefetch-Scope` header — `"source"` → `sourceKey` (intercept
|
|
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.
|
|
62
151
|
*/
|
|
63
152
|
function executePrefetchFetch(
|
|
64
|
-
|
|
153
|
+
wildcardKey: string,
|
|
154
|
+
sourceKey: string,
|
|
65
155
|
fetchUrl: string,
|
|
156
|
+
forceSourceScope: boolean,
|
|
157
|
+
expectedRouterId?: string,
|
|
66
158
|
signal?: AbortSignal,
|
|
67
|
-
): Promise<
|
|
159
|
+
): Promise<DecodedPrefetch | null> {
|
|
68
160
|
const gen = currentGeneration();
|
|
69
|
-
|
|
161
|
+
const inflightKeys = forceSourceScope
|
|
162
|
+
? [sourceKey]
|
|
163
|
+
: [wildcardKey, sourceKey];
|
|
164
|
+
for (const k of inflightKeys) markPrefetchInflight(k);
|
|
165
|
+
|
|
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
|
+
}
|
|
70
206
|
|
|
71
|
-
const promise: Promise<
|
|
207
|
+
const promise: Promise<DecodedPrefetch | null> = fetch(fetchUrl, {
|
|
72
208
|
priority: "low" as RequestPriority,
|
|
73
|
-
|
|
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,
|
|
74
216
|
headers: {
|
|
75
217
|
"X-Rango-State": getRangoState(),
|
|
76
218
|
"X-RSC-Router-Client-Path": window.location.href,
|
|
@@ -78,67 +220,233 @@ function executePrefetchFetch(
|
|
|
78
220
|
},
|
|
79
221
|
})
|
|
80
222
|
.then((response) => {
|
|
81
|
-
if (!response.ok) return null;
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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;
|
|
237
|
+
}
|
|
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,
|
|
90
279
|
};
|
|
91
|
-
storePrefetch(
|
|
92
|
-
|
|
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;
|
|
93
305
|
})
|
|
94
306
|
.catch(() => null)
|
|
95
307
|
.finally(() => {
|
|
96
|
-
clearPrefetchInflight(
|
|
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);
|
|
97
314
|
});
|
|
98
315
|
|
|
99
|
-
|
|
316
|
+
setInflightPromiseWithAliases(inflightKeys, promise);
|
|
100
317
|
return promise;
|
|
101
318
|
}
|
|
102
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Dedup check for prefetch entry presence.
|
|
322
|
+
*
|
|
323
|
+
* Forced `:source` must NOT dedupe against a pre-existing wildcard entry —
|
|
324
|
+
* otherwise the source slot would stay unpopulated and navigation from
|
|
325
|
+
* this source would fall through to the (potentially wrong) wildcard
|
|
326
|
+
* response, defeating the opt-out.
|
|
327
|
+
*/
|
|
328
|
+
function hasPrefetchHit(
|
|
329
|
+
forceSourceScope: boolean,
|
|
330
|
+
wildcardKey: string,
|
|
331
|
+
sourceKey: string,
|
|
332
|
+
): boolean {
|
|
333
|
+
return forceSourceScope
|
|
334
|
+
? hasPrefetch(sourceKey)
|
|
335
|
+
: hasPrefetch(wildcardKey) || hasPrefetch(sourceKey);
|
|
336
|
+
}
|
|
337
|
+
|
|
103
338
|
/**
|
|
104
339
|
* Prefetch (direct): fetch with low priority and store in in-memory cache.
|
|
105
340
|
* Used by hover strategy -- fires immediately without queueing.
|
|
341
|
+
*
|
|
342
|
+
* By default the wildcard key (Rango-state-keyed) is used for inflight
|
|
343
|
+
* dedup and for responses that are not source-sensitive; source-scoped
|
|
344
|
+
* storage is automatic when the server emits `X-RSC-Prefetch-Scope: source`.
|
|
345
|
+
*
|
|
346
|
+
* Pass `prefetchKey=":source"` to force source-scoped inflight + storage
|
|
347
|
+
* (e.g. when the target uses a custom `revalidate()` that reads
|
|
348
|
+
* `currentUrl` and the wildcard slot would serve the wrong diff).
|
|
106
349
|
*/
|
|
107
350
|
export function prefetchDirect(
|
|
108
351
|
url: string,
|
|
109
352
|
segmentIds: string[],
|
|
110
353
|
version?: string,
|
|
354
|
+
routerId?: string,
|
|
355
|
+
prefetchKey?: ":source",
|
|
111
356
|
): void {
|
|
112
357
|
if (!shouldPrefetch()) return;
|
|
113
358
|
|
|
114
|
-
const targetUrl = buildPrefetchUrl(url, segmentIds, version);
|
|
359
|
+
const targetUrl = buildPrefetchUrl(url, segmentIds, version, routerId);
|
|
115
360
|
if (!targetUrl) return;
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
361
|
+
const forceSourceScope = prefetchKey === ":source";
|
|
362
|
+
// Skip same-page prefetch — a same-page diff is trivial and would corrupt
|
|
363
|
+
// the wildcard cache entry used for cross-page navigation.
|
|
364
|
+
// When `:source` is forced the entry is source-scoped (single-aliased to
|
|
365
|
+
// itself), so it cannot poison any shared slot — allow it.
|
|
366
|
+
if (!forceSourceScope && isSamePage(url)) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const sourceHref = window.location.href;
|
|
370
|
+
const rangoState = getRangoState();
|
|
371
|
+
const wildcardKey = buildPrefetchKey(rangoState, targetUrl);
|
|
372
|
+
const sourceKey = buildSourceKey(rangoState, sourceHref, targetUrl);
|
|
373
|
+
if (hasPrefetchHit(forceSourceScope, wildcardKey, sourceKey)) {
|
|
374
|
+
debugLog("[prefetch] direct dedup (key already exists)", {
|
|
375
|
+
url,
|
|
376
|
+
wildcardKey,
|
|
377
|
+
sourceKey,
|
|
378
|
+
forceSourceScope,
|
|
379
|
+
});
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
debugLog("[prefetch] direct fetch", {
|
|
383
|
+
url,
|
|
384
|
+
wildcardKey,
|
|
385
|
+
sourceKey,
|
|
386
|
+
source: sourceHref,
|
|
387
|
+
forceSourceScope,
|
|
388
|
+
});
|
|
389
|
+
executePrefetchFetch(
|
|
390
|
+
wildcardKey,
|
|
391
|
+
sourceKey,
|
|
392
|
+
targetUrl.toString(),
|
|
393
|
+
forceSourceScope,
|
|
394
|
+
routerId,
|
|
395
|
+
);
|
|
119
396
|
}
|
|
120
397
|
|
|
121
398
|
/**
|
|
122
399
|
* Prefetch (queued): goes through the concurrency-limited queue.
|
|
123
400
|
* Used by viewport/render strategies to avoid flooding the server.
|
|
124
|
-
* Returns the
|
|
401
|
+
* Returns the inflight key (wildcard by default, source-scoped when
|
|
402
|
+
* `prefetchKey=":source"` is passed).
|
|
125
403
|
*/
|
|
126
404
|
export function prefetchQueued(
|
|
127
405
|
url: string,
|
|
128
406
|
segmentIds: string[],
|
|
129
407
|
version?: string,
|
|
408
|
+
routerId?: string,
|
|
409
|
+
prefetchKey?: ":source",
|
|
130
410
|
): string {
|
|
131
411
|
if (!shouldPrefetch()) return "";
|
|
132
|
-
const targetUrl = buildPrefetchUrl(url, segmentIds, version);
|
|
412
|
+
const targetUrl = buildPrefetchUrl(url, segmentIds, version, routerId);
|
|
133
413
|
if (!targetUrl) return "";
|
|
134
|
-
const
|
|
135
|
-
if (
|
|
414
|
+
const forceSourceScope = prefetchKey === ":source";
|
|
415
|
+
if (!forceSourceScope && isSamePage(url)) {
|
|
416
|
+
return "";
|
|
417
|
+
}
|
|
418
|
+
const sourceHref = window.location.href;
|
|
419
|
+
const rangoState = getRangoState();
|
|
420
|
+
const wildcardKey = buildPrefetchKey(rangoState, targetUrl);
|
|
421
|
+
const sourceKey = buildSourceKey(rangoState, sourceHref, targetUrl);
|
|
422
|
+
const queueKey = forceSourceScope ? sourceKey : wildcardKey;
|
|
423
|
+
if (hasPrefetchHit(forceSourceScope, wildcardKey, sourceKey)) {
|
|
424
|
+
debugLog("[prefetch] queued dedup (key already exists)", {
|
|
425
|
+
url,
|
|
426
|
+
wildcardKey,
|
|
427
|
+
sourceKey,
|
|
428
|
+
forceSourceScope,
|
|
429
|
+
});
|
|
430
|
+
return queueKey;
|
|
431
|
+
}
|
|
136
432
|
const fetchUrlStr = targetUrl.toString();
|
|
137
|
-
enqueuePrefetch(
|
|
433
|
+
enqueuePrefetch(queueKey, (signal) => {
|
|
138
434
|
// Re-check at execution time: a hover-triggered prefetchDirect may
|
|
139
435
|
// have started or completed this key while the item sat in the queue.
|
|
140
|
-
if (
|
|
141
|
-
|
|
436
|
+
if (hasPrefetchHit(forceSourceScope, wildcardKey, sourceKey)) {
|
|
437
|
+
return Promise.resolve();
|
|
438
|
+
}
|
|
439
|
+
if (!forceSourceScope && isSamePage(url)) {
|
|
440
|
+
return Promise.resolve();
|
|
441
|
+
}
|
|
442
|
+
return executePrefetchFetch(
|
|
443
|
+
wildcardKey,
|
|
444
|
+
sourceKey,
|
|
445
|
+
fetchUrlStr,
|
|
446
|
+
forceSourceScope,
|
|
447
|
+
routerId,
|
|
448
|
+
signal,
|
|
449
|
+
).then(() => {});
|
|
142
450
|
});
|
|
143
|
-
return
|
|
451
|
+
return queueKey;
|
|
144
452
|
}
|
|
@@ -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
|
}
|
|
@@ -108,10 +111,29 @@ export function enqueuePrefetch(
|
|
|
108
111
|
scheduleDrain();
|
|
109
112
|
}
|
|
110
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Normalize a URL-like string for keep-alive matching: parse against a
|
|
116
|
+
* placeholder origin and strip internal `_rsc_*` query params. Returns
|
|
117
|
+
* `pathname + search` so comparisons ignore hash and the internal params
|
|
118
|
+
* that prefetch appends to targets (`_rsc_partial`, `_rsc_segments`,
|
|
119
|
+
* `_rsc_v`, `_rsc_rid`, `_rsc_stale`).
|
|
120
|
+
*/
|
|
121
|
+
function normalizeForMatch(urlish: string): string {
|
|
122
|
+
try {
|
|
123
|
+
const u = new URL(urlish, "http://placeholder");
|
|
124
|
+
for (const k of [...u.searchParams.keys()]) {
|
|
125
|
+
if (k.startsWith("_rsc_")) u.searchParams.delete(k);
|
|
126
|
+
}
|
|
127
|
+
return u.pathname + u.search;
|
|
128
|
+
} catch {
|
|
129
|
+
return urlish;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
111
133
|
/**
|
|
112
134
|
* Cancel queued prefetches and abort in-flight ones that don't match
|
|
113
135
|
* the current navigation target. If `keepUrl` is provided, the
|
|
114
|
-
* executing prefetch whose key
|
|
136
|
+
* executing prefetch whose key targets that URL is kept alive so
|
|
115
137
|
* navigation can reuse its response via consumeInflightPrefetch.
|
|
116
138
|
*
|
|
117
139
|
* Called when a navigation starts via the NavigationProvider's
|
|
@@ -124,11 +146,23 @@ export function cancelAllPrefetches(keepUrl?: string | null): void {
|
|
|
124
146
|
drainGeneration++;
|
|
125
147
|
|
|
126
148
|
// Abort in-flight prefetches that aren't for the navigation target.
|
|
127
|
-
//
|
|
128
|
-
//
|
|
149
|
+
// Key shapes (see prefetch/cache.ts buildPrefetchKey):
|
|
150
|
+
// wildcard: "rangoState\0/target?..."
|
|
151
|
+
// source-scoped: "rangoState\0sourceHref\0/target?..."
|
|
152
|
+
// The target portion is always the final \0-delimited segment and
|
|
153
|
+
// includes internal `_rsc_*` params (from buildPrefetchUrl); keepUrl
|
|
154
|
+
// comes from NavigationProvider's pendingUrl which is the bare
|
|
155
|
+
// navigation target. Normalize both sides before comparing.
|
|
156
|
+
const normalizedKeep = keepUrl ? normalizeForMatch(keepUrl) : null;
|
|
129
157
|
for (const [key, ac] of abortControllers) {
|
|
130
|
-
const
|
|
131
|
-
|
|
158
|
+
const lastNul = key.lastIndexOf("\0");
|
|
159
|
+
const target = lastNul >= 0 ? key.substring(lastNul + 1) : "";
|
|
160
|
+
if (
|
|
161
|
+
normalizedKeep &&
|
|
162
|
+
target &&
|
|
163
|
+
normalizeForMatch(target) === normalizedKeep
|
|
164
|
+
)
|
|
165
|
+
continue;
|
|
132
166
|
ac.abort();
|
|
133
167
|
abortControllers.delete(key);
|
|
134
168
|
if (executing.delete(key)) {
|