@rangojs/router 0.0.0-experimental.b9cb8739 → 0.0.0-experimental.bd6e11bc
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 +196 -43
- package/dist/bin/rango.js +277 -99
- package/dist/testing/vitest.js +48 -0
- package/dist/vite/index.js +2779 -1064
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +57 -11
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +243 -21
- package/skills/caching/SKILL.md +155 -6
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +229 -20
- package/skills/host-router/SKILL.md +45 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +46 -4
- package/skills/layout/SKILL.md +28 -7
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +273 -53
- package/skills/middleware/SKILL.md +49 -12
- package/skills/migrate-nextjs/SKILL.md +562 -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 +197 -6
- package/skills/prerender/SKILL.md +123 -100
- package/skills/rango/SKILL.md +242 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +66 -9
- package/skills/route/SKILL.md +88 -4
- package/skills/router-setup/SKILL.md +90 -5
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +716 -0
- package/skills/typesafety/SKILL.md +329 -27
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/__internal.ts +1 -1
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +91 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +102 -16
- package/src/browser/navigation-client.ts +164 -59
- package/src/browser/navigation-store.ts +75 -17
- package/src/browser/navigation-transaction.ts +21 -37
- package/src/browser/partial-update.ts +139 -38
- package/src/browser/prefetch/cache.ts +175 -15
- package/src/browser/prefetch/fetch.ts +180 -33
- package/src/browser/prefetch/queue.ts +123 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +81 -9
- package/src/browser/react/NavigationProvider.tsx +110 -33
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +23 -64
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +43 -10
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +191 -74
- package/src/browser/scroll-restoration.ts +41 -14
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +31 -36
- package/src/browser/types.ts +57 -5
- 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 +2 -0
- package/src/build/route-trie.ts +52 -25
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +278 -88
- 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-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +76 -49
- package/src/cache/cf/cf-cache-store.ts +501 -18
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +94 -238
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +65 -12
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +12 -5
- package/src/index.ts +61 -11
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +141 -80
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -15
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +435 -260
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +110 -34
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +113 -1
- package/src/router/error-handling.ts +1 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +77 -38
- package/src/router/intercept-resolution.ts +15 -22
- package/src/router/lazy-includes.ts +12 -9
- package/src/router/loader-resolution.ts +174 -22
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +31 -16
- package/src/router/match-api.ts +128 -192
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +136 -106
- package/src/router/match-middleware/cache-store.ts +54 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +61 -5
- package/src/router/match-result.ts +125 -10
- package/src/router/metrics.ts +7 -2
- package/src/router/middleware-types.ts +21 -34
- package/src/router/middleware.ts +103 -90
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/prerender-match.ts +110 -10
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +286 -0
- package/src/router/revalidation.ts +58 -2
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +77 -28
- package/src/router/router-options.ts +76 -11
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +223 -24
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +466 -285
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +18 -13
- package/src/router/types.ts +9 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +91 -23
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +440 -381
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +18 -2
- package/src/rsc/response-route-handler.ts +46 -53
- package/src/rsc/rsc-rendering.ts +41 -48
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +25 -37
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +17 -3
- package/src/search-params.ts +4 -4
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +219 -67
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +277 -61
- package/src/server/cookie-store.ts +28 -4
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +204 -60
- package/src/ssr/index.tsx +9 -1
- package/src/static-handler.ts +19 -7
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +106 -0
- package/src/testing/internal/context.ts +255 -0
- package/src/testing/render-route.tsx +565 -0
- package/src/testing/run-loader.ts +296 -0
- package/src/testing/run-middleware.ts +179 -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 +183 -0
- package/src/types/cache-types.ts +4 -4
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +194 -72
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +41 -15
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +19 -1
- package/src/types/segments.ts +37 -1
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +50 -9
- package/src/urls/path-helper.ts +63 -63
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +487 -44
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +34 -37
- package/src/vite/discovery/discover-routers.ts +105 -51
- 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 +188 -93
- 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 +46 -6
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +6 -0
- package/src/vite/plugin-types.ts +111 -72
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- 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 +214 -0
- package/src/vite/plugins/expose-action-id.ts +55 -33
- package/src/vite/plugins/expose-id-utils.ts +24 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +12 -35
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +544 -317
- package/src/vite/plugins/performance-tracks.ts +92 -0
- package/src/vite/plugins/refresh-cmd.ts +88 -26
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +72 -3
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +265 -226
- package/src/vite/router-discovery.ts +920 -137
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +38 -5
- package/src/vite/utils/shared-utils.ts +109 -27
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -14,11 +14,39 @@ const addTransitionType: ((type: string) => void) | undefined =
|
|
|
14
14
|
import type { RenderSegmentsOptions } from "../segment-system.js";
|
|
15
15
|
import { reconcileSegments } from "./segment-reconciler.js";
|
|
16
16
|
import type { ReconcileActor } from "./segment-reconciler.js";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
hasActiveIntercept as hasActiveInterceptSlots,
|
|
19
|
+
isInterceptSegment,
|
|
20
|
+
} from "./intercept-utils.js";
|
|
18
21
|
import type { BoundTransaction } from "./navigation-transaction.js";
|
|
19
22
|
import { ServerRedirect } from "../errors.js";
|
|
20
23
|
import { debugLog } from "./logging.js";
|
|
21
24
|
import { validateRedirectOrigin } from "./validate-redirect-origin.js";
|
|
25
|
+
import type { NavigationUpdate } from "./types.js";
|
|
26
|
+
|
|
27
|
+
/** Build a scroll payload from the commit's scroll option */
|
|
28
|
+
function toScrollPayload(
|
|
29
|
+
scroll: boolean | undefined,
|
|
30
|
+
): NonNullable<NavigationUpdate["scroll"]> {
|
|
31
|
+
return { enabled: scroll !== false ? scroll : false };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether to wrap an update in startViewTransition.
|
|
36
|
+
*
|
|
37
|
+
* Intercept-driven updates only mutate the parallel slot — the main outlet
|
|
38
|
+
* shows the same content — so transitions on the underlying main segments
|
|
39
|
+
* shouldn't fire (otherwise their elements get hoisted above the modal).
|
|
40
|
+
*/
|
|
41
|
+
function shouldStartViewTransition(segments: ResolvedSegment[]): boolean {
|
|
42
|
+
let hasIntercept = false;
|
|
43
|
+
let hasTransition = false;
|
|
44
|
+
for (const s of segments) {
|
|
45
|
+
if (isInterceptSegment(s)) hasIntercept = true;
|
|
46
|
+
else if (s.transition) hasTransition = true;
|
|
47
|
+
}
|
|
48
|
+
return !hasIntercept && hasTransition;
|
|
49
|
+
}
|
|
22
50
|
|
|
23
51
|
/**
|
|
24
52
|
* Configuration for creating a partial updater
|
|
@@ -31,8 +59,15 @@ export interface PartialUpdateConfig {
|
|
|
31
59
|
segments: ResolvedSegment[],
|
|
32
60
|
options?: RenderSegmentsOptions,
|
|
33
61
|
) => Promise<ReactNode> | ReactNode;
|
|
34
|
-
/** RSC version
|
|
35
|
-
|
|
62
|
+
/** RSC version getter — returns the current version (may change after HMR) */
|
|
63
|
+
getVersion?: () => string | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Replace the active app-shell when a cross-app navigation is detected.
|
|
66
|
+
* Called before the full-update tree replacement renders, so the new
|
|
67
|
+
* payload's rootLayout, basename, and version are picked up. Theme,
|
|
68
|
+
* warmup, and prefetch TTL are not part of the shell — see AppShell.
|
|
69
|
+
*/
|
|
70
|
+
applyAppShell?: (next: import("./app-shell.js").AppShell) => void;
|
|
36
71
|
}
|
|
37
72
|
|
|
38
73
|
/**
|
|
@@ -68,7 +103,7 @@ export type UpdateMode =
|
|
|
68
103
|
/** Source URL for intercept restore (popstate cache miss) */
|
|
69
104
|
interceptSourceUrl?: string;
|
|
70
105
|
}
|
|
71
|
-
| { type: "leave-intercept" }
|
|
106
|
+
| { type: "leave-intercept"; interceptSourceUrl?: string }
|
|
72
107
|
| { type: "stale-revalidation"; interceptSourceUrl?: string }
|
|
73
108
|
| { type: "action"; interceptSourceUrl?: string };
|
|
74
109
|
|
|
@@ -96,7 +131,14 @@ export type PartialUpdater = (
|
|
|
96
131
|
export function createPartialUpdater(
|
|
97
132
|
config: PartialUpdateConfig,
|
|
98
133
|
): PartialUpdater {
|
|
99
|
-
const {
|
|
134
|
+
const {
|
|
135
|
+
store,
|
|
136
|
+
client,
|
|
137
|
+
onUpdate,
|
|
138
|
+
renderSegments,
|
|
139
|
+
getVersion = () => undefined,
|
|
140
|
+
applyAppShell,
|
|
141
|
+
} = config;
|
|
100
142
|
|
|
101
143
|
/**
|
|
102
144
|
* Get current page's cached segments as an array
|
|
@@ -127,13 +169,7 @@ export function createPartialUpdater(
|
|
|
127
169
|
// Capture history key at start for stale revalidation consistency check
|
|
128
170
|
const historyKeyAtStart = store.getHistoryKey();
|
|
129
171
|
|
|
130
|
-
|
|
131
|
-
const interceptSourceUrl =
|
|
132
|
-
mode.type === "stale-revalidation" ||
|
|
133
|
-
mode.type === "action" ||
|
|
134
|
-
mode.type === "navigate"
|
|
135
|
-
? mode.interceptSourceUrl
|
|
136
|
-
: undefined;
|
|
172
|
+
const interceptSourceUrl = mode.interceptSourceUrl;
|
|
137
173
|
|
|
138
174
|
// When leaving intercept, filter out intercept-specific segments
|
|
139
175
|
let segments: string[];
|
|
@@ -153,9 +189,16 @@ export function createPartialUpdater(
|
|
|
153
189
|
segments = segmentIds ?? segmentState.currentSegmentIds;
|
|
154
190
|
}
|
|
155
191
|
|
|
156
|
-
// For intercept revalidation, use the intercept source URL as previousUrl
|
|
192
|
+
// For intercept revalidation, use the intercept source URL as previousUrl.
|
|
193
|
+
// For leave-intercept, tx.currentUrl captures window.location.href at tx
|
|
194
|
+
// creation, which on popstate is already the destination URL and would
|
|
195
|
+
// tell the server "from == to". segmentState.currentUrl still points at
|
|
196
|
+
// the URL the cached segments render (the intercept URL), which is the
|
|
197
|
+
// correct "from" for the server's diff computation.
|
|
157
198
|
const previousUrl =
|
|
158
|
-
|
|
199
|
+
mode.type === "leave-intercept"
|
|
200
|
+
? segmentState.currentUrl || tx.currentUrl
|
|
201
|
+
: interceptSourceUrl || tx.currentUrl || segmentState.currentUrl;
|
|
159
202
|
|
|
160
203
|
debugLog(`\n[Browser] >>> NAVIGATION`);
|
|
161
204
|
debugLog(`[Browser] From: ${previousUrl}`);
|
|
@@ -169,11 +212,14 @@ export function createPartialUpdater(
|
|
|
169
212
|
// When navigating with targetCacheSegments, use those for consistency.
|
|
170
213
|
// Otherwise fall back to current page's segments (for same-route revalidation).
|
|
171
214
|
const targetCache =
|
|
172
|
-
mode.type === "navigate"
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
215
|
+
mode.type === "navigate" && mode.targetCacheSegments?.length
|
|
216
|
+
? mode.targetCacheSegments
|
|
217
|
+
: undefined;
|
|
218
|
+
const cachedSegs = targetCache ?? getCurrentCachedSegments();
|
|
219
|
+
const cachedSegsSource = targetCache ? "history-cache" : "current-page";
|
|
220
|
+
debugLog(
|
|
221
|
+
`[Browser] cachedSegs source: ${cachedSegsSource} (${cachedSegs.length} segments: ${cachedSegs.map((s) => s.id).join(", ")})`,
|
|
222
|
+
);
|
|
177
223
|
|
|
178
224
|
// Fetch partial payload (no abort signal - RSC doesn't support it well)
|
|
179
225
|
let fetchResult: Awaited<ReturnType<NavigationClient["fetchPartial"]>>;
|
|
@@ -185,7 +231,8 @@ export function createPartialUpdater(
|
|
|
185
231
|
// (action redirect sends empty segments for a fresh render).
|
|
186
232
|
staleRevalidation:
|
|
187
233
|
mode.type === "stale-revalidation" || segments.length === 0,
|
|
188
|
-
version,
|
|
234
|
+
version: getVersion(),
|
|
235
|
+
routerId: store.getRouterId?.(),
|
|
189
236
|
});
|
|
190
237
|
// Mark navigation as streaming (response received, now parsing RSC).
|
|
191
238
|
// Called after fetchPartial so pendingUrl stays set during the network wait,
|
|
@@ -198,6 +245,32 @@ export function createPartialUpdater(
|
|
|
198
245
|
streamingToken.end();
|
|
199
246
|
});
|
|
200
247
|
|
|
248
|
+
// Detect app switch: if routerId changed, the navigation crossed into
|
|
249
|
+
// a different router (e.g., via host router path mount). Downgrade
|
|
250
|
+
// partial to full so the entire tree is replaced without reconciliation
|
|
251
|
+
// against stale segments from the previous app, and replace the app
|
|
252
|
+
// shell (rootLayout, basename, version) so the target app's document
|
|
253
|
+
// and router config take effect instead of remaining captured from the
|
|
254
|
+
// initial load. Theme, warmup, and prefetch TTL are intentionally
|
|
255
|
+
// document-lifetime (see AppShell doc); a new document navigation
|
|
256
|
+
// applies them.
|
|
257
|
+
if (payload.metadata?.routerId) {
|
|
258
|
+
const prevRouterId = store.getRouterId?.();
|
|
259
|
+
if (prevRouterId && prevRouterId !== payload.metadata.routerId) {
|
|
260
|
+
debugLog(
|
|
261
|
+
`[Browser] App switch detected (${prevRouterId} → ${payload.metadata.routerId}), forcing full update`,
|
|
262
|
+
);
|
|
263
|
+
payload.metadata.isPartial = false;
|
|
264
|
+
applyAppShell?.({
|
|
265
|
+
routerId: payload.metadata.routerId,
|
|
266
|
+
rootLayout: payload.metadata.rootLayout,
|
|
267
|
+
basename: payload.metadata.basename,
|
|
268
|
+
version: payload.metadata.version,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
store.setRouterId?.(payload.metadata.routerId);
|
|
272
|
+
}
|
|
273
|
+
|
|
201
274
|
// Handle server-side redirect with state
|
|
202
275
|
if (payload.metadata?.redirect) {
|
|
203
276
|
if (signal?.aborted) {
|
|
@@ -237,7 +310,7 @@ export function createPartialUpdater(
|
|
|
237
310
|
.filter(Boolean) as ResolvedSegment[];
|
|
238
311
|
|
|
239
312
|
// When navigating with cached segments to a different route, render them.
|
|
240
|
-
if (mode.type === "navigate" && targetCache
|
|
313
|
+
if (mode.type === "navigate" && targetCache) {
|
|
241
314
|
debugLog(
|
|
242
315
|
"[Browser] No diff but navigating with cached segments - rendering target route",
|
|
243
316
|
);
|
|
@@ -246,7 +319,21 @@ export function createPartialUpdater(
|
|
|
246
319
|
forceAwait: true,
|
|
247
320
|
});
|
|
248
321
|
|
|
249
|
-
tx.commit(
|
|
322
|
+
const { scroll: commitScroll } = tx.commit(
|
|
323
|
+
matchedIds,
|
|
324
|
+
existingSegments,
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
// tx.commit() cached the source page's handleData because
|
|
328
|
+
// eventController hasn't been updated yet. Overwrite with the
|
|
329
|
+
// correct cached handleData to prevent cache corruption on
|
|
330
|
+
// subsequent navigations to this same URL.
|
|
331
|
+
if (mode.targetCacheHandleData) {
|
|
332
|
+
store.updateCacheHandleData(
|
|
333
|
+
store.getHistoryKey(),
|
|
334
|
+
mode.targetCacheHandleData,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
250
337
|
|
|
251
338
|
// Include cachedHandleData in metadata so NavigationProvider can restore
|
|
252
339
|
// breadcrumbs and other handle data from cache.
|
|
@@ -260,12 +347,10 @@ export function createPartialUpdater(
|
|
|
260
347
|
...metadataWithoutHandles,
|
|
261
348
|
cachedHandleData: mode.targetCacheHandleData,
|
|
262
349
|
},
|
|
350
|
+
scroll: toScrollPayload(commitScroll),
|
|
263
351
|
};
|
|
264
352
|
|
|
265
|
-
|
|
266
|
-
(s) => s.transition,
|
|
267
|
-
);
|
|
268
|
-
if (cachedHasTransition) {
|
|
353
|
+
if (shouldStartViewTransition(existingSegments)) {
|
|
269
354
|
startTransition(() => {
|
|
270
355
|
if (addTransitionType) {
|
|
271
356
|
addTransitionType("navigation");
|
|
@@ -290,11 +375,15 @@ export function createPartialUpdater(
|
|
|
290
375
|
forceAwait: true,
|
|
291
376
|
});
|
|
292
377
|
|
|
293
|
-
tx.commit(
|
|
378
|
+
const { scroll: leaveScroll } = tx.commit(
|
|
379
|
+
matchedIds,
|
|
380
|
+
existingSegments,
|
|
381
|
+
);
|
|
294
382
|
|
|
295
383
|
onUpdate({
|
|
296
384
|
root: newTree,
|
|
297
385
|
metadata: payload.metadata,
|
|
386
|
+
scroll: toScrollPayload(leaveScroll),
|
|
298
387
|
});
|
|
299
388
|
|
|
300
389
|
debugLog("[Browser] Navigation complete (left intercept)");
|
|
@@ -426,7 +515,11 @@ export function createPartialUpdater(
|
|
|
426
515
|
: serverLocationState
|
|
427
516
|
? { serverState: serverLocationState }
|
|
428
517
|
: undefined;
|
|
429
|
-
tx.commit(
|
|
518
|
+
const { scroll: navScroll } = tx.commit(
|
|
519
|
+
allSegmentIds,
|
|
520
|
+
reconciled.segments,
|
|
521
|
+
overrides,
|
|
522
|
+
);
|
|
430
523
|
|
|
431
524
|
// For stale revalidation: verify history key hasn't changed before updating UI
|
|
432
525
|
if (mode.type === "stale-revalidation") {
|
|
@@ -441,8 +534,10 @@ export function createPartialUpdater(
|
|
|
441
534
|
|
|
442
535
|
debugLog("[partial-update] updating document");
|
|
443
536
|
|
|
444
|
-
// Emit update to trigger React render
|
|
445
|
-
|
|
537
|
+
// Emit update to trigger React render.
|
|
538
|
+
// Scroll info is included so NavigationProvider applies it after React commits.
|
|
539
|
+
const hasTransition = shouldStartViewTransition(reconciled.segments);
|
|
540
|
+
const scrollPayload = toScrollPayload(navScroll);
|
|
446
541
|
|
|
447
542
|
if (mode.type === "action" || mode.type === "stale-revalidation") {
|
|
448
543
|
startTransition(() => {
|
|
@@ -452,6 +547,7 @@ export function createPartialUpdater(
|
|
|
452
547
|
onUpdate({
|
|
453
548
|
root: newTree,
|
|
454
549
|
metadata: payload.metadata!,
|
|
550
|
+
scroll: scrollPayload,
|
|
455
551
|
});
|
|
456
552
|
});
|
|
457
553
|
} else if (hasTransition) {
|
|
@@ -462,12 +558,14 @@ export function createPartialUpdater(
|
|
|
462
558
|
onUpdate({
|
|
463
559
|
root: newTree,
|
|
464
560
|
metadata: payload.metadata!,
|
|
561
|
+
scroll: scrollPayload,
|
|
465
562
|
});
|
|
466
563
|
});
|
|
467
564
|
} else {
|
|
468
565
|
onUpdate({
|
|
469
566
|
root: newTree,
|
|
470
567
|
metadata: payload.metadata!,
|
|
568
|
+
scroll: scrollPayload,
|
|
471
569
|
});
|
|
472
570
|
}
|
|
473
571
|
|
|
@@ -494,15 +592,14 @@ export function createPartialUpdater(
|
|
|
494
592
|
}
|
|
495
593
|
|
|
496
594
|
const fullUpdateServerState = payload.metadata?.locationState;
|
|
497
|
-
|
|
498
|
-
tx.commit(segmentIds, segments, {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
595
|
+
const { scroll: fullScroll } = fullUpdateServerState
|
|
596
|
+
? tx.commit(segmentIds, segments, {
|
|
597
|
+
serverState: fullUpdateServerState,
|
|
598
|
+
})
|
|
599
|
+
: tx.commit(segmentIds, segments);
|
|
502
600
|
|
|
503
|
-
const fullHasTransition = segments
|
|
504
|
-
|
|
505
|
-
);
|
|
601
|
+
const fullHasTransition = shouldStartViewTransition(segments);
|
|
602
|
+
const fullScrollPayload = toScrollPayload(fullScroll);
|
|
506
603
|
|
|
507
604
|
if (mode.type === "stale-revalidation") {
|
|
508
605
|
await rawStreamComplete;
|
|
@@ -513,6 +610,7 @@ export function createPartialUpdater(
|
|
|
513
610
|
onUpdate({
|
|
514
611
|
root: newTree,
|
|
515
612
|
metadata: payload.metadata!,
|
|
613
|
+
scroll: fullScrollPayload,
|
|
516
614
|
});
|
|
517
615
|
});
|
|
518
616
|
} else if (mode.type === "action") {
|
|
@@ -523,6 +621,7 @@ export function createPartialUpdater(
|
|
|
523
621
|
onUpdate({
|
|
524
622
|
root: newTree,
|
|
525
623
|
metadata: payload.metadata!,
|
|
624
|
+
scroll: fullScrollPayload,
|
|
526
625
|
});
|
|
527
626
|
});
|
|
528
627
|
} else if (fullHasTransition) {
|
|
@@ -533,12 +632,14 @@ export function createPartialUpdater(
|
|
|
533
632
|
onUpdate({
|
|
534
633
|
root: newTree,
|
|
535
634
|
metadata: payload.metadata!,
|
|
635
|
+
scroll: fullScrollPayload,
|
|
536
636
|
});
|
|
537
637
|
});
|
|
538
638
|
} else {
|
|
539
639
|
onUpdate({
|
|
540
640
|
root: newTree,
|
|
541
641
|
metadata: payload.metadata!,
|
|
642
|
+
scroll: fullScrollPayload,
|
|
542
643
|
});
|
|
543
644
|
}
|
|
544
645
|
|
|
@@ -1,16 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Prefetch Cache
|
|
3
3
|
*
|
|
4
|
-
* In-memory cache storing
|
|
5
|
-
* on subsequent navigation.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* In-memory cache storing prefetched Response objects for instant cache hits
|
|
5
|
+
* on subsequent navigation. Two key scopes are in play:
|
|
6
|
+
* - Wildcard (default): built by `buildPrefetchKey(rangoState, target)` —
|
|
7
|
+
* shape `rangoState\0/target?...`. Shared across all source pages and
|
|
8
|
+
* invalidated automatically when Rango state bumps (deploy or
|
|
9
|
+
* server-action invalidation).
|
|
10
|
+
* - Source-scoped: built by `buildSourceKey(rangoState, sourceHref, target)`
|
|
11
|
+
* — shape `rangoState\0sourceHref\0/target?...`. Embeds the Rango state
|
|
12
|
+
* (so rotation invalidates source-scoped entries too) plus the source
|
|
13
|
+
* href (so each originating page gets its own slot). Populated when the
|
|
14
|
+
* server tags a response with `X-RSC-Prefetch-Scope: source` (intercept
|
|
15
|
+
* modals etc.), OR when a Link opts in with `prefetchKey=":source"` — in
|
|
16
|
+
* both cases so source-sensitive responses cannot bleed into navigations
|
|
17
|
+
* from other pages.
|
|
18
|
+
*
|
|
19
|
+
* Also tracks in-flight prefetch promises. Each promise resolves to the
|
|
20
|
+
* navigation branch of a tee'd Response, allowing navigation to adopt a
|
|
21
|
+
* still-downloading prefetch without reparsing or buffering the body. A
|
|
22
|
+
* single promise can be registered under multiple alias keys (see
|
|
23
|
+
* `setInflightPromiseWithAliases`) so same-source navigations adopt via
|
|
24
|
+
* their source key while cross-source ones fall through to the wildcard
|
|
25
|
+
* alias — with consume/clear atomically removing every alias.
|
|
8
26
|
*
|
|
9
27
|
* Replaces the previous browser HTTP cache approach which was unreliable
|
|
10
28
|
* due to response draining race conditions and browser inconsistencies.
|
|
11
29
|
*/
|
|
12
30
|
|
|
13
|
-
import {
|
|
31
|
+
import { abortAllPrefetches } from "./queue.js";
|
|
14
32
|
import { invalidateRangoState } from "../rango-state.js";
|
|
15
33
|
|
|
16
34
|
// Default TTL: 5 minutes. Overridden by initPrefetchCache() with
|
|
@@ -44,19 +62,78 @@ interface PrefetchCacheEntry {
|
|
|
44
62
|
const cache = new Map<string, PrefetchCacheEntry>();
|
|
45
63
|
const inflight = new Set<string>();
|
|
46
64
|
|
|
65
|
+
/**
|
|
66
|
+
* In-flight promise map. When a prefetch fetch is in progress, its
|
|
67
|
+
* Promise<Response | null> is stored here so navigation can await
|
|
68
|
+
* it instead of starting a duplicate request.
|
|
69
|
+
*/
|
|
70
|
+
const inflightPromises = new Map<string, Promise<Response | null>>();
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Alias map for in-flight promises registered under multiple keys (see
|
|
74
|
+
* dual inflight in prefetch/fetch.ts). Records each key's sibling set so
|
|
75
|
+
* that consuming or clearing any one key atomically removes every alias —
|
|
76
|
+
* guaranteeing a single consumer for the shared Response stream.
|
|
77
|
+
*/
|
|
78
|
+
const inflightAliases = new Map<string, string[]>();
|
|
79
|
+
|
|
47
80
|
// Generation counter incremented on each clearPrefetchCache(). Fetches that
|
|
48
81
|
// started before a clear carry a stale generation and must not store their
|
|
49
82
|
// response (the data may be stale due to a server action invalidation).
|
|
50
83
|
let generation = 0;
|
|
51
84
|
|
|
52
85
|
/**
|
|
53
|
-
* Build a
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
86
|
+
* Build a cache key by combining a scope prefix with the target URL.
|
|
87
|
+
*
|
|
88
|
+
* Low-level primitive — callers that want a specific scope should use
|
|
89
|
+
* one of:
|
|
90
|
+
* - Wildcard (source-agnostic): prefix is the Rango state value from
|
|
91
|
+
* `getRangoState()`. Shared across all source pages. Invalidated
|
|
92
|
+
* automatically when Rango state bumps (deploy or server-action).
|
|
93
|
+
* Key shape: `rangoState\0/target?...`.
|
|
94
|
+
* - Source-scoped: use `buildSourceKey()`. Key shape:
|
|
95
|
+
* `rangoState\0sourceHref\0/target?...` — embeds the Rango state so
|
|
96
|
+
* rotation invalidates source-scoped entries alongside wildcard ones,
|
|
97
|
+
* plus the source page href so the key is unique per originating page.
|
|
98
|
+
* Populated either when the server tags a response with
|
|
99
|
+
* `X-RSC-Prefetch-Scope: source` (intercept modals, etc.) or when a
|
|
100
|
+
* Link opts in via `prefetchKey=":source"`.
|
|
101
|
+
*
|
|
102
|
+
* The `_rsc_segments` query param that travels in the target URL means
|
|
103
|
+
* clients with different mounted segment trees naturally get different
|
|
104
|
+
* keys — so segment-level diffs remain consistent across both scopes.
|
|
105
|
+
*/
|
|
106
|
+
export function buildPrefetchKey(prefix: string, targetUrl: URL): string {
|
|
107
|
+
return prefix + "\0" + targetUrl.pathname + targetUrl.search;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Build a source-scoped cache key. Key shape:
|
|
112
|
+
* `rangoState\0sourceHref\0/target?...`.
|
|
113
|
+
*
|
|
114
|
+
* - `rangoState` is included so state rotation invalidates source-scoped
|
|
115
|
+
* entries alongside wildcard ones.
|
|
116
|
+
* - `sourceHref` makes the key unique per originating page.
|
|
117
|
+
*/
|
|
118
|
+
export function buildSourceKey(
|
|
119
|
+
rangoState: string,
|
|
120
|
+
sourceHref: string,
|
|
121
|
+
targetUrl: URL,
|
|
122
|
+
): string {
|
|
123
|
+
return buildPrefetchKey(rangoState + "\0" + sourceHref, targetUrl);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Walk an inflight key plus any sibling aliases registered via
|
|
128
|
+
* `setInflightPromiseWithAliases`, invoking `fn` for each.
|
|
57
129
|
*/
|
|
58
|
-
|
|
59
|
-
|
|
130
|
+
function forEachAlias(key: string, fn: (k: string) => void): void {
|
|
131
|
+
const aliases = inflightAliases.get(key);
|
|
132
|
+
if (aliases) {
|
|
133
|
+
for (const k of aliases) fn(k);
|
|
134
|
+
} else {
|
|
135
|
+
fn(key);
|
|
136
|
+
}
|
|
60
137
|
}
|
|
61
138
|
|
|
62
139
|
/**
|
|
@@ -78,6 +155,9 @@ export function hasPrefetch(key: string): boolean {
|
|
|
78
155
|
* Consume a cached prefetch response. Returns null if not found or expired.
|
|
79
156
|
* One-time consumption: the entry is deleted after retrieval.
|
|
80
157
|
* Returns null when caching is disabled (TTL <= 0).
|
|
158
|
+
*
|
|
159
|
+
* Does NOT check in-flight prefetches — use consumeInflightPrefetch()
|
|
160
|
+
* for that (returns a Promise instead of a Response).
|
|
81
161
|
*/
|
|
82
162
|
export function consumePrefetch(key: string): Response | null {
|
|
83
163
|
if (cacheTTL <= 0) return null;
|
|
@@ -91,10 +171,39 @@ export function consumePrefetch(key: string): Response | null {
|
|
|
91
171
|
return entry.response;
|
|
92
172
|
}
|
|
93
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Consume an in-flight prefetch promise. Returns null if no prefetch is
|
|
176
|
+
* in-flight for this key. The returned Promise resolves to the buffered
|
|
177
|
+
* Response (or null if the fetch failed/was aborted).
|
|
178
|
+
*
|
|
179
|
+
* One-time consumption: the promise entry is removed (along with any
|
|
180
|
+
* sibling aliases registered via `setInflightPromiseWithAliases`) so a
|
|
181
|
+
* second call on any alias returns null — only one caller can adopt the
|
|
182
|
+
* shared Response stream. The `inflight` set entry is intentionally
|
|
183
|
+
* kept so that `hasPrefetch()` continues to return true while the
|
|
184
|
+
* underlying fetch is still downloading — this prevents
|
|
185
|
+
* `prefetchDirect()` or other callers from starting a duplicate request
|
|
186
|
+
* during the handoff window. The inflight flag is cleaned up naturally
|
|
187
|
+
* by `clearPrefetchInflight()` in the fetch's `.finally()`.
|
|
188
|
+
*/
|
|
189
|
+
export function consumeInflightPrefetch(
|
|
190
|
+
key: string,
|
|
191
|
+
): Promise<Response | null> | null {
|
|
192
|
+
const promise = inflightPromises.get(key);
|
|
193
|
+
if (!promise) return null;
|
|
194
|
+
// Remove the promise under every alias so a second consumer cannot
|
|
195
|
+
// adopt the same stream and race on the body. `inflightAliases` is
|
|
196
|
+
// intentionally preserved — `clearPrefetchInflight()` in the fetch's
|
|
197
|
+
// `.finally()` still needs it to clear every inflight flag; deleting
|
|
198
|
+
// here would strand the sibling's flag forever.
|
|
199
|
+
forEachAlias(key, (k) => inflightPromises.delete(k));
|
|
200
|
+
return promise;
|
|
201
|
+
}
|
|
202
|
+
|
|
94
203
|
/**
|
|
95
204
|
* Store a prefetch response in the in-memory cache.
|
|
96
|
-
* The response
|
|
97
|
-
*
|
|
205
|
+
* The response should be a clone() of the original so the caller can
|
|
206
|
+
* still consume the body. The clone's body streams independently.
|
|
98
207
|
*
|
|
99
208
|
* Skips storage if the generation has changed since the fetch started
|
|
100
209
|
* (a server action invalidated the cache mid-flight).
|
|
@@ -136,19 +245,70 @@ export function markPrefetchInflight(key: string): void {
|
|
|
136
245
|
inflight.add(key);
|
|
137
246
|
}
|
|
138
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Store the in-flight Promise for a prefetch so navigation can reuse it.
|
|
250
|
+
*/
|
|
251
|
+
export function setInflightPromise(
|
|
252
|
+
key: string,
|
|
253
|
+
promise: Promise<Response | null>,
|
|
254
|
+
): void {
|
|
255
|
+
inflightPromises.set(key, promise);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Store the same in-flight Promise under multiple keys, recording them
|
|
260
|
+
* as sibling aliases. Consuming or clearing any one alias atomically
|
|
261
|
+
* removes every entry, guaranteeing the shared Response stream has a
|
|
262
|
+
* single consumer even when navigation looks up either key.
|
|
263
|
+
*/
|
|
264
|
+
export function setInflightPromiseWithAliases(
|
|
265
|
+
keys: string[],
|
|
266
|
+
promise: Promise<Response | null>,
|
|
267
|
+
): void {
|
|
268
|
+
for (const k of keys) {
|
|
269
|
+
inflightPromises.set(k, promise);
|
|
270
|
+
inflightAliases.set(k, keys);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
139
274
|
export function clearPrefetchInflight(key: string): void {
|
|
140
|
-
|
|
275
|
+
forEachAlias(key, (k) => {
|
|
276
|
+
inflight.delete(k);
|
|
277
|
+
inflightPromises.delete(k);
|
|
278
|
+
inflightAliases.delete(k);
|
|
279
|
+
});
|
|
141
280
|
}
|
|
142
281
|
|
|
143
282
|
/**
|
|
144
283
|
* Invalidate all prefetch state. Called when server actions mutate data.
|
|
145
284
|
* Clears the in-memory cache, cancels in-flight prefetches, and rotates
|
|
146
285
|
* the Rango state key so CDN-cached responses are also invalidated.
|
|
286
|
+
*
|
|
287
|
+
* Uses abortAllPrefetches (hard cancel) because in-flight responses
|
|
288
|
+
* may contain stale data after a mutation.
|
|
147
289
|
*/
|
|
148
290
|
export function clearPrefetchCache(): void {
|
|
149
291
|
generation++;
|
|
150
292
|
inflight.clear();
|
|
293
|
+
inflightPromises.clear();
|
|
294
|
+
inflightAliases.clear();
|
|
151
295
|
cache.clear();
|
|
152
|
-
|
|
296
|
+
abortAllPrefetches();
|
|
153
297
|
invalidateRangoState();
|
|
154
298
|
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Drop all in-memory prefetch state for this tab without rotating rango-state.
|
|
302
|
+
*
|
|
303
|
+
* Use for local-only invalidations (e.g. app switch in this tab) where
|
|
304
|
+
* other tabs should NOT observe a state rotation. Unlike clearPrefetchCache,
|
|
305
|
+
* does not call invalidateRangoState, so the shared X-Rango-State token
|
|
306
|
+
* stays intact and siblings in the old app keep their prefetches.
|
|
307
|
+
*/
|
|
308
|
+
export function clearPrefetchCacheLocal(): void {
|
|
309
|
+
generation++;
|
|
310
|
+
inflight.clear();
|
|
311
|
+
inflightPromises.clear();
|
|
312
|
+
cache.clear();
|
|
313
|
+
abortAllPrefetches();
|
|
314
|
+
}
|