@rangojs/router 0.0.0-experimental.31 → 0.0.0-experimental.3232cd17
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 +4 -0
- package/README.md +198 -44
- package/dist/bin/rango.js +287 -105
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3248 -1117
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +73 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +107 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +245 -21
- package/skills/caching/SKILL.md +302 -6
- 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 +270 -30
- package/skills/host-router/SKILL.md +82 -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 +294 -30
- 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 +203 -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 +97 -5
- package/skills/router-setup/SKILL.md +90 -5
- 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 +329 -27
- package/skills/use-cache/SKILL.md +36 -5
- 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/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 +86 -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/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +148 -19
- package/src/browser/navigation-client.ts +187 -67
- 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/partial-update.ts +123 -94
- package/src/browser/prefetch/cache.ts +214 -36
- package/src/browser/prefetch/fetch.ts +260 -38
- 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 +93 -11
- package/src/browser/react/NavigationProvider.tsx +115 -34
- 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 +215 -76
- package/src/browser/scroll-restoration.ts +46 -39
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +176 -50
- package/src/browser/types.ts +95 -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 +5 -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 +278 -96
- 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 +149 -43
- package/src/cache/cache-scope.ts +148 -81
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2550 -93
- package/src/cache/cf/index.ts +11 -17
- package/src/cache/document-cache.ts +78 -27
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +23 -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/taint.ts +55 -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 +84 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +36 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +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 +52 -26
- package/src/index.ts +100 -38
- 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 +437 -274
- 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 -0
- package/src/route-definition/redirect.ts +52 -10
- 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 +108 -9
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +45 -22
- package/src/router/handler-context.ts +83 -41
- package/src/router/intercept-resolution.ts +25 -23
- package/src/router/lazy-includes.ts +19 -53
- package/src/router/loader-resolution.ts +213 -30
- 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 +27 -6
- package/src/router/match-middleware/cache-lookup.ts +205 -249
- package/src/router/match-middleware/cache-store.ts +45 -32
- 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 -40
- package/src/router/metrics.ts +5 -34
- package/src/router/middleware-types.ts +13 -142
- package/src/router/middleware.ts +173 -143
- 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 +192 -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 +6 -28
- 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 +242 -75
- package/src/router/segment-resolution/helpers.ts +64 -25
- package/src/router/segment-resolution/loader-cache.ts +41 -37
- package/src/router/segment-resolution/revalidation.ts +456 -372
- 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 +2 -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 +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +134 -43
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +492 -383
- 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 +23 -3
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +30 -3
- 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 +56 -54
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +74 -67
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +25 -6
- 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 +272 -129
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +309 -61
- 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 +348 -128
- package/src/ssr/index.tsx +23 -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 +233 -81
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +107 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +19 -7
- package/src/types/segments.ts +37 -14
- 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 -6
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +8 -0
- package/src/vite/plugin-types.ts +187 -69
- 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 +49 -98
- 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 +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/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +41 -20
- package/src/vite/plugins/virtual-entries.ts +2 -17
- package/src/vite/rango.ts +257 -289
- package/src/vite/router-discovery.ts +930 -140
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- 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/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 +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/router/middleware-cookies.ts +0 -55
|
@@ -4,12 +4,21 @@ import type {
|
|
|
4
4
|
NavigateOptionsInternal,
|
|
5
5
|
ResolvedSegment,
|
|
6
6
|
} from "./types.js";
|
|
7
|
+
import { setAppVersion } from "./app-version.js";
|
|
8
|
+
import { isActionFenceActive } from "./action-fence.js";
|
|
9
|
+
import { getRangoState } from "./rango-state.js";
|
|
7
10
|
import * as React from "react";
|
|
8
11
|
import { startTransition } from "react";
|
|
9
12
|
import {
|
|
10
13
|
createNavigationTransaction,
|
|
11
14
|
resolveNavigationState,
|
|
12
15
|
} from "./navigation-transaction.js";
|
|
16
|
+
import { buildHistoryState, pushHistoryWithIdx } from "./history-state.js";
|
|
17
|
+
import {
|
|
18
|
+
handleNavigationStart,
|
|
19
|
+
handleNavigationEnd,
|
|
20
|
+
ensureHistoryKey,
|
|
21
|
+
} from "./scroll-restoration.js";
|
|
13
22
|
|
|
14
23
|
// addTransitionType is only available in React experimental
|
|
15
24
|
const addTransitionType: ((type: string) => void) | undefined =
|
|
@@ -18,7 +27,6 @@ const addTransitionType: ((type: string) => void) | undefined =
|
|
|
18
27
|
import { setupLinkInterception } from "./link-interceptor.js";
|
|
19
28
|
import { createPartialUpdater } from "./partial-update.js";
|
|
20
29
|
import { generateHistoryKey } from "./navigation-store.js";
|
|
21
|
-
import { handleNavigationEnd } from "./scroll-restoration.js";
|
|
22
30
|
import type { EventController } from "./event-controller.js";
|
|
23
31
|
import { isInterceptOnlyCache } from "./intercept-utils.js";
|
|
24
32
|
import {
|
|
@@ -35,11 +43,6 @@ if (typeof Symbol.dispose === "undefined") {
|
|
|
35
43
|
(Symbol as any).dispose = Symbol("Symbol.dispose");
|
|
36
44
|
}
|
|
37
45
|
|
|
38
|
-
/** Get IDs of non-loader segments (layouts, routes, parallels). */
|
|
39
|
-
function getNonLoaderSegmentIds(segments: ResolvedSegment[]): string[] {
|
|
40
|
-
return segments.filter((s) => s.type !== "loader").map((s) => s.id);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
46
|
export { createNavigationTransaction };
|
|
44
47
|
|
|
45
48
|
/**
|
|
@@ -47,7 +50,7 @@ export { createNavigationTransaction };
|
|
|
47
50
|
*/
|
|
48
51
|
export interface NavigationBridgeConfigWithController extends NavigationBridgeConfig {
|
|
49
52
|
eventController: EventController;
|
|
50
|
-
/** RSC version from initial payload metadata */
|
|
53
|
+
/** RSC version from initial payload metadata. */
|
|
51
54
|
version?: string;
|
|
52
55
|
}
|
|
53
56
|
|
|
@@ -67,8 +70,8 @@ export interface NavigationBridgeConfigWithController extends NavigationBridgeCo
|
|
|
67
70
|
export function createNavigationBridge(
|
|
68
71
|
config: NavigationBridgeConfigWithController,
|
|
69
72
|
): NavigationBridge {
|
|
70
|
-
const { store, client, eventController, onUpdate, renderSegments
|
|
71
|
-
|
|
73
|
+
const { store, client, eventController, onUpdate, renderSegments } = config;
|
|
74
|
+
let version = config.version;
|
|
72
75
|
|
|
73
76
|
// Create shared partial updater
|
|
74
77
|
const fetchPartialUpdate = createPartialUpdater({
|
|
@@ -76,7 +79,7 @@ export function createNavigationBridge(
|
|
|
76
79
|
client,
|
|
77
80
|
onUpdate,
|
|
78
81
|
renderSegments,
|
|
79
|
-
version,
|
|
82
|
+
getVersion: () => version,
|
|
80
83
|
});
|
|
81
84
|
|
|
82
85
|
return {
|
|
@@ -114,6 +117,81 @@ export function createNavigationBridge(
|
|
|
114
117
|
return;
|
|
115
118
|
}
|
|
116
119
|
|
|
120
|
+
// Shallow navigation: skip RSC fetch when revalidate is false
|
|
121
|
+
// and the pathname hasn't changed (search param / hash only change).
|
|
122
|
+
if (
|
|
123
|
+
options?.revalidate === false &&
|
|
124
|
+
targetUrl.pathname === new URL(window.location.href).pathname
|
|
125
|
+
) {
|
|
126
|
+
// Preserve intercept context from the current history entry so that
|
|
127
|
+
// popstate uses the correct cache key (:intercept suffix) and restores
|
|
128
|
+
// the right full-page vs modal semantics.
|
|
129
|
+
const currentHistoryState = window.history.state;
|
|
130
|
+
const isIntercept = currentHistoryState?.intercept === true;
|
|
131
|
+
const interceptSourceUrl = isIntercept
|
|
132
|
+
? currentHistoryState?.sourceUrl
|
|
133
|
+
: undefined;
|
|
134
|
+
|
|
135
|
+
const historyKey = generateHistoryKey(url, { intercept: isIntercept });
|
|
136
|
+
|
|
137
|
+
// Copy current segments to the new history key so back/forward restores instantly
|
|
138
|
+
const currentKey = store.getHistoryKey();
|
|
139
|
+
const currentCache = store.getCachedSegments(currentKey);
|
|
140
|
+
if (currentCache?.segments) {
|
|
141
|
+
const currentHandleData = eventController.getHandleState().data;
|
|
142
|
+
store.cacheSegmentsForHistory(
|
|
143
|
+
historyKey,
|
|
144
|
+
currentCache.segments,
|
|
145
|
+
currentHandleData,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Save current scroll position before changing URL
|
|
150
|
+
handleNavigationStart();
|
|
151
|
+
|
|
152
|
+
// Snapshot old state before pushState/replaceState overwrites it
|
|
153
|
+
const oldState = window.history.state;
|
|
154
|
+
|
|
155
|
+
// Update browser URL (carry intercept context into history state)
|
|
156
|
+
const historyState = buildHistoryState(
|
|
157
|
+
resolvedState,
|
|
158
|
+
{
|
|
159
|
+
intercept: isIntercept || undefined,
|
|
160
|
+
sourceUrl: interceptSourceUrl,
|
|
161
|
+
},
|
|
162
|
+
{},
|
|
163
|
+
);
|
|
164
|
+
pushHistoryWithIdx(historyState, url, options?.replace ?? false);
|
|
165
|
+
|
|
166
|
+
// Ensure new history entry has a scroll restoration key
|
|
167
|
+
ensureHistoryKey();
|
|
168
|
+
|
|
169
|
+
// Notify useLocationState() hooks when state changes
|
|
170
|
+
const hasOldState =
|
|
171
|
+
oldState &&
|
|
172
|
+
typeof oldState === "object" &&
|
|
173
|
+
("state" in oldState ||
|
|
174
|
+
Object.keys(oldState).some((k) => k.startsWith("__rsc_ls_")));
|
|
175
|
+
const hasNewState =
|
|
176
|
+
historyState &&
|
|
177
|
+
("state" in historyState ||
|
|
178
|
+
Object.keys(historyState).some((k) => k.startsWith("__rsc_ls_")));
|
|
179
|
+
if (hasOldState || hasNewState) {
|
|
180
|
+
window.dispatchEvent(new Event("__rsc_locationstate"));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Update store history key so future navigations reference the right cache
|
|
184
|
+
store.setHistoryKey(historyKey);
|
|
185
|
+
store.setCurrentUrl(url);
|
|
186
|
+
|
|
187
|
+
// Notify hooks — location updates, state stays idle
|
|
188
|
+
eventController.setLocation(targetUrl);
|
|
189
|
+
|
|
190
|
+
// Handle post-navigation scroll
|
|
191
|
+
handleNavigationEnd({ scroll: options.scroll });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
117
195
|
// Only abort pending requests when navigating to a different route
|
|
118
196
|
// Same-route navigation (e.g., /todos -> /todos) should not cancel in-flight actions
|
|
119
197
|
const currentPath = new URL(window.location.href).pathname;
|
|
@@ -181,18 +259,24 @@ export function createNavigationBridge(
|
|
|
181
259
|
// 2. routes that CAN be intercepted - we don't know if this navigation will intercept
|
|
182
260
|
// 3. when leaving intercept - we need fresh non-intercept segments from server
|
|
183
261
|
// 4. redirect-with-state - force re-render so hooks read fresh state
|
|
262
|
+
// 5. stale cache - server action invalidated it, need fresh data with loading state
|
|
184
263
|
const hasUsableCache =
|
|
185
264
|
cachedSegments &&
|
|
186
265
|
cachedSegments.length > 0 &&
|
|
187
266
|
!isInterceptOnlyCache(cachedSegments) &&
|
|
188
267
|
!hasInterceptCache &&
|
|
189
268
|
!isLeavingIntercept &&
|
|
269
|
+
!cached?.stale &&
|
|
190
270
|
!options?._skipCache;
|
|
191
271
|
|
|
272
|
+
// Forward navigations always await fetchPartialUpdate before rendering,
|
|
273
|
+
// so useNavigation should always report "loading". skipLoadingState is
|
|
274
|
+
// only used for popstate background revalidation (line ~526) where
|
|
275
|
+
// cached content renders instantly without a network wait.
|
|
192
276
|
const tx = createNavigationTransaction(store, eventController, url, {
|
|
193
277
|
...options,
|
|
194
278
|
state: resolvedState,
|
|
195
|
-
skipLoadingState:
|
|
279
|
+
skipLoadingState: false,
|
|
196
280
|
});
|
|
197
281
|
|
|
198
282
|
// REVALIDATE: Fetch fresh data from server
|
|
@@ -200,7 +284,7 @@ export function createNavigationBridge(
|
|
|
200
284
|
await fetchPartialUpdate(
|
|
201
285
|
url,
|
|
202
286
|
hasUsableCache
|
|
203
|
-
?
|
|
287
|
+
? cachedSegments!.map((s) => s.id)
|
|
204
288
|
: options?._skipCache
|
|
205
289
|
? [] // Action redirect: send no segments so server renders everything fresh
|
|
206
290
|
: undefined,
|
|
@@ -332,6 +416,15 @@ export function createNavigationBridge(
|
|
|
332
416
|
eventController.abortAllActions();
|
|
333
417
|
}
|
|
334
418
|
|
|
419
|
+
// Popstate that exits an intercept to a non-intercept destination. The
|
|
420
|
+
// fallback fetch path below needs `leave-intercept` mode so it filters
|
|
421
|
+
// the cached @modal segment from the request and forces a re-render —
|
|
422
|
+
// otherwise a cache-miss popstate whose server response has an empty
|
|
423
|
+
// diff hits the "no changes" branch in partial-update and the modal
|
|
424
|
+
// stays on screen.
|
|
425
|
+
const isLeavingIntercept =
|
|
426
|
+
!isIntercept && currentInterceptSource !== null;
|
|
427
|
+
|
|
335
428
|
// Compute history key from URL (with intercept suffix if applicable)
|
|
336
429
|
const historyKey = generateHistoryKey(url, { intercept: isIntercept });
|
|
337
430
|
|
|
@@ -355,11 +448,22 @@ export function createNavigationBridge(
|
|
|
355
448
|
// Helper to check if streaming is in progress
|
|
356
449
|
const isStreaming = () => eventController.getState().isStreaming;
|
|
357
450
|
|
|
451
|
+
// Surface any external rotation of the rango state cookie (a server
|
|
452
|
+
// Set-Cookie, a sibling tab, a cookie clear) BEFORE reading the stale bit.
|
|
453
|
+
// The divergence observer only runs inside getRangoState() — fetch-time —
|
|
454
|
+
// so a popstate-first interaction would otherwise serve a pre-mutation
|
|
455
|
+
// page as fresh and never fetch to trigger the observer. Reading here lets
|
|
456
|
+
// the observer mark the history cache stale so getCachedSegments sees it.
|
|
457
|
+
getRangoState();
|
|
458
|
+
|
|
358
459
|
// Check if we can restore from history cache
|
|
359
460
|
const cached = store.getCachedSegments(historyKey);
|
|
360
461
|
const cachedSegments = cached?.segments;
|
|
361
462
|
const cachedHandleData = cached?.handleData;
|
|
362
|
-
|
|
463
|
+
// While an action is in flight the fence persists no stale flag, so OR it
|
|
464
|
+
// in here: a popstate during the flight serves the cached entry AND
|
|
465
|
+
// revalidates (SWR) instead of serving it as fresh.
|
|
466
|
+
const isStale = (cached?.stale ?? false) || isActionFenceActive();
|
|
363
467
|
|
|
364
468
|
if (cachedSegments && cachedSegments.length > 0) {
|
|
365
469
|
// Update store to point to this history entry
|
|
@@ -368,6 +472,12 @@ export function createNavigationBridge(
|
|
|
368
472
|
store.setCurrentUrl(url);
|
|
369
473
|
store.setPath(new URL(url).pathname);
|
|
370
474
|
|
|
475
|
+
// Restore router identity from cache so subsequent navigations
|
|
476
|
+
// don't falsely detect an app switch.
|
|
477
|
+
if (cached?.routerId) {
|
|
478
|
+
store.setRouterId?.(cached.routerId);
|
|
479
|
+
}
|
|
480
|
+
|
|
371
481
|
// Render from cache - force await to skip loading fallbacks
|
|
372
482
|
try {
|
|
373
483
|
const root = await renderSegments(cachedSegments, {
|
|
@@ -393,8 +503,16 @@ export function createNavigationBridge(
|
|
|
393
503
|
cachedHandleData,
|
|
394
504
|
params: cachedParams,
|
|
395
505
|
},
|
|
506
|
+
scroll: { restore: true, isStreaming },
|
|
396
507
|
};
|
|
397
|
-
|
|
508
|
+
// Intercept-driven popstate (entering OR leaving an intercept) only
|
|
509
|
+
// mutates the parallel slot; the main outlet shows the same content.
|
|
510
|
+
// Skip startViewTransition in those cases — same rationale as the
|
|
511
|
+
// intercept guard in partial-update.ts's hasTransition computation.
|
|
512
|
+
const hasTransition =
|
|
513
|
+
!isIntercept &&
|
|
514
|
+
!isLeavingIntercept &&
|
|
515
|
+
cachedSegments.some((s) => s.transition);
|
|
398
516
|
if (hasTransition) {
|
|
399
517
|
startTransition(() => {
|
|
400
518
|
if (addTransitionType) {
|
|
@@ -406,14 +524,11 @@ export function createNavigationBridge(
|
|
|
406
524
|
onUpdate(popstateUpdate);
|
|
407
525
|
}
|
|
408
526
|
|
|
409
|
-
// Restore scroll position for back/forward navigation
|
|
410
|
-
handleNavigationEnd({ restore: true, isStreaming });
|
|
411
|
-
|
|
412
527
|
// SWR: If stale, trigger background revalidation
|
|
413
528
|
if (isStale) {
|
|
414
529
|
debugLog("[Browser] Cache is stale, background revalidating...");
|
|
415
530
|
// Background revalidation - don't await, just fire and forget
|
|
416
|
-
const segmentIds =
|
|
531
|
+
const segmentIds = cachedSegments.map((s) => s.id);
|
|
417
532
|
|
|
418
533
|
const tx = createNavigationTransaction(
|
|
419
534
|
store,
|
|
@@ -478,7 +593,11 @@ export function createNavigationBridge(
|
|
|
478
593
|
intercept: isIntercept,
|
|
479
594
|
interceptSourceUrl,
|
|
480
595
|
}),
|
|
481
|
-
isIntercept
|
|
596
|
+
isIntercept
|
|
597
|
+
? { type: "navigate", interceptSourceUrl }
|
|
598
|
+
: isLeavingIntercept
|
|
599
|
+
? { type: "leave-intercept" }
|
|
600
|
+
: undefined,
|
|
482
601
|
);
|
|
483
602
|
// Restore scroll position after fetch completes
|
|
484
603
|
handleNavigationEnd({ restore: true, isStreaming });
|
|
@@ -555,6 +674,16 @@ export function createNavigationBridge(
|
|
|
555
674
|
window.removeEventListener("pageshow", handlePageShow);
|
|
556
675
|
};
|
|
557
676
|
},
|
|
677
|
+
|
|
678
|
+
getVersion(): string | undefined {
|
|
679
|
+
return version;
|
|
680
|
+
},
|
|
681
|
+
|
|
682
|
+
updateVersion(newVersion: string): void {
|
|
683
|
+
version = newVersion;
|
|
684
|
+
setAppVersion(newVersion);
|
|
685
|
+
store.clearHistoryCache();
|
|
686
|
+
},
|
|
558
687
|
};
|
|
559
688
|
}
|
|
560
689
|
|
|
@@ -12,12 +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";
|
|
20
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
buildPrefetchKey,
|
|
25
|
+
buildSourceKey,
|
|
26
|
+
consumeInflightPrefetch,
|
|
27
|
+
consumePrefetch,
|
|
28
|
+
type DecodedPrefetch,
|
|
29
|
+
} from "./prefetch/cache.js";
|
|
21
30
|
|
|
22
31
|
/**
|
|
23
32
|
* Create a navigation client for fetching RSC payloads
|
|
@@ -26,8 +35,10 @@ import { buildPrefetchKey, consumePrefetch } from "./prefetch/cache.js";
|
|
|
26
35
|
* deserializing the response using the RSC runtime.
|
|
27
36
|
*
|
|
28
37
|
* Checks the in-memory prefetch cache before making a network request.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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.
|
|
31
42
|
*
|
|
32
43
|
* @param deps - RSC browser dependencies (createFromFetch)
|
|
33
44
|
* @returns NavigationClient instance
|
|
@@ -57,6 +68,7 @@ export function createNavigationClient(
|
|
|
57
68
|
staleRevalidation,
|
|
58
69
|
interceptSourceUrl,
|
|
59
70
|
version,
|
|
71
|
+
routerId,
|
|
60
72
|
hmr,
|
|
61
73
|
} = options;
|
|
62
74
|
|
|
@@ -84,50 +96,130 @@ export function createNavigationClient(
|
|
|
84
96
|
if (version) {
|
|
85
97
|
fetchUrl.searchParams.set("_rsc_v", version);
|
|
86
98
|
}
|
|
99
|
+
if (routerId) {
|
|
100
|
+
fetchUrl.searchParams.set("_rsc_rid", routerId);
|
|
101
|
+
}
|
|
87
102
|
|
|
88
|
-
// Check in-memory prefetch cache before making a network
|
|
89
|
-
//
|
|
90
|
-
//
|
|
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.
|
|
91
110
|
// Skip cache for stale revalidation (needs fresh data), HMR (needs
|
|
92
111
|
// fresh modules), and intercept contexts (source-dependent responses).
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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);
|
|
98
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
|
-
|
|
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;
|
|
106
173
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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();
|
|
110
182
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
+
};
|
|
208
|
+
|
|
209
|
+
/** Start a fresh navigation fetch (no cache / inflight hit). */
|
|
210
|
+
const doFreshFetch = (): Promise<Response> => {
|
|
124
211
|
if (tx) {
|
|
125
212
|
browserDebugLog(tx, "fetching", {
|
|
126
213
|
path: `${fetchUrl.pathname}${fetchUrl.search}`,
|
|
127
214
|
});
|
|
128
215
|
}
|
|
129
216
|
|
|
130
|
-
|
|
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 }),
|
|
131
223
|
headers: {
|
|
132
224
|
"X-RSC-Router-Client-Path": previousUrl,
|
|
133
225
|
"X-Rango-State": getRangoState(),
|
|
@@ -139,43 +231,11 @@ export function createNavigationClient(
|
|
|
139
231
|
},
|
|
140
232
|
signal,
|
|
141
233
|
}).then((response) => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (reload === "blocked") {
|
|
145
|
-
resolveStreamComplete();
|
|
146
|
-
return emptyResponse();
|
|
147
|
-
}
|
|
148
|
-
if (reload) {
|
|
149
|
-
if (tx) {
|
|
150
|
-
browserDebugLog(tx, "version mismatch, reloading", {
|
|
151
|
-
reloadUrl: reload.url,
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
window.location.href = reload.url;
|
|
155
|
-
return new Promise<Response>(() => {});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Server-side redirect without state: the server returned 204 with
|
|
159
|
-
// X-RSC-Redirect instead of a 3xx (which fetch would auto-follow
|
|
160
|
-
// to a URL rendering full HTML). Throw ServerRedirect so the
|
|
161
|
-
// navigation bridge catches it and re-navigates with _skipCache.
|
|
162
|
-
const redirect = extractRscHeaderUrl(response, "X-RSC-Redirect");
|
|
163
|
-
if (redirect === "blocked") {
|
|
164
|
-
resolveStreamComplete();
|
|
165
|
-
return emptyResponse();
|
|
166
|
-
}
|
|
167
|
-
if (redirect) {
|
|
168
|
-
if (tx) {
|
|
169
|
-
browserDebugLog(tx, "server redirect", {
|
|
170
|
-
redirectUrl: redirect.url,
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
resolveStreamComplete();
|
|
174
|
-
throw new ServerRedirect(redirect.url, undefined);
|
|
175
|
-
}
|
|
234
|
+
const validated = validateRscHeaders(response, "fetch");
|
|
235
|
+
if (validated instanceof Promise) return validated;
|
|
176
236
|
|
|
177
237
|
return teeWithCompletion(
|
|
178
|
-
|
|
238
|
+
validated,
|
|
179
239
|
() => {
|
|
180
240
|
if (tx) browserDebugLog(tx, "stream complete");
|
|
181
241
|
resolveStreamComplete();
|
|
@@ -183,11 +243,71 @@ export function createNavigationClient(
|
|
|
183
243
|
signal,
|
|
184
244
|
);
|
|
185
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,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
let payloadPromise: Promise<RscPayload>;
|
|
262
|
+
let streamCompletePromise: Promise<void>;
|
|
263
|
+
|
|
264
|
+
if (cachedEntry) {
|
|
265
|
+
if (tx) {
|
|
266
|
+
browserDebugLog(tx, "prefetch cache hit (warm)", {
|
|
267
|
+
key: hitKey,
|
|
268
|
+
wildcard: hitKey === wildcardKey,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
payloadPromise = cachedEntry.payload;
|
|
272
|
+
streamCompletePromise = cachedEntry.streamComplete;
|
|
273
|
+
} else if (inflightEntryPromise) {
|
|
274
|
+
if (tx) {
|
|
275
|
+
browserDebugLog(tx, "reusing inflight prefetch", {
|
|
276
|
+
key: hitKey,
|
|
277
|
+
wildcard: hitKey === wildcardKey,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
const adoptedViaWildcard = hitKey === wildcardKey;
|
|
281
|
+
const entry = await inflightEntryPromise;
|
|
282
|
+
if (!entry) {
|
|
283
|
+
if (tx) {
|
|
284
|
+
browserDebugLog(tx, "inflight prefetch unavailable, refetching");
|
|
285
|
+
}
|
|
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
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
({ payload: payloadPromise, streamComplete: streamCompletePromise } =
|
|
305
|
+
freshResult());
|
|
186
306
|
}
|
|
187
307
|
|
|
188
308
|
try {
|
|
189
|
-
|
|
190
|
-
|
|
309
|
+
const payload = await payloadPromise;
|
|
310
|
+
|
|
191
311
|
if (tx) {
|
|
192
312
|
browserDebugLog(tx, "response received", {
|
|
193
313
|
isPartial: payload.metadata?.isPartial,
|
|
@@ -195,7 +315,7 @@ export function createNavigationClient(
|
|
|
195
315
|
diffCount: payload.metadata?.diff?.length ?? 0,
|
|
196
316
|
});
|
|
197
317
|
}
|
|
198
|
-
return { payload, streamComplete };
|
|
318
|
+
return { payload, streamComplete: streamCompletePromise };
|
|
199
319
|
} catch (error) {
|
|
200
320
|
// Convert network-level errors to NetworkError for proper handling
|
|
201
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
|
+
}
|