@rangojs/router 0.0.0-experimental.124 → 0.0.0-experimental.126
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 +6 -4
- package/dist/bin/rango.js +3 -4
- package/dist/vite/index.js +315 -68
- package/package.json +19 -18
- package/skills/breadcrumbs/SKILL.md +60 -0
- package/skills/hooks/SKILL.md +2 -2
- package/skills/route/SKILL.md +6 -0
- package/skills/server-actions/SKILL.md +25 -1
- package/skills/testing/SKILL.md +17 -17
- package/skills/testing/cache-prerender.md +29 -3
- package/skills/testing/flight.md +13 -10
- package/skills/testing/render-handler.md +3 -0
- package/skills/testing/server-tree.md +1 -1
- package/skills/testing/setup.md +1 -1
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +1 -1
- package/src/browser/action-fence.ts +10 -0
- package/src/browser/event-controller.ts +1 -83
- package/src/browser/navigation-store-handle.ts +3 -4
- package/src/browser/navigation-store.ts +0 -39
- package/src/browser/navigation-transaction.ts +0 -32
- package/src/browser/partial-update.ts +23 -84
- package/src/browser/prefetch/cache.ts +6 -45
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +2 -23
- 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 -45
- 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-router.ts +2 -1
- 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 +10 -3
- package/src/browser/server-action-bridge.ts +51 -3
- package/src/browser/types.ts +23 -5
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/index.ts +8 -9
- package/src/build/route-trie.ts +46 -11
- 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 +48 -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 +72 -45
- package/src/cache/cf/index.ts +0 -24
- package/src/cache/document-cache.ts +10 -36
- package/src/cache/handle-snapshot.ts +0 -40
- package/src/cache/index.ts +0 -27
- package/src/cache/memory-segment-store.ts +0 -52
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/types.ts +0 -98
- package/src/client.rsc.tsx +4 -22
- package/src/client.tsx +19 -32
- package/src/context-var.ts +12 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/handle.ts +2 -12
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +6 -0
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +1 -65
- package/src/host/testing.ts +0 -16
- package/src/host/types.ts +6 -2
- package/src/href-client.ts +0 -4
- package/src/index.rsc.ts +27 -2
- package/src/index.ts +7 -0
- package/src/internal-debug.ts +2 -4
- package/src/loader.rsc.ts +4 -15
- package/src/loader.ts +3 -9
- 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 +34 -0
- package/src/redirect-origin.ts +100 -0
- 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 +43 -9
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-map-builder.ts +0 -16
- package/src/router/content-negotiation.ts +0 -13
- package/src/router/error-handling.ts +12 -16
- package/src/router/find-match.ts +4 -31
- package/src/router/intercept-resolution.ts +10 -1
- package/src/router/lazy-includes.ts +1 -57
- package/src/router/loader-resolution.ts +25 -23
- 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 +0 -43
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +96 -179
- 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 -22
- 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-types.ts +0 -116
- package/src/router/middleware.ts +77 -60
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +5 -56
- package/src/router/prerender-match.ts +56 -51
- package/src/router/request-classification.ts +1 -38
- package/src/router/revalidation.ts +14 -62
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +0 -27
- package/src/router/router-interfaces.ts +10 -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 +35 -23
- package/src/router/segment-resolution/revalidation.ts +188 -283
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +0 -3
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +0 -22
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +66 -45
- package/src/router/types.ts +1 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +8 -11
- package/src/rsc/handler-context.ts +1 -0
- package/src/rsc/handler.ts +20 -4
- package/src/rsc/helpers.ts +71 -3
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/origin-guard.ts +9 -15
- package/src/rsc/progressive-enhancement.ts +10 -1
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-route-handler.ts +23 -18
- package/src/rsc/rsc-rendering.ts +2 -7
- package/src/rsc/runtime-warnings.ts +14 -0
- package/src/rsc/server-action.ts +34 -29
- package/src/rsc/types.ts +6 -3
- 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/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +29 -92
- package/src/ssr/index.tsx +14 -14
- package/src/static-handler.ts +2 -27
- package/src/testing/cache-status.ts +44 -48
- package/src/testing/collect-handle.ts +1 -24
- package/src/testing/dispatch.ts +43 -6
- package/src/testing/e2e/index.ts +1 -22
- package/src/testing/e2e/matchers.ts +0 -16
- package/src/testing/flight-matchers.ts +0 -13
- package/src/testing/flight-normalize.ts +3 -30
- package/src/testing/flight.ts +46 -48
- package/src/testing/generated-routes.ts +1 -41
- package/src/testing/index.ts +1 -21
- package/src/testing/internal/context.ts +3 -45
- package/src/testing/internal/seed-vars.ts +0 -26
- package/src/testing/render-handler.ts +31 -61
- package/src/testing/render-route.tsx +75 -103
- package/src/testing/run-loader.ts +0 -96
- package/src/testing/run-middleware.ts +0 -26
- 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 +4 -14
- package/src/types/handler-context.ts +28 -9
- 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 +28 -18
- package/src/vite/discovery/prerender-collection.ts +2 -4
- package/src/vite/discovery/state.ts +5 -0
- package/src/vite/discovery/virtual-module-codegen.ts +1 -11
- package/src/vite/plugin-types.ts +35 -9
- 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/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +21 -46
- 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 +2 -108
- package/src/vite/router-discovery.ts +9 -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/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -237,10 +237,6 @@ export interface EventController {
|
|
|
237
237
|
hadAnyConcurrentActions(): boolean;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
// ============================================================================
|
|
241
|
-
// Default States
|
|
242
|
-
// ============================================================================
|
|
243
|
-
|
|
244
240
|
const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
245
241
|
state: "idle",
|
|
246
242
|
actionId: null,
|
|
@@ -261,16 +257,12 @@ function matchesActionId(
|
|
|
261
257
|
entryActionId: string,
|
|
262
258
|
): boolean {
|
|
263
259
|
if (subscriptionId.includes("#")) {
|
|
264
|
-
// Full ID: exact match
|
|
265
260
|
return subscriptionId === entryActionId;
|
|
266
261
|
}
|
|
267
|
-
// Action name only: suffix match (matches "anything#actionName")
|
|
268
262
|
return entryActionId.endsWith(`#${subscriptionId}`);
|
|
269
263
|
}
|
|
270
264
|
|
|
271
|
-
//
|
|
272
|
-
// setTimeout(0) batching prevents render storms. Each notifier owns its timer
|
|
273
|
-
// so listener kinds coalesce independently.
|
|
265
|
+
// Batch rapid notifications into one microtask to prevent render storms
|
|
274
266
|
function makeDebouncedNotifier(listeners: Set<() => void>): () => void {
|
|
275
267
|
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
276
268
|
return () => {
|
|
@@ -282,13 +274,6 @@ function makeDebouncedNotifier(listeners: Set<() => void>): () => void {
|
|
|
282
274
|
};
|
|
283
275
|
}
|
|
284
276
|
|
|
285
|
-
// ============================================================================
|
|
286
|
-
// Implementation
|
|
287
|
-
// ============================================================================
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Configuration for creating an event controller
|
|
291
|
-
*/
|
|
292
277
|
export interface EventControllerConfig {
|
|
293
278
|
initialLocation?: NavigationLocation;
|
|
294
279
|
}
|
|
@@ -306,51 +291,34 @@ export interface EventControllerConfig {
|
|
|
306
291
|
export function createEventController(
|
|
307
292
|
config?: EventControllerConfig,
|
|
308
293
|
): EventController {
|
|
309
|
-
// ========================================================================
|
|
310
|
-
// Source of Truth
|
|
311
|
-
// ========================================================================
|
|
312
|
-
|
|
313
|
-
// Current navigation in progress (null = idle)
|
|
314
294
|
let currentNavigation: NavigationEntry | null = null;
|
|
315
295
|
|
|
316
|
-
// All in-flight actions (keyed by unique instance ID)
|
|
317
296
|
const inflightActions = new Map<string, ActionEntry>();
|
|
318
297
|
|
|
319
|
-
// Committed location (updated when navigation completes)
|
|
320
298
|
let location: NavigationLocation =
|
|
321
299
|
config?.initialLocation ??
|
|
322
300
|
(typeof window !== "undefined"
|
|
323
301
|
? new URL(window.location.href)
|
|
324
302
|
: new URL("/", "http://localhost"));
|
|
325
303
|
|
|
326
|
-
// Track if any concurrent actions occurred (for consolidation)
|
|
327
304
|
let hadAnyConcurrentActions = false;
|
|
328
305
|
|
|
329
|
-
// Track segments revalidated by concurrent actions
|
|
330
306
|
const concurrentRevalidatedSegments = new Set<string>();
|
|
331
307
|
|
|
332
|
-
// Active streaming count (independent of navigation/action lifecycle)
|
|
333
308
|
let activeStreamCount = 0;
|
|
334
309
|
|
|
335
|
-
// Handle data from RSC payload
|
|
336
310
|
let handleData: HandleData = {};
|
|
337
311
|
let handleSegmentOrder: string[] = [];
|
|
338
312
|
let routeSegmentIds: string[] = [];
|
|
339
313
|
|
|
340
|
-
// Merged route params from current match
|
|
341
314
|
let routeParams: Record<string, string> = {};
|
|
342
315
|
|
|
343
|
-
// ========================================================================
|
|
344
|
-
// Listeners
|
|
345
|
-
// ========================================================================
|
|
346
|
-
|
|
347
316
|
const stateListeners = new Set<StateListener>();
|
|
348
317
|
const actionListeners = new Map<string, Set<ActionStateListener>>();
|
|
349
318
|
const handleListeners = new Set<HandleListener>();
|
|
350
319
|
|
|
351
320
|
const notify = makeDebouncedNotifier(stateListeners);
|
|
352
321
|
|
|
353
|
-
// Debounce per-action notifications
|
|
354
322
|
const actionNotifyTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
|
355
323
|
|
|
356
324
|
function notifyAction(actionId: string) {
|
|
@@ -362,8 +330,6 @@ export function createEventController(
|
|
|
362
330
|
actionId,
|
|
363
331
|
setTimeout(() => {
|
|
364
332
|
actionNotifyTimeouts.delete(actionId);
|
|
365
|
-
// Notify all listeners whose subscription ID matches this action
|
|
366
|
-
// This includes exact matches and suffix matches (e.g., "addToCart" matches "hash#addToCart")
|
|
367
333
|
for (const [subscriptionId, listeners] of actionListeners) {
|
|
368
334
|
if (matchesActionId(subscriptionId, actionId)) {
|
|
369
335
|
const state = getActionState(subscriptionId);
|
|
@@ -376,12 +342,7 @@ export function createEventController(
|
|
|
376
342
|
|
|
377
343
|
const notifyHandles = makeDebouncedNotifier(handleListeners);
|
|
378
344
|
|
|
379
|
-
// ========================================================================
|
|
380
|
-
// Derived State
|
|
381
|
-
// ========================================================================
|
|
382
|
-
|
|
383
345
|
function getState(): DerivedNavigationState {
|
|
384
|
-
// Build inflight actions list (for compatibility with existing API)
|
|
385
346
|
const inflightActionsList: InflightAction[] = [...inflightActions.values()]
|
|
386
347
|
.filter((a) => a.phase !== "settling")
|
|
387
348
|
.map((a) => ({
|
|
@@ -391,15 +352,12 @@ export function createEventController(
|
|
|
391
352
|
startedAt: a.startedAt,
|
|
392
353
|
}));
|
|
393
354
|
|
|
394
|
-
// State: loading if navigation OR actions are in progress
|
|
395
|
-
// Background revalidations (skipLoadingState) don't affect visible state
|
|
396
355
|
const hasActiveActions = inflightActionsList.length > 0;
|
|
397
356
|
const isVisibleNavigation =
|
|
398
357
|
currentNavigation !== null &&
|
|
399
358
|
!currentNavigation.options?.skipLoadingState;
|
|
400
359
|
const state = isVisibleNavigation || hasActiveActions ? "loading" : "idle";
|
|
401
360
|
|
|
402
|
-
// Streaming: true if any active streams (navigation or action) or loading
|
|
403
361
|
const isStreaming = activeStreamCount > 0 || state === "loading";
|
|
404
362
|
|
|
405
363
|
return {
|
|
@@ -421,8 +379,6 @@ export function createEventController(
|
|
|
421
379
|
}
|
|
422
380
|
|
|
423
381
|
function getActionState(actionId: string): TrackedActionState {
|
|
424
|
-
// Prefer the most-recent non-settling entry; fall back to most-recent
|
|
425
|
-
// settling so a just-settled action's result/error stays readable.
|
|
426
382
|
const entry = [...inflightActions.values()]
|
|
427
383
|
.filter((a) => matchesActionId(actionId, a.actionId))
|
|
428
384
|
.reduce<ActionEntry | undefined>((best, a) => {
|
|
@@ -437,7 +393,6 @@ export function createEventController(
|
|
|
437
393
|
return { ...DEFAULT_ACTION_STATE };
|
|
438
394
|
}
|
|
439
395
|
|
|
440
|
-
// Derive state from phase
|
|
441
396
|
let state: ActionLifecycleState;
|
|
442
397
|
switch (entry.phase) {
|
|
443
398
|
case "fetching":
|
|
@@ -881,40 +836,3 @@ export function createEventController(
|
|
|
881
836
|
hadAnyConcurrentActions: () => hadAnyConcurrentActions,
|
|
882
837
|
};
|
|
883
838
|
}
|
|
884
|
-
|
|
885
|
-
// ============================================================================
|
|
886
|
-
// Singleton
|
|
887
|
-
// ============================================================================
|
|
888
|
-
|
|
889
|
-
let controllerInstance: EventController | null = null;
|
|
890
|
-
|
|
891
|
-
/**
|
|
892
|
-
* Initialize the global event controller
|
|
893
|
-
*/
|
|
894
|
-
export function initEventController(
|
|
895
|
-
config?: EventControllerConfig,
|
|
896
|
-
): EventController {
|
|
897
|
-
if (!controllerInstance) {
|
|
898
|
-
controllerInstance = createEventController(config);
|
|
899
|
-
}
|
|
900
|
-
return controllerInstance;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
/**
|
|
904
|
-
* Get the global event controller
|
|
905
|
-
*/
|
|
906
|
-
export function getEventController(): EventController {
|
|
907
|
-
if (!controllerInstance) {
|
|
908
|
-
throw new Error(
|
|
909
|
-
"Event controller not initialized. Call initEventController first.",
|
|
910
|
-
);
|
|
911
|
-
}
|
|
912
|
-
return controllerInstance;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
/**
|
|
916
|
-
* Reset the controller instance (for testing)
|
|
917
|
-
*/
|
|
918
|
-
export function resetEventController(): void {
|
|
919
|
-
controllerInstance = null;
|
|
920
|
-
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A module-level handle to the active navigation store.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* This handle is the live reference for code that needs the store but does not
|
|
4
|
+
* The boot path (`rsc-router.tsx`) calls `createNavigationStore()` directly;
|
|
5
|
+
* there is no global store singleton. This handle is the live reference for
|
|
6
|
+
* code that needs the store but does not
|
|
8
7
|
* receive it by argument: the jar-divergence observer (below) and the client
|
|
9
8
|
* seat of `invalidateClientCache()` (added later).
|
|
10
9
|
*
|
|
@@ -813,42 +813,3 @@ export function createNavigationStore(
|
|
|
813
813
|
},
|
|
814
814
|
};
|
|
815
815
|
}
|
|
816
|
-
|
|
817
|
-
// Singleton store instance
|
|
818
|
-
let storeInstance: NavigationStore | null = null;
|
|
819
|
-
|
|
820
|
-
/**
|
|
821
|
-
* Initialize the global navigation store
|
|
822
|
-
*
|
|
823
|
-
* Should be called once during app initialization.
|
|
824
|
-
* Subsequent calls return the existing instance.
|
|
825
|
-
*/
|
|
826
|
-
export function initNavigationStore(
|
|
827
|
-
config?: NavigationStoreConfig,
|
|
828
|
-
): NavigationStore {
|
|
829
|
-
if (!storeInstance) {
|
|
830
|
-
storeInstance = createNavigationStore(config);
|
|
831
|
-
}
|
|
832
|
-
return storeInstance;
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
/**
|
|
836
|
-
* Get the global navigation store
|
|
837
|
-
*
|
|
838
|
-
* Throws if store hasn't been initialized.
|
|
839
|
-
*/
|
|
840
|
-
export function getNavigationStore(): NavigationStore {
|
|
841
|
-
if (!storeInstance) {
|
|
842
|
-
throw new Error(
|
|
843
|
-
"Navigation store not initialized. Call initNavigationStore first.",
|
|
844
|
-
);
|
|
845
|
-
}
|
|
846
|
-
return storeInstance;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
/**
|
|
850
|
-
* Reset the store instance (for testing)
|
|
851
|
-
*/
|
|
852
|
-
export function resetNavigationStore(): void {
|
|
853
|
-
storeInstance = null;
|
|
854
|
-
}
|
|
@@ -13,7 +13,6 @@ import type { EventController, NavigationHandle } from "./event-controller.js";
|
|
|
13
13
|
import { debugLog } from "./logging.js";
|
|
14
14
|
import { buildHistoryState, pushHistoryWithIdx } from "./history-state.js";
|
|
15
15
|
|
|
16
|
-
// Re-export for consumers that import from navigation-transaction
|
|
17
16
|
export { resolveNavigationState } from "./history-state.js";
|
|
18
17
|
|
|
19
18
|
/** Check if a history state object contains location state keys. */
|
|
@@ -25,7 +24,6 @@ function hasLocationState(state: unknown): boolean {
|
|
|
25
24
|
);
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
// Polyfill Symbol.dispose for Safari and older browsers
|
|
29
27
|
if (typeof Symbol.dispose === "undefined") {
|
|
30
28
|
(Symbol as any).dispose = Symbol("Symbol.dispose");
|
|
31
29
|
}
|
|
@@ -114,7 +112,6 @@ export function createNavigationTransaction(
|
|
|
114
112
|
let committed = false;
|
|
115
113
|
const currentUrl = window.location.href;
|
|
116
114
|
|
|
117
|
-
// Start navigation in event controller (this sets loading state)
|
|
118
115
|
const handle = eventController.startNavigation(url, options);
|
|
119
116
|
|
|
120
117
|
/**
|
|
@@ -138,72 +135,50 @@ export function createNavigationTransaction(
|
|
|
138
135
|
|
|
139
136
|
const parsedUrl = new URL(url, window.location.origin);
|
|
140
137
|
|
|
141
|
-
// Generate history key from URL (with intercept suffix for separate caching)
|
|
142
138
|
const historyKey = generateHistoryKey(url, { intercept });
|
|
143
139
|
|
|
144
|
-
// For cache-only commits (stale revalidation), only update cache and return
|
|
145
|
-
// Don't touch store state or history - user may have navigated elsewhere
|
|
146
140
|
if (cacheOnly) {
|
|
147
141
|
const currentHandleData = eventController.getHandleState().data;
|
|
148
142
|
store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
|
|
149
|
-
// Complete the navigation handle so currentNavigation is cleared.
|
|
150
|
-
// Without this, the entry lingers and weakens state-machine invariants.
|
|
151
143
|
handle.complete(parsedUrl);
|
|
152
144
|
debugLog("[Browser] Cache-only commit, historyKey:", historyKey);
|
|
153
145
|
return { scroll: false };
|
|
154
146
|
}
|
|
155
147
|
|
|
156
|
-
// Save current scroll position before navigating
|
|
157
148
|
handleNavigationStart();
|
|
158
149
|
|
|
159
|
-
// Update segment state atomically
|
|
160
150
|
store.setSegmentIds(segmentIds);
|
|
161
151
|
store.setCurrentUrl(url);
|
|
162
152
|
store.setPath(parsedUrl.pathname);
|
|
163
153
|
|
|
164
154
|
store.setHistoryKey(historyKey);
|
|
165
155
|
|
|
166
|
-
// Cache segments with current handleData for this history entry
|
|
167
156
|
const currentHandleData = eventController.getHandleState().data;
|
|
168
157
|
store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
|
|
169
158
|
|
|
170
|
-
// For server actions, skip URL/history updates but still complete navigation
|
|
171
159
|
if (storeOnly) {
|
|
172
160
|
debugLog("[Browser] Store updated (action)");
|
|
173
|
-
// Complete navigation to clear loading state
|
|
174
161
|
handle.complete(parsedUrl);
|
|
175
162
|
return { scroll: false };
|
|
176
163
|
}
|
|
177
164
|
|
|
178
|
-
// Build history state - include user state, intercept info, and server-set state
|
|
179
165
|
const historyState = buildHistoryState(
|
|
180
166
|
opts.state,
|
|
181
167
|
{ intercept, sourceUrl: interceptSourceUrl },
|
|
182
168
|
serverState,
|
|
183
169
|
);
|
|
184
170
|
|
|
185
|
-
// Snapshot old state before pushState/replaceState overwrites it.
|
|
186
|
-
// Used to detect when location state is being cleared.
|
|
187
171
|
const oldState = window.history.state;
|
|
188
172
|
|
|
189
|
-
// Update browser URL (stamps history.state.idx for back() first-entry detection)
|
|
190
173
|
pushHistoryWithIdx(historyState, url, replace ?? false);
|
|
191
|
-
// Ensure new history entry has a scroll restoration key
|
|
192
174
|
ensureHistoryKey();
|
|
193
175
|
|
|
194
|
-
// Notify location state hooks when either old or new state carries
|
|
195
|
-
// location state. This covers both "set new state" and "clear old state"
|
|
196
|
-
// for same-page navigations where components don't remount.
|
|
197
176
|
if (hasLocationState(oldState) || hasLocationState(historyState)) {
|
|
198
177
|
window.dispatchEvent(new Event("__rsc_locationstate"));
|
|
199
178
|
}
|
|
200
179
|
|
|
201
|
-
// Complete the navigation in event controller (sets idle state, updates location)
|
|
202
180
|
handle.complete(parsedUrl);
|
|
203
181
|
|
|
204
|
-
// NOTE: Scroll is NOT handled here. The caller (partial-update.ts) handles
|
|
205
|
-
// scroll AFTER onUpdate() so React has the new content before we scroll.
|
|
206
|
-
|
|
207
182
|
debugLog(
|
|
208
183
|
"[Browser] Navigation committed, historyKey:",
|
|
209
184
|
historyKey,
|
|
@@ -217,10 +192,6 @@ export function createNavigationTransaction(
|
|
|
217
192
|
handle,
|
|
218
193
|
commit,
|
|
219
194
|
|
|
220
|
-
/**
|
|
221
|
-
* Create a bound transaction with pre-configured URL options
|
|
222
|
-
* segmentIds and segments provided at commit time (after they're resolved)
|
|
223
|
-
*/
|
|
224
195
|
with(
|
|
225
196
|
opts: Omit<CommitOptions, "segmentIds" | "segments">,
|
|
226
197
|
): BoundTransaction {
|
|
@@ -264,13 +235,10 @@ export function createNavigationTransaction(
|
|
|
264
235
|
},
|
|
265
236
|
|
|
266
237
|
[Symbol.dispose]() {
|
|
267
|
-
// Superseded: another navigation took over.
|
|
268
238
|
if (handle.signal.aborted) {
|
|
269
239
|
return;
|
|
270
240
|
}
|
|
271
241
|
|
|
272
|
-
// Failed (not committed): keep the target URL -- the error UI owns it.
|
|
273
|
-
// Just reset the event controller to idle.
|
|
274
242
|
if (!committed) {
|
|
275
243
|
handle[Symbol.dispose]();
|
|
276
244
|
}
|
|
@@ -21,23 +21,18 @@ import {
|
|
|
21
21
|
import type { BoundTransaction } from "./navigation-transaction.js";
|
|
22
22
|
import { ServerRedirect } from "../errors.js";
|
|
23
23
|
import { debugLog } from "./logging.js";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
validateRedirectOrigin,
|
|
26
|
+
validateExternalRedirect,
|
|
27
|
+
} from "./validate-redirect-origin.js";
|
|
25
28
|
import type { NavigationUpdate } from "./types.js";
|
|
26
29
|
|
|
27
|
-
/** Build a scroll payload from the commit's scroll option */
|
|
28
30
|
function toScrollPayload(
|
|
29
31
|
scroll: boolean | undefined,
|
|
30
32
|
): NonNullable<NavigationUpdate["scroll"]> {
|
|
31
33
|
return { enabled: scroll !== false ? scroll : false };
|
|
32
34
|
}
|
|
33
35
|
|
|
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
36
|
function shouldStartViewTransition(segments: ResolvedSegment[]): boolean {
|
|
42
37
|
let hasIntercept = false;
|
|
43
38
|
let hasTransition = false;
|
|
@@ -112,15 +107,6 @@ export type PartialUpdater = (
|
|
|
112
107
|
mode?: UpdateMode,
|
|
113
108
|
) => Promise<void>;
|
|
114
109
|
|
|
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
110
|
export function createPartialUpdater(
|
|
125
111
|
config: PartialUpdateConfig,
|
|
126
112
|
): PartialUpdater {
|
|
@@ -132,21 +118,12 @@ export function createPartialUpdater(
|
|
|
132
118
|
getVersion = () => undefined,
|
|
133
119
|
} = config;
|
|
134
120
|
|
|
135
|
-
/**
|
|
136
|
-
* Get current page's cached segments as an array
|
|
137
|
-
*/
|
|
138
121
|
function getCurrentCachedSegments(): ResolvedSegment[] {
|
|
139
122
|
const currentKey = store.getHistoryKey();
|
|
140
123
|
const cached = store.getCachedSegments(currentKey);
|
|
141
124
|
return cached?.segments || [];
|
|
142
125
|
}
|
|
143
126
|
|
|
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
127
|
async function fetchPartialUpdate(
|
|
151
128
|
targetUrl: string,
|
|
152
129
|
segmentIds: string[] | undefined,
|
|
@@ -158,20 +135,16 @@ export function createPartialUpdater(
|
|
|
158
135
|
const segmentState = store.getSegmentState();
|
|
159
136
|
const url = targetUrl || window.location.href;
|
|
160
137
|
|
|
161
|
-
// Capture history key at start for stale revalidation consistency check
|
|
162
138
|
const historyKeyAtStart = store.getHistoryKey();
|
|
163
139
|
|
|
164
140
|
const interceptSourceUrl = mode.interceptSourceUrl;
|
|
165
141
|
|
|
166
|
-
// When leaving intercept, filter out intercept-specific segments
|
|
167
142
|
let segments: string[];
|
|
168
143
|
if (mode.type === "leave-intercept") {
|
|
169
144
|
const currentSegments = segmentIds ?? segmentState.currentSegmentIds;
|
|
170
145
|
const currentCached = getCurrentCachedSegments();
|
|
171
146
|
const interceptIds = new Set(
|
|
172
|
-
currentCached
|
|
173
|
-
.filter((s) => s.namespace?.startsWith("intercept:"))
|
|
174
|
-
.map((s) => s.id),
|
|
147
|
+
currentCached.filter(isInterceptSegment).map((s) => s.id),
|
|
175
148
|
);
|
|
176
149
|
segments = currentSegments.filter((id) => !interceptIds.has(id));
|
|
177
150
|
debugLog(
|
|
@@ -181,12 +154,6 @@ export function createPartialUpdater(
|
|
|
181
154
|
segments = segmentIds ?? segmentState.currentSegmentIds;
|
|
182
155
|
}
|
|
183
156
|
|
|
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
157
|
const previousUrl =
|
|
191
158
|
mode.type === "leave-intercept"
|
|
192
159
|
? segmentState.currentUrl || tx.currentUrl
|
|
@@ -200,9 +167,6 @@ export function createPartialUpdater(
|
|
|
200
167
|
debugLog(`[Browser] Intercept context from: ${interceptSourceUrl}`);
|
|
201
168
|
}
|
|
202
169
|
|
|
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
170
|
const targetCache =
|
|
207
171
|
mode.type === "navigate" && mode.targetCacheSegments?.length
|
|
208
172
|
? mode.targetCacheSegments
|
|
@@ -213,22 +177,16 @@ export function createPartialUpdater(
|
|
|
213
177
|
`[Browser] cachedSegs source: ${cachedSegsSource} (${cachedSegs.length} segments: ${cachedSegs.map((s) => s.id).join(", ")})`,
|
|
214
178
|
);
|
|
215
179
|
|
|
216
|
-
// Fetch partial payload (no abort signal - RSC doesn't support it well)
|
|
217
180
|
let fetchResult: Awaited<ReturnType<NavigationClient["fetchPartial"]>>;
|
|
218
181
|
fetchResult = await client.fetchPartial({
|
|
219
182
|
targetUrl: url,
|
|
220
183
|
segmentIds: segments,
|
|
221
184
|
previousUrl,
|
|
222
|
-
// Mark stale when explicitly requested OR when no segments are sent
|
|
223
|
-
// (action redirect sends empty segments for a fresh render).
|
|
224
185
|
staleRevalidation:
|
|
225
186
|
mode.type === "stale-revalidation" || segments.length === 0,
|
|
226
187
|
version: getVersion(),
|
|
227
188
|
routerId: store.getRouterId?.(),
|
|
228
189
|
});
|
|
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
190
|
const streamingToken = tx.startStreaming();
|
|
233
191
|
const { payload, streamComplete: rawStreamComplete } = fetchResult;
|
|
234
192
|
debugLog("payload.metadata", payload.metadata);
|
|
@@ -237,13 +195,6 @@ export function createPartialUpdater(
|
|
|
237
195
|
streamingToken.end();
|
|
238
196
|
});
|
|
239
197
|
|
|
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
198
|
const currentRouterId = store.getRouterId?.();
|
|
248
199
|
if (
|
|
249
200
|
payload.metadata?.routerId &&
|
|
@@ -258,12 +209,29 @@ export function createPartialUpdater(
|
|
|
258
209
|
return;
|
|
259
210
|
}
|
|
260
211
|
|
|
261
|
-
// Handle server-side redirect with state
|
|
262
212
|
if (payload.metadata?.redirect) {
|
|
263
213
|
if (signal?.aborted) {
|
|
264
214
|
debugLog("[Browser] Ignoring stale redirect (aborted)");
|
|
265
215
|
return;
|
|
266
216
|
}
|
|
217
|
+
// Explicit off-host redirect (redirect(url, { external: true })):
|
|
218
|
+
// hard-navigate, but still scheme-validate (http/https only). external
|
|
219
|
+
// waives the same-origin check the app opted out of, NOT scheme safety, so
|
|
220
|
+
// a forged payload carrying a javascript:/data: URL cannot script via
|
|
221
|
+
// location.assign.
|
|
222
|
+
if (payload.metadata.redirect.external) {
|
|
223
|
+
const externalUrl = validateExternalRedirect(
|
|
224
|
+
payload.metadata.redirect.url,
|
|
225
|
+
window.location.origin,
|
|
226
|
+
);
|
|
227
|
+
if (!externalUrl) {
|
|
228
|
+
debugLog("[Browser] Ignoring blocked external redirect payload");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
debugLog("[Browser] External redirect (hard navigation)");
|
|
232
|
+
window.location.assign(externalUrl);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
267
235
|
const redirectUrl = validateRedirectOrigin(
|
|
268
236
|
payload.metadata.redirect.url,
|
|
269
237
|
window.location.origin,
|
|
@@ -288,7 +256,6 @@ export function createPartialUpdater(
|
|
|
288
256
|
debugLog(`[Browser] Partial update - matched: ${matched?.join(", ")}`);
|
|
289
257
|
debugLog(`[Browser] Diff: ${diff?.join(", ")}`);
|
|
290
258
|
|
|
291
|
-
// If diff is empty, nothing changed on server side.
|
|
292
259
|
if (!diff || diff.length === 0) {
|
|
293
260
|
const matchedIds = matched || [];
|
|
294
261
|
const cacheMap = new Map(cachedSegs.map((s) => [s.id, s]));
|
|
@@ -296,7 +263,6 @@ export function createPartialUpdater(
|
|
|
296
263
|
.map((id: string) => cacheMap.get(id))
|
|
297
264
|
.filter(Boolean) as ResolvedSegment[];
|
|
298
265
|
|
|
299
|
-
// When navigating with cached segments to a different route, render them.
|
|
300
266
|
if (mode.type === "navigate" && targetCache) {
|
|
301
267
|
debugLog(
|
|
302
268
|
"[Browser] No diff but navigating with cached segments - rendering target route",
|
|
@@ -311,10 +277,6 @@ export function createPartialUpdater(
|
|
|
311
277
|
existingSegments,
|
|
312
278
|
);
|
|
313
279
|
|
|
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
280
|
if (mode.targetCacheHandleData) {
|
|
319
281
|
store.updateCacheHandleData(
|
|
320
282
|
store.getHistoryKey(),
|
|
@@ -322,10 +284,6 @@ export function createPartialUpdater(
|
|
|
322
284
|
);
|
|
323
285
|
}
|
|
324
286
|
|
|
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
287
|
const { handles: _unusedHandles, ...metadataWithoutHandles } =
|
|
330
288
|
payload.metadata!;
|
|
331
289
|
const cachedUpdate = {
|
|
@@ -352,7 +310,6 @@ export function createPartialUpdater(
|
|
|
352
310
|
return;
|
|
353
311
|
}
|
|
354
312
|
|
|
355
|
-
// When leaving intercept, force re-render even with empty diff
|
|
356
313
|
if (mode.type === "leave-intercept") {
|
|
357
314
|
debugLog(
|
|
358
315
|
"[Browser] Leaving intercept - forcing re-render to remove modal",
|
|
@@ -377,7 +334,6 @@ export function createPartialUpdater(
|
|
|
377
334
|
return;
|
|
378
335
|
}
|
|
379
336
|
|
|
380
|
-
// Same route revalidation with no changes - skip UI update
|
|
381
337
|
debugLog(
|
|
382
338
|
"[Browser] No changes - all revalidations returned false, keeping existing UI",
|
|
383
339
|
);
|
|
@@ -386,7 +342,6 @@ export function createPartialUpdater(
|
|
|
386
342
|
return;
|
|
387
343
|
}
|
|
388
344
|
|
|
389
|
-
// Reconcile server segments with cached segments (single source of truth)
|
|
390
345
|
const matchedIds = matched || [];
|
|
391
346
|
const actor: ReconcileActor =
|
|
392
347
|
mode.type === "stale-revalidation" || mode.type === "action"
|
|
@@ -402,7 +357,6 @@ export function createPartialUpdater(
|
|
|
402
357
|
insertMissingDiff: true,
|
|
403
358
|
});
|
|
404
359
|
|
|
405
|
-
// HMR RESILIENCE: Check if we're missing any matched segments
|
|
406
360
|
const reconciledIdSet = new Set(reconciled.segments.map((s) => s.id));
|
|
407
361
|
const missingIds = matchedIds.filter(
|
|
408
362
|
(id: string) => !reconciledIdSet.has(id),
|
|
@@ -430,7 +384,6 @@ export function createPartialUpdater(
|
|
|
430
384
|
`[Browser] HMR detected: Missing ${missingCount} segments. Refetching all...`,
|
|
431
385
|
);
|
|
432
386
|
|
|
433
|
-
// Refetch with empty segments = server sends everything
|
|
434
387
|
return fetchPartialUpdate(url, [], true, signal, tx, mode);
|
|
435
388
|
}
|
|
436
389
|
|
|
@@ -439,7 +392,6 @@ export function createPartialUpdater(
|
|
|
439
392
|
return;
|
|
440
393
|
}
|
|
441
394
|
|
|
442
|
-
// Rebuild tree on client (await for loader data resolution)
|
|
443
395
|
const renderOptions = {
|
|
444
396
|
isAction: mode.type === "action",
|
|
445
397
|
forceAwait: mode.type === "stale-revalidation",
|
|
@@ -462,21 +414,15 @@ export function createPartialUpdater(
|
|
|
462
414
|
])
|
|
463
415
|
: renderSegments(reconciled.mainSegments, renderOptions));
|
|
464
416
|
|
|
465
|
-
// Final abort check before committing - another navigation may have started
|
|
466
417
|
if (signal?.aborted) {
|
|
467
418
|
debugLog("[Browser] Ignoring stale navigation (aborted before commit)");
|
|
468
419
|
return;
|
|
469
420
|
}
|
|
470
421
|
|
|
471
|
-
// Check if this is an intercept response (any slot is active)
|
|
472
422
|
const isInterceptResponse = hasActiveInterceptSlots(
|
|
473
423
|
payload.metadata?.slots,
|
|
474
424
|
);
|
|
475
425
|
|
|
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
426
|
const effectiveInterceptSource =
|
|
481
427
|
interceptSourceUrl || segmentState.currentUrl;
|
|
482
428
|
if (mode.type !== "action" && mode.type !== "stale-revalidation") {
|
|
@@ -487,9 +433,6 @@ export function createPartialUpdater(
|
|
|
487
433
|
}
|
|
488
434
|
}
|
|
489
435
|
|
|
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
436
|
const allSegmentIds = matchedIds;
|
|
494
437
|
const serverLocationState = payload.metadata?.locationState;
|
|
495
438
|
const overrides: CommitOverrides | undefined = isInterceptResponse
|
|
@@ -508,7 +451,6 @@ export function createPartialUpdater(
|
|
|
508
451
|
overrides,
|
|
509
452
|
);
|
|
510
453
|
|
|
511
|
-
// For stale revalidation: verify history key hasn't changed before updating UI
|
|
512
454
|
if (mode.type === "stale-revalidation") {
|
|
513
455
|
const historyKeyNow = store.getHistoryKey();
|
|
514
456
|
if (historyKeyNow !== historyKeyAtStart) {
|
|
@@ -521,8 +463,6 @@ export function createPartialUpdater(
|
|
|
521
463
|
|
|
522
464
|
debugLog("[partial-update] updating document");
|
|
523
465
|
|
|
524
|
-
// Emit update to trigger React render.
|
|
525
|
-
// Scroll info is included so NavigationProvider applies it after React commits.
|
|
526
466
|
const hasTransition = shouldStartViewTransition(reconciled.segments);
|
|
527
467
|
const scrollPayload = toScrollPayload(navScroll);
|
|
528
468
|
|
|
@@ -559,7 +499,6 @@ export function createPartialUpdater(
|
|
|
559
499
|
debugLog("[Browser] Navigation complete");
|
|
560
500
|
return;
|
|
561
501
|
} else {
|
|
562
|
-
// Full update (fallback)
|
|
563
502
|
console.warn(`[Browser] Full update (fallback)`);
|
|
564
503
|
|
|
565
504
|
const segments = payload.metadata?.segments || [];
|