@rangojs/router 0.0.0-experimental.135c6902 → 0.0.0-experimental.136
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 +3386 -1111
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +78 -25
- 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 +247 -23
- package/skills/caching/SKILL.md +281 -11
- 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 +289 -53
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +914 -0
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +144 -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 +101 -5
- package/skills/router-setup/SKILL.md +117 -10
- 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 +332 -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 +199 -86
- 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 +174 -109
- package/src/browser/prefetch/cache.ts +222 -69
- package/src/browser/prefetch/fetch.ts +347 -39
- package/src/browser/prefetch/queue.ts +113 -32
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +102 -15
- package/src/browser/react/NavigationProvider.tsx +300 -122
- 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 +125 -28
- 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 -61
- package/src/browser/types.ts +115 -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 +239 -52
- package/src/cache/cache-scope.ts +234 -87
- 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/taint.ts +55 -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 +84 -2
- 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 +149 -74
- 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 +83 -39
- 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 +274 -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 +54 -25
- package/src/router/match-middleware/cache-lookup.ts +205 -271
- package/src/router/match-middleware/cache-store.ts +81 -50
- 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 +93 -39
- package/src/router/metrics.ts +5 -34
- package/src/router/middleware-types.ts +13 -142
- package/src/router/middleware.ts +268 -171
- 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/prefetch-limits.ts +37 -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 -27
- package/src/router/router-interfaces.ts +129 -35
- package/src/router/router-options.ts +202 -15
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +197 -92
- package/src/router/segment-resolution/helpers.ts +115 -30
- package/src/router/segment-resolution/loader-cache.ts +76 -39
- package/src/router/segment-resolution/revalidation.ts +392 -338
- 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 +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +174 -54
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +660 -518
- 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 +127 -31
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +133 -13
- 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 +115 -73
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +287 -113
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +36 -6
- 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 +269 -202
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +235 -51
- 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 +442 -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 +234 -82
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +44 -15
- 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 +89 -0
- 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 +119 -111
- 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/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,34 +1,100 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Prefetch Cache
|
|
3
3
|
*
|
|
4
|
-
* In-memory cache storing
|
|
5
|
-
* on subsequent navigation.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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:
|
|
11
|
+
* - Wildcard (default): built by `buildPrefetchKey(rangoState, target)` —
|
|
12
|
+
* shape `rangoState\0/target?...`. Shared across all source pages and
|
|
13
|
+
* invalidated automatically when Rango state bumps (deploy or
|
|
14
|
+
* server-action invalidation).
|
|
15
|
+
* - Source-scoped: built by `buildSourceKey(rangoState, sourceHref, target)`
|
|
16
|
+
* — shape `rangoState\0sourceHref\0/target?...`. Embeds the Rango state
|
|
17
|
+
* (so rotation invalidates source-scoped entries too) plus the source
|
|
18
|
+
* href (so each originating page gets its own slot). Populated when the
|
|
19
|
+
* server tags a response with `X-RSC-Prefetch-Scope: source` (intercept
|
|
20
|
+
* modals etc.), OR when a Link opts in with `prefetchKey=":source"` — in
|
|
21
|
+
* both cases so source-sensitive responses cannot bleed into navigations
|
|
22
|
+
* from other pages.
|
|
8
23
|
*
|
|
9
24
|
* Also tracks in-flight prefetch promises. Each promise resolves to the
|
|
10
|
-
*
|
|
11
|
-
* still-downloading prefetch without
|
|
25
|
+
* decoded prefetch entry (or null), letting navigation adopt a
|
|
26
|
+
* still-downloading prefetch without issuing a duplicate request. A
|
|
27
|
+
* single promise can be registered under multiple alias keys (see
|
|
28
|
+
* `setInflightPromiseWithAliases`) so same-source navigations adopt via
|
|
29
|
+
* their source key while cross-source ones fall through to the wildcard
|
|
30
|
+
* alias — with consume/clear atomically removing every alias.
|
|
12
31
|
*
|
|
13
32
|
* Replaces the previous browser HTTP cache approach which was unreliable
|
|
14
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().
|
|
15
40
|
*/
|
|
16
41
|
|
|
17
42
|
import { abortAllPrefetches } from "./queue.js";
|
|
18
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
|
+
}
|
|
19
77
|
|
|
20
|
-
// Default TTL: 5 minutes. Overridden by initPrefetchCache() with
|
|
21
|
-
// the server-configured prefetchCacheTTL from router options.
|
|
22
|
-
// 0 disables the in-memory cache entirely.
|
|
23
78
|
let cacheTTL = 300_000;
|
|
24
79
|
|
|
80
|
+
// Max stored entries before FIFO eviction. Mirrors DEFAULT_PREFETCH_CACHE_SIZE
|
|
81
|
+
// (router/prefetch-limits.ts); kept as a local literal so the client bundle
|
|
82
|
+
// doesn't pull in router-layer code, matching the cacheTTL default above.
|
|
83
|
+
// Overridden at startup by initPrefetchCache from server metadata.
|
|
84
|
+
let maxPrefetchCacheSize = 100;
|
|
85
|
+
|
|
25
86
|
/**
|
|
26
|
-
* Initialize the prefetch cache with the configured TTL.
|
|
27
|
-
* Called once at app startup with the
|
|
28
|
-
*
|
|
87
|
+
* Initialize the prefetch cache with the configured TTL and max size.
|
|
88
|
+
* Called once at app startup with the values from server metadata. Each
|
|
89
|
+
* argument is applied only when provided, so a caller can set just the TTL.
|
|
90
|
+
* A TTL of 0 disables the in-memory cache and all prefetching. A size below 1
|
|
91
|
+
* is ignored (the default is kept) — disabling prefetch is the TTL's job.
|
|
29
92
|
*/
|
|
30
|
-
export function initPrefetchCache(ttlMs
|
|
31
|
-
cacheTTL = ttlMs;
|
|
93
|
+
export function initPrefetchCache(ttlMs?: number, maxSize?: number): void {
|
|
94
|
+
if (ttlMs !== undefined) cacheTTL = ttlMs;
|
|
95
|
+
if (maxSize !== undefined && Number.isFinite(maxSize) && maxSize >= 1) {
|
|
96
|
+
maxPrefetchCacheSize = Math.floor(maxSize);
|
|
97
|
+
}
|
|
32
98
|
}
|
|
33
99
|
|
|
34
100
|
/**
|
|
@@ -38,36 +104,75 @@ export function initPrefetchCache(ttlMs: number): void {
|
|
|
38
104
|
export function isPrefetchCacheDisabled(): boolean {
|
|
39
105
|
return cacheTTL <= 0;
|
|
40
106
|
}
|
|
41
|
-
const MAX_PREFETCH_CACHE_SIZE = 50;
|
|
42
107
|
|
|
43
108
|
interface PrefetchCacheEntry {
|
|
44
|
-
|
|
109
|
+
entry: DecodedPrefetch;
|
|
45
110
|
timestamp: number;
|
|
46
111
|
}
|
|
47
112
|
|
|
48
113
|
const cache = new Map<string, PrefetchCacheEntry>();
|
|
49
114
|
const inflight = new Set<string>();
|
|
50
115
|
|
|
116
|
+
const inflightPromises = new Map<string, Promise<DecodedPrefetch | null>>();
|
|
117
|
+
|
|
118
|
+
const inflightAliases = new Map<string, string[]>();
|
|
119
|
+
|
|
120
|
+
const adoptedKeys = new Set<string>();
|
|
121
|
+
|
|
122
|
+
let generation = 0;
|
|
123
|
+
|
|
51
124
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
125
|
+
* Build a cache key by combining a scope prefix with the target URL.
|
|
126
|
+
*
|
|
127
|
+
* Low-level primitive — callers that want a specific scope should use
|
|
128
|
+
* one of:
|
|
129
|
+
* - Wildcard (source-agnostic): prefix is the Rango state value from
|
|
130
|
+
* `getRangoState()`. Shared across all source pages. Invalidated
|
|
131
|
+
* automatically when Rango state bumps (deploy or server-action).
|
|
132
|
+
* Key shape: `rangoState\0/target?...`.
|
|
133
|
+
* - Source-scoped: use `buildSourceKey()`. Key shape:
|
|
134
|
+
* `rangoState\0sourceHref\0/target?...` — embeds the Rango state so
|
|
135
|
+
* rotation invalidates source-scoped entries alongside wildcard ones,
|
|
136
|
+
* plus the source page href so the key is unique per originating page.
|
|
137
|
+
* Populated either when the server tags a response with
|
|
138
|
+
* `X-RSC-Prefetch-Scope: source` (intercept modals, etc.) or when a
|
|
139
|
+
* Link opts in via `prefetchKey=":source"`.
|
|
140
|
+
*
|
|
141
|
+
* The `_rsc_segments` query param that travels in the target URL means
|
|
142
|
+
* clients with different mounted segment trees naturally get different
|
|
143
|
+
* keys — so segment-level diffs remain consistent across both scopes.
|
|
55
144
|
*/
|
|
56
|
-
|
|
145
|
+
export function buildPrefetchKey(prefix: string, targetUrl: URL): string {
|
|
146
|
+
return prefix + "\0" + targetUrl.pathname + targetUrl.search;
|
|
147
|
+
}
|
|
57
148
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Build a source-scoped cache key. Key shape:
|
|
151
|
+
* `rangoState\0sourceHref\0/target?...`.
|
|
152
|
+
*
|
|
153
|
+
* - `rangoState` is included so state rotation invalidates source-scoped
|
|
154
|
+
* entries alongside wildcard ones.
|
|
155
|
+
* - `sourceHref` makes the key unique per originating page.
|
|
156
|
+
*/
|
|
157
|
+
export function buildSourceKey(
|
|
158
|
+
rangoState: string,
|
|
159
|
+
sourceHref: string,
|
|
160
|
+
targetUrl: URL,
|
|
161
|
+
): string {
|
|
162
|
+
return buildPrefetchKey(rangoState + "\0" + sourceHref, targetUrl);
|
|
163
|
+
}
|
|
62
164
|
|
|
63
165
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* different pages gets separate entries — the server response varies
|
|
67
|
-
* based on the source page context (diff-based rendering).
|
|
166
|
+
* Walk an inflight key plus any sibling aliases registered via
|
|
167
|
+
* `setInflightPromiseWithAliases`, invoking `fn` for each.
|
|
68
168
|
*/
|
|
69
|
-
|
|
70
|
-
|
|
169
|
+
function forEachAlias(key: string, fn: (k: string) => void): void {
|
|
170
|
+
const aliases = inflightAliases.get(key);
|
|
171
|
+
if (aliases) {
|
|
172
|
+
for (const k of aliases) fn(k);
|
|
173
|
+
} else {
|
|
174
|
+
fn(key);
|
|
175
|
+
}
|
|
71
176
|
}
|
|
72
177
|
|
|
73
178
|
/**
|
|
@@ -86,14 +191,14 @@ export function hasPrefetch(key: string): boolean {
|
|
|
86
191
|
}
|
|
87
192
|
|
|
88
193
|
/**
|
|
89
|
-
* Consume a cached prefetch
|
|
90
|
-
* One-time consumption: the entry is deleted after retrieval.
|
|
194
|
+
* Consume a cached, eagerly-decoded prefetch. Returns null if not found or
|
|
195
|
+
* expired. One-time consumption: the entry is deleted after retrieval.
|
|
91
196
|
* Returns null when caching is disabled (TTL <= 0).
|
|
92
197
|
*
|
|
93
198
|
* Does NOT check in-flight prefetches — use consumeInflightPrefetch()
|
|
94
|
-
* for that (returns a Promise instead of a
|
|
199
|
+
* for that (returns a Promise instead of a resolved entry).
|
|
95
200
|
*/
|
|
96
|
-
export function consumePrefetch(key: string):
|
|
201
|
+
export function consumePrefetch(key: string): DecodedPrefetch | null {
|
|
97
202
|
if (cacheTTL <= 0) return null;
|
|
98
203
|
const entry = cache.get(key);
|
|
99
204
|
if (!entry) return null;
|
|
@@ -102,63 +207,100 @@ export function consumePrefetch(key: string): Response | null {
|
|
|
102
207
|
return null;
|
|
103
208
|
}
|
|
104
209
|
cache.delete(key);
|
|
105
|
-
return entry.
|
|
210
|
+
return entry.entry;
|
|
106
211
|
}
|
|
107
212
|
|
|
108
213
|
/**
|
|
109
214
|
* Consume an in-flight prefetch promise. Returns null if no prefetch is
|
|
110
|
-
* in-flight for this key. The returned Promise resolves to the
|
|
111
|
-
*
|
|
215
|
+
* in-flight for this key. The returned Promise resolves to the decoded
|
|
216
|
+
* prefetch entry (or null if the fetch failed/was aborted, or carried a
|
|
217
|
+
* control header the navigation must re-fetch to honor).
|
|
112
218
|
*
|
|
113
|
-
* One-time consumption: the promise entry is removed
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
219
|
+
* One-time consumption: the promise entry is removed (along with any
|
|
220
|
+
* sibling aliases registered via `setInflightPromiseWithAliases`) so a
|
|
221
|
+
* second call on any alias returns null — only one caller can adopt the
|
|
222
|
+
* shared Response stream. The `inflight` set entry is intentionally
|
|
223
|
+
* kept so that `hasPrefetch()` continues to return true while the
|
|
224
|
+
* underlying fetch is still downloading — this prevents
|
|
225
|
+
* `prefetchDirect()` or other callers from starting a duplicate request
|
|
226
|
+
* during the handoff window. The inflight flag is cleaned up naturally
|
|
227
|
+
* by `clearPrefetchInflight()` in the fetch's `.finally()`.
|
|
120
228
|
*/
|
|
121
229
|
export function consumeInflightPrefetch(
|
|
122
230
|
key: string,
|
|
123
|
-
): Promise<
|
|
231
|
+
): Promise<DecodedPrefetch | null> | null {
|
|
124
232
|
const promise = inflightPromises.get(key);
|
|
125
233
|
if (!promise) return null;
|
|
126
|
-
// Remove the promise
|
|
127
|
-
|
|
234
|
+
// Remove the promise under every alias so a second consumer cannot
|
|
235
|
+
// adopt the same stream and race on the body, and mark every alias as
|
|
236
|
+
// adopted so the pending `storePrefetch` (which resolves later, after this
|
|
237
|
+
// adoption) does not leave the now-owned, single-use entry in the cache map.
|
|
238
|
+
// `inflightAliases` is intentionally preserved — `clearPrefetchInflight()` in
|
|
239
|
+
// the fetch's `.finally()` still needs it to clear every inflight flag and
|
|
240
|
+
// adopted marker; deleting here would strand the sibling's flag forever.
|
|
241
|
+
forEachAlias(key, (k) => {
|
|
242
|
+
inflightPromises.delete(k);
|
|
243
|
+
adoptedKeys.add(k);
|
|
244
|
+
});
|
|
128
245
|
return promise;
|
|
129
246
|
}
|
|
130
247
|
|
|
131
248
|
/**
|
|
132
|
-
* Store
|
|
133
|
-
* The response should be a clone() of the original so the caller can
|
|
134
|
-
* still consume the body. The clone's body streams independently.
|
|
249
|
+
* Store an eagerly-decoded prefetch in the in-memory cache.
|
|
135
250
|
*
|
|
136
251
|
* Skips storage if the generation has changed since the fetch started
|
|
137
252
|
* (a server action invalidated the cache mid-flight).
|
|
138
253
|
*/
|
|
139
254
|
export function storePrefetch(
|
|
140
255
|
key: string,
|
|
141
|
-
|
|
256
|
+
entry: DecodedPrefetch,
|
|
142
257
|
fetchGeneration: number,
|
|
143
258
|
): void {
|
|
144
259
|
if (cacheTTL <= 0) return;
|
|
145
260
|
if (fetchGeneration !== generation) return;
|
|
146
261
|
|
|
262
|
+
// If a navigation already adopted this prefetch's in-flight promise, it owns
|
|
263
|
+
// the single-use entry (and has drained its handle generator). Do NOT also
|
|
264
|
+
// publish it to the cache map, or a later navigation would be served the
|
|
265
|
+
// exhausted entry and lose that route's handles. Clear the marker (under all
|
|
266
|
+
// aliases) now that the decision is made.
|
|
267
|
+
if (adoptedKeys.has(key)) {
|
|
268
|
+
forEachAlias(key, (k) => adoptedKeys.delete(k));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
147
272
|
// Evict expired entries
|
|
148
273
|
const now = Date.now();
|
|
149
|
-
for (const [k,
|
|
150
|
-
if (now -
|
|
274
|
+
for (const [k, cached] of cache) {
|
|
275
|
+
if (now - cached.timestamp > cacheTTL) {
|
|
151
276
|
cache.delete(k);
|
|
152
277
|
}
|
|
153
278
|
}
|
|
154
279
|
|
|
155
280
|
// FIFO eviction if at capacity
|
|
156
|
-
if (cache.size >=
|
|
281
|
+
if (cache.size >= maxPrefetchCacheSize) {
|
|
157
282
|
const oldest = cache.keys().next().value;
|
|
158
283
|
if (oldest) cache.delete(oldest);
|
|
159
284
|
}
|
|
160
285
|
|
|
161
|
-
cache.set(key, {
|
|
286
|
+
cache.set(key, { entry, timestamp: now });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Remove a single stored prefetch entry. Used to evict an entry whose body
|
|
291
|
+
* stream stalled after headers arrived (its payload / streamComplete never
|
|
292
|
+
* settle), so future prefetches and navigation refetch instead of dedupe-ing
|
|
293
|
+
* against — and awaiting — a stuck entry.
|
|
294
|
+
*
|
|
295
|
+
* Identity-guarded: only evicts when the entry CURRENTLY stored under `key` is
|
|
296
|
+
* the exact `entry` we published. A generation check alone is insufficient —
|
|
297
|
+
* after this entry is consumed (consumePrefetch) and a fresh prefetch
|
|
298
|
+
* republishes under the SAME key in the SAME generation, a gen-only guard would
|
|
299
|
+
* delete that valid newer entry. Reference identity drops only our own stalled
|
|
300
|
+
* entry.
|
|
301
|
+
*/
|
|
302
|
+
export function removePrefetch(key: string, entry: DecodedPrefetch): void {
|
|
303
|
+
if (cache.get(key)?.entry === entry) cache.delete(key);
|
|
162
304
|
}
|
|
163
305
|
|
|
164
306
|
/**
|
|
@@ -173,33 +315,44 @@ export function markPrefetchInflight(key: string): void {
|
|
|
173
315
|
inflight.add(key);
|
|
174
316
|
}
|
|
175
317
|
|
|
176
|
-
/**
|
|
177
|
-
* Store the in-flight Promise for a prefetch so navigation can reuse it.
|
|
178
|
-
*/
|
|
179
318
|
export function setInflightPromise(
|
|
180
319
|
key: string,
|
|
181
|
-
promise: Promise<
|
|
320
|
+
promise: Promise<DecodedPrefetch | null>,
|
|
182
321
|
): void {
|
|
183
322
|
inflightPromises.set(key, promise);
|
|
184
323
|
}
|
|
185
324
|
|
|
325
|
+
/**
|
|
326
|
+
* Store the same in-flight Promise under multiple keys, recording them
|
|
327
|
+
* as sibling aliases. Consuming or clearing any one alias atomically
|
|
328
|
+
* removes every entry, guaranteeing the shared Response stream has a
|
|
329
|
+
* single consumer even when navigation looks up either key.
|
|
330
|
+
*/
|
|
331
|
+
export function setInflightPromiseWithAliases(
|
|
332
|
+
keys: string[],
|
|
333
|
+
promise: Promise<DecodedPrefetch | null>,
|
|
334
|
+
): void {
|
|
335
|
+
for (const k of keys) {
|
|
336
|
+
inflightPromises.set(k, promise);
|
|
337
|
+
inflightAliases.set(k, keys);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
186
341
|
export function clearPrefetchInflight(key: string): void {
|
|
187
|
-
|
|
188
|
-
|
|
342
|
+
forEachAlias(key, (k) => {
|
|
343
|
+
inflight.delete(k);
|
|
344
|
+
inflightPromises.delete(k);
|
|
345
|
+
inflightAliases.delete(k);
|
|
346
|
+
adoptedKeys.delete(k);
|
|
347
|
+
});
|
|
189
348
|
}
|
|
190
349
|
|
|
191
|
-
/**
|
|
192
|
-
* Invalidate all prefetch state. Called when server actions mutate data.
|
|
193
|
-
* Clears the in-memory cache, cancels in-flight prefetches, and rotates
|
|
194
|
-
* the Rango state key so CDN-cached responses are also invalidated.
|
|
195
|
-
*
|
|
196
|
-
* Uses abortAllPrefetches (hard cancel) because in-flight responses
|
|
197
|
-
* may contain stale data after a mutation.
|
|
198
|
-
*/
|
|
199
350
|
export function clearPrefetchCache(): void {
|
|
200
351
|
generation++;
|
|
201
352
|
inflight.clear();
|
|
202
353
|
inflightPromises.clear();
|
|
354
|
+
inflightAliases.clear();
|
|
355
|
+
adoptedKeys.clear();
|
|
203
356
|
cache.clear();
|
|
204
357
|
abortAllPrefetches();
|
|
205
358
|
invalidateRangoState();
|