@rangojs/router 0.0.0-experimental.79 → 0.0.0-experimental.7d061845
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 +120 -25
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +2138 -841
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +68 -21
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +220 -30
- package/skills/caching/SKILL.md +116 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +3 -1
- 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 +26 -4
- package/skills/layout/SKILL.md +6 -7
- package/skills/links/SKILL.md +247 -17
- package/skills/loader/SKILL.md +219 -9
- package/skills/middleware/SKILL.md +15 -9
- package/skills/migrate-nextjs/SKILL.md +4 -2
- package/skills/migrate-react-router/SKILL.md +5 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +12 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +242 -24
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +66 -9
- package/skills/route/SKILL.md +33 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +816 -0
- package/skills/typesafety/SKILL.md +319 -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/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/event-controller.ts +86 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +65 -9
- package/src/browser/navigation-client.ts +45 -25
- package/src/browser/navigation-store.ts +32 -9
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +52 -26
- package/src/browser/prefetch/cache.ts +124 -26
- package/src/browser/prefetch/fetch.ts +114 -38
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +18 -13
- package/src/browser/react/NavigationProvider.tsx +72 -31
- 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 +17 -9
- 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 +22 -2
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +64 -22
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +21 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-trie.ts +2 -1
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-scope.ts +28 -42
- package/src/cache/cf/cf-cache-store.ts +54 -13
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +10 -8
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +26 -13
- 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 +9 -4
- package/src/index.ts +16 -6
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +21 -6
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender.ts +4 -4
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -39
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +253 -265
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +43 -15
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +19 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/handler-context.ts +21 -41
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +3 -3
- package/src/router/loader-resolution.ts +19 -2
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/cache-lookup.ts +44 -91
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +53 -32
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +15 -26
- package/src/router/middleware.ts +99 -84
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/prerender-match.ts +1 -1
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/router/revalidation.ts +58 -2
- package/src/router/router-interfaces.ts +45 -28
- package/src/router/router-options.ts +40 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +27 -6
- package/src/router/segment-resolution/revalidation.ts +147 -106
- package/src/router/segment-resolution/view-transition-default.ts +36 -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 +8 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +38 -23
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +28 -69
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-route-handler.ts +46 -53
- package/src/rsc/rsc-rendering.ts +35 -51
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +17 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +8 -2
- package/src/search-params.ts +4 -4
- package/src/segment-system.tsx +122 -56
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +118 -51
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +20 -42
- package/src/ssr/index.tsx +5 -1
- package/src/static-handler.ts +1 -1
- 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 +57 -0
- package/src/testing/flight-tree.ts +332 -0
- package/src/testing/flight.entry.ts +46 -0
- package/src/testing/flight.ts +224 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +106 -0
- package/src/testing/internal/context.ts +304 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +42 -0
- package/src/testing/render-handler.ts +267 -0
- package/src/testing/render-route.tsx +565 -0
- package/src/testing/run-loader.ts +341 -0
- package/src/testing/run-middleware.ts +188 -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 +270 -0
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +68 -50
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +5 -6
- package/src/types/request-scope.ts +126 -0
- package/src/types/segments.ts +35 -1
- package/src/urls/include-helper.ts +10 -53
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +11 -3
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +101 -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 +67 -26
- 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 +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- 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 +54 -30
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- 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 +496 -486
- package/src/vite/plugins/performance-tracks.ts +29 -25
- 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 +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +116 -29
- package/src/vite/router-discovery.ts +750 -100
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +1 -1
- 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 +5 -4
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from "./scroll-restoration.js";
|
|
12
12
|
import type { EventController, NavigationHandle } from "./event-controller.js";
|
|
13
13
|
import { debugLog } from "./logging.js";
|
|
14
|
-
import { buildHistoryState } from "./history-state.js";
|
|
14
|
+
import { buildHistoryState, pushHistoryWithIdx } from "./history-state.js";
|
|
15
15
|
|
|
16
16
|
// Re-export for consumers that import from navigation-transaction
|
|
17
17
|
export { resolveNavigationState } from "./history-state.js";
|
|
@@ -186,12 +186,8 @@ export function createNavigationTransaction(
|
|
|
186
186
|
// Used to detect when location state is being cleared.
|
|
187
187
|
const oldState = window.history.state;
|
|
188
188
|
|
|
189
|
-
// Update browser URL
|
|
190
|
-
|
|
191
|
-
window.history.replaceState(historyState, "", url);
|
|
192
|
-
} else {
|
|
193
|
-
window.history.pushState(historyState, "", url);
|
|
194
|
-
}
|
|
189
|
+
// Update browser URL (stamps history.state.idx for back() first-entry detection)
|
|
190
|
+
pushHistoryWithIdx(historyState, url, replace ?? false);
|
|
195
191
|
// Ensure new history entry has a scroll restoration key
|
|
196
192
|
ensureHistoryKey();
|
|
197
193
|
|
|
@@ -240,30 +236,16 @@ export function createNavigationTransaction(
|
|
|
240
236
|
segments: ResolvedSegment[],
|
|
241
237
|
overrides?: BoundCommitOverrides,
|
|
242
238
|
) => {
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
// Allow overrides to force replace (e.g., for intercepts)
|
|
247
|
-
const finalReplace =
|
|
248
|
-
overrides?.replace !== undefined ? overrides.replace : opts.replace;
|
|
249
|
-
// Intercept info: overrides take precedence, fallback to opts
|
|
250
|
-
const intercept =
|
|
251
|
-
overrides?.intercept !== undefined
|
|
252
|
-
? overrides.intercept
|
|
253
|
-
: opts.intercept;
|
|
239
|
+
const finalScroll = overrides?.scroll ?? opts.scroll;
|
|
240
|
+
const finalReplace = overrides?.replace ?? opts.replace;
|
|
241
|
+
const intercept = overrides?.intercept ?? opts.intercept;
|
|
254
242
|
const interceptSourceUrl =
|
|
255
|
-
overrides?.interceptSourceUrl
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
//
|
|
259
|
-
const cacheOnly =
|
|
260
|
-
overrides?.cacheOnly !== undefined
|
|
261
|
-
? overrides.cacheOnly
|
|
262
|
-
: opts.cacheOnly;
|
|
263
|
-
// User state: overrides take precedence, fallback to opts
|
|
243
|
+
overrides?.interceptSourceUrl ?? opts.interceptSourceUrl;
|
|
244
|
+
const cacheOnly = overrides?.cacheOnly ?? opts.cacheOnly;
|
|
245
|
+
// state is `unknown` (null is meaningful) so `??` would wrongly drop a
|
|
246
|
+
// null override; serverState always comes from overrides, never opts.
|
|
264
247
|
const state =
|
|
265
248
|
overrides?.state !== undefined ? overrides.state : opts.state;
|
|
266
|
-
// Server-set location state: only from overrides (set by partial-update)
|
|
267
249
|
const serverState = overrides?.serverState;
|
|
268
250
|
return commit({
|
|
269
251
|
...opts,
|
|
@@ -14,7 +14,10 @@ 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";
|
|
@@ -28,6 +31,23 @@ function toScrollPayload(
|
|
|
28
31
|
return { enabled: scroll !== false ? scroll : false };
|
|
29
32
|
}
|
|
30
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
|
+
}
|
|
50
|
+
|
|
31
51
|
/**
|
|
32
52
|
* Configuration for creating a partial updater
|
|
33
53
|
*/
|
|
@@ -41,6 +61,13 @@ export interface PartialUpdateConfig {
|
|
|
41
61
|
) => Promise<ReactNode> | ReactNode;
|
|
42
62
|
/** RSC version getter — returns the current version (may change after HMR) */
|
|
43
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;
|
|
44
71
|
}
|
|
45
72
|
|
|
46
73
|
/**
|
|
@@ -76,7 +103,7 @@ export type UpdateMode =
|
|
|
76
103
|
/** Source URL for intercept restore (popstate cache miss) */
|
|
77
104
|
interceptSourceUrl?: string;
|
|
78
105
|
}
|
|
79
|
-
| { type: "leave-intercept" }
|
|
106
|
+
| { type: "leave-intercept"; interceptSourceUrl?: string }
|
|
80
107
|
| { type: "stale-revalidation"; interceptSourceUrl?: string }
|
|
81
108
|
| { type: "action"; interceptSourceUrl?: string };
|
|
82
109
|
|
|
@@ -110,6 +137,7 @@ export function createPartialUpdater(
|
|
|
110
137
|
onUpdate,
|
|
111
138
|
renderSegments,
|
|
112
139
|
getVersion = () => undefined,
|
|
140
|
+
applyAppShell,
|
|
113
141
|
} = config;
|
|
114
142
|
|
|
115
143
|
/**
|
|
@@ -141,13 +169,7 @@ export function createPartialUpdater(
|
|
|
141
169
|
// Capture history key at start for stale revalidation consistency check
|
|
142
170
|
const historyKeyAtStart = store.getHistoryKey();
|
|
143
171
|
|
|
144
|
-
|
|
145
|
-
const interceptSourceUrl =
|
|
146
|
-
mode.type === "stale-revalidation" ||
|
|
147
|
-
mode.type === "action" ||
|
|
148
|
-
mode.type === "navigate"
|
|
149
|
-
? mode.interceptSourceUrl
|
|
150
|
-
: undefined;
|
|
172
|
+
const interceptSourceUrl = mode.interceptSourceUrl;
|
|
151
173
|
|
|
152
174
|
// When leaving intercept, filter out intercept-specific segments
|
|
153
175
|
let segments: string[];
|
|
@@ -190,13 +212,11 @@ export function createPartialUpdater(
|
|
|
190
212
|
// When navigating with targetCacheSegments, use those for consistency.
|
|
191
213
|
// Otherwise fall back to current page's segments (for same-route revalidation).
|
|
192
214
|
const targetCache =
|
|
193
|
-
mode.type === "navigate"
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const cachedSegsSource =
|
|
199
|
-
targetCache && targetCache.length > 0 ? "history-cache" : "current-page";
|
|
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";
|
|
200
220
|
debugLog(
|
|
201
221
|
`[Browser] cachedSegs source: ${cachedSegsSource} (${cachedSegs.length} segments: ${cachedSegs.map((s) => s.id).join(", ")})`,
|
|
202
222
|
);
|
|
@@ -228,7 +248,12 @@ export function createPartialUpdater(
|
|
|
228
248
|
// Detect app switch: if routerId changed, the navigation crossed into
|
|
229
249
|
// a different router (e.g., via host router path mount). Downgrade
|
|
230
250
|
// partial to full so the entire tree is replaced without reconciliation
|
|
231
|
-
// against stale segments from the previous app
|
|
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.
|
|
232
257
|
if (payload.metadata?.routerId) {
|
|
233
258
|
const prevRouterId = store.getRouterId?.();
|
|
234
259
|
if (prevRouterId && prevRouterId !== payload.metadata.routerId) {
|
|
@@ -236,6 +261,12 @@ export function createPartialUpdater(
|
|
|
236
261
|
`[Browser] App switch detected (${prevRouterId} → ${payload.metadata.routerId}), forcing full update`,
|
|
237
262
|
);
|
|
238
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
|
+
});
|
|
239
270
|
}
|
|
240
271
|
store.setRouterId?.(payload.metadata.routerId);
|
|
241
272
|
}
|
|
@@ -279,7 +310,7 @@ export function createPartialUpdater(
|
|
|
279
310
|
.filter(Boolean) as ResolvedSegment[];
|
|
280
311
|
|
|
281
312
|
// When navigating with cached segments to a different route, render them.
|
|
282
|
-
if (mode.type === "navigate" && targetCache
|
|
313
|
+
if (mode.type === "navigate" && targetCache) {
|
|
283
314
|
debugLog(
|
|
284
315
|
"[Browser] No diff but navigating with cached segments - rendering target route",
|
|
285
316
|
);
|
|
@@ -319,10 +350,7 @@ export function createPartialUpdater(
|
|
|
319
350
|
scroll: toScrollPayload(commitScroll),
|
|
320
351
|
};
|
|
321
352
|
|
|
322
|
-
|
|
323
|
-
(s) => s.transition,
|
|
324
|
-
);
|
|
325
|
-
if (cachedHasTransition) {
|
|
353
|
+
if (shouldStartViewTransition(existingSegments)) {
|
|
326
354
|
startTransition(() => {
|
|
327
355
|
if (addTransitionType) {
|
|
328
356
|
addTransitionType("navigation");
|
|
@@ -508,7 +536,7 @@ export function createPartialUpdater(
|
|
|
508
536
|
|
|
509
537
|
// Emit update to trigger React render.
|
|
510
538
|
// Scroll info is included so NavigationProvider applies it after React commits.
|
|
511
|
-
const hasTransition = reconciled.
|
|
539
|
+
const hasTransition = shouldStartViewTransition(reconciled.segments);
|
|
512
540
|
const scrollPayload = toScrollPayload(navScroll);
|
|
513
541
|
|
|
514
542
|
if (mode.type === "action" || mode.type === "stale-revalidation") {
|
|
@@ -570,9 +598,7 @@ export function createPartialUpdater(
|
|
|
570
598
|
})
|
|
571
599
|
: tx.commit(segmentIds, segments);
|
|
572
600
|
|
|
573
|
-
const fullHasTransition = segments
|
|
574
|
-
(s: ResolvedSegment) => s.transition,
|
|
575
|
-
);
|
|
601
|
+
const fullHasTransition = shouldStartViewTransition(segments);
|
|
576
602
|
const fullScrollPayload = toScrollPayload(fullScroll);
|
|
577
603
|
|
|
578
604
|
if (mode.type === "stale-revalidation") {
|
|
@@ -2,13 +2,27 @@
|
|
|
2
2
|
* Prefetch Cache
|
|
3
3
|
*
|
|
4
4
|
* In-memory cache storing prefetched Response objects for instant cache hits
|
|
5
|
-
* on subsequent navigation.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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.
|
|
8
18
|
*
|
|
9
19
|
* Also tracks in-flight prefetch promises. Each promise resolves to the
|
|
10
20
|
* navigation branch of a tee'd Response, allowing navigation to adopt a
|
|
11
|
-
* still-downloading prefetch without reparsing or buffering the body.
|
|
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.
|
|
12
26
|
*
|
|
13
27
|
* Replaces the previous browser HTTP cache approach which was unreliable
|
|
14
28
|
* due to response draining race conditions and browser inconsistencies.
|
|
@@ -55,29 +69,71 @@ const inflight = new Set<string>();
|
|
|
55
69
|
*/
|
|
56
70
|
const inflightPromises = new Map<string, Promise<Response | null>>();
|
|
57
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
|
+
|
|
58
80
|
// Generation counter incremented on each clearPrefetchCache(). Fetches that
|
|
59
81
|
// started before a clear carry a stale generation and must not store their
|
|
60
82
|
// response (the data may be stale due to a server action invalidation).
|
|
61
83
|
let generation = 0;
|
|
62
84
|
|
|
63
85
|
/**
|
|
64
|
-
* Build a cache key
|
|
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"`.
|
|
65
101
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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?...`.
|
|
69
113
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
114
|
+
* - `rangoState` is included so state rotation invalidates source-scoped
|
|
115
|
+
* entries alongside wildcard ones.
|
|
116
|
+
* - `sourceHref` makes the key unique per originating page.
|
|
73
117
|
*/
|
|
74
|
-
export function
|
|
118
|
+
export function buildSourceKey(
|
|
119
|
+
rangoState: string,
|
|
75
120
|
sourceHref: string,
|
|
76
121
|
targetUrl: URL,
|
|
77
|
-
prefetchKey?: string | ((from: string) => string),
|
|
78
122
|
): string {
|
|
79
|
-
|
|
80
|
-
|
|
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.
|
|
129
|
+
*/
|
|
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
|
+
}
|
|
81
137
|
}
|
|
82
138
|
|
|
83
139
|
/**
|
|
@@ -120,21 +176,27 @@ export function consumePrefetch(key: string): Response | null {
|
|
|
120
176
|
* in-flight for this key. The returned Promise resolves to the buffered
|
|
121
177
|
* Response (or null if the fetch failed/was aborted).
|
|
122
178
|
*
|
|
123
|
-
* One-time consumption: the promise entry is removed
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
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()`.
|
|
130
188
|
*/
|
|
131
189
|
export function consumeInflightPrefetch(
|
|
132
190
|
key: string,
|
|
133
191
|
): Promise<Response | null> | null {
|
|
134
192
|
const promise = inflightPromises.get(key);
|
|
135
193
|
if (!promise) return null;
|
|
136
|
-
// Remove the promise
|
|
137
|
-
|
|
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));
|
|
138
200
|
return promise;
|
|
139
201
|
}
|
|
140
202
|
|
|
@@ -193,9 +255,28 @@ export function setInflightPromise(
|
|
|
193
255
|
inflightPromises.set(key, promise);
|
|
194
256
|
}
|
|
195
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
|
+
|
|
196
274
|
export function clearPrefetchInflight(key: string): void {
|
|
197
|
-
|
|
198
|
-
|
|
275
|
+
forEachAlias(key, (k) => {
|
|
276
|
+
inflight.delete(k);
|
|
277
|
+
inflightPromises.delete(k);
|
|
278
|
+
inflightAliases.delete(k);
|
|
279
|
+
});
|
|
199
280
|
}
|
|
200
281
|
|
|
201
282
|
/**
|
|
@@ -210,7 +291,24 @@ export function clearPrefetchCache(): void {
|
|
|
210
291
|
generation++;
|
|
211
292
|
inflight.clear();
|
|
212
293
|
inflightPromises.clear();
|
|
294
|
+
inflightAliases.clear();
|
|
213
295
|
cache.clear();
|
|
214
296
|
abortAllPrefetches();
|
|
215
297
|
invalidateRangoState();
|
|
216
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
|
+
}
|