@rangojs/router 0.0.0-experimental.122 → 0.0.0-experimental.125
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/dist/bin/rango.js +10 -6
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +55 -48
- package/package.json +61 -21
- package/skills/caching/SKILL.md +2 -1
- package/skills/hooks/SKILL.md +40 -29
- package/skills/host-router/SKILL.md +16 -2
- package/skills/intercept/SKILL.md +4 -2
- package/skills/layout/SKILL.md +11 -6
- package/skills/loader/SKILL.md +6 -2
- package/skills/middleware/SKILL.md +4 -2
- package/skills/migrate-nextjs/SKILL.md +3 -1
- package/skills/parallel/SKILL.md +9 -4
- package/skills/rango/SKILL.md +12 -0
- package/skills/route/SKILL.md +10 -2
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +98 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +89 -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 +118 -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/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +1 -1
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +1 -83
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +14 -1
- package/src/browser/navigation-client.ts +14 -1
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +26 -51
- package/src/browser/navigation-transaction.ts +0 -32
- package/src/browser/partial-update.ts +1 -83
- package/src/browser/prefetch/cache.ts +6 -45
- package/src/browser/prefetch/fetch.ts +7 -0
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +157 -99
- package/src/browser/react/Link.tsx +0 -2
- package/src/browser/react/NavigationProvider.tsx +2 -1
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +0 -2
- package/src/browser/react/index.ts +0 -51
- package/src/browser/react/location-state-shared.ts +0 -13
- package/src/browser/react/location-state.ts +0 -1
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +0 -5
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +0 -3
- package/src/browser/react/use-params.ts +0 -2
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/rsc-router.tsx +12 -4
- package/src/browser/server-action-bridge.ts +77 -15
- package/src/browser/types.ts +7 -2
- package/src/browser/validate-redirect-origin.ts +4 -5
- package/src/build/route-trie.ts +3 -0
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/router-processing.ts +0 -8
- package/src/cache/cache-policy.ts +0 -54
- package/src/cache/cache-runtime.ts +27 -24
- package/src/cache/cache-scope.ts +0 -27
- package/src/cache/cache-tag.ts +0 -37
- package/src/cache/cf/cf-cache-store.ts +94 -46
- package/src/cache/cf/index.ts +0 -24
- package/src/cache/document-cache.ts +11 -36
- package/src/cache/handle-snapshot.ts +0 -40
- package/src/cache/index.ts +0 -27
- package/src/cache/memory-segment-store.ts +2 -48
- package/src/cache/profile-registry.ts +7 -3
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/types.ts +0 -98
- package/src/client.rsc.tsx +1 -22
- package/src/client.tsx +14 -38
- package/src/component-utils.ts +19 -0
- package/src/deps/ssr.ts +0 -1
- package/src/handle.ts +28 -18
- package/src/handles/MetaTags.tsx +0 -14
- 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 +6 -0
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +1 -65
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +6 -2
- package/src/href-client.ts +0 -4
- package/src/index.rsc.ts +42 -3
- package/src/index.ts +31 -1
- package/src/internal-debug.ts +2 -4
- package/src/loader.rsc.ts +19 -9
- package/src/loader.ts +12 -4
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +23 -30
- package/src/prerender.ts +58 -3
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +1 -44
- package/src/route-definition/dsl-helpers.ts +7 -19
- package/src/route-definition/helpers-types.ts +3 -3
- package/src/route-definition/redirect.ts +11 -1
- package/src/route-map-builder.ts +0 -16
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +0 -13
- package/src/router/error-handling.ts +12 -16
- package/src/router/find-match.ts +4 -30
- package/src/router/intercept-resolution.ts +10 -1
- package/src/router/lazy-includes.ts +1 -57
- package/src/router/loader-resolution.ts +3 -2
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +1 -25
- package/src/router/match-api.ts +0 -20
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +57 -58
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +1 -54
- package/src/router/match-middleware/cache-store.ts +0 -31
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -21
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +1 -52
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-cookies.ts +0 -13
- package/src/router/middleware-types.ts +0 -115
- package/src/router/middleware.ts +7 -30
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +1 -33
- package/src/router/prerender-match.ts +33 -45
- package/src/router/request-classification.ts +1 -38
- package/src/router/revalidation.ts +5 -58
- package/src/router/router-context.ts +0 -26
- package/src/router/router-interfaces.ts +7 -0
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +25 -57
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +10 -13
- package/src/router/segment-resolution/revalidation.ts +5 -42
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/state-cookie-name.ts +33 -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 +63 -40
- package/src/router/types.ts +1 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +40 -9
- package/src/rsc/handler.ts +14 -2
- package/src/rsc/helpers.ts +34 -0
- package/src/rsc/origin-guard.ts +0 -12
- package/src/rsc/progressive-enhancement.ts +4 -1
- package/src/rsc/rsc-rendering.ts +4 -7
- package/src/rsc/runtime-warnings.ts +14 -0
- package/src/rsc/server-action.ts +30 -28
- package/src/rsc/types.ts +2 -1
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +0 -16
- package/src/segment-loader-promise.ts +14 -2
- package/src/segment-system.tsx +79 -88
- package/src/server/cookie-store.ts +52 -1
- package/src/server/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +74 -77
- package/src/ssr/index.tsx +14 -14
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +119 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +581 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +127 -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 +186 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +98 -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 +311 -0
- package/src/testing/render-route.tsx +504 -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/error-types.ts +25 -89
- package/src/types/global-namespace.ts +15 -15
- package/src/types/handler-context.ts +16 -13
- package/src/types/index.ts +0 -10
- package/src/types/request-scope.ts +0 -19
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +0 -13
- package/src/urls/include-helper.ts +0 -4
- package/src/urls/index.ts +0 -6
- package/src/urls/path-helper-types.ts +2 -2
- package/src/urls/path-helper.ts +0 -54
- package/src/urls/urls-function.ts +0 -13
- package/src/use-loader.tsx +0 -186
- package/src/vite/discovery/bundle-postprocess.ts +2 -1
- package/src/vite/discovery/discover-routers.ts +6 -7
- package/src/vite/discovery/virtual-module-codegen.ts +1 -11
- package/src/vite/plugin-types.ts +3 -1
- package/src/vite/plugins/cjs-to-esm.ts +0 -11
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +0 -10
- package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
- package/src/vite/plugins/expose-action-id.ts +2 -73
- package/src/vite/plugins/expose-id-utils.ts +0 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
- package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
- package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
- package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
- package/src/vite/plugins/expose-internal-ids.ts +10 -0
- package/src/vite/plugins/performance-tracks.ts +0 -3
- package/src/vite/plugins/use-cache-transform.ts +0 -36
- package/src/vite/plugins/version-injector.ts +0 -20
- package/src/vite/plugins/version-plugin.ts +1 -49
- package/src/vite/plugins/virtual-entries.ts +0 -15
- package/src/vite/rango.ts +1 -108
- package/src/vite/router-discovery.ts +2 -1
- package/src/vite/utils/ast-handler-extract.ts +0 -16
- package/src/vite/utils/bundle-analysis.ts +6 -13
- package/src/vite/utils/client-chunks.ts +0 -6
- package/src/vite/utils/forward-user-plugins.ts +0 -22
- package/src/vite/utils/manifest-utils.ts +0 -4
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +0 -35
- package/src/vite/utils/shared-utils.ts +3 -35
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
|
@@ -24,20 +24,12 @@ import { debugLog } from "./logging.js";
|
|
|
24
24
|
import { validateRedirectOrigin } from "./validate-redirect-origin.js";
|
|
25
25
|
import type { NavigationUpdate } from "./types.js";
|
|
26
26
|
|
|
27
|
-
/** Build a scroll payload from the commit's scroll option */
|
|
28
27
|
function toScrollPayload(
|
|
29
28
|
scroll: boolean | undefined,
|
|
30
29
|
): NonNullable<NavigationUpdate["scroll"]> {
|
|
31
30
|
return { enabled: scroll !== false ? scroll : false };
|
|
32
31
|
}
|
|
33
32
|
|
|
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
33
|
function shouldStartViewTransition(segments: ResolvedSegment[]): boolean {
|
|
42
34
|
let hasIntercept = false;
|
|
43
35
|
let hasTransition = false;
|
|
@@ -112,15 +104,6 @@ export type PartialUpdater = (
|
|
|
112
104
|
mode?: UpdateMode,
|
|
113
105
|
) => Promise<void>;
|
|
114
106
|
|
|
115
|
-
/**
|
|
116
|
-
* Create a partial updater for fetching and applying RSC partial updates
|
|
117
|
-
*
|
|
118
|
-
* This function is shared between navigation-bridge and server-action-bridge
|
|
119
|
-
* to handle partial RSC updates with HMR resilience.
|
|
120
|
-
*
|
|
121
|
-
* @param config - Partial update configuration
|
|
122
|
-
* @returns fetchPartialUpdate function
|
|
123
|
-
*/
|
|
124
107
|
export function createPartialUpdater(
|
|
125
108
|
config: PartialUpdateConfig,
|
|
126
109
|
): PartialUpdater {
|
|
@@ -132,21 +115,12 @@ export function createPartialUpdater(
|
|
|
132
115
|
getVersion = () => undefined,
|
|
133
116
|
} = config;
|
|
134
117
|
|
|
135
|
-
/**
|
|
136
|
-
* Get current page's cached segments as an array
|
|
137
|
-
*/
|
|
138
118
|
function getCurrentCachedSegments(): ResolvedSegment[] {
|
|
139
119
|
const currentKey = store.getHistoryKey();
|
|
140
120
|
const cached = store.getCachedSegments(currentKey);
|
|
141
121
|
return cached?.segments || [];
|
|
142
122
|
}
|
|
143
123
|
|
|
144
|
-
/**
|
|
145
|
-
* Fetch partial update and trigger UI update
|
|
146
|
-
*
|
|
147
|
-
* @param tx - Transaction for committing segment state (required)
|
|
148
|
-
* @param signal - AbortSignal to check if navigation is stale (not for aborting fetch)
|
|
149
|
-
*/
|
|
150
124
|
async function fetchPartialUpdate(
|
|
151
125
|
targetUrl: string,
|
|
152
126
|
segmentIds: string[] | undefined,
|
|
@@ -158,20 +132,16 @@ export function createPartialUpdater(
|
|
|
158
132
|
const segmentState = store.getSegmentState();
|
|
159
133
|
const url = targetUrl || window.location.href;
|
|
160
134
|
|
|
161
|
-
// Capture history key at start for stale revalidation consistency check
|
|
162
135
|
const historyKeyAtStart = store.getHistoryKey();
|
|
163
136
|
|
|
164
137
|
const interceptSourceUrl = mode.interceptSourceUrl;
|
|
165
138
|
|
|
166
|
-
// When leaving intercept, filter out intercept-specific segments
|
|
167
139
|
let segments: string[];
|
|
168
140
|
if (mode.type === "leave-intercept") {
|
|
169
141
|
const currentSegments = segmentIds ?? segmentState.currentSegmentIds;
|
|
170
142
|
const currentCached = getCurrentCachedSegments();
|
|
171
143
|
const interceptIds = new Set(
|
|
172
|
-
currentCached
|
|
173
|
-
.filter((s) => s.namespace?.startsWith("intercept:"))
|
|
174
|
-
.map((s) => s.id),
|
|
144
|
+
currentCached.filter(isInterceptSegment).map((s) => s.id),
|
|
175
145
|
);
|
|
176
146
|
segments = currentSegments.filter((id) => !interceptIds.has(id));
|
|
177
147
|
debugLog(
|
|
@@ -181,12 +151,6 @@ export function createPartialUpdater(
|
|
|
181
151
|
segments = segmentIds ?? segmentState.currentSegmentIds;
|
|
182
152
|
}
|
|
183
153
|
|
|
184
|
-
// For intercept revalidation, use the intercept source URL as previousUrl.
|
|
185
|
-
// For leave-intercept, tx.currentUrl captures window.location.href at tx
|
|
186
|
-
// creation, which on popstate is already the destination URL and would
|
|
187
|
-
// tell the server "from == to". segmentState.currentUrl still points at
|
|
188
|
-
// the URL the cached segments render (the intercept URL), which is the
|
|
189
|
-
// correct "from" for the server's diff computation.
|
|
190
154
|
const previousUrl =
|
|
191
155
|
mode.type === "leave-intercept"
|
|
192
156
|
? segmentState.currentUrl || tx.currentUrl
|
|
@@ -200,9 +164,6 @@ export function createPartialUpdater(
|
|
|
200
164
|
debugLog(`[Browser] Intercept context from: ${interceptSourceUrl}`);
|
|
201
165
|
}
|
|
202
166
|
|
|
203
|
-
// Get cached segments for merging with server diff.
|
|
204
|
-
// When navigating with targetCacheSegments, use those for consistency.
|
|
205
|
-
// Otherwise fall back to current page's segments (for same-route revalidation).
|
|
206
167
|
const targetCache =
|
|
207
168
|
mode.type === "navigate" && mode.targetCacheSegments?.length
|
|
208
169
|
? mode.targetCacheSegments
|
|
@@ -213,22 +174,16 @@ export function createPartialUpdater(
|
|
|
213
174
|
`[Browser] cachedSegs source: ${cachedSegsSource} (${cachedSegs.length} segments: ${cachedSegs.map((s) => s.id).join(", ")})`,
|
|
214
175
|
);
|
|
215
176
|
|
|
216
|
-
// Fetch partial payload (no abort signal - RSC doesn't support it well)
|
|
217
177
|
let fetchResult: Awaited<ReturnType<NavigationClient["fetchPartial"]>>;
|
|
218
178
|
fetchResult = await client.fetchPartial({
|
|
219
179
|
targetUrl: url,
|
|
220
180
|
segmentIds: segments,
|
|
221
181
|
previousUrl,
|
|
222
|
-
// Mark stale when explicitly requested OR when no segments are sent
|
|
223
|
-
// (action redirect sends empty segments for a fresh render).
|
|
224
182
|
staleRevalidation:
|
|
225
183
|
mode.type === "stale-revalidation" || segments.length === 0,
|
|
226
184
|
version: getVersion(),
|
|
227
185
|
routerId: store.getRouterId?.(),
|
|
228
186
|
});
|
|
229
|
-
// Mark navigation as streaming (response received, now parsing RSC).
|
|
230
|
-
// Called after fetchPartial so pendingUrl stays set during the network wait,
|
|
231
|
-
// allowing useLinkStatus to show per-link pending indicators.
|
|
232
187
|
const streamingToken = tx.startStreaming();
|
|
233
188
|
const { payload, streamComplete: rawStreamComplete } = fetchResult;
|
|
234
189
|
debugLog("payload.metadata", payload.metadata);
|
|
@@ -237,13 +192,6 @@ export function createPartialUpdater(
|
|
|
237
192
|
streamingToken.end();
|
|
238
193
|
});
|
|
239
194
|
|
|
240
|
-
// Integrity guard (defense in depth). The server redirects on a cross-app
|
|
241
|
-
// routerId mismatch (X-RSC-Reload), so a partial payload's routerId must
|
|
242
|
-
// match this client's. If it doesn't — a stale/edge cache keyed without the
|
|
243
|
-
// routerId, a proxy mixing app responses, or a server classification bug —
|
|
244
|
-
// do NOT splice a foreign app's segments and client references into this
|
|
245
|
-
// document. Force a full reload so the server re-establishes the
|
|
246
|
-
// authoritative document for this URL.
|
|
247
195
|
const currentRouterId = store.getRouterId?.();
|
|
248
196
|
if (
|
|
249
197
|
payload.metadata?.routerId &&
|
|
@@ -258,7 +206,6 @@ export function createPartialUpdater(
|
|
|
258
206
|
return;
|
|
259
207
|
}
|
|
260
208
|
|
|
261
|
-
// Handle server-side redirect with state
|
|
262
209
|
if (payload.metadata?.redirect) {
|
|
263
210
|
if (signal?.aborted) {
|
|
264
211
|
debugLog("[Browser] Ignoring stale redirect (aborted)");
|
|
@@ -288,7 +235,6 @@ export function createPartialUpdater(
|
|
|
288
235
|
debugLog(`[Browser] Partial update - matched: ${matched?.join(", ")}`);
|
|
289
236
|
debugLog(`[Browser] Diff: ${diff?.join(", ")}`);
|
|
290
237
|
|
|
291
|
-
// If diff is empty, nothing changed on server side.
|
|
292
238
|
if (!diff || diff.length === 0) {
|
|
293
239
|
const matchedIds = matched || [];
|
|
294
240
|
const cacheMap = new Map(cachedSegs.map((s) => [s.id, s]));
|
|
@@ -296,7 +242,6 @@ export function createPartialUpdater(
|
|
|
296
242
|
.map((id: string) => cacheMap.get(id))
|
|
297
243
|
.filter(Boolean) as ResolvedSegment[];
|
|
298
244
|
|
|
299
|
-
// When navigating with cached segments to a different route, render them.
|
|
300
245
|
if (mode.type === "navigate" && targetCache) {
|
|
301
246
|
debugLog(
|
|
302
247
|
"[Browser] No diff but navigating with cached segments - rendering target route",
|
|
@@ -311,10 +256,6 @@ export function createPartialUpdater(
|
|
|
311
256
|
existingSegments,
|
|
312
257
|
);
|
|
313
258
|
|
|
314
|
-
// tx.commit() cached the source page's handleData because
|
|
315
|
-
// eventController hasn't been updated yet. Overwrite with the
|
|
316
|
-
// correct cached handleData to prevent cache corruption on
|
|
317
|
-
// subsequent navigations to this same URL.
|
|
318
259
|
if (mode.targetCacheHandleData) {
|
|
319
260
|
store.updateCacheHandleData(
|
|
320
261
|
store.getHistoryKey(),
|
|
@@ -322,10 +263,6 @@ export function createPartialUpdater(
|
|
|
322
263
|
);
|
|
323
264
|
}
|
|
324
265
|
|
|
325
|
-
// Include cachedHandleData in metadata so NavigationProvider can restore
|
|
326
|
-
// breadcrumbs and other handle data from cache.
|
|
327
|
-
// Remove `handles` from metadata to prevent NavigationProvider from
|
|
328
|
-
// processing an empty handles stream, which would clear the cached breadcrumbs.
|
|
329
266
|
const { handles: _unusedHandles, ...metadataWithoutHandles } =
|
|
330
267
|
payload.metadata!;
|
|
331
268
|
const cachedUpdate = {
|
|
@@ -352,7 +289,6 @@ export function createPartialUpdater(
|
|
|
352
289
|
return;
|
|
353
290
|
}
|
|
354
291
|
|
|
355
|
-
// When leaving intercept, force re-render even with empty diff
|
|
356
292
|
if (mode.type === "leave-intercept") {
|
|
357
293
|
debugLog(
|
|
358
294
|
"[Browser] Leaving intercept - forcing re-render to remove modal",
|
|
@@ -377,7 +313,6 @@ export function createPartialUpdater(
|
|
|
377
313
|
return;
|
|
378
314
|
}
|
|
379
315
|
|
|
380
|
-
// Same route revalidation with no changes - skip UI update
|
|
381
316
|
debugLog(
|
|
382
317
|
"[Browser] No changes - all revalidations returned false, keeping existing UI",
|
|
383
318
|
);
|
|
@@ -386,7 +321,6 @@ export function createPartialUpdater(
|
|
|
386
321
|
return;
|
|
387
322
|
}
|
|
388
323
|
|
|
389
|
-
// Reconcile server segments with cached segments (single source of truth)
|
|
390
324
|
const matchedIds = matched || [];
|
|
391
325
|
const actor: ReconcileActor =
|
|
392
326
|
mode.type === "stale-revalidation" || mode.type === "action"
|
|
@@ -402,7 +336,6 @@ export function createPartialUpdater(
|
|
|
402
336
|
insertMissingDiff: true,
|
|
403
337
|
});
|
|
404
338
|
|
|
405
|
-
// HMR RESILIENCE: Check if we're missing any matched segments
|
|
406
339
|
const reconciledIdSet = new Set(reconciled.segments.map((s) => s.id));
|
|
407
340
|
const missingIds = matchedIds.filter(
|
|
408
341
|
(id: string) => !reconciledIdSet.has(id),
|
|
@@ -430,7 +363,6 @@ export function createPartialUpdater(
|
|
|
430
363
|
`[Browser] HMR detected: Missing ${missingCount} segments. Refetching all...`,
|
|
431
364
|
);
|
|
432
365
|
|
|
433
|
-
// Refetch with empty segments = server sends everything
|
|
434
366
|
return fetchPartialUpdate(url, [], true, signal, tx, mode);
|
|
435
367
|
}
|
|
436
368
|
|
|
@@ -439,7 +371,6 @@ export function createPartialUpdater(
|
|
|
439
371
|
return;
|
|
440
372
|
}
|
|
441
373
|
|
|
442
|
-
// Rebuild tree on client (await for loader data resolution)
|
|
443
374
|
const renderOptions = {
|
|
444
375
|
isAction: mode.type === "action",
|
|
445
376
|
forceAwait: mode.type === "stale-revalidation",
|
|
@@ -462,21 +393,15 @@ export function createPartialUpdater(
|
|
|
462
393
|
])
|
|
463
394
|
: renderSegments(reconciled.mainSegments, renderOptions));
|
|
464
395
|
|
|
465
|
-
// Final abort check before committing - another navigation may have started
|
|
466
396
|
if (signal?.aborted) {
|
|
467
397
|
debugLog("[Browser] Ignoring stale navigation (aborted before commit)");
|
|
468
398
|
return;
|
|
469
399
|
}
|
|
470
400
|
|
|
471
|
-
// Check if this is an intercept response (any slot is active)
|
|
472
401
|
const isInterceptResponse = hasActiveInterceptSlots(
|
|
473
402
|
payload.metadata?.slots,
|
|
474
403
|
);
|
|
475
404
|
|
|
476
|
-
// Track intercept context (only on navigation, not actions or stale revalidation)
|
|
477
|
-
// Use the authoritative source from mode/history state when restoring an
|
|
478
|
-
// intercept via popstate cache miss; fall back to the current URL for fresh
|
|
479
|
-
// intercept navigations.
|
|
480
405
|
const effectiveInterceptSource =
|
|
481
406
|
interceptSourceUrl || segmentState.currentUrl;
|
|
482
407
|
if (mode.type !== "action" && mode.type !== "stale-revalidation") {
|
|
@@ -487,9 +412,6 @@ export function createPartialUpdater(
|
|
|
487
412
|
}
|
|
488
413
|
}
|
|
489
414
|
|
|
490
|
-
// Commit navigation - use server's matched as the authoritative segment ID list.
|
|
491
|
-
// reconciled.segments may be missing IDs (e.g., loader segments not in diff or cache)
|
|
492
|
-
// but the server's matched always includes all expected segment IDs.
|
|
493
415
|
const allSegmentIds = matchedIds;
|
|
494
416
|
const serverLocationState = payload.metadata?.locationState;
|
|
495
417
|
const overrides: CommitOverrides | undefined = isInterceptResponse
|
|
@@ -508,7 +430,6 @@ export function createPartialUpdater(
|
|
|
508
430
|
overrides,
|
|
509
431
|
);
|
|
510
432
|
|
|
511
|
-
// For stale revalidation: verify history key hasn't changed before updating UI
|
|
512
433
|
if (mode.type === "stale-revalidation") {
|
|
513
434
|
const historyKeyNow = store.getHistoryKey();
|
|
514
435
|
if (historyKeyNow !== historyKeyAtStart) {
|
|
@@ -521,8 +442,6 @@ export function createPartialUpdater(
|
|
|
521
442
|
|
|
522
443
|
debugLog("[partial-update] updating document");
|
|
523
444
|
|
|
524
|
-
// Emit update to trigger React render.
|
|
525
|
-
// Scroll info is included so NavigationProvider applies it after React commits.
|
|
526
445
|
const hasTransition = shouldStartViewTransition(reconciled.segments);
|
|
527
446
|
const scrollPayload = toScrollPayload(navScroll);
|
|
528
447
|
|
|
@@ -559,7 +478,6 @@ export function createPartialUpdater(
|
|
|
559
478
|
debugLog("[Browser] Navigation complete");
|
|
560
479
|
return;
|
|
561
480
|
} else {
|
|
562
|
-
// Full update (fallback)
|
|
563
481
|
console.warn(`[Browser] Full update (fallback)`);
|
|
564
482
|
|
|
565
483
|
const segments = payload.metadata?.segments || [];
|
|
@@ -31,6 +31,12 @@
|
|
|
31
31
|
*
|
|
32
32
|
* Replaces the previous browser HTTP cache approach which was unreliable
|
|
33
33
|
* due to response draining race conditions and browser inconsistencies.
|
|
34
|
+
*
|
|
35
|
+
* State here lives in module-level singletons (cache, inflight, generation,
|
|
36
|
+
* cacheTTL, etc.) rather than a per-instance factory. This is correct because
|
|
37
|
+
* exactly one router is live per document — an SPA navigation crossing a
|
|
38
|
+
* host-router boundary forces a full document reload — so the singletons are
|
|
39
|
+
* effectively per-document. Unit tests reset them via clearPrefetchCache().
|
|
34
40
|
*/
|
|
35
41
|
|
|
36
42
|
import { abortAllPrefetches } from "./queue.js";
|
|
@@ -61,9 +67,6 @@ export interface DecodedPrefetch {
|
|
|
61
67
|
scope: "source" | "wildcard";
|
|
62
68
|
}
|
|
63
69
|
|
|
64
|
-
// Default TTL: 5 minutes. Overridden by initPrefetchCache() with
|
|
65
|
-
// the server-configured prefetchCacheTTL from router options.
|
|
66
|
-
// 0 disables the in-memory cache entirely.
|
|
67
70
|
let cacheTTL = 300_000;
|
|
68
71
|
|
|
69
72
|
/**
|
|
@@ -92,41 +95,12 @@ interface PrefetchCacheEntry {
|
|
|
92
95
|
const cache = new Map<string, PrefetchCacheEntry>();
|
|
93
96
|
const inflight = new Set<string>();
|
|
94
97
|
|
|
95
|
-
/**
|
|
96
|
-
* In-flight promise map. When a prefetch fetch+decode is in progress, its
|
|
97
|
-
* Promise<DecodedPrefetch | null> is stored here so navigation can await it
|
|
98
|
-
* instead of starting a duplicate request. Resolves to null when the prefetch
|
|
99
|
-
* failed, was aborted, or carried a control header (reload/redirect) that the
|
|
100
|
-
* navigation must re-fetch to act on.
|
|
101
|
-
*/
|
|
102
98
|
const inflightPromises = new Map<string, Promise<DecodedPrefetch | null>>();
|
|
103
99
|
|
|
104
|
-
/**
|
|
105
|
-
* Alias map for in-flight promises registered under multiple keys (see
|
|
106
|
-
* dual inflight in prefetch/fetch.ts). Records each key's sibling set so
|
|
107
|
-
* that consuming or clearing any one key atomically removes every alias —
|
|
108
|
-
* guaranteeing a single consumer for the shared decode.
|
|
109
|
-
*/
|
|
110
100
|
const inflightAliases = new Map<string, string[]>();
|
|
111
101
|
|
|
112
|
-
/**
|
|
113
|
-
* Keys whose in-flight prefetch promise was adopted by a navigation (via
|
|
114
|
-
* `consumeInflightPrefetch`). A `DecodedPrefetch` carries a single-use
|
|
115
|
-
* `metadata.handles` async generator; the adopter drains it. The same entry is
|
|
116
|
-
* also published to the `cache` map by `storePrefetch` when the fetch resolves
|
|
117
|
-
* — which runs AFTER adoption (adoption only succeeds while the fetch is still
|
|
118
|
-
* in flight, so the entry is not yet cached). Without this guard the adopted,
|
|
119
|
-
* now-drained entry would be left in the cache and served to a later navigation
|
|
120
|
-
* whose handle generator yields nothing, silently dropping that route's
|
|
121
|
-
* breadcrumbs. Recording the adopted keys lets `storePrefetch` skip publishing
|
|
122
|
-
* them, keeping the existing one-time-consumption contract (a consumed prefetch
|
|
123
|
-
* is gone; the next navigation re-fetches).
|
|
124
|
-
*/
|
|
125
102
|
const adoptedKeys = new Set<string>();
|
|
126
103
|
|
|
127
|
-
// Generation counter incremented on each clearPrefetchCache(). Fetches that
|
|
128
|
-
// started before a clear carry a stale generation and must not store their
|
|
129
|
-
// response (the data may be stale due to a server action invalidation).
|
|
130
104
|
let generation = 0;
|
|
131
105
|
|
|
132
106
|
/**
|
|
@@ -306,9 +280,6 @@ export function markPrefetchInflight(key: string): void {
|
|
|
306
280
|
inflight.add(key);
|
|
307
281
|
}
|
|
308
282
|
|
|
309
|
-
/**
|
|
310
|
-
* Store the in-flight Promise for a prefetch so navigation can reuse it.
|
|
311
|
-
*/
|
|
312
283
|
export function setInflightPromise(
|
|
313
284
|
key: string,
|
|
314
285
|
promise: Promise<DecodedPrefetch | null>,
|
|
@@ -337,20 +308,10 @@ export function clearPrefetchInflight(key: string): void {
|
|
|
337
308
|
inflight.delete(k);
|
|
338
309
|
inflightPromises.delete(k);
|
|
339
310
|
inflightAliases.delete(k);
|
|
340
|
-
// Clear any adopted marker too, so a fetch that failed before storePrefetch
|
|
341
|
-
// (the marker's normal consumer) does not strand it across the next prefetch.
|
|
342
311
|
adoptedKeys.delete(k);
|
|
343
312
|
});
|
|
344
313
|
}
|
|
345
314
|
|
|
346
|
-
/**
|
|
347
|
-
* Invalidate all prefetch state. Called when server actions mutate data.
|
|
348
|
-
* Clears the in-memory cache, cancels in-flight prefetches, and rotates
|
|
349
|
-
* the Rango state key so CDN-cached responses are also invalidated.
|
|
350
|
-
*
|
|
351
|
-
* Uses abortAllPrefetches (hard cancel) because in-flight responses
|
|
352
|
-
* may contain stale data after a mutation.
|
|
353
|
-
*/
|
|
354
315
|
export function clearPrefetchCache(): void {
|
|
355
316
|
generation++;
|
|
356
317
|
inflight.clear();
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type DecodedPrefetch,
|
|
28
28
|
} from "./cache.js";
|
|
29
29
|
import { getRangoState } from "../rango-state.js";
|
|
30
|
+
import { isActionFenceActive } from "../action-fence.js";
|
|
30
31
|
import { enqueuePrefetch } from "./queue.js";
|
|
31
32
|
import { shouldPrefetch } from "./policy.js";
|
|
32
33
|
import { debugLog } from "../logging.js";
|
|
@@ -150,6 +151,12 @@ function executePrefetchFetch(
|
|
|
150
151
|
|
|
151
152
|
const promise: Promise<DecodedPrefetch | null> = fetch(fetchUrl, {
|
|
152
153
|
priority: "low" as RequestPriority,
|
|
154
|
+
// During an action's flight the state is not rotated, so the old
|
|
155
|
+
// X-Rango-State still matches the Vary-keyed HTTP-cache entry; bypass it so
|
|
156
|
+
// a prefetch fetches fresh rather than warming the map with stale bytes (the
|
|
157
|
+
// fence's HTTP-cache-bypass requirement applies to prefetch as well as
|
|
158
|
+
// navigation fetches).
|
|
159
|
+
...(isActionFenceActive() && { cache: "no-store" as RequestCache }),
|
|
153
160
|
signal,
|
|
154
161
|
headers: {
|
|
155
162
|
"X-Rango-State": getRangoState(),
|
|
@@ -71,10 +71,13 @@ function scheduleDrain(): void {
|
|
|
71
71
|
Promise.race([waitForViewportImages(), wait(IMAGE_WAIT_TIMEOUT)]),
|
|
72
72
|
)
|
|
73
73
|
.then(() => {
|
|
74
|
-
|
|
75
|
-
//
|
|
76
|
-
//
|
|
74
|
+
// Stale drain: a cancel/abort happened while we were waiting, and a fresh
|
|
75
|
+
// scheduleDrain may already own drainScheduled for the new generation.
|
|
76
|
+
// Bail WITHOUT clearing the flag so we don't clobber the live wait's
|
|
77
|
+
// single-in-flight-drain coalescing (clearing it here would let the next
|
|
78
|
+
// enqueue start a third overlapping wait).
|
|
77
79
|
if (gen !== drainGeneration) return;
|
|
80
|
+
drainScheduled = false;
|
|
78
81
|
if (queue.length > 0) drain();
|
|
79
82
|
});
|
|
80
83
|
}
|