@rangojs/router 0.0.0-experimental.97 → 0.0.0-experimental.98914650
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/README.md +24 -9
- package/dist/bin/rango.js +157 -63
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +1584 -639
- package/package.json +71 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +60 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +222 -30
- package/skills/caching/SKILL.md +263 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +3 -1
- package/skills/hooks/SKILL.md +235 -28
- package/skills/host-router/SKILL.md +122 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +29 -5
- package/skills/layout/SKILL.md +13 -9
- package/skills/links/SKILL.md +173 -17
- package/skills/loader/SKILL.md +170 -23
- package/skills/middleware/SKILL.md +16 -10
- package/skills/migrate-nextjs/SKILL.md +38 -16
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +11 -7
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +250 -25
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +114 -47
- package/skills/route/SKILL.md +42 -5
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +78 -42
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +124 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +92 -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 +121 -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 +120 -0
- package/skills/typesafety/SKILL.md +316 -26
- package/skills/use-cache/SKILL.md +36 -5
- package/skills/vercel/SKILL.md +107 -0
- package/skills/view-transitions/SKILL.md +294 -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 +14 -27
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +37 -143
- 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 +30 -59
- package/src/browser/navigation-client.ts +96 -84
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +32 -82
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/partial-update.ts +60 -127
- package/src/browser/prefetch/cache.ts +82 -72
- package/src/browser/prefetch/fetch.ts +108 -33
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +157 -115
- package/src/browser/react/Link.tsx +0 -2
- package/src/browser/react/NavigationProvider.tsx +41 -48
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +0 -2
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +166 -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-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +0 -3
- 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 +20 -5
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/response-adapter.ts +52 -1
- package/src/browser/rsc-router.tsx +70 -34
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +168 -44
- package/src/browser/types.ts +36 -21
- 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 +3 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +89 -10
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +122 -22
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +68 -28
- package/src/cache/cache-runtime.ts +134 -32
- package/src/cache/cache-scope.ts +100 -74
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2255 -238
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +61 -20
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +22 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +33 -100
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +799 -0
- package/src/client.rsc.tsx +6 -21
- package/src/client.tsx +25 -61
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +17 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +31 -23
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +7 -50
- 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 +63 -9
- package/src/index.ts +64 -9
- package/src/internal-debug.ts +2 -4
- 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/network-error-thrower.tsx +1 -6
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +61 -6
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +9 -0
- package/src/reverse.ts +65 -40
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +244 -281
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +40 -17
- package/src/route-definition/redirect.ts +43 -9
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +19 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -15
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +4 -41
- package/src/router/intercept-resolution.ts +14 -19
- package/src/router/lazy-includes.ts +9 -46
- package/src/router/loader-resolution.ts +91 -46
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +18 -29
- package/src/router/match-api.ts +0 -20
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +57 -58
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +150 -271
- package/src/router/match-middleware/cache-store.ts +3 -33
- 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 +31 -80
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +5 -112
- package/src/router/middleware.ts +118 -133
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +62 -67
- package/src/router/prerender-match.ts +99 -63
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +28 -62
- package/src/router/revalidation.ts +50 -56
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +0 -27
- package/src/router/router-interfaces.ts +68 -35
- package/src/router/router-options.ts +55 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +44 -63
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +40 -37
- package/src/router/segment-resolution/revalidation.ts +203 -285
- 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 +36 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +0 -3
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +87 -48
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +80 -41
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +83 -78
- package/src/rsc/helpers.ts +93 -5
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +12 -1
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +76 -62
- package/src/rsc/rsc-rendering.ts +41 -60
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +62 -67
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +10 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-loader-promise.ts +14 -2
- package/src/segment-system.tsx +199 -142
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +150 -51
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +165 -87
- package/src/ssr/index.tsx +14 -14
- 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 +618 -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 +232 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +348 -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 +330 -0
- package/src/testing/render-route.tsx +566 -0
- package/src/testing/run-loader.ts +378 -0
- package/src/testing/run-middleware.ts +205 -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 +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- 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 +97 -22
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +6 -3
- package/src/types/request-scope.ts +0 -19
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +18 -14
- package/src/urls/include-helper.ts +9 -56
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +19 -5
- package/src/urls/path-helper.ts +17 -106
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +20 -19
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +292 -107
- package/src/vite/debug.ts +1 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/discover-routers.ts +95 -82
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/prerender-collection.ts +26 -34
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/state.ts +39 -1
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +185 -10
- package/src/vite/plugins/cjs-to-esm.ts +3 -18
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +12 -11
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
- package/src/vite/plugins/expose-action-id.ts +4 -75
- package/src/vite/plugins/expose-id-utils.ts +3 -54
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
- package/src/vite/plugins/expose-internal-ids.ts +57 -67
- package/src/vite/plugins/performance-tracks.ts +9 -16
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +26 -49
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +2 -32
- package/src/vite/plugins/version-plugin.ts +32 -23
- package/src/vite/plugins/virtual-entries.ts +35 -17
- package/src/vite/rango.ts +148 -115
- package/src/vite/router-discovery.ts +220 -68
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -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 +1 -73
- package/src/vite/utils/prerender-utils.ts +0 -34
- package/src/vite/utils/shared-utils.ts +95 -43
- 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/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,42 @@
|
|
|
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
|
+
}
|
|
33
69
|
|
|
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
70
|
let cacheTTL = 300_000;
|
|
38
71
|
|
|
39
72
|
/**
|
|
@@ -55,31 +88,19 @@ export function isPrefetchCacheDisabled(): boolean {
|
|
|
55
88
|
const MAX_PREFETCH_CACHE_SIZE = 50;
|
|
56
89
|
|
|
57
90
|
interface PrefetchCacheEntry {
|
|
58
|
-
|
|
91
|
+
entry: DecodedPrefetch;
|
|
59
92
|
timestamp: number;
|
|
60
93
|
}
|
|
61
94
|
|
|
62
95
|
const cache = new Map<string, PrefetchCacheEntry>();
|
|
63
96
|
const inflight = new Set<string>();
|
|
64
97
|
|
|
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>>();
|
|
98
|
+
const inflightPromises = new Map<string, Promise<DecodedPrefetch | null>>();
|
|
71
99
|
|
|
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
100
|
const inflightAliases = new Map<string, string[]>();
|
|
79
101
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// response (the data may be stale due to a server action invalidation).
|
|
102
|
+
const adoptedKeys = new Set<string>();
|
|
103
|
+
|
|
83
104
|
let generation = 0;
|
|
84
105
|
|
|
85
106
|
/**
|
|
@@ -152,14 +173,14 @@ export function hasPrefetch(key: string): boolean {
|
|
|
152
173
|
}
|
|
153
174
|
|
|
154
175
|
/**
|
|
155
|
-
* Consume a cached prefetch
|
|
156
|
-
* One-time consumption: the entry is deleted after retrieval.
|
|
176
|
+
* Consume a cached, eagerly-decoded prefetch. Returns null if not found or
|
|
177
|
+
* expired. One-time consumption: the entry is deleted after retrieval.
|
|
157
178
|
* Returns null when caching is disabled (TTL <= 0).
|
|
158
179
|
*
|
|
159
180
|
* Does NOT check in-flight prefetches — use consumeInflightPrefetch()
|
|
160
|
-
* for that (returns a Promise instead of a
|
|
181
|
+
* for that (returns a Promise instead of a resolved entry).
|
|
161
182
|
*/
|
|
162
|
-
export function consumePrefetch(key: string):
|
|
183
|
+
export function consumePrefetch(key: string): DecodedPrefetch | null {
|
|
163
184
|
if (cacheTTL <= 0) return null;
|
|
164
185
|
const entry = cache.get(key);
|
|
165
186
|
if (!entry) return null;
|
|
@@ -168,13 +189,14 @@ export function consumePrefetch(key: string): Response | null {
|
|
|
168
189
|
return null;
|
|
169
190
|
}
|
|
170
191
|
cache.delete(key);
|
|
171
|
-
return entry.
|
|
192
|
+
return entry.entry;
|
|
172
193
|
}
|
|
173
194
|
|
|
174
195
|
/**
|
|
175
196
|
* 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
|
-
*
|
|
197
|
+
* in-flight for this key. The returned Promise resolves to the decoded
|
|
198
|
+
* prefetch entry (or null if the fetch failed/was aborted, or carried a
|
|
199
|
+
* control header the navigation must re-fetch to honor).
|
|
178
200
|
*
|
|
179
201
|
* One-time consumption: the promise entry is removed (along with any
|
|
180
202
|
* sibling aliases registered via `setInflightPromiseWithAliases`) so a
|
|
@@ -188,38 +210,51 @@ export function consumePrefetch(key: string): Response | null {
|
|
|
188
210
|
*/
|
|
189
211
|
export function consumeInflightPrefetch(
|
|
190
212
|
key: string,
|
|
191
|
-
): Promise<
|
|
213
|
+
): Promise<DecodedPrefetch | null> | null {
|
|
192
214
|
const promise = inflightPromises.get(key);
|
|
193
215
|
if (!promise) return null;
|
|
194
216
|
// 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
|
-
|
|
217
|
+
// adopt the same stream and race on the body, and mark every alias as
|
|
218
|
+
// adopted so the pending `storePrefetch` (which resolves later, after this
|
|
219
|
+
// adoption) does not leave the now-owned, single-use entry in the cache map.
|
|
220
|
+
// `inflightAliases` is intentionally preserved — `clearPrefetchInflight()` in
|
|
221
|
+
// the fetch's `.finally()` still needs it to clear every inflight flag and
|
|
222
|
+
// adopted marker; deleting here would strand the sibling's flag forever.
|
|
223
|
+
forEachAlias(key, (k) => {
|
|
224
|
+
inflightPromises.delete(k);
|
|
225
|
+
adoptedKeys.add(k);
|
|
226
|
+
});
|
|
200
227
|
return promise;
|
|
201
228
|
}
|
|
202
229
|
|
|
203
230
|
/**
|
|
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.
|
|
231
|
+
* Store an eagerly-decoded prefetch in the in-memory cache.
|
|
207
232
|
*
|
|
208
233
|
* Skips storage if the generation has changed since the fetch started
|
|
209
234
|
* (a server action invalidated the cache mid-flight).
|
|
210
235
|
*/
|
|
211
236
|
export function storePrefetch(
|
|
212
237
|
key: string,
|
|
213
|
-
|
|
238
|
+
entry: DecodedPrefetch,
|
|
214
239
|
fetchGeneration: number,
|
|
215
240
|
): void {
|
|
216
241
|
if (cacheTTL <= 0) return;
|
|
217
242
|
if (fetchGeneration !== generation) return;
|
|
218
243
|
|
|
244
|
+
// If a navigation already adopted this prefetch's in-flight promise, it owns
|
|
245
|
+
// the single-use entry (and has drained its handle generator). Do NOT also
|
|
246
|
+
// publish it to the cache map, or a later navigation would be served the
|
|
247
|
+
// exhausted entry and lose that route's handles. Clear the marker (under all
|
|
248
|
+
// aliases) now that the decision is made.
|
|
249
|
+
if (adoptedKeys.has(key)) {
|
|
250
|
+
forEachAlias(key, (k) => adoptedKeys.delete(k));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
219
254
|
// Evict expired entries
|
|
220
255
|
const now = Date.now();
|
|
221
|
-
for (const [k,
|
|
222
|
-
if (now -
|
|
256
|
+
for (const [k, cached] of cache) {
|
|
257
|
+
if (now - cached.timestamp > cacheTTL) {
|
|
223
258
|
cache.delete(k);
|
|
224
259
|
}
|
|
225
260
|
}
|
|
@@ -230,7 +265,7 @@ export function storePrefetch(
|
|
|
230
265
|
if (oldest) cache.delete(oldest);
|
|
231
266
|
}
|
|
232
267
|
|
|
233
|
-
cache.set(key, {
|
|
268
|
+
cache.set(key, { entry, timestamp: now });
|
|
234
269
|
}
|
|
235
270
|
|
|
236
271
|
/**
|
|
@@ -245,12 +280,9 @@ export function markPrefetchInflight(key: string): void {
|
|
|
245
280
|
inflight.add(key);
|
|
246
281
|
}
|
|
247
282
|
|
|
248
|
-
/**
|
|
249
|
-
* Store the in-flight Promise for a prefetch so navigation can reuse it.
|
|
250
|
-
*/
|
|
251
283
|
export function setInflightPromise(
|
|
252
284
|
key: string,
|
|
253
|
-
promise: Promise<
|
|
285
|
+
promise: Promise<DecodedPrefetch | null>,
|
|
254
286
|
): void {
|
|
255
287
|
inflightPromises.set(key, promise);
|
|
256
288
|
}
|
|
@@ -263,7 +295,7 @@ export function setInflightPromise(
|
|
|
263
295
|
*/
|
|
264
296
|
export function setInflightPromiseWithAliases(
|
|
265
297
|
keys: string[],
|
|
266
|
-
promise: Promise<
|
|
298
|
+
promise: Promise<DecodedPrefetch | null>,
|
|
267
299
|
): void {
|
|
268
300
|
for (const k of keys) {
|
|
269
301
|
inflightPromises.set(k, promise);
|
|
@@ -276,39 +308,17 @@ export function clearPrefetchInflight(key: string): void {
|
|
|
276
308
|
inflight.delete(k);
|
|
277
309
|
inflightPromises.delete(k);
|
|
278
310
|
inflightAliases.delete(k);
|
|
311
|
+
adoptedKeys.delete(k);
|
|
279
312
|
});
|
|
280
313
|
}
|
|
281
314
|
|
|
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
315
|
export function clearPrefetchCache(): void {
|
|
291
316
|
generation++;
|
|
292
317
|
inflight.clear();
|
|
293
318
|
inflightPromises.clear();
|
|
294
319
|
inflightAliases.clear();
|
|
320
|
+
adoptedKeys.clear();
|
|
295
321
|
cache.clear();
|
|
296
322
|
abortAllPrefetches();
|
|
297
323
|
invalidateRangoState();
|
|
298
324
|
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Drop all in-memory prefetch state for this tab without rotating rango-state.
|
|
302
|
-
*
|
|
303
|
-
* Use for local-only invalidations (e.g. app switch in this tab) where
|
|
304
|
-
* other tabs should NOT observe a state rotation. Unlike clearPrefetchCache,
|
|
305
|
-
* does not call invalidateRangoState, so the shared X-Rango-State token
|
|
306
|
-
* stays intact and siblings in the old app keep their prefetches.
|
|
307
|
-
*/
|
|
308
|
-
export function clearPrefetchCacheLocal(): void {
|
|
309
|
-
generation++;
|
|
310
|
-
inflight.clear();
|
|
311
|
-
inflightPromises.clear();
|
|
312
|
-
cache.clear();
|
|
313
|
-
abortAllPrefetches();
|
|
314
|
-
}
|
|
@@ -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 {
|
|
@@ -20,11 +24,35 @@ import {
|
|
|
20
24
|
storePrefetch,
|
|
21
25
|
clearPrefetchInflight,
|
|
22
26
|
currentGeneration,
|
|
27
|
+
type DecodedPrefetch,
|
|
23
28
|
} from "./cache.js";
|
|
24
29
|
import { getRangoState } from "../rango-state.js";
|
|
30
|
+
import { isActionFenceActive } from "../action-fence.js";
|
|
25
31
|
import { enqueuePrefetch } from "./queue.js";
|
|
26
32
|
import { shouldPrefetch } from "./policy.js";
|
|
27
33
|
import { debugLog } from "../logging.js";
|
|
34
|
+
import { teeWithCompletion, isForeignRouterId } from "../response-adapter.js";
|
|
35
|
+
import type { RscPayload } from "../types.js";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Decoder injected at app startup (see setPrefetchDecoder). This is
|
|
39
|
+
* `deps.createFromFetch` — decoupled from the RSC runtime exactly like the
|
|
40
|
+
* navigation client. Prefetch decodes through it so the route's client chunks
|
|
41
|
+
* are pulled during the prefetch, not on click.
|
|
42
|
+
*/
|
|
43
|
+
type PrefetchDecoder = (response: Promise<Response>) => Promise<RscPayload>;
|
|
44
|
+
|
|
45
|
+
let decoder: PrefetchDecoder | null = null;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Wire the RSC decoder used to eagerly decode prefetched responses. Called
|
|
49
|
+
* once from initBrowserApp with the same createFromFetch the navigation client
|
|
50
|
+
* uses. Until set, prefetch warming is inert (prefetches are skipped) — the
|
|
51
|
+
* browser app always sets it before any Link can fire.
|
|
52
|
+
*/
|
|
53
|
+
export function setPrefetchDecoder(fn: PrefetchDecoder): void {
|
|
54
|
+
decoder = fn;
|
|
55
|
+
}
|
|
28
56
|
|
|
29
57
|
/**
|
|
30
58
|
* Check if a URL resolves to the current page (same pathname + search).
|
|
@@ -78,40 +106,57 @@ function buildPrefetchUrl(
|
|
|
78
106
|
}
|
|
79
107
|
|
|
80
108
|
/**
|
|
81
|
-
* Core prefetch fetch logic. Fetches the response,
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* reuse an in-flight prefetch via
|
|
109
|
+
* Core prefetch fetch logic. Fetches the response, eagerly decodes it, and
|
|
110
|
+
* stores the decoded payload in the in-memory cache. The returned Promise
|
|
111
|
+
* resolves to the decoded entry (or null on failure / control header) so
|
|
112
|
+
* navigation can safely reuse an in-flight prefetch via
|
|
113
|
+
* consumeInflightPrefetch().
|
|
114
|
+
*
|
|
115
|
+
* Eager decode is the warming step: createFromFetch parses the Flight stream,
|
|
116
|
+
* which resolves the route's client references and imports its JS chunks. The
|
|
117
|
+
* stored payload is reused as-is by navigation, so the click loads no new code.
|
|
118
|
+
*
|
|
119
|
+
* Control headers are NOT acted on here. A speculative prefetch must never
|
|
120
|
+
* reload the page or throw a redirect — if the response carries X-RSC-Reload
|
|
121
|
+
* or X-RSC-Redirect, we drop it (resolve null) and let the real navigation
|
|
122
|
+
* re-fetch and honor it.
|
|
85
123
|
*
|
|
86
124
|
* Inflight + storage key selection:
|
|
87
125
|
*
|
|
88
126
|
* - `forceSourceScope` (Link opted in with `prefetchKey=":source"`): single
|
|
89
|
-
* inflight registration under `sourceKey`;
|
|
90
|
-
*
|
|
127
|
+
* inflight registration under `sourceKey`; entry stored under `sourceKey`.
|
|
128
|
+
* No wildcard leak is possible.
|
|
91
129
|
*
|
|
92
130
|
* - Otherwise: dual inflight registration under both `wildcardKey` and
|
|
93
131
|
* `sourceKey` so same-source navigations adopt directly via their own
|
|
94
132
|
* source key. Storage key is chosen at response time from the
|
|
95
133
|
* `X-RSC-Prefetch-Scope` header — `"source"` → `sourceKey` (intercept
|
|
96
|
-
* modals etc.), anything else → `wildcardKey`.
|
|
97
|
-
* that adopted via `wildcardKey`
|
|
98
|
-
*
|
|
134
|
+
* modals etc.), anything else → `wildcardKey`. The entry records its scope
|
|
135
|
+
* so cross-source navigations that adopted via `wildcardKey` can bail out
|
|
136
|
+
* in `navigation-client.ts` when the adopted entry turns out source-scoped.
|
|
99
137
|
*/
|
|
100
138
|
function executePrefetchFetch(
|
|
101
139
|
wildcardKey: string,
|
|
102
140
|
sourceKey: string,
|
|
103
141
|
fetchUrl: string,
|
|
104
142
|
forceSourceScope: boolean,
|
|
143
|
+
expectedRouterId?: string,
|
|
105
144
|
signal?: AbortSignal,
|
|
106
|
-
): Promise<
|
|
145
|
+
): Promise<DecodedPrefetch | null> {
|
|
107
146
|
const gen = currentGeneration();
|
|
108
147
|
const inflightKeys = forceSourceScope
|
|
109
148
|
? [sourceKey]
|
|
110
149
|
: [wildcardKey, sourceKey];
|
|
111
150
|
for (const k of inflightKeys) markPrefetchInflight(k);
|
|
112
151
|
|
|
113
|
-
const promise: Promise<
|
|
152
|
+
const promise: Promise<DecodedPrefetch | null> = fetch(fetchUrl, {
|
|
114
153
|
priority: "low" as RequestPriority,
|
|
154
|
+
// During an action's flight the state is not rotated, so the old
|
|
155
|
+
// X-Rango-State still matches the Vary-keyed HTTP-cache entry; bypass it so
|
|
156
|
+
// a prefetch fetches fresh rather than warming the map with stale bytes (the
|
|
157
|
+
// fence's HTTP-cache-bypass requirement applies to prefetch as well as
|
|
158
|
+
// navigation fetches).
|
|
159
|
+
...(isActionFenceActive() && { cache: "no-store" as RequestCache }),
|
|
115
160
|
signal,
|
|
116
161
|
headers: {
|
|
117
162
|
"X-Rango-State": getRangoState(),
|
|
@@ -120,25 +165,53 @@ function executePrefetchFetch(
|
|
|
120
165
|
},
|
|
121
166
|
})
|
|
122
167
|
.then((response) => {
|
|
123
|
-
if (!response.ok) return null;
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
statusText: response.statusText,
|
|
132
|
-
};
|
|
133
|
-
let storageKey: string;
|
|
134
|
-
if (forceSourceScope) {
|
|
135
|
-
storageKey = sourceKey;
|
|
136
|
-
} else {
|
|
137
|
-
const scope = response.headers.get("x-rsc-prefetch-scope");
|
|
138
|
-
storageKey = scope === "source" ? sourceKey : wildcardKey;
|
|
168
|
+
if (!response.ok || !decoder) return null;
|
|
169
|
+
// Control headers mean this response is stale (reload) or redirecting.
|
|
170
|
+
// Don't warm it — drop so navigation re-fetches and acts on the header.
|
|
171
|
+
if (
|
|
172
|
+
response.headers.has("X-RSC-Reload") ||
|
|
173
|
+
response.headers.has("X-RSC-Redirect")
|
|
174
|
+
) {
|
|
175
|
+
return null;
|
|
139
176
|
}
|
|
140
|
-
|
|
141
|
-
|
|
177
|
+
// Integrity check: never warm (or decode/import the chunks of) a foreign
|
|
178
|
+
// app's payload. A speculative prefetch must never reload — just drop it;
|
|
179
|
+
// navigation re-fetches and the server steers it.
|
|
180
|
+
if (isForeignRouterId(response, expectedRouterId)) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const scope: "source" | "wildcard" =
|
|
185
|
+
forceSourceScope ||
|
|
186
|
+
response.headers.get("x-rsc-prefetch-scope") === "source"
|
|
187
|
+
? "source"
|
|
188
|
+
: "wildcard";
|
|
189
|
+
const storageKey = scope === "source" ? sourceKey : wildcardKey;
|
|
190
|
+
|
|
191
|
+
// Track stream completion off a tee so navigation's scroll/revalidation
|
|
192
|
+
// gating matches the fresh-fetch path; decode the other branch.
|
|
193
|
+
let resolveStreamComplete!: () => void;
|
|
194
|
+
const streamComplete = new Promise<void>((resolve) => {
|
|
195
|
+
resolveStreamComplete = resolve;
|
|
196
|
+
});
|
|
197
|
+
const tracked = teeWithCompletion(
|
|
198
|
+
response,
|
|
199
|
+
() => resolveStreamComplete(),
|
|
200
|
+
signal,
|
|
201
|
+
// Speculative prefetch: a never-consumed/aborted stream error is benign.
|
|
202
|
+
true,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Eager decode: parsing the Flight stream imports the route's client
|
|
206
|
+
// chunks now, not on click.
|
|
207
|
+
const payload = decoder(Promise.resolve(tracked));
|
|
208
|
+
// Mark handled so an unconsumed prefetch decode error stays quiet; the
|
|
209
|
+
// error is still surfaced to navigation if it consumes the entry.
|
|
210
|
+
payload.catch(() => {});
|
|
211
|
+
|
|
212
|
+
const entry: DecodedPrefetch = { payload, streamComplete, scope };
|
|
213
|
+
storePrefetch(storageKey, entry, gen);
|
|
214
|
+
return entry;
|
|
142
215
|
})
|
|
143
216
|
.catch(() => null)
|
|
144
217
|
.finally(() => {
|
|
@@ -223,6 +296,7 @@ export function prefetchDirect(
|
|
|
223
296
|
sourceKey,
|
|
224
297
|
targetUrl.toString(),
|
|
225
298
|
forceSourceScope,
|
|
299
|
+
routerId,
|
|
226
300
|
);
|
|
227
301
|
}
|
|
228
302
|
|
|
@@ -275,6 +349,7 @@ export function prefetchQueued(
|
|
|
275
349
|
sourceKey,
|
|
276
350
|
fetchUrlStr,
|
|
277
351
|
forceSourceScope,
|
|
352
|
+
routerId,
|
|
278
353
|
signal,
|
|
279
354
|
).then(() => {});
|
|
280
355
|
});
|
|
@@ -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
|
}
|