@rangojs/router 0.0.0-experimental.a769fbe7 → 0.0.0-experimental.ac99d918
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 +211 -43
- package/dist/bin/rango.js +279 -102
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3313 -1160
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +62 -11
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +63 -1
- 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 +364 -0
- package/skills/hooks/SKILL.md +250 -30
- package/skills/host-router/SKILL.md +124 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +49 -5
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +223 -9
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +77 -7
- package/skills/prerender/SKILL.md +123 -100
- package/skills/rango/SKILL.md +250 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +122 -47
- package/skills/route/SKILL.md +66 -5
- package/skills/router-setup/SKILL.md +38 -3
- 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 +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 +319 -27
- 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 +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/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +81 -147
- 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 +66 -14
- package/src/browser/navigation-client.ts +172 -109
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +76 -67
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/partial-update.ts +79 -93
- package/src/browser/prefetch/cache.ts +180 -62
- package/src/browser/prefetch/fetch.ts +252 -39
- package/src/browser/prefetch/queue.ts +42 -8
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +72 -10
- package/src/browser/react/NavigationProvider.tsx +83 -31
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +49 -7
- 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 +23 -69
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +22 -5
- 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 +52 -1
- package/src/browser/rsc-router.tsx +111 -24
- package/src/browser/scroll-restoration.ts +29 -19
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +176 -62
- package/src/browser/types.ts +60 -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 +6 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +137 -32
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- 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 +333 -94
- package/src/build/route-types/scan-filter.ts +9 -2
- 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 +2256 -241
- 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 +108 -290
- 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/browser.ts +0 -1
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +70 -22
- 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 +69 -10
- package/src/index.ts +112 -14
- 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-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +37 -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 +413 -275
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +107 -32
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +50 -8
- 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 +0 -16
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +108 -9
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +46 -30
- package/src/router/intercept-resolution.ts +23 -23
- package/src/router/lazy-includes.ts +15 -52
- package/src/router/loader-resolution.ts +207 -30
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +40 -42
- package/src/router/match-api.ts +120 -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 +0 -7
- package/src/router/match-middleware/cache-lookup.ts +161 -262
- 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 +45 -14
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +87 -39
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +7 -140
- package/src/router/middleware.ts +169 -140
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +109 -63
- package/src/router/prerender-match.ts +190 -54
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +63 -55
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +0 -27
- package/src/router/router-interfaces.ts +100 -35
- package/src/router/router-options.ts +91 -11
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +119 -65
- 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 +329 -305
- 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 +91 -46
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +128 -42
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +492 -409
- package/src/rsc/helpers.ts +162 -46
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +18 -3
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +28 -4
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +90 -63
- package/src/rsc/rsc-rendering.ts +53 -56
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +74 -69
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +22 -9
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +134 -0
- package/src/segment-system.tsx +208 -201
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +211 -52
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +26 -24
- package/src/server/loader-registry.ts +10 -28
- package/src/server/request-context.ts +289 -124
- package/src/ssr/index.tsx +22 -15
- 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 +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 +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +124 -70
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +40 -11
- package/src/types/request-scope.ts +107 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +12 -7
- package/src/types/segments.ts +36 -15
- 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 +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 +192 -99
- 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 +51 -4
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +8 -0
- package/src/vite/plugin-types.ts +236 -6
- package/src/vite/plugins/cjs-to-esm.ts +8 -18
- 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 +11 -50
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
- package/src/vite/plugins/expose-internal-ids.ts +554 -317
- package/src/vite/plugins/performance-tracks.ts +65 -207
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +32 -23
- package/src/vite/plugins/virtual-entries.ts +46 -17
- package/src/vite/rango.ts +207 -125
- package/src/vite/router-discovery.ts +931 -133
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- 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/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 +27 -29
- package/src/vite/utils/shared-utils.ts +97 -44
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/debug-channel.ts +0 -93
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -12,17 +12,20 @@ import {
|
|
|
12
12
|
startBrowserTransaction,
|
|
13
13
|
} from "./logging.js";
|
|
14
14
|
import { getRangoState } from "./rango-state.js";
|
|
15
|
-
import {
|
|
16
|
-
import { findSourceMapURL } from "../deps/browser.js";
|
|
15
|
+
import { isActionFenceActive } from "./action-fence.js";
|
|
17
16
|
import {
|
|
18
17
|
extractRscHeaderUrl,
|
|
19
18
|
emptyResponse,
|
|
19
|
+
handleReloadHeader,
|
|
20
20
|
teeWithCompletion,
|
|
21
|
+
isForeignRouterId,
|
|
21
22
|
} from "./response-adapter.js";
|
|
22
23
|
import {
|
|
23
24
|
buildPrefetchKey,
|
|
25
|
+
buildSourceKey,
|
|
24
26
|
consumeInflightPrefetch,
|
|
25
27
|
consumePrefetch,
|
|
28
|
+
type DecodedPrefetch,
|
|
26
29
|
} from "./prefetch/cache.js";
|
|
27
30
|
|
|
28
31
|
/**
|
|
@@ -32,8 +35,10 @@ import {
|
|
|
32
35
|
* deserializing the response using the RSC runtime.
|
|
33
36
|
*
|
|
34
37
|
* Checks the in-memory prefetch cache before making a network request.
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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.
|
|
37
42
|
*
|
|
38
43
|
* @param deps - RSC browser dependencies (createFromFetch)
|
|
39
44
|
* @returns NavigationClient instance
|
|
@@ -63,6 +68,7 @@ export function createNavigationClient(
|
|
|
63
68
|
staleRevalidation,
|
|
64
69
|
interceptSourceUrl,
|
|
65
70
|
version,
|
|
71
|
+
routerId,
|
|
66
72
|
hmr,
|
|
67
73
|
} = options;
|
|
68
74
|
|
|
@@ -90,40 +96,115 @@ export function createNavigationClient(
|
|
|
90
96
|
if (version) {
|
|
91
97
|
fetchUrl.searchParams.set("_rsc_v", version);
|
|
92
98
|
}
|
|
99
|
+
if (routerId) {
|
|
100
|
+
fetchUrl.searchParams.set("_rsc_rid", routerId);
|
|
101
|
+
}
|
|
93
102
|
|
|
94
|
-
// Check completed in-memory prefetch cache before making a network
|
|
95
|
-
//
|
|
96
|
-
//
|
|
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.
|
|
97
110
|
// Skip cache for stale revalidation (needs fresh data), HMR (needs
|
|
98
111
|
// fresh modules), and intercept contexts (source-dependent responses).
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
+
}
|
|
106
146
|
// Track when the stream completes
|
|
107
147
|
let resolveStreamComplete: () => void;
|
|
108
148
|
const streamComplete = new Promise<void>((resolve) => {
|
|
109
149
|
resolveStreamComplete = resolve;
|
|
110
150
|
});
|
|
111
151
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
:
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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;
|
|
173
|
+
|
|
174
|
+
// Server-side redirect without state: the server returned 204 with
|
|
175
|
+
// X-RSC-Redirect instead of a 3xx (which fetch would auto-follow
|
|
176
|
+
// to a URL rendering full HTML). Throw ServerRedirect so the
|
|
177
|
+
// navigation bridge catches it and re-navigates with _skipCache.
|
|
178
|
+
const redirect = extractRscHeaderUrl(response, "X-RSC-Redirect");
|
|
179
|
+
if (redirect === "blocked") {
|
|
180
|
+
resolveStreamComplete();
|
|
181
|
+
return emptyResponse();
|
|
182
|
+
}
|
|
183
|
+
if (redirect) {
|
|
184
|
+
if (tx) {
|
|
185
|
+
browserDebugLog(tx, `server redirect (${source})`, {
|
|
186
|
+
redirectUrl: redirect.url,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
resolveStreamComplete();
|
|
190
|
+
throw new ServerRedirect(redirect.url, undefined);
|
|
191
|
+
}
|
|
192
|
+
|
|
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
|
+
};
|
|
127
208
|
|
|
128
209
|
/** Start a fresh navigation fetch (no cache / inflight hit). */
|
|
129
210
|
const doFreshFetch = (): Promise<Response> => {
|
|
@@ -134,6 +215,11 @@ export function createNavigationClient(
|
|
|
134
215
|
}
|
|
135
216
|
|
|
136
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 }),
|
|
137
223
|
headers: {
|
|
138
224
|
"X-RSC-Router-Client-Path": previousUrl,
|
|
139
225
|
"X-Rango-State": getRangoState(),
|
|
@@ -142,47 +228,14 @@ export function createNavigationClient(
|
|
|
142
228
|
"X-RSC-Router-Intercept-Source": interceptSourceUrl,
|
|
143
229
|
}),
|
|
144
230
|
...(hmr && { "X-RSC-HMR": "1" }),
|
|
145
|
-
...(debugId && { [DEBUG_ID_HEADER]: debugId }),
|
|
146
231
|
},
|
|
147
232
|
signal,
|
|
148
233
|
}).then((response) => {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (reload === "blocked") {
|
|
152
|
-
resolveStreamComplete();
|
|
153
|
-
return emptyResponse();
|
|
154
|
-
}
|
|
155
|
-
if (reload) {
|
|
156
|
-
if (tx) {
|
|
157
|
-
browserDebugLog(tx, "version mismatch, reloading", {
|
|
158
|
-
reloadUrl: reload.url,
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
window.location.href = reload.url;
|
|
162
|
-
return new Promise<Response>(() => {});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Server-side redirect without state: the server returned 204 with
|
|
166
|
-
// X-RSC-Redirect instead of a 3xx (which fetch would auto-follow
|
|
167
|
-
// to a URL rendering full HTML). Throw ServerRedirect so the
|
|
168
|
-
// navigation bridge catches it and re-navigates with _skipCache.
|
|
169
|
-
const redirect = extractRscHeaderUrl(response, "X-RSC-Redirect");
|
|
170
|
-
if (redirect === "blocked") {
|
|
171
|
-
resolveStreamComplete();
|
|
172
|
-
return emptyResponse();
|
|
173
|
-
}
|
|
174
|
-
if (redirect) {
|
|
175
|
-
if (tx) {
|
|
176
|
-
browserDebugLog(tx, "server redirect", {
|
|
177
|
-
redirectUrl: redirect.url,
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
resolveStreamComplete();
|
|
181
|
-
throw new ServerRedirect(redirect.url, undefined);
|
|
182
|
-
}
|
|
234
|
+
const validated = validateRscHeaders(response, "fetch");
|
|
235
|
+
if (validated instanceof Promise) return validated;
|
|
183
236
|
|
|
184
237
|
return teeWithCompletion(
|
|
185
|
-
|
|
238
|
+
validated,
|
|
186
239
|
() => {
|
|
187
240
|
if (tx) browserDebugLog(tx, "stream complete");
|
|
188
241
|
resolveStreamComplete();
|
|
@@ -192,59 +245,69 @@ export function createNavigationClient(
|
|
|
192
245
|
});
|
|
193
246
|
};
|
|
194
247
|
|
|
195
|
-
|
|
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,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
let payloadPromise: Promise<RscPayload>;
|
|
262
|
+
let streamCompletePromise: Promise<void>;
|
|
196
263
|
|
|
197
|
-
if (
|
|
264
|
+
if (cachedEntry) {
|
|
198
265
|
if (tx) {
|
|
199
|
-
browserDebugLog(tx, "prefetch cache hit", {
|
|
266
|
+
browserDebugLog(tx, "prefetch cache hit (warm)", {
|
|
267
|
+
key: hitKey,
|
|
268
|
+
wildcard: hitKey === wildcardKey,
|
|
269
|
+
});
|
|
200
270
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
return teeWithCompletion(
|
|
205
|
-
response,
|
|
206
|
-
() => {
|
|
207
|
-
if (tx) browserDebugLog(tx, "stream complete (from cache)");
|
|
208
|
-
resolveStreamComplete();
|
|
209
|
-
},
|
|
210
|
-
signal,
|
|
211
|
-
);
|
|
212
|
-
});
|
|
213
|
-
} else if (inflightResponsePromise) {
|
|
271
|
+
payloadPromise = cachedEntry.payload;
|
|
272
|
+
streamCompletePromise = cachedEntry.streamComplete;
|
|
273
|
+
} else if (inflightEntryPromise) {
|
|
214
274
|
if (tx) {
|
|
215
|
-
browserDebugLog(tx, "reusing inflight prefetch", {
|
|
275
|
+
browserDebugLog(tx, "reusing inflight prefetch", {
|
|
276
|
+
key: hitKey,
|
|
277
|
+
wildcard: hitKey === wildcardKey,
|
|
278
|
+
});
|
|
216
279
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
return doFreshFetch();
|
|
280
|
+
const adoptedViaWildcard = hitKey === wildcardKey;
|
|
281
|
+
const entry = await inflightEntryPromise;
|
|
282
|
+
if (!entry) {
|
|
283
|
+
if (tx) {
|
|
284
|
+
browserDebugLog(tx, "inflight prefetch unavailable, refetching");
|
|
223
285
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
286
|
+
({ payload: payloadPromise, streamComplete: streamCompletePromise } =
|
|
287
|
+
freshResult());
|
|
288
|
+
} else if (adoptedViaWildcard && entry.scope === "source") {
|
|
289
|
+
// A wildcard-adopted inflight that turned out source-scoped was
|
|
290
|
+
// built for a different source page. Discard and refetch.
|
|
291
|
+
if (tx) {
|
|
292
|
+
browserDebugLog(
|
|
293
|
+
tx,
|
|
294
|
+
"wildcard inflight turned out source-scoped, refetching",
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
({ payload: payloadPromise, streamComplete: streamCompletePromise } =
|
|
298
|
+
freshResult());
|
|
299
|
+
} else {
|
|
300
|
+
payloadPromise = entry.payload;
|
|
301
|
+
streamCompletePromise = entry.streamComplete;
|
|
302
|
+
}
|
|
236
303
|
} else {
|
|
237
|
-
|
|
304
|
+
({ payload: payloadPromise, streamComplete: streamCompletePromise } =
|
|
305
|
+
freshResult());
|
|
238
306
|
}
|
|
239
307
|
|
|
240
308
|
try {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
responsePromise,
|
|
244
|
-
{
|
|
245
|
-
...(debugChannel && { debugChannel, findSourceMapURL }),
|
|
246
|
-
},
|
|
247
|
-
);
|
|
309
|
+
const payload = await payloadPromise;
|
|
310
|
+
|
|
248
311
|
if (tx) {
|
|
249
312
|
browserDebugLog(tx, "response received", {
|
|
250
313
|
isPartial: payload.metadata?.isPartial,
|
|
@@ -252,7 +315,7 @@ export function createNavigationClient(
|
|
|
252
315
|
diffCount: payload.metadata?.diff?.length ?? 0,
|
|
253
316
|
});
|
|
254
317
|
}
|
|
255
|
-
return { payload, streamComplete };
|
|
318
|
+
return { payload, streamComplete: streamCompletePromise };
|
|
256
319
|
} catch (error) {
|
|
257
320
|
// Convert network-level errors to NetworkError for proper handling
|
|
258
321
|
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
|
-
}
|