@rangojs/router 0.18.0 → 0.19.0
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/types/browser/event-controller.d.ts +3 -3
- package/dist/types/browser/history-state.d.ts +2 -0
- package/dist/types/browser/navigation-transaction.d.ts +20 -2
- package/dist/types/browser/partial-update.d.ts +16 -1
- package/dist/types/browser/prefetch/cache.d.ts +6 -6
- package/dist/types/browser/react/Link.d.ts +19 -9
- package/dist/types/browser/react/context.d.ts +2 -2
- package/dist/types/browser/react/location-state-shared.d.ts +95 -56
- package/dist/types/browser/react/location-state.d.ts +1 -1
- package/dist/types/browser/transition-when.d.ts +80 -0
- package/dist/types/browser/types.d.ts +104 -9
- package/dist/types/build/generate-manifest.d.ts +8 -0
- package/dist/types/cache/background-task.d.ts +24 -2
- package/dist/types/cache/cache-exec-scope.d.ts +31 -0
- package/dist/types/cache/cache-key-utils.d.ts +44 -13
- package/dist/types/cache/cache-policy.d.ts +1 -1
- package/dist/types/cache/cache-runtime.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +117 -35
- package/dist/types/cache/cache-tag.d.ts +24 -4
- package/dist/types/cache/cf/cf-cache-store.d.ts +55 -44
- package/dist/types/cache/cf/cf-shell-frame.d.ts +1 -1
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +0 -15
- package/dist/types/cache/document-cache.d.ts +10 -0
- package/dist/types/cache/handle-snapshot.d.ts +40 -8
- package/dist/types/cache/invalidation-order.d.ts +23 -0
- package/dist/types/cache/request-tag-mask.d.ts +54 -0
- package/dist/types/cache/shell-snapshot.d.ts +88 -141
- package/dist/types/cache/tag-invalidation.d.ts +38 -14
- package/dist/types/cache/types.d.ts +68 -61
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +23 -16
- package/dist/types/client-urls/navigation.d.ts +28 -2
- package/dist/types/client-urls/server-projection.d.ts +5 -2
- package/dist/types/client-urls/types.d.ts +5 -5
- package/dist/types/client.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +1 -1
- package/dist/types/route-definition/helpers-types.d.ts +11 -11
- package/dist/types/route-name.d.ts +7 -0
- package/dist/types/router/error-handling.d.ts +1 -1
- package/dist/types/router/intercept-resolution.d.ts +2 -0
- package/dist/types/router/match-context.d.ts +5 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
- package/dist/types/router/router-options.d.ts +4 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +18 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +14 -40
- package/dist/types/router/segment-resolution/mask-nested.d.ts +11 -3
- package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
- package/dist/types/router/transition-when.d.ts +24 -10
- package/dist/types/rsc/attach-transition-when.d.ts +18 -0
- package/dist/types/rsc/full-payload.d.ts +14 -7
- package/dist/types/rsc/response-cache-serve.d.ts +15 -12
- package/dist/types/rsc/shell-build-manifest.d.ts +4 -3
- package/dist/types/rsc/shell-capture-constants.d.ts +77 -20
- package/dist/types/rsc/shell-capture.d.ts +78 -81
- package/dist/types/rsc/shell-serve.d.ts +19 -48
- package/dist/types/rsc/types.d.ts +2 -0
- package/dist/types/segment-system.d.ts +7 -0
- package/dist/types/server/context.d.ts +89 -61
- package/dist/types/server/cookie-store.d.ts +30 -18
- package/dist/types/server/handle-store.d.ts +52 -1
- package/dist/types/server/request-context.d.ts +66 -45
- package/dist/types/server/request-headers.d.ts +44 -0
- package/dist/types/server.d.ts +1 -0
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/index.d.ts +3 -2
- package/dist/types/testing/internal/action-id.d.ts +7 -0
- package/dist/types/testing/location-state-key.d.ts +24 -0
- package/dist/types/testing/render-route.d.ts +17 -1
- package/dist/types/testing/run-transition-when.d.ts +44 -69
- package/dist/types/testing/serve-shell-request.d.ts +7 -6
- package/dist/types/testing/shell-status.d.ts +34 -22
- package/dist/types/transition-when-ref.d.ts +79 -0
- package/dist/types/types/cache-types.d.ts +15 -3
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/segments.d.ts +86 -71
- package/dist/types/urls/path-helper-types.d.ts +3 -4
- package/dist/types/urls/pattern-types.d.ts +3 -4
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
- package/dist/vite/index.js +960 -97
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +76 -49
- package/skills/caching/SKILL.md +167 -69
- package/skills/client-urls/SKILL.md +1 -1
- package/skills/comparison/references/framework-comparison.md +8 -5
- package/skills/document-cache/SKILL.md +24 -0
- package/skills/hooks/SKILL.md +3 -1
- package/skills/hooks/navigation.md +23 -2
- package/skills/hooks/state.md +98 -6
- package/skills/intercept/SKILL.md +29 -12
- package/skills/loader/SKILL.md +52 -21
- package/skills/middleware/SKILL.md +18 -0
- package/skills/migrate-nextjs/SKILL.md +3 -3
- package/skills/observability/SKILL.md +6 -5
- package/skills/parallel/SKILL.md +1 -1
- package/skills/ppr/SKILL.md +129 -77
- package/skills/prerender/SKILL.md +3 -3
- package/skills/rango/SKILL.md +25 -6
- package/skills/route/SKILL.md +2 -0
- package/skills/testing/SKILL.md +27 -25
- package/skills/testing/cache-prerender.md +9 -9
- package/skills/testing/client-components.md +45 -3
- package/skills/testing/loader.md +3 -2
- package/skills/testing/middleware.md +1 -0
- package/skills/testing/predicates.md +49 -49
- package/skills/testing/render-handler.md +1 -0
- package/skills/testing/serve-shell-request.md +14 -0
- package/skills/testing/server-actions.md +9 -3
- package/skills/theme/SKILL.md +8 -4
- package/skills/typesafety/env-and-bindings.md +8 -0
- package/skills/use-cache/SKILL.md +64 -19
- package/skills/view-transitions/SKILL.md +150 -29
- package/src/__augment-tests__/augmented.check.ts +23 -0
- package/src/browser/event-controller.ts +4 -4
- package/src/browser/history-state.ts +52 -1
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +77 -18
- package/src/browser/navigation-client.ts +4 -3
- package/src/browser/navigation-store.ts +43 -1
- package/src/browser/navigation-transaction.ts +31 -11
- package/src/browser/partial-update.ts +146 -35
- package/src/browser/prefetch/cache.ts +6 -6
- package/src/browser/prefetch/fetch.ts +2 -2
- package/src/browser/react/Link.tsx +38 -33
- package/src/browser/react/NavigationProvider.tsx +2 -2
- package/src/browser/react/context.ts +2 -2
- package/src/browser/react/location-state-shared.ts +241 -49
- package/src/browser/react/location-state.ts +5 -2
- package/src/browser/rsc-router.tsx +4 -0
- package/src/browser/server-action-bridge.ts +84 -15
- package/src/browser/transition-when.ts +205 -0
- package/src/browser/types.ts +124 -10
- package/src/build/generate-manifest.ts +77 -1
- package/src/cache/background-task.ts +55 -2
- package/src/cache/cache-exec-scope.ts +48 -1
- package/src/cache/cache-key-utils.ts +56 -16
- package/src/cache/cache-policy.ts +9 -4
- package/src/cache/cache-runtime.ts +197 -144
- package/src/cache/cache-scope.ts +304 -112
- package/src/cache/cache-tag.ts +105 -13
- package/src/cache/cf/cf-cache-store.ts +152 -76
- package/src/cache/cf/cf-shell-frame.ts +2 -2
- package/src/cache/cf/cf-tag-marker-memo.ts +11 -100
- package/src/cache/document-cache.ts +53 -7
- package/src/cache/handle-snapshot.ts +52 -9
- package/src/cache/invalidation-order.ts +84 -0
- package/src/cache/request-tag-mask.ts +147 -0
- package/src/cache/shell-snapshot.ts +130 -348
- package/src/cache/tag-invalidation.ts +57 -51
- package/src/cache/types.ts +71 -67
- package/src/cache/vercel/vercel-cache-store.ts +61 -58
- package/src/client-urls/client-root.tsx +8 -4
- package/src/client-urls/client-urls.ts +8 -6
- package/src/client-urls/navigation.ts +55 -4
- package/src/client-urls/server-projection.ts +31 -10
- package/src/client-urls/types.ts +12 -5
- package/src/client.tsx +2 -0
- package/src/host/cookie-handler.ts +2 -1
- package/src/index.rsc.ts +3 -0
- package/src/index.ts +3 -0
- package/src/loader-redirect.tsx +7 -3
- package/src/prerender/build-shell-capture.ts +155 -146
- package/src/route-definition/dsl-helpers.ts +2 -0
- package/src/route-definition/helpers-types.ts +11 -11
- package/src/route-name.ts +12 -0
- package/src/router/content-negotiation.ts +4 -1
- package/src/router/error-handling.ts +5 -2
- package/src/router/handler-context.ts +10 -7
- package/src/router/intercept-resolution.ts +11 -1
- package/src/router/loader-resolution.ts +28 -3
- package/src/router/logging.ts +5 -3
- package/src/router/match-api.ts +62 -34
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +9 -29
- package/src/router/match-middleware/background-revalidation.ts +3 -5
- package/src/router/match-middleware/cache-lookup.ts +35 -19
- package/src/router/match-middleware/cache-store.ts +15 -9
- package/src/router/match-result.ts +1 -0
- package/src/router/middleware.ts +2 -5
- package/src/router/navigation-snapshot.ts +6 -5
- package/src/router/prerender-match.ts +4 -0
- package/src/router/request-classification.ts +7 -4
- package/src/router/router-options.ts +4 -1
- package/src/router/segment-resolution/fresh.ts +0 -4
- package/src/router/segment-resolution/loader-cache.ts +150 -25
- package/src/router/segment-resolution/loader-mask.ts +3 -4
- package/src/router/segment-resolution/loader-snapshot.ts +15 -78
- package/src/router/segment-resolution/mask-nested.ts +47 -22
- package/src/router/segment-resolution/revalidation.ts +0 -3
- package/src/router/segment-resolution/view-transition-default.ts +6 -21
- package/src/router/telemetry.ts +6 -3
- package/src/router/transition-when.ts +34 -52
- package/src/router.ts +2 -1
- package/src/rsc/attach-transition-when.ts +47 -0
- package/src/rsc/full-payload.ts +30 -10
- package/src/rsc/handler.ts +12 -10
- package/src/rsc/loader-fetch.ts +2 -1
- package/src/rsc/manifest-init.ts +4 -0
- package/src/rsc/origin-guard.ts +8 -5
- package/src/rsc/progressive-enhancement.ts +7 -30
- package/src/rsc/response-cache-serve.ts +68 -80
- package/src/rsc/response-route-handler.ts +2 -2
- package/src/rsc/rsc-rendering.ts +206 -252
- package/src/rsc/server-action.ts +8 -20
- package/src/rsc/shell-build-manifest.ts +22 -20
- package/src/rsc/shell-capture-constants.ts +136 -20
- package/src/rsc/shell-capture.ts +352 -589
- package/src/rsc/shell-serve.ts +73 -109
- package/src/rsc/ssr-setup.ts +3 -2
- package/src/rsc/types.ts +2 -0
- package/src/segment-system.tsx +40 -7
- package/src/server/context.ts +189 -124
- package/src/server/cookie-store.ts +153 -127
- package/src/server/handle-store.ts +245 -27
- package/src/server/request-context.ts +123 -84
- package/src/server/request-headers.ts +134 -0
- package/src/server.ts +4 -0
- package/src/testing/dispatch.ts +4 -7
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/index.ts +5 -0
- package/src/testing/internal/action-id.ts +25 -0
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/location-state-key.ts +45 -0
- package/src/testing/render-handler.ts +1 -1
- package/src/testing/render-route.tsx +109 -16
- package/src/testing/run-client-revalidate.ts +3 -19
- package/src/testing/run-loader.ts +5 -1
- package/src/testing/run-transition-when.ts +109 -172
- package/src/testing/serve-shell-request.ts +10 -7
- package/src/testing/shell-status.ts +60 -56
- package/src/transition-when-ref.ts +220 -0
- package/src/types/cache-types.ts +15 -3
- package/src/types/index.ts +3 -0
- package/src/types/segments.ts +94 -91
- package/src/urls/path-helper-types.ts +3 -4
- package/src/urls/pattern-types.ts +3 -4
- package/src/vite/discovery/discover-routers.ts +3 -0
- package/src/vite/discovery/shell-prerender-phase.ts +0 -2
- package/src/vite/discovery/virtual-module-codegen.ts +16 -6
- package/src/vite/plugins/transition-when-hoist.ts +1064 -0
- package/src/vite/rango.ts +2 -0
- package/src/vite/router-discovery.ts +75 -5
- package/dist/types/rsc/transition-gate.d.ts +0 -23
- package/src/rsc/transition-gate.ts +0 -86
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
HistoryEntryMemory,
|
|
2
3
|
NavigationLocation,
|
|
3
4
|
SegmentState,
|
|
4
5
|
NavigationStore,
|
|
@@ -13,10 +14,18 @@ import {
|
|
|
13
14
|
getRangoState,
|
|
14
15
|
getRangoStateCookieName,
|
|
15
16
|
} from "./rango-state.js";
|
|
17
|
+
import { getHistoryStateKey } from "./scroll-restoration.js";
|
|
18
|
+
import { setCurrentHistoryStateListener } from "./react/location-state-shared.js";
|
|
16
19
|
|
|
17
20
|
// Maximum number of history entries to cache (URLs visited)
|
|
18
21
|
const HISTORY_CACHE_SIZE = 20;
|
|
19
22
|
|
|
23
|
+
// Bound on remembered history entries (transition({ when }) sources), matching
|
|
24
|
+
// scroll-restoration's MAX_SCROLL_ENTRIES: deep enough for any realistic
|
|
25
|
+
// back/forward run, bounded for long sessions. An evicted entry reads as
|
|
26
|
+
// unknown (undefined route name and state), never guessed.
|
|
27
|
+
const HISTORY_ENTRY_MEMORY_SIZE = 200;
|
|
28
|
+
|
|
20
29
|
// Cache entry:
|
|
21
30
|
// [url-key, segments, stale, handleData?, routerId?, navInstance?, handlesPending?]
|
|
22
31
|
// stale=true means the data may be outdated and should be revalidated on access.
|
|
@@ -233,6 +242,11 @@ export function createNavigationStore(
|
|
|
233
242
|
// Current history key (set on navigation, stored in history.state)
|
|
234
243
|
let currentHistoryKey = config?.initialHistoryKey || generateHistoryKey();
|
|
235
244
|
|
|
245
|
+
// Per-history-entry memory (rememberDisplayedEntry), keyed by the entry's
|
|
246
|
+
// history.state.key — unique per entry, unlike the URL-derived history key.
|
|
247
|
+
const entryMemory = new Map<string, HistoryEntryMemory>();
|
|
248
|
+
let displayedEntryKey: string | undefined;
|
|
249
|
+
|
|
236
250
|
// Store initial segments if provided (not stale)
|
|
237
251
|
if (config?.initialHistoryKey && config?.initialSegments) {
|
|
238
252
|
historyCache.push([
|
|
@@ -375,7 +389,7 @@ export function createNavigationStore(
|
|
|
375
389
|
}
|
|
376
390
|
}
|
|
377
391
|
|
|
378
|
-
|
|
392
|
+
const store: NavigationStore = {
|
|
379
393
|
// ========================================================================
|
|
380
394
|
// Internal Segment State (for bridges)
|
|
381
395
|
// ========================================================================
|
|
@@ -426,6 +440,27 @@ export function createNavigationStore(
|
|
|
426
440
|
currentHistoryKey = key;
|
|
427
441
|
},
|
|
428
442
|
|
|
443
|
+
rememberDisplayedEntry(routeName?: string): void {
|
|
444
|
+
if (typeof window === "undefined") return;
|
|
445
|
+
const key = getHistoryStateKey();
|
|
446
|
+
const previous = entryMemory.get(key);
|
|
447
|
+
entryMemory.delete(key);
|
|
448
|
+
entryMemory.set(key, {
|
|
449
|
+
routeName: routeName ?? previous?.routeName,
|
|
450
|
+
state: window.history.state,
|
|
451
|
+
});
|
|
452
|
+
if (entryMemory.size > HISTORY_ENTRY_MEMORY_SIZE) {
|
|
453
|
+
const oldest = entryMemory.keys().next().value;
|
|
454
|
+
if (oldest !== undefined) entryMemory.delete(oldest);
|
|
455
|
+
}
|
|
456
|
+
displayedEntryKey = key;
|
|
457
|
+
},
|
|
458
|
+
|
|
459
|
+
getHistoryEntryMemory(entryKey?: string): HistoryEntryMemory | undefined {
|
|
460
|
+
const key = entryKey ?? displayedEntryKey;
|
|
461
|
+
return key === undefined ? undefined : entryMemory.get(key);
|
|
462
|
+
},
|
|
463
|
+
|
|
429
464
|
/**
|
|
430
465
|
* Current nav-instance token: the instance of the most recently committed
|
|
431
466
|
* navigation (the value last written by cacheSegmentsForHistory). A late
|
|
@@ -682,4 +717,11 @@ export function createNavigationStore(
|
|
|
682
717
|
});
|
|
683
718
|
},
|
|
684
719
|
};
|
|
720
|
+
|
|
721
|
+
// Writes to the displayed entry after its commit (Def.write()/delete(), a
|
|
722
|
+
// flash read, merged server state) refresh its memory.
|
|
723
|
+
if (typeof window !== "undefined") {
|
|
724
|
+
setCurrentHistoryStateListener(() => store.rememberDisplayedEntry());
|
|
725
|
+
}
|
|
726
|
+
return store;
|
|
685
727
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
2
|
+
NavigateOptionsInternal,
|
|
3
3
|
NavigationStore,
|
|
4
4
|
ResolvedSegment,
|
|
5
5
|
StreamingToken,
|
|
@@ -13,21 +13,13 @@ import type { EventController, NavigationHandle } from "./event-controller.js";
|
|
|
13
13
|
import { debugLog } from "./logging.js";
|
|
14
14
|
import {
|
|
15
15
|
buildHistoryState,
|
|
16
|
+
hasLocationState,
|
|
16
17
|
mergeLocationState,
|
|
17
18
|
pushHistoryWithIdx,
|
|
18
19
|
} from "./history-state.js";
|
|
19
20
|
|
|
20
21
|
export { resolveNavigationState } from "./history-state.js";
|
|
21
22
|
|
|
22
|
-
/** Check if a history state object contains location state keys. */
|
|
23
|
-
function hasLocationState(state: unknown): boolean {
|
|
24
|
-
if (!state || typeof state !== "object") return false;
|
|
25
|
-
return (
|
|
26
|
-
"state" in state ||
|
|
27
|
-
Object.keys(state).some((k) => k.startsWith("__rsc_ls_"))
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
23
|
if (typeof Symbol.dispose === "undefined") {
|
|
32
24
|
(Symbol as any).dispose = Symbol("Symbol.dispose");
|
|
33
25
|
}
|
|
@@ -59,6 +51,14 @@ interface CommitOptions {
|
|
|
59
51
|
* merged in. The page left was saved by handleTraversalStart.
|
|
60
52
|
*/
|
|
61
53
|
traversal?: boolean;
|
|
54
|
+
/** The committed route's name (payload metadata), remembered per history entry. */
|
|
55
|
+
routeName?: string;
|
|
56
|
+
/**
|
|
57
|
+
* transition({ when }) decision already made at this navigation's first
|
|
58
|
+
* presentation (the optimistic clientUrls() swap). Not a commit input: the
|
|
59
|
+
* partial updater reuses it instead of deciding again.
|
|
60
|
+
*/
|
|
61
|
+
transitionGatedOff?: boolean;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/**
|
|
@@ -79,6 +79,8 @@ interface BoundCommitOverrides {
|
|
|
79
79
|
cacheOnly?: boolean;
|
|
80
80
|
/** Server-set location state to merge into history.pushState */
|
|
81
81
|
serverState?: Record<string, unknown>;
|
|
82
|
+
/** The committed route's name (payload metadata), remembered per history entry. */
|
|
83
|
+
routeName?: string;
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
/**
|
|
@@ -86,6 +88,14 @@ interface BoundCommitOverrides {
|
|
|
86
88
|
*/
|
|
87
89
|
export interface BoundTransaction {
|
|
88
90
|
readonly currentUrl: string;
|
|
91
|
+
/** Bound `replace` option (history replace instead of push). */
|
|
92
|
+
readonly replace?: boolean;
|
|
93
|
+
/** Bound `traversal` option: a back/forward navigation. */
|
|
94
|
+
readonly traversal?: boolean;
|
|
95
|
+
/** Bound user state (resolved), pushed with the entry at commit. */
|
|
96
|
+
readonly state?: unknown;
|
|
97
|
+
/** Bound `transitionGatedOff`: the decision made at first presentation. */
|
|
98
|
+
readonly transitionGatedOff?: boolean;
|
|
89
99
|
/** Start streaming and get a token to end it when the stream completes */
|
|
90
100
|
startStreaming(): StreamingToken;
|
|
91
101
|
/** Commit the navigation. Returns the effective scroll option for the caller to handle. */
|
|
@@ -117,7 +127,7 @@ export function createNavigationTransaction(
|
|
|
117
127
|
store: NavigationStore,
|
|
118
128
|
eventController: EventController,
|
|
119
129
|
url: string,
|
|
120
|
-
options?:
|
|
130
|
+
options?: NavigateOptionsInternal & { skipLoadingState?: boolean },
|
|
121
131
|
): NavigationTransaction {
|
|
122
132
|
let committed = false;
|
|
123
133
|
const currentUrl = window.location.href;
|
|
@@ -168,6 +178,9 @@ export function createNavigationTransaction(
|
|
|
168
178
|
store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
|
|
169
179
|
|
|
170
180
|
if (storeOnly) {
|
|
181
|
+
// Same entry (an action refetch): refresh its state, keep its name
|
|
182
|
+
// unless the payload names it.
|
|
183
|
+
store.rememberDisplayedEntry(opts.routeName);
|
|
171
184
|
debugLog("[Browser] Store updated (action)");
|
|
172
185
|
handle.complete(parsedUrl);
|
|
173
186
|
return { scroll: false };
|
|
@@ -177,6 +190,7 @@ export function createNavigationTransaction(
|
|
|
177
190
|
if (serverState && Object.keys(serverState).length > 0) {
|
|
178
191
|
mergeLocationState(serverState);
|
|
179
192
|
}
|
|
193
|
+
store.rememberDisplayedEntry(opts.routeName);
|
|
180
194
|
handle.complete(parsedUrl);
|
|
181
195
|
debugLog("[Browser] Traversal committed, historyKey:", historyKey);
|
|
182
196
|
return { scroll };
|
|
@@ -192,6 +206,7 @@ export function createNavigationTransaction(
|
|
|
192
206
|
|
|
193
207
|
pushHistoryWithIdx(historyState, url, replace ?? false);
|
|
194
208
|
ensureHistoryKey();
|
|
209
|
+
store.rememberDisplayedEntry(opts.routeName);
|
|
195
210
|
|
|
196
211
|
if (hasLocationState(oldState) || hasLocationState(historyState)) {
|
|
197
212
|
window.dispatchEvent(new Event("__rsc_locationstate"));
|
|
@@ -219,6 +234,10 @@ export function createNavigationTransaction(
|
|
|
219
234
|
get currentUrl() {
|
|
220
235
|
return currentUrl;
|
|
221
236
|
},
|
|
237
|
+
replace: opts.replace,
|
|
238
|
+
traversal: opts.traversal,
|
|
239
|
+
state: opts.state,
|
|
240
|
+
transitionGatedOff: opts.transitionGatedOff,
|
|
222
241
|
startStreaming() {
|
|
223
242
|
return handle.startStreaming();
|
|
224
243
|
},
|
|
@@ -249,6 +268,7 @@ export function createNavigationTransaction(
|
|
|
249
268
|
interceptSourceUrl,
|
|
250
269
|
cacheOnly,
|
|
251
270
|
serverState,
|
|
271
|
+
routeName: overrides?.routeName ?? opts.routeName,
|
|
252
272
|
});
|
|
253
273
|
},
|
|
254
274
|
};
|
|
@@ -36,6 +36,13 @@ import {
|
|
|
36
36
|
collectClientRevalidationDecisions,
|
|
37
37
|
setActiveInterceptTargets,
|
|
38
38
|
} from "../client-urls/navigation.js";
|
|
39
|
+
import {
|
|
40
|
+
decideCommitGatedOff,
|
|
41
|
+
mergeSegmentParams,
|
|
42
|
+
type TransitionWhenActionInput,
|
|
43
|
+
} from "./transition-when.js";
|
|
44
|
+
import { buildHistoryState } from "./history-state.js";
|
|
45
|
+
import type { TransitionWhenKind } from "../types/segments.js";
|
|
39
46
|
|
|
40
47
|
function toScrollPayload(
|
|
41
48
|
scroll: boolean | undefined,
|
|
@@ -43,9 +50,16 @@ function toScrollPayload(
|
|
|
43
50
|
return { enabled: scroll !== false ? scroll : false };
|
|
44
51
|
}
|
|
45
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Hold/animate decision for a commit: a non-intercept segment carries a
|
|
55
|
+
* transition, no intercept is presented, and this navigation's
|
|
56
|
+
* transition({ when }) decision did not gate it off.
|
|
57
|
+
*/
|
|
46
58
|
export function shouldStartViewTransition(
|
|
47
|
-
segments: ResolvedSegment[],
|
|
59
|
+
segments: readonly ResolvedSegment[],
|
|
60
|
+
transitionGatedOff: boolean = false,
|
|
48
61
|
): boolean {
|
|
62
|
+
if (transitionGatedOff) return false;
|
|
49
63
|
let hasIntercept = false;
|
|
50
64
|
let hasTransition = false;
|
|
51
65
|
for (const s of segments) {
|
|
@@ -107,6 +121,8 @@ export interface CommitOverrides {
|
|
|
107
121
|
interceptSourceUrl?: string;
|
|
108
122
|
/** Server-set location state to merge into history.pushState */
|
|
109
123
|
serverState?: Record<string, unknown>;
|
|
124
|
+
/** The committed route's name (payload metadata), remembered per history entry. */
|
|
125
|
+
routeName?: string;
|
|
110
126
|
}
|
|
111
127
|
|
|
112
128
|
/**
|
|
@@ -132,10 +148,21 @@ export type UpdateMode =
|
|
|
132
148
|
* boundaries do not animate the identical repaint.
|
|
133
149
|
*/
|
|
134
150
|
optimisticPresented?: boolean;
|
|
151
|
+
/**
|
|
152
|
+
* A router.refresh() (or cross-tab refresh) of the current URL:
|
|
153
|
+
* transition({ when }) sees kind "revalidate", not "replace".
|
|
154
|
+
*/
|
|
155
|
+
refresh?: boolean;
|
|
135
156
|
}
|
|
136
157
|
| { type: "leave-intercept"; interceptSourceUrl?: string }
|
|
137
158
|
| { type: "stale-revalidation"; interceptSourceUrl?: string }
|
|
138
|
-
| {
|
|
159
|
+
| {
|
|
160
|
+
type: "action";
|
|
161
|
+
interceptSourceUrl?: string;
|
|
162
|
+
actionId?: string;
|
|
163
|
+
/** The action's FormData and result, for transition({ when }) `action`. */
|
|
164
|
+
action?: TransitionWhenActionInput;
|
|
165
|
+
};
|
|
139
166
|
|
|
140
167
|
/**
|
|
141
168
|
* Type for the fetchPartialUpdate function
|
|
@@ -213,6 +240,54 @@ export function createPartialUpdater(
|
|
|
213
240
|
}
|
|
214
241
|
}
|
|
215
242
|
|
|
243
|
+
// transition({ when }). Every decision below runs before tx.commit()
|
|
244
|
+
// moves the store, so `from` is still the location being left:
|
|
245
|
+
// segmentState.currentUrl is the committed URL even on a popstate cache
|
|
246
|
+
// miss, where tx.currentUrl and window.location already point at the
|
|
247
|
+
// target.
|
|
248
|
+
const whenKind: TransitionWhenKind =
|
|
249
|
+
mode.type === "action"
|
|
250
|
+
? "action"
|
|
251
|
+
: mode.type === "stale-revalidation" ||
|
|
252
|
+
(mode.type === "navigate" && mode.refresh)
|
|
253
|
+
? "revalidate"
|
|
254
|
+
: tx.traversal
|
|
255
|
+
? "pop"
|
|
256
|
+
: tx.replace
|
|
257
|
+
? "replace"
|
|
258
|
+
: "push";
|
|
259
|
+
// Decide once per navigation (browser/transition-when.ts): reuse the
|
|
260
|
+
// decision the optimistic clientUrls() swap made; otherwise decide over
|
|
261
|
+
// every segment this commit presents, kept or re-sent (#989). The
|
|
262
|
+
// destination state is what the commit will leave on the entry.
|
|
263
|
+
const decideGatedOff = (
|
|
264
|
+
segmentsToCommit: readonly ResolvedSegment[],
|
|
265
|
+
metadata: {
|
|
266
|
+
params?: Record<string, string>;
|
|
267
|
+
routeName?: string;
|
|
268
|
+
locationState?: Record<string, unknown>;
|
|
269
|
+
},
|
|
270
|
+
): boolean =>
|
|
271
|
+
tx.transitionGatedOff ??
|
|
272
|
+
// A stale revalidation the user navigated away from is discarded after
|
|
273
|
+
// the render (history-key check below): its predicate must not run.
|
|
274
|
+
((mode.type === "stale-revalidation" &&
|
|
275
|
+
store.getHistoryKey() !== historyKeyAtStart) ||
|
|
276
|
+
decideCommitGatedOff(store, segmentsToCommit, whenKind, {
|
|
277
|
+
to: () => ({
|
|
278
|
+
url,
|
|
279
|
+
params: metadata.params ?? mergeSegmentParams(segmentsToCommit),
|
|
280
|
+
routeName: metadata.routeName,
|
|
281
|
+
state: tx.traversal
|
|
282
|
+
? { ...window.history.state, ...metadata.locationState }
|
|
283
|
+
: buildHistoryState(tx.state, undefined, metadata.locationState),
|
|
284
|
+
}),
|
|
285
|
+
action:
|
|
286
|
+
mode.type === "action"
|
|
287
|
+
? { ...mode.action, id: mode.actionId }
|
|
288
|
+
: undefined,
|
|
289
|
+
}));
|
|
290
|
+
|
|
216
291
|
const targetCache =
|
|
217
292
|
mode.type === "navigate" && mode.targetCacheSegments?.length
|
|
218
293
|
? mode.targetCacheSegments
|
|
@@ -349,13 +424,19 @@ export function createPartialUpdater(
|
|
|
349
424
|
"[Browser] No diff but navigating with cached segments - rendering target route",
|
|
350
425
|
);
|
|
351
426
|
|
|
427
|
+
const cachedGatedOff = decideGatedOff(
|
|
428
|
+
existingSegments,
|
|
429
|
+
payload.metadata,
|
|
430
|
+
);
|
|
352
431
|
const newTree = await renderSegments(existingSegments, {
|
|
353
432
|
forceAwait: true,
|
|
433
|
+
transitionGatedOff: cachedGatedOff,
|
|
354
434
|
});
|
|
355
435
|
|
|
356
436
|
const { scroll: commitScroll } = tx.commit(
|
|
357
437
|
matchedIds,
|
|
358
438
|
existingSegments,
|
|
439
|
+
{ routeName: payload.metadata.routeName },
|
|
359
440
|
);
|
|
360
441
|
|
|
361
442
|
if (mode.targetCacheHandleData) {
|
|
@@ -376,7 +457,7 @@ export function createPartialUpdater(
|
|
|
376
457
|
scroll: toScrollPayload(commitScroll),
|
|
377
458
|
};
|
|
378
459
|
|
|
379
|
-
if (shouldStartViewTransition(existingSegments)) {
|
|
460
|
+
if (shouldStartViewTransition(existingSegments, cachedGatedOff)) {
|
|
380
461
|
commitInTransition(onUpdate, existingSegments, cachedUpdate, [
|
|
381
462
|
"navigation",
|
|
382
463
|
]);
|
|
@@ -400,6 +481,7 @@ export function createPartialUpdater(
|
|
|
400
481
|
const { scroll: leaveScroll } = tx.commit(
|
|
401
482
|
matchedIds,
|
|
402
483
|
existingSegments,
|
|
484
|
+
{ routeName: payload.metadata.routeName },
|
|
403
485
|
);
|
|
404
486
|
|
|
405
487
|
onUpdate({
|
|
@@ -415,7 +497,9 @@ export function createPartialUpdater(
|
|
|
415
497
|
debugLog(
|
|
416
498
|
"[Browser] No changes - all revalidations returned false, keeping existing UI",
|
|
417
499
|
);
|
|
418
|
-
tx.commit(matchedIds, existingSegments
|
|
500
|
+
tx.commit(matchedIds, existingSegments, {
|
|
501
|
+
routeName: payload.metadata.routeName,
|
|
502
|
+
});
|
|
419
503
|
debugLog("[Browser] Navigation complete (no re-render)");
|
|
420
504
|
return;
|
|
421
505
|
}
|
|
@@ -477,7 +561,13 @@ export function createPartialUpdater(
|
|
|
477
561
|
return;
|
|
478
562
|
}
|
|
479
563
|
|
|
564
|
+
// Decide before the tree is built: on a gated-off commit every segment
|
|
565
|
+
// keeps its <ViewTransition> with "none" classes, so the decision must
|
|
566
|
+
// reach renderSegments. The response carries no decision, so a reused
|
|
567
|
+
// prefetch is decided against the real source.
|
|
568
|
+
const gatedOff = decideGatedOff(reconciled.segments, payload.metadata);
|
|
480
569
|
const renderOptions = {
|
|
570
|
+
transitionGatedOff: gatedOff,
|
|
481
571
|
isAction: mode.type === "action",
|
|
482
572
|
// forceAwait unwraps the ROUTER loader promises during render so they
|
|
483
573
|
// land without a loading()/fallback frame. A fully-prefetched nav has
|
|
@@ -557,16 +647,15 @@ export function createPartialUpdater(
|
|
|
557
647
|
|
|
558
648
|
const allSegmentIds = matchedIds;
|
|
559
649
|
const serverLocationState = payload.metadata?.locationState;
|
|
560
|
-
const overrides: CommitOverrides
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
: serverLocationState
|
|
568
|
-
|
|
569
|
-
: undefined;
|
|
650
|
+
const overrides: CommitOverrides = {
|
|
651
|
+
routeName: payload.metadata?.routeName,
|
|
652
|
+
...(isInterceptResponse && {
|
|
653
|
+
scroll: false,
|
|
654
|
+
intercept: true,
|
|
655
|
+
interceptSourceUrl: effectiveInterceptSource,
|
|
656
|
+
}),
|
|
657
|
+
...(serverLocationState && { serverState: serverLocationState }),
|
|
658
|
+
};
|
|
570
659
|
const { scroll: navScroll } = tx.commit(
|
|
571
660
|
allSegmentIds,
|
|
572
661
|
reconciled.segments,
|
|
@@ -585,7 +674,10 @@ export function createPartialUpdater(
|
|
|
585
674
|
|
|
586
675
|
debugLog("[partial-update] updating document");
|
|
587
676
|
|
|
588
|
-
const hasTransition = shouldStartViewTransition(
|
|
677
|
+
const hasTransition = shouldStartViewTransition(
|
|
678
|
+
reconciled.segments,
|
|
679
|
+
gatedOff,
|
|
680
|
+
);
|
|
589
681
|
const optimisticPresented =
|
|
590
682
|
mode.type === "navigate" && mode.optimisticPresented === true;
|
|
591
683
|
// [VT-DIAG] Gated behind INTERNAL_RANGO_DEBUG. Reports which reconciled
|
|
@@ -600,6 +692,7 @@ export function createPartialUpdater(
|
|
|
600
692
|
withTransition: reconciled.segments
|
|
601
693
|
.filter((s) => s.transition)
|
|
602
694
|
.map((s) => s.id),
|
|
695
|
+
transitionGatedOff: gatedOff,
|
|
603
696
|
all: reconciled.segments.map((s) => s.id),
|
|
604
697
|
});
|
|
605
698
|
}
|
|
@@ -617,7 +710,10 @@ export function createPartialUpdater(
|
|
|
617
710
|
metadata: payload.metadata!,
|
|
618
711
|
scroll: scrollPayload,
|
|
619
712
|
};
|
|
620
|
-
if (
|
|
713
|
+
if (
|
|
714
|
+
!gatedOff &&
|
|
715
|
+
(mode.type === "action" || mode.type === "stale-revalidation")
|
|
716
|
+
) {
|
|
621
717
|
commitInTransition(
|
|
622
718
|
onUpdate,
|
|
623
719
|
reconciled.mainSegments,
|
|
@@ -633,7 +729,10 @@ export function createPartialUpdater(
|
|
|
633
729
|
? ["navigation", OPTIMISTIC_COMMIT_TRANSITION_TYPE]
|
|
634
730
|
: ["navigation"],
|
|
635
731
|
);
|
|
636
|
-
} else if (
|
|
732
|
+
} else if (
|
|
733
|
+
!gatedOff &&
|
|
734
|
+
(fullyPrefetched || isSameStructureNav || optimisticPresented)
|
|
735
|
+
) {
|
|
637
736
|
// Content-hold commit, two triggers. Fully-prefetched nav: the payload
|
|
638
737
|
// is fully resolved (forceAwait above), so the transition commits
|
|
639
738
|
// synchronously — no fallback flash. Same-structure nav: the re-run
|
|
@@ -663,10 +762,14 @@ export function createPartialUpdater(
|
|
|
663
762
|
optimisticPresented ? [OPTIMISTIC_COMMIT_TRANSITION_TYPE] : [],
|
|
664
763
|
);
|
|
665
764
|
} else {
|
|
666
|
-
// Cold/partially-prefetched nav that mounts NEW segments
|
|
667
|
-
// commit
|
|
668
|
-
//
|
|
669
|
-
//
|
|
765
|
+
// Cold/partially-prefetched nav that mounts NEW segments, or any
|
|
766
|
+
// commit transition({ when }) gated off (navigation, action refetch,
|
|
767
|
+
// stale revalidation): normal commit so fallbacks stream
|
|
768
|
+
// like a first load and the click has visible feedback. A gated-off
|
|
769
|
+
// segment keeps its key, so its loading() fallback comes from its
|
|
770
|
+
// boundary re-suspending on this urgent commit, not from a remount
|
|
771
|
+
// (#995). Explicit transition() routes keep the content-hold via the
|
|
772
|
+
// hasTransition branch above (the opt-in).
|
|
670
773
|
onUpdate(update);
|
|
671
774
|
}
|
|
672
775
|
|
|
@@ -684,7 +787,10 @@ export function createPartialUpdater(
|
|
|
684
787
|
|
|
685
788
|
const segmentIds = segments.map((s: ResolvedSegment) => s.id);
|
|
686
789
|
|
|
687
|
-
const
|
|
790
|
+
const fullGatedOff = decideGatedOff(segments, payload.metadata ?? {});
|
|
791
|
+
const newTree = await renderSegments(segments, {
|
|
792
|
+
transitionGatedOff: fullGatedOff,
|
|
793
|
+
});
|
|
688
794
|
|
|
689
795
|
if (signal?.aborted) {
|
|
690
796
|
debugLog("[Browser] Ignoring stale navigation (aborted before commit)");
|
|
@@ -692,13 +798,15 @@ export function createPartialUpdater(
|
|
|
692
798
|
}
|
|
693
799
|
|
|
694
800
|
const fullUpdateServerState = payload.metadata?.locationState;
|
|
695
|
-
const { scroll: fullScroll } =
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
: tx.commit(segmentIds, segments);
|
|
801
|
+
const { scroll: fullScroll } = tx.commit(segmentIds, segments, {
|
|
802
|
+
routeName: payload.metadata?.routeName,
|
|
803
|
+
...(fullUpdateServerState && { serverState: fullUpdateServerState }),
|
|
804
|
+
});
|
|
700
805
|
|
|
701
|
-
const fullHasTransition = shouldStartViewTransition(
|
|
806
|
+
const fullHasTransition = shouldStartViewTransition(
|
|
807
|
+
segments,
|
|
808
|
+
fullGatedOff,
|
|
809
|
+
);
|
|
702
810
|
const fullScrollPayload = toScrollPayload(fullScroll);
|
|
703
811
|
const fullUpdate: NavigationUpdate = {
|
|
704
812
|
root: newTree,
|
|
@@ -720,13 +828,16 @@ export function createPartialUpdater(
|
|
|
720
828
|
);
|
|
721
829
|
return;
|
|
722
830
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
831
|
+
if (fullGatedOff) onUpdate(fullUpdate);
|
|
832
|
+
else {
|
|
833
|
+
commitInTransition(
|
|
834
|
+
onUpdate,
|
|
835
|
+
segments,
|
|
836
|
+
fullUpdate,
|
|
837
|
+
fullHasTransition ? ["action"] : [],
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
} else if (mode.type === "action" && !fullGatedOff) {
|
|
730
841
|
commitInTransition(
|
|
731
842
|
onUpdate,
|
|
732
843
|
segments,
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
* - Source-scoped: built by `buildSourceKey(rangoState, sourceHref, target)`
|
|
16
16
|
* — shape `rangoState\0sourceHref\0/target?...`. Embeds the Rango state
|
|
17
17
|
* (so rotation invalidates source-scoped entries too), the source href, and
|
|
18
|
-
* the exact source segment tree. Populated when the
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* both cases so source-sensitive responses
|
|
22
|
-
* from other pages.
|
|
18
|
+
* the exact source segment tree. Populated when the server tags a
|
|
19
|
+
* response with `X-RSC-Prefetch-Scope: source` (a route an intercept
|
|
20
|
+
* targets, matched or not), OR when a Link opts in with
|
|
21
|
+
* `prefetchKey=":source"` — in both cases so source-sensitive responses
|
|
22
|
+
* cannot bleed into navigations from other pages.
|
|
23
23
|
*
|
|
24
24
|
* Also tracks in-flight prefetch promises. Each promise resolves to the
|
|
25
25
|
* decoded prefetch entry (or null), letting navigation adopt a
|
|
@@ -172,7 +172,7 @@ let generation = 0;
|
|
|
172
172
|
* rotation invalidates source-scoped entries alongside wildcard ones,
|
|
173
173
|
* plus the source page href so the key is unique per originating page.
|
|
174
174
|
* Populated either when the server tags a response with
|
|
175
|
-
* `X-RSC-Prefetch-Scope: source` (intercept
|
|
175
|
+
* `X-RSC-Prefetch-Scope: source` (a route an intercept targets) or when a
|
|
176
176
|
* Link opts in via `prefetchKey=":source"`.
|
|
177
177
|
*
|
|
178
178
|
* Source-scoped keys retain `_rsc_segments` because their exact source tree is
|
|
@@ -189,8 +189,8 @@ function buildPrefetchUrl(
|
|
|
189
189
|
* - Otherwise: dual inflight registration under both `wildcardKey` and
|
|
190
190
|
* `sourceKey` so same-source navigations adopt directly via their own
|
|
191
191
|
* source key. Storage key is chosen at response time from the
|
|
192
|
-
* `X-RSC-Prefetch-Scope` header — `"source"` → `sourceKey` (
|
|
193
|
-
*
|
|
192
|
+
* `X-RSC-Prefetch-Scope` header — `"source"` → `sourceKey` (a route an
|
|
193
|
+
* intercept targets), anything else → `wildcardKey`. The entry records its scope
|
|
194
194
|
* so cross-source navigations that adopted via `wildcardKey` can bail out
|
|
195
195
|
* in `navigation-client.ts` when the adopted entry turns out source-scoped.
|
|
196
196
|
*/
|