@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1c0bdfad
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 +17 -0
- package/README.md +291 -61
- package/dist/bin/rango.js +544 -143
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3744 -1329
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +67 -13
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +312 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +247 -23
- package/skills/caching/SKILL.md +322 -19
- package/skills/composability/SKILL.md +27 -2
- 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 +282 -60
- package/skills/host-router/SKILL.md +278 -0
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +50 -6
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +297 -31
- 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 +203 -7
- package/skills/prerender/SKILL.md +155 -111
- package/skills/rango/SKILL.md +251 -23
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +123 -48
- package/skills/route/SKILL.md +104 -9
- package/skills/router-setup/SKILL.md +124 -11
- 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 +125 -222
- 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 +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 +123 -0
- package/skills/typesafety/SKILL.md +357 -52
- package/skills/use-cache/SKILL.md +46 -14
- 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 +67 -40
- package/src/bin/rango.ts +18 -0
- 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 +197 -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/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +200 -30
- package/src/browser/navigation-client.ts +217 -58
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +76 -67
- package/src/browser/navigation-transaction.ts +18 -66
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +187 -112
- package/src/browser/prefetch/cache.ts +312 -30
- package/src/browser/prefetch/fetch.ts +344 -47
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +126 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +125 -18
- package/src/browser/react/NavigationProvider.tsx +135 -120
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- 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 +80 -5
- package/src/browser/rsc-router.tsx +226 -75
- package/src/browser/scroll-restoration.ts +54 -42
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +619 -442
- package/src/browser/types.ts +115 -11
- 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 +7 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +182 -37
- 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 +392 -106
- 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 +214 -48
- package/src/cache/cache-scope.ts +236 -89
- 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 +2224 -171
- 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 +11 -17
- package/src/cache/document-cache.ts +89 -27
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +11 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +31 -31
- package/src/cache/read-through-swr.ts +41 -11
- 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 +45 -21
- package/src/client.tsx +120 -336
- 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/debug.ts +2 -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 +56 -19
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +95 -0
- 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 -5
- 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 +79 -29
- package/src/index.ts +149 -65
- 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 +63 -26
- 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 +7 -72
- package/src/route-definition/dsl-helpers.ts +469 -276
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +113 -37
- package/src/route-definition/index.ts +3 -3
- package/src/route-definition/redirect.ts +53 -12
- package/src/route-definition/resolve-handler-use.ts +161 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -17
- package/src/route-types.ts +37 -41
- 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 +45 -22
- package/src/router/handler-context.ts +110 -39
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +50 -24
- package/src/router/lazy-includes.ts +19 -53
- package/src/router/loader-resolution.ts +274 -56
- package/src/router/logging.ts +5 -8
- package/src/router/manifest.ts +49 -45
- package/src/router/match-api.ts +121 -205
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +33 -6
- package/src/router/match-middleware/cache-lookup.ts +214 -263
- package/src/router/match-middleware/cache-store.ts +73 -33
- package/src/router/match-middleware/intercept-resolution.ts +8 -28
- package/src/router/match-middleware/segment-resolution.ts +52 -18
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +104 -49
- package/src/router/metrics.ts +217 -26
- package/src/router/middleware-types.ts +24 -110
- package/src/router/middleware.ts +384 -197
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +148 -91
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +199 -56
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +144 -74
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +8 -28
- package/src/router/router-interfaces.ts +129 -36
- package/src/router/router-options.ts +185 -23
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +281 -76
- package/src/router/segment-resolution/helpers.ts +116 -31
- package/src/router/segment-resolution/loader-cache.ts +63 -37
- package/src/router/segment-resolution/revalidation.ts +493 -391
- 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 +5 -1
- package/src/router/segment-wrappers.ts +8 -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 +180 -58
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +182 -54
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +702 -460
- package/src/rsc/helpers.ts +168 -46
- package/src/rsc/index.ts +2 -25
- 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 +98 -19
- 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 +126 -106
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +269 -114
- package/src/rsc/ssr-setup.ts +144 -0
- package/src/rsc/types.ts +34 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +49 -41
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +149 -0
- package/src/segment-system.tsx +281 -129
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +317 -63
- 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 +26 -46
- package/src/server/request-context.ts +425 -177
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +25 -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 +99 -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 +343 -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/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 +3 -19
- 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 +236 -88
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +10 -45
- package/src/types/route-entry.ts +19 -7
- package/src/types/segments.ts +37 -19
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +58 -11
- 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 -18
- package/src/use-loader.tsx +346 -89
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +64 -91
- package/src/vite/discovery/discover-routers.ts +147 -88
- 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 +247 -145
- 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 +61 -13
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +10 -3
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +155 -65
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- package/src/vite/plugins/client-ref-dedup.ts +120 -0
- 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 +49 -98
- 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 +127 -0
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +46 -37
- package/src/vite/plugins/virtual-entries.ts +13 -18
- package/src/vite/rango.ts +241 -287
- package/src/vite/router-discovery.ts +956 -149
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +4 -4
- 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 +141 -34
- package/src/vite/utils/shared-utils.ts +92 -42
- package/CLAUDE.md +0 -5
- 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 -6
- package/src/network-error-thrower.tsx +0 -23
- package/src/route-definition/route-function.ts +0 -119
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -12,11 +12,21 @@ import {
|
|
|
12
12
|
startBrowserTransaction,
|
|
13
13
|
} from "./logging.js";
|
|
14
14
|
import { getRangoState } from "./rango-state.js";
|
|
15
|
+
import { isActionFenceActive } from "./action-fence.js";
|
|
15
16
|
import {
|
|
16
17
|
extractRscHeaderUrl,
|
|
17
18
|
emptyResponse,
|
|
19
|
+
handleReloadHeader,
|
|
18
20
|
teeWithCompletion,
|
|
21
|
+
isForeignRouterId,
|
|
19
22
|
} from "./response-adapter.js";
|
|
23
|
+
import {
|
|
24
|
+
buildPrefetchKey,
|
|
25
|
+
buildSourceKey,
|
|
26
|
+
consumeInflightPrefetch,
|
|
27
|
+
consumePrefetch,
|
|
28
|
+
type DecodedPrefetch,
|
|
29
|
+
} from "./prefetch/cache.js";
|
|
20
30
|
|
|
21
31
|
/**
|
|
22
32
|
* Create a navigation client for fetching RSC payloads
|
|
@@ -24,21 +34,14 @@ import {
|
|
|
24
34
|
* The client handles building URLs with RSC parameters and
|
|
25
35
|
* deserializing the response using the RSC runtime.
|
|
26
36
|
*
|
|
37
|
+
* Checks the in-memory prefetch cache before making a network request.
|
|
38
|
+
* Tries the source-scoped key first (populated when the server tagged
|
|
39
|
+
* the response as source-sensitive via `X-RSC-Prefetch-Scope: source`)
|
|
40
|
+
* and falls back to the Rango-state-keyed wildcard slot used for the
|
|
41
|
+
* common source-agnostic case.
|
|
42
|
+
*
|
|
27
43
|
* @param deps - RSC browser dependencies (createFromFetch)
|
|
28
44
|
* @returns NavigationClient instance
|
|
29
|
-
*
|
|
30
|
-
* @example
|
|
31
|
-
* ```typescript
|
|
32
|
-
* import { createFromFetch } from "@vitejs/plugin-rsc/browser";
|
|
33
|
-
*
|
|
34
|
-
* const client = createNavigationClient({ createFromFetch });
|
|
35
|
-
*
|
|
36
|
-
* const payload = await client.fetchPartial({
|
|
37
|
-
* targetUrl: "/shop/products",
|
|
38
|
-
* segmentIds: ["root", "shop"],
|
|
39
|
-
* previousUrl: "/",
|
|
40
|
-
* });
|
|
41
|
-
* ```
|
|
42
45
|
*/
|
|
43
46
|
export function createNavigationClient(
|
|
44
47
|
deps: Pick<RscBrowserDependencies, "createFromFetch">,
|
|
@@ -47,8 +50,9 @@ export function createNavigationClient(
|
|
|
47
50
|
/**
|
|
48
51
|
* Fetch a partial RSC payload for navigation
|
|
49
52
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
53
|
+
* First checks the in-memory prefetch cache for a matching entry.
|
|
54
|
+
* If found, uses the cached response instantly. Otherwise sends
|
|
55
|
+
* current segment IDs to the server for diff-based rendering.
|
|
52
56
|
*
|
|
53
57
|
* @param options - Fetch options
|
|
54
58
|
* @returns RSC payload with segments and metadata, plus stream completion promise
|
|
@@ -64,6 +68,7 @@ export function createNavigationClient(
|
|
|
64
68
|
staleRevalidation,
|
|
65
69
|
interceptSourceUrl,
|
|
66
70
|
version,
|
|
71
|
+
routerId,
|
|
67
72
|
hmr,
|
|
68
73
|
} = options;
|
|
69
74
|
|
|
@@ -80,7 +85,8 @@ export function createNavigationClient(
|
|
|
80
85
|
});
|
|
81
86
|
}
|
|
82
87
|
|
|
83
|
-
// Build fetch URL with partial rendering params
|
|
88
|
+
// Build fetch URL with partial rendering params (used for both
|
|
89
|
+
// cache key lookup and actual fetch if cache misses)
|
|
84
90
|
const fetchUrl = new URL(targetUrl, window.location.origin);
|
|
85
91
|
fetchUrl.searchParams.set("_rsc_partial", "true");
|
|
86
92
|
fetchUrl.searchParams.set("_rsc_segments", segmentIds.join(","));
|
|
@@ -90,46 +96,80 @@ export function createNavigationClient(
|
|
|
90
96
|
if (version) {
|
|
91
97
|
fetchUrl.searchParams.set("_rsc_v", version);
|
|
92
98
|
}
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
path: `${fetchUrl.pathname}${fetchUrl.search}`,
|
|
96
|
-
});
|
|
99
|
+
if (routerId) {
|
|
100
|
+
fetchUrl.searchParams.set("_rsc_rid", routerId);
|
|
97
101
|
}
|
|
98
102
|
|
|
103
|
+
// Check completed in-memory prefetch cache before making a network
|
|
104
|
+
// request. Try the source-scoped key first (populated when the server
|
|
105
|
+
// tagged the prefetch response as source-sensitive, e.g. intercepts,
|
|
106
|
+
// or when a Link opted in with `prefetchKey=":source"`), then fall
|
|
107
|
+
// back to the wildcard slot shared across source pages.
|
|
108
|
+
// Both keys embed the Rango state, so state rotation (deploy or
|
|
109
|
+
// server-action invalidation) auto-invalidates both scopes.
|
|
110
|
+
// Skip cache for stale revalidation (needs fresh data), HMR (needs
|
|
111
|
+
// fresh modules), and intercept contexts (source-dependent responses).
|
|
112
|
+
// Suspend prefetch consumption while an action is in flight: a queued
|
|
113
|
+
// prefetch holds pre-mutation data and must not be served until the
|
|
114
|
+
// action's response decides whether anything changed.
|
|
115
|
+
const canUsePrefetch =
|
|
116
|
+
!staleRevalidation &&
|
|
117
|
+
!hmr &&
|
|
118
|
+
!interceptSourceUrl &&
|
|
119
|
+
!isActionFenceActive();
|
|
120
|
+
const rangoState = getRangoState();
|
|
121
|
+
const wildcardKey = buildPrefetchKey(rangoState, fetchUrl);
|
|
122
|
+
const cacheKey = buildSourceKey(rangoState, previousUrl, fetchUrl);
|
|
123
|
+
|
|
124
|
+
let cachedEntry: DecodedPrefetch | null = null;
|
|
125
|
+
let hitKey: string | null = null;
|
|
126
|
+
if (canUsePrefetch) {
|
|
127
|
+
cachedEntry = consumePrefetch(cacheKey);
|
|
128
|
+
if (cachedEntry) {
|
|
129
|
+
hitKey = cacheKey;
|
|
130
|
+
} else {
|
|
131
|
+
cachedEntry = consumePrefetch(wildcardKey);
|
|
132
|
+
if (cachedEntry) hitKey = wildcardKey;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let inflightEntryPromise: Promise<DecodedPrefetch | null> | null = null;
|
|
137
|
+
if (canUsePrefetch && !cachedEntry) {
|
|
138
|
+
inflightEntryPromise = consumeInflightPrefetch(cacheKey);
|
|
139
|
+
if (inflightEntryPromise) {
|
|
140
|
+
hitKey = cacheKey;
|
|
141
|
+
} else {
|
|
142
|
+
inflightEntryPromise = consumeInflightPrefetch(wildcardKey);
|
|
143
|
+
if (inflightEntryPromise) hitKey = wildcardKey;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
99
146
|
// Track when the stream completes
|
|
100
147
|
let resolveStreamComplete: () => void;
|
|
101
148
|
const streamComplete = new Promise<void>((resolve) => {
|
|
102
149
|
resolveStreamComplete = resolve;
|
|
103
150
|
});
|
|
104
151
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
browserDebugLog(tx, "version mismatch, reloading", {
|
|
127
|
-
reloadUrl: reload.url,
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
window.location.href = reload.url;
|
|
131
|
-
return new Promise<Response>(() => {});
|
|
132
|
-
}
|
|
152
|
+
/**
|
|
153
|
+
* Validate RSC control headers on any response (fresh, cached, or
|
|
154
|
+
* in-flight). Handles version-mismatch reloads and server redirects.
|
|
155
|
+
* Returns the response unchanged when no control header is present.
|
|
156
|
+
*/
|
|
157
|
+
const validateRscHeaders = (
|
|
158
|
+
response: Response,
|
|
159
|
+
source: string,
|
|
160
|
+
): Response | Promise<Response> => {
|
|
161
|
+
// Version mismatch — server wants a full page reload
|
|
162
|
+
const reloadResult = handleReloadHeader(response, {
|
|
163
|
+
onBlocked: resolveStreamComplete,
|
|
164
|
+
onReload: (url) => {
|
|
165
|
+
if (tx) {
|
|
166
|
+
browserDebugLog(tx, `version mismatch, reloading (${source})`, {
|
|
167
|
+
reloadUrl: url,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
if (reloadResult) return reloadResult;
|
|
133
173
|
|
|
134
174
|
// Server-side redirect without state: the server returned 204 with
|
|
135
175
|
// X-RSC-Redirect instead of a 3xx (which fetch would auto-follow
|
|
@@ -142,7 +182,7 @@ export function createNavigationClient(
|
|
|
142
182
|
}
|
|
143
183
|
if (redirect) {
|
|
144
184
|
if (tx) {
|
|
145
|
-
browserDebugLog(tx,
|
|
185
|
+
browserDebugLog(tx, `server redirect (${source})`, {
|
|
146
186
|
redirectUrl: redirect.url,
|
|
147
187
|
});
|
|
148
188
|
}
|
|
@@ -150,19 +190,134 @@ export function createNavigationClient(
|
|
|
150
190
|
throw new ServerRedirect(redirect.url, undefined);
|
|
151
191
|
}
|
|
152
192
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
193
|
+
// Integrity check (pre-decode): refuse a foreign app's content response
|
|
194
|
+
// before createFromFetch imports its chunks. Ordered AFTER the reload
|
|
195
|
+
// and redirect handlers — control responses are never stamped with
|
|
196
|
+
// X-RSC-Router-Id, so they are steered first and never reach here.
|
|
197
|
+
if (isForeignRouterId(response, routerId)) {
|
|
198
|
+
if (tx) {
|
|
199
|
+
browserDebugLog(tx, `router id mismatch, reloading (${source})`);
|
|
200
|
+
}
|
|
201
|
+
resolveStreamComplete();
|
|
202
|
+
window.location.href = targetUrl;
|
|
203
|
+
return new Promise<Response>(() => {});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return response;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/** Start a fresh navigation fetch (no cache / inflight hit). */
|
|
210
|
+
const doFreshFetch = (): Promise<Response> => {
|
|
211
|
+
if (tx) {
|
|
212
|
+
browserDebugLog(tx, "fetching", {
|
|
213
|
+
path: `${fetchUrl.pathname}${fetchUrl.search}`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return fetch(fetchUrl, {
|
|
218
|
+
// During an action's flight the state is not rotated, so the old
|
|
219
|
+
// X-Rango-State still matches the Vary-keyed HTTP-cache entry; bypass
|
|
220
|
+
// it so a genuine mid-action navigation fetches fresh instead of being
|
|
221
|
+
// served the stale prefetched bytes.
|
|
222
|
+
...(isActionFenceActive() && { cache: "no-store" as RequestCache }),
|
|
223
|
+
headers: {
|
|
224
|
+
"X-RSC-Router-Client-Path": previousUrl,
|
|
225
|
+
"X-Rango-State": getRangoState(),
|
|
226
|
+
...(tx && { "X-RSC-Router-Request-Id": tx.requestId }),
|
|
227
|
+
...(interceptSourceUrl && {
|
|
228
|
+
"X-RSC-Router-Intercept-Source": interceptSourceUrl,
|
|
229
|
+
}),
|
|
230
|
+
...(hmr && { "X-RSC-HMR": "1" }),
|
|
158
231
|
},
|
|
159
232
|
signal,
|
|
160
|
-
)
|
|
233
|
+
}).then((response) => {
|
|
234
|
+
const validated = validateRscHeaders(response, "fetch");
|
|
235
|
+
if (validated instanceof Promise) return validated;
|
|
236
|
+
|
|
237
|
+
return teeWithCompletion(
|
|
238
|
+
validated,
|
|
239
|
+
() => {
|
|
240
|
+
if (tx) browserDebugLog(tx, "stream complete");
|
|
241
|
+
resolveStreamComplete();
|
|
242
|
+
},
|
|
243
|
+
signal,
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// A warm prefetch hit returns its eagerly-decoded payload directly: the
|
|
249
|
+
// route's chunks were imported during the prefetch, so this click runs
|
|
250
|
+
// no decode and no network. Only the fresh path runs createFromFetch and
|
|
251
|
+
// resolves the local streamComplete (via doFreshFetch's teeWithCompletion
|
|
252
|
+
// and the control-header short-circuits in validateRscHeaders).
|
|
253
|
+
const freshResult = (): {
|
|
254
|
+
payload: Promise<RscPayload>;
|
|
255
|
+
streamComplete: Promise<void>;
|
|
256
|
+
} => ({
|
|
257
|
+
payload: deps.createFromFetch<RscPayload>(doFreshFetch()),
|
|
258
|
+
streamComplete,
|
|
161
259
|
});
|
|
162
260
|
|
|
261
|
+
let payloadPromise: Promise<RscPayload>;
|
|
262
|
+
let streamCompletePromise: Promise<void>;
|
|
263
|
+
// True only for a prefetch-cache hit whose stream had already fully drained
|
|
264
|
+
// (complete === true). A still-streaming hit and the fresh path stay false,
|
|
265
|
+
// so only a fully-warmed prefetch commits in a transition (no fallback flash).
|
|
266
|
+
let fullyPrefetched = false;
|
|
267
|
+
|
|
268
|
+
if (cachedEntry) {
|
|
269
|
+
if (tx) {
|
|
270
|
+
browserDebugLog(tx, "prefetch cache hit (warm)", {
|
|
271
|
+
key: hitKey,
|
|
272
|
+
wildcard: hitKey === wildcardKey,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
payloadPromise = cachedEntry.payload;
|
|
276
|
+
streamCompletePromise = cachedEntry.streamComplete;
|
|
277
|
+
// Only a hit whose stream already fully drained is "fully prefetched";
|
|
278
|
+
// a still-streaming hit must keep streaming its fallbacks like a cold load.
|
|
279
|
+
fullyPrefetched = cachedEntry.complete;
|
|
280
|
+
} else if (inflightEntryPromise) {
|
|
281
|
+
if (tx) {
|
|
282
|
+
browserDebugLog(tx, "reusing inflight prefetch", {
|
|
283
|
+
key: hitKey,
|
|
284
|
+
wildcard: hitKey === wildcardKey,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
const adoptedViaWildcard = hitKey === wildcardKey;
|
|
288
|
+
const entry = await inflightEntryPromise;
|
|
289
|
+
if (!entry) {
|
|
290
|
+
if (tx) {
|
|
291
|
+
browserDebugLog(tx, "inflight prefetch unavailable, refetching");
|
|
292
|
+
}
|
|
293
|
+
({ payload: payloadPromise, streamComplete: streamCompletePromise } =
|
|
294
|
+
freshResult());
|
|
295
|
+
} else if (adoptedViaWildcard && entry.scope === "source") {
|
|
296
|
+
// A wildcard-adopted inflight that turned out source-scoped was
|
|
297
|
+
// built for a different source page. Discard and refetch.
|
|
298
|
+
if (tx) {
|
|
299
|
+
browserDebugLog(
|
|
300
|
+
tx,
|
|
301
|
+
"wildcard inflight turned out source-scoped, refetching",
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
({ payload: payloadPromise, streamComplete: streamCompletePromise } =
|
|
305
|
+
freshResult());
|
|
306
|
+
} else {
|
|
307
|
+
payloadPromise = entry.payload;
|
|
308
|
+
streamCompletePromise = entry.streamComplete;
|
|
309
|
+
// Adopted inflight is normally still streaming (false), but read the
|
|
310
|
+
// flag in case it completed between publish and adoption.
|
|
311
|
+
fullyPrefetched = entry.complete;
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
({ payload: payloadPromise, streamComplete: streamCompletePromise } =
|
|
315
|
+
freshResult());
|
|
316
|
+
}
|
|
317
|
+
|
|
163
318
|
try {
|
|
164
|
-
|
|
165
|
-
|
|
319
|
+
const payload = await payloadPromise;
|
|
320
|
+
|
|
166
321
|
if (tx) {
|
|
167
322
|
browserDebugLog(tx, "response received", {
|
|
168
323
|
isPartial: payload.metadata?.isPartial,
|
|
@@ -170,7 +325,11 @@ export function createNavigationClient(
|
|
|
170
325
|
diffCount: payload.metadata?.diff?.length ?? 0,
|
|
171
326
|
});
|
|
172
327
|
}
|
|
173
|
-
return {
|
|
328
|
+
return {
|
|
329
|
+
payload,
|
|
330
|
+
streamComplete: streamCompletePromise,
|
|
331
|
+
fullyPrefetched,
|
|
332
|
+
};
|
|
174
333
|
} catch (error) {
|
|
175
334
|
// Convert network-level errors to NetworkError for proper handling
|
|
176
335
|
if (isNetworkError(error)) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A module-level handle to the active navigation store.
|
|
3
|
+
*
|
|
4
|
+
* The boot path (`rsc-router.tsx`) calls `createNavigationStore()` directly;
|
|
5
|
+
* there is no global store singleton. This handle is the live reference for
|
|
6
|
+
* code that needs the store but does not
|
|
7
|
+
* receive it by argument: the jar-divergence observer (below) and the client
|
|
8
|
+
* seat of `invalidateClientCache()` (added later).
|
|
9
|
+
*
|
|
10
|
+
* Dependency-light on purpose: it imports only `setRangoStateObserver` and the
|
|
11
|
+
* store type, so pulling it into the default root entry does not drag the
|
|
12
|
+
* navigation store into bundles that previously lacked it.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { setRangoStateObserver } from "./rango-state.js";
|
|
16
|
+
import type { NavigationStore } from "./types.js";
|
|
17
|
+
|
|
18
|
+
let registeredStore: NavigationStore | null = null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Register the active navigation store at boot, and wire the jar-divergence
|
|
22
|
+
* observer: when a per-request cookie read detects an EXTERNAL rotation (a
|
|
23
|
+
* sibling tab, a server `Set-Cookie`, or a cookie clear), mark this tab's
|
|
24
|
+
* history cache stale. The history cache is not state-keyed, so the value
|
|
25
|
+
* rotation alone does not reach it. No broadcast, no prefetch clear, no
|
|
26
|
+
* re-rotation — the value already changed externally.
|
|
27
|
+
*/
|
|
28
|
+
export function registerNavigationStore(store: NavigationStore): void {
|
|
29
|
+
registeredStore = store;
|
|
30
|
+
setRangoStateObserver(() => {
|
|
31
|
+
registeredStore?.markHistoryCacheStale();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The active navigation store, or null before boot has registered it. */
|
|
36
|
+
export function getRegisteredStore(): NavigationStore | null {
|
|
37
|
+
return registeredStore;
|
|
38
|
+
}
|
|
@@ -28,9 +28,15 @@ const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
|
28
28
|
// Maximum number of history entries to cache (URLs visited)
|
|
29
29
|
const HISTORY_CACHE_SIZE = 20;
|
|
30
30
|
|
|
31
|
-
// Cache entry: [url-key, segments, stale, handleData?]
|
|
31
|
+
// Cache entry: [url-key, segments, stale, handleData?, routerId?]
|
|
32
32
|
// stale=true means the data may be outdated and should be revalidated on access
|
|
33
|
-
type HistoryCacheEntry = [
|
|
33
|
+
type HistoryCacheEntry = [
|
|
34
|
+
string,
|
|
35
|
+
ResolvedSegment[],
|
|
36
|
+
boolean,
|
|
37
|
+
HandleData?,
|
|
38
|
+
string?,
|
|
39
|
+
];
|
|
34
40
|
|
|
35
41
|
/**
|
|
36
42
|
* Shallow clone handleData to avoid reference sharing between cache entries.
|
|
@@ -124,14 +130,14 @@ export interface NavigationStoreConfig {
|
|
|
124
130
|
|
|
125
131
|
/**
|
|
126
132
|
* Enable cross-tab cache invalidation via BroadcastChannel (default: true)
|
|
127
|
-
* When cache is cleared (via server actions or
|
|
133
|
+
* When cache is cleared (via server actions or invalidateClientCache()),
|
|
128
134
|
* other tabs will also clear their cache
|
|
129
135
|
*/
|
|
130
136
|
crossTabSync?: boolean;
|
|
131
137
|
|
|
132
138
|
/**
|
|
133
139
|
* Auto-refresh when another tab mutates data on the same path (default: true)
|
|
134
|
-
* Triggered when cache is cleared via server actions or
|
|
140
|
+
* Triggered when cache is cleared via server actions or invalidateClientCache()
|
|
135
141
|
* Requires crossTabSync to be enabled
|
|
136
142
|
*/
|
|
137
143
|
crossTabAutoRefresh?: boolean;
|
|
@@ -258,6 +264,11 @@ export function createNavigationStore(
|
|
|
258
264
|
// Used to maintain intercept context during action revalidation
|
|
259
265
|
let interceptSourceUrl: string | null = null;
|
|
260
266
|
|
|
267
|
+
// Router identity - tracks which router is currently active.
|
|
268
|
+
// When this changes on a partial response, the client forces a full
|
|
269
|
+
// tree replacement instead of reconciling with stale segments.
|
|
270
|
+
let currentRouterId: string | undefined;
|
|
271
|
+
|
|
261
272
|
// Action state tracking (for useAction hook)
|
|
262
273
|
// Maps action function ID to its tracked state
|
|
263
274
|
const actionStates = new Map<string, TrackedActionState>();
|
|
@@ -269,18 +280,17 @@ export function createNavigationStore(
|
|
|
269
280
|
/**
|
|
270
281
|
* Create a debounced function that batches rapid calls
|
|
271
282
|
*/
|
|
283
|
+
// A non-keyed notifier is the keyed one restricted to a single constant key;
|
|
284
|
+
// its own keyed instance means the "" key never collides with action keys.
|
|
272
285
|
function createDebouncedNotifier<T extends (...args: any[]) => void>(
|
|
273
286
|
fn: T,
|
|
274
287
|
ms: number = 20,
|
|
275
288
|
): T {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
fn(...args);
|
|
282
|
-
}, ms);
|
|
283
|
-
}) as T;
|
|
289
|
+
const keyed = createKeyedDebouncedNotifier(
|
|
290
|
+
(_key: string, ...args: any[]) => fn(...args),
|
|
291
|
+
ms,
|
|
292
|
+
);
|
|
293
|
+
return ((...args: Parameters<T>) => keyed("", ...args)) as T;
|
|
284
294
|
}
|
|
285
295
|
|
|
286
296
|
/**
|
|
@@ -325,12 +335,24 @@ export function createNavigationStore(
|
|
|
325
335
|
}
|
|
326
336
|
|
|
327
337
|
/**
|
|
328
|
-
* Mark
|
|
338
|
+
* Mark every history entry stale WITHOUT touching the prefetch caches or the
|
|
339
|
+
* rango state. Used by the jar-divergence observer: an external rotation has
|
|
340
|
+
* already changed the state value (so prefetch/HTTP entries strand under the
|
|
341
|
+
* retired key), and this tab must NOT re-rotate — only the history cache,
|
|
342
|
+
* which is not state-keyed, needs marking.
|
|
329
343
|
*/
|
|
330
|
-
function
|
|
344
|
+
function markHistoryStale(): void {
|
|
331
345
|
for (let i = 0; i < historyCache.length; i++) {
|
|
332
346
|
historyCache[i][2] = true;
|
|
333
347
|
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Mark all cache entries as stale (internal - does not broadcast). Also
|
|
352
|
+
* clears the prefetch caches, which rotates the rango state.
|
|
353
|
+
*/
|
|
354
|
+
function markCacheAsStaleInternal(): void {
|
|
355
|
+
markHistoryStale();
|
|
334
356
|
clearPrefetchCache();
|
|
335
357
|
}
|
|
336
358
|
|
|
@@ -571,10 +593,17 @@ export function createNavigationStore(
|
|
|
571
593
|
segments,
|
|
572
594
|
false,
|
|
573
595
|
clonedHandleData,
|
|
596
|
+
currentRouterId,
|
|
574
597
|
];
|
|
575
598
|
} else {
|
|
576
599
|
// Add new entry at the end (not stale)
|
|
577
|
-
historyCache.push([
|
|
600
|
+
historyCache.push([
|
|
601
|
+
historyKey,
|
|
602
|
+
segments,
|
|
603
|
+
false,
|
|
604
|
+
clonedHandleData,
|
|
605
|
+
currentRouterId,
|
|
606
|
+
]);
|
|
578
607
|
// Remove oldest entries if over limit
|
|
579
608
|
while (historyCache.length > cacheSize) {
|
|
580
609
|
historyCache.shift();
|
|
@@ -586,14 +615,22 @@ export function createNavigationStore(
|
|
|
586
615
|
* Get cached segments for a history entry
|
|
587
616
|
* Returns { segments, stale, handleData } or undefined if not cached
|
|
588
617
|
*/
|
|
589
|
-
getCachedSegments(
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
618
|
+
getCachedSegments(historyKey: string):
|
|
619
|
+
| {
|
|
620
|
+
segments: ResolvedSegment[];
|
|
621
|
+
stale: boolean;
|
|
622
|
+
handleData?: HandleData;
|
|
623
|
+
routerId?: string;
|
|
624
|
+
}
|
|
593
625
|
| undefined {
|
|
594
626
|
const entry = historyCache.find(([key]) => key === historyKey);
|
|
595
627
|
if (!entry) return undefined;
|
|
596
|
-
return {
|
|
628
|
+
return {
|
|
629
|
+
segments: entry[1],
|
|
630
|
+
stale: entry[2],
|
|
631
|
+
handleData: entry[3],
|
|
632
|
+
routerId: entry[4],
|
|
633
|
+
};
|
|
597
634
|
},
|
|
598
635
|
|
|
599
636
|
/**
|
|
@@ -621,6 +658,7 @@ export function createNavigationStore(
|
|
|
621
658
|
entry[1],
|
|
622
659
|
entry[2],
|
|
623
660
|
clonedHandleData,
|
|
661
|
+
entry[4], // preserve routerId
|
|
624
662
|
];
|
|
625
663
|
}
|
|
626
664
|
},
|
|
@@ -633,6 +671,16 @@ export function createNavigationStore(
|
|
|
633
671
|
markCacheAsStaleInternal();
|
|
634
672
|
},
|
|
635
673
|
|
|
674
|
+
/**
|
|
675
|
+
* Mark every history entry stale WITHOUT clearing the prefetch caches or
|
|
676
|
+
* rotating the rango state. The jar-divergence observer calls this after an
|
|
677
|
+
* external rotation has already changed the state value, so re-rotating
|
|
678
|
+
* here would ping-pong with the tab that rotated.
|
|
679
|
+
*/
|
|
680
|
+
markHistoryCacheStale(): void {
|
|
681
|
+
markHistoryStale();
|
|
682
|
+
},
|
|
683
|
+
|
|
636
684
|
/**
|
|
637
685
|
* Clear the history cache and broadcast to other tabs
|
|
638
686
|
* Use this for hard invalidation when data is definitely stale
|
|
@@ -649,14 +697,6 @@ export function createNavigationStore(
|
|
|
649
697
|
markStaleAndBroadcast();
|
|
650
698
|
},
|
|
651
699
|
|
|
652
|
-
/**
|
|
653
|
-
* Broadcast cache invalidation to other tabs without clearing local cache
|
|
654
|
-
* Used after consolidation fetch where local cache has fresh data
|
|
655
|
-
*/
|
|
656
|
-
broadcastCacheInvalidation(): void {
|
|
657
|
-
broadcastInvalidation();
|
|
658
|
-
},
|
|
659
|
-
|
|
660
700
|
/**
|
|
661
701
|
* Set the callback to invoke when cross-tab refresh is triggered
|
|
662
702
|
* Called by navigation bridge during initialization
|
|
@@ -687,6 +727,14 @@ export function createNavigationStore(
|
|
|
687
727
|
interceptSourceUrl = url;
|
|
688
728
|
},
|
|
689
729
|
|
|
730
|
+
getRouterId(): string | undefined {
|
|
731
|
+
return currentRouterId;
|
|
732
|
+
},
|
|
733
|
+
|
|
734
|
+
setRouterId(id: string): void {
|
|
735
|
+
currentRouterId = id;
|
|
736
|
+
},
|
|
737
|
+
|
|
690
738
|
// ========================================================================
|
|
691
739
|
// UI Update Notifications
|
|
692
740
|
// ========================================================================
|
|
@@ -765,42 +813,3 @@ export function createNavigationStore(
|
|
|
765
813
|
},
|
|
766
814
|
};
|
|
767
815
|
}
|
|
768
|
-
|
|
769
|
-
// Singleton store instance
|
|
770
|
-
let storeInstance: NavigationStore | null = null;
|
|
771
|
-
|
|
772
|
-
/**
|
|
773
|
-
* Initialize the global navigation store
|
|
774
|
-
*
|
|
775
|
-
* Should be called once during app initialization.
|
|
776
|
-
* Subsequent calls return the existing instance.
|
|
777
|
-
*/
|
|
778
|
-
export function initNavigationStore(
|
|
779
|
-
config?: NavigationStoreConfig,
|
|
780
|
-
): NavigationStore {
|
|
781
|
-
if (!storeInstance) {
|
|
782
|
-
storeInstance = createNavigationStore(config);
|
|
783
|
-
}
|
|
784
|
-
return storeInstance;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
/**
|
|
788
|
-
* Get the global navigation store
|
|
789
|
-
*
|
|
790
|
-
* Throws if store hasn't been initialized.
|
|
791
|
-
*/
|
|
792
|
-
export function getNavigationStore(): NavigationStore {
|
|
793
|
-
if (!storeInstance) {
|
|
794
|
-
throw new Error(
|
|
795
|
-
"Navigation store not initialized. Call initNavigationStore first.",
|
|
796
|
-
);
|
|
797
|
-
}
|
|
798
|
-
return storeInstance;
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
/**
|
|
802
|
-
* Reset the store instance (for testing)
|
|
803
|
-
*/
|
|
804
|
-
export function resetNavigationStore(): void {
|
|
805
|
-
storeInstance = null;
|
|
806
|
-
}
|