@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26
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 +294 -28
- package/dist/bin/rango.js +355 -47
- package/dist/vite/index.js +1658 -1239
- package/package.json +3 -3
- package/skills/cache-guide/SKILL.md +9 -5
- package/skills/caching/SKILL.md +4 -4
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +40 -29
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +229 -15
- package/skills/middleware/SKILL.md +109 -30
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +189 -19
- package/skills/rango/SKILL.md +1 -2
- package/skills/response-routes/SKILL.md +3 -3
- package/skills/route/SKILL.md +44 -3
- package/skills/router-setup/SKILL.md +80 -3
- package/skills/theme/SKILL.md +5 -4
- package/skills/typesafety/SKILL.md +59 -16
- package/skills/use-cache/SKILL.md +16 -2
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +56 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +29 -48
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +19 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +66 -443
- package/src/browser/navigation-client.ts +34 -62
- package/src/browser/navigation-store.ts +4 -33
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/partial-update.ts +103 -151
- package/src/browser/prefetch/cache.ts +67 -0
- package/src/browser/prefetch/fetch.ts +137 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +154 -44
- package/src/browser/react/NavigationProvider.tsx +32 -0
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +29 -11
- package/src/browser/react/location-state.ts +6 -4
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +23 -45
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +21 -64
- package/src/browser/react/use-navigation.ts +7 -32
- package/src/browser/react/use-params.ts +5 -34
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +3 -6
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +75 -114
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +46 -22
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +458 -405
- package/src/browser/types.ts +21 -35
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +38 -13
- package/src/build/generate-route-types.ts +4 -0
- package/src/build/index.ts +1 -0
- package/src/build/route-trie.ts +19 -3
- package/src/build/route-types/codegen.ts +13 -4
- package/src/build/route-types/include-resolution.ts +13 -0
- package/src/build/route-types/per-module-writer.ts +15 -3
- package/src/build/route-types/router-processing.ts +170 -18
- package/src/build/runtime-discovery.ts +13 -1
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +136 -123
- package/src/cache/cache-scope.ts +76 -83
- package/src/cache/cf/cf-cache-store.ts +12 -7
- package/src/cache/document-cache.ts +93 -69
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/index.ts +0 -15
- package/src/cache/memory-segment-store.ts +43 -69
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +140 -117
- package/src/cache/taint.ts +30 -3
- package/src/cache/types.ts +1 -115
- package/src/client.rsc.tsx +0 -1
- package/src/client.tsx +53 -76
- package/src/errors.ts +6 -1
- package/src/handle.ts +1 -1
- package/src/handles/MetaTags.tsx +5 -2
- package/src/host/cookie-handler.ts +8 -3
- package/src/host/index.ts +0 -3
- package/src/host/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +53 -10
- package/src/index.ts +73 -43
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +60 -18
- package/src/prerender.ts +76 -18
- package/src/reverse.ts +11 -7
- package/src/root-error-boundary.tsx +30 -26
- package/src/route-definition/dsl-helpers.ts +9 -6
- package/src/route-definition/index.ts +0 -3
- package/src/route-definition/redirect.ts +15 -3
- package/src/route-map-builder.ts +38 -2
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +7 -0
- package/src/router/content-negotiation.ts +1 -1
- package/src/router/debug-manifest.ts +16 -3
- package/src/router/handler-context.ts +96 -17
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +6 -11
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +62 -54
- package/src/router/match-context.ts +3 -0
- package/src/router/match-handlers.ts +185 -11
- package/src/router/match-middleware/background-revalidation.ts +65 -85
- package/src/router/match-middleware/cache-lookup.ts +78 -10
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/match-result.ts +0 -9
- package/src/router/metrics.ts +233 -13
- package/src/router/middleware-types.ts +34 -39
- package/src/router/middleware.ts +290 -130
- package/src/router/pattern-matching.ts +61 -10
- package/src/router/prerender-match.ts +36 -6
- package/src/router/preview-match.ts +7 -1
- package/src/router/revalidation.ts +61 -2
- package/src/router/router-context.ts +15 -0
- package/src/router/router-interfaces.ts +158 -40
- package/src/router/router-options.ts +223 -1
- package/src/router/router-registry.ts +5 -2
- package/src/router/segment-resolution/fresh.ts +165 -242
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +102 -98
- package/src/router/segment-resolution/revalidation.ts +394 -272
- package/src/router/segment-resolution/static-store.ts +2 -2
- package/src/router/segment-resolution.ts +1 -3
- package/src/router/segment-wrappers.ts +3 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +20 -2
- package/src/router/types.ts +7 -1
- package/src/router.ts +203 -18
- package/src/rsc/handler-context.ts +13 -2
- package/src/rsc/handler.ts +489 -438
- package/src/rsc/helpers.ts +125 -5
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/manifest-init.ts +3 -2
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +245 -19
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +47 -43
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +166 -66
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +20 -2
- package/src/search-params.ts +38 -23
- package/src/server/context.ts +61 -7
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +84 -12
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +275 -49
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +67 -28
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +4 -18
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +6 -1
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +22 -0
- package/src/types/handler-context.ts +103 -16
- package/src/types/index.ts +1 -1
- package/src/types/loader-types.ts +9 -6
- package/src/types/route-config.ts +17 -26
- package/src/types/route-entry.ts +28 -0
- package/src/types/segments.ts +0 -5
- package/src/urls/include-helper.ts +49 -8
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +30 -12
- package/src/urls/path-helper.ts +17 -2
- package/src/urls/pattern-types.ts +21 -1
- package/src/urls/response-types.ts +29 -7
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +27 -9
- package/src/vite/discovery/bundle-postprocess.ts +32 -52
- package/src/vite/discovery/discover-routers.ts +52 -26
- package/src/vite/discovery/prerender-collection.ts +58 -41
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/state.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/index.ts +10 -51
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/expose-internal-ids.ts +4 -3
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/plugins/version-plugin.ts +188 -18
- package/src/vite/rango.ts +61 -36
- package/src/vite/router-discovery.ts +173 -100
- package/src/vite/utils/prerender-utils.ts +81 -0
- package/src/vite/utils/shared-utils.ts +19 -9
- package/skills/testing/SKILL.md +0 -226
- package/src/browser/lru-cache.ts +0 -61
- package/src/browser/react/prefetch.ts +0 -27
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/route-definition/route-function.ts +0 -119
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/{CLAUDE.md → AGENTS.md} +0 -0
|
@@ -15,9 +15,10 @@ import type { RenderSegmentsOptions } from "../segment-system.js";
|
|
|
15
15
|
import { reconcileSegments } from "./segment-reconciler.js";
|
|
16
16
|
import type { ReconcileActor } from "./segment-reconciler.js";
|
|
17
17
|
import { hasActiveIntercept as hasActiveInterceptSlots } from "./intercept-utils.js";
|
|
18
|
-
import type { BoundTransaction } from "./navigation-
|
|
18
|
+
import type { BoundTransaction } from "./navigation-transaction.js";
|
|
19
19
|
import { ServerRedirect } from "../errors.js";
|
|
20
20
|
import { debugLog } from "./logging.js";
|
|
21
|
+
import { validateRedirectOrigin } from "./validate-redirect-origin.js";
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Configuration for creating a partial updater
|
|
@@ -51,9 +52,25 @@ export interface CommitOverrides {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
/**
|
|
54
|
-
*
|
|
55
|
-
* Transaction encapsulates all store mutations for atomic commit
|
|
55
|
+
* Discriminated update mode for partial updates.
|
|
56
56
|
*/
|
|
57
|
+
export type UpdateMode =
|
|
58
|
+
| {
|
|
59
|
+
type: "navigate";
|
|
60
|
+
/** Cached segments for the target URL. When provided, these are used to build
|
|
61
|
+
* the segment map instead of the current page's segments. This ensures consistency
|
|
62
|
+
* when we send cached segment IDs to the server - if the server returns empty diff,
|
|
63
|
+
* we use the same segments we told the server we have. */
|
|
64
|
+
targetCacheSegments?: ResolvedSegment[];
|
|
65
|
+
/** Cached handle data for the target URL. When server returns empty diff and we're
|
|
66
|
+
* rendering from cache, this is passed to the UI to restore breadcrumbs etc. */
|
|
67
|
+
targetCacheHandleData?: Record<string, Record<string, unknown[]>>;
|
|
68
|
+
/** Source URL for intercept restore (popstate cache miss) */
|
|
69
|
+
interceptSourceUrl?: string;
|
|
70
|
+
}
|
|
71
|
+
| { type: "leave-intercept" }
|
|
72
|
+
| { type: "stale-revalidation"; interceptSourceUrl?: string }
|
|
73
|
+
| { type: "action"; interceptSourceUrl?: string };
|
|
57
74
|
|
|
58
75
|
/**
|
|
59
76
|
* Type for the fetchPartialUpdate function
|
|
@@ -63,24 +80,9 @@ export type PartialUpdater = (
|
|
|
63
80
|
segmentIds: string[] | undefined,
|
|
64
81
|
isRetry: boolean,
|
|
65
82
|
signal: AbortSignal | undefined,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
staleRevalidation?: boolean;
|
|
70
|
-
interceptSourceUrl?: string;
|
|
71
|
-
/** Cached segments for the target URL. When provided, these are used to build
|
|
72
|
-
* the segment map instead of the current page's segments. This ensures consistency
|
|
73
|
-
* when we send cached segment IDs to the server - if the server returns empty diff,
|
|
74
|
-
* we use the same segments we told the server we have. */
|
|
75
|
-
targetCacheSegments?: ResolvedSegment[];
|
|
76
|
-
/** Cached handle data for the target URL. When server returns empty diff and we're
|
|
77
|
-
* rendering from cache, this is passed to the UI to restore breadcrumbs etc. */
|
|
78
|
-
targetCacheHandleData?: Record<string, Record<string, unknown[]>>;
|
|
79
|
-
/** When true, we're leaving an intercept state - don't use current segment IDs
|
|
80
|
-
* as fallback and force a fresh render from server */
|
|
81
|
-
leavingIntercept?: boolean;
|
|
82
|
-
},
|
|
83
|
-
) => Promise<Promise<void>>;
|
|
83
|
+
tx: BoundTransaction,
|
|
84
|
+
mode?: UpdateMode,
|
|
85
|
+
) => Promise<void>;
|
|
84
86
|
|
|
85
87
|
/**
|
|
86
88
|
* Create a partial updater for fetching and applying RSC partial updates
|
|
@@ -90,18 +92,6 @@ export type PartialUpdater = (
|
|
|
90
92
|
*
|
|
91
93
|
* @param config - Partial update configuration
|
|
92
94
|
* @returns fetchPartialUpdate function
|
|
93
|
-
*
|
|
94
|
-
* @example
|
|
95
|
-
* ```typescript
|
|
96
|
-
* const fetchPartialUpdate = createPartialUpdater({
|
|
97
|
-
* store,
|
|
98
|
-
* client,
|
|
99
|
-
* onUpdate: (update) => store.emit(update),
|
|
100
|
-
* renderSegments,
|
|
101
|
-
* });
|
|
102
|
-
*
|
|
103
|
-
* await fetchPartialUpdate('/new-page');
|
|
104
|
-
* ```
|
|
105
95
|
*/
|
|
106
96
|
export function createPartialUpdater(
|
|
107
97
|
config: PartialUpdateConfig,
|
|
@@ -119,7 +109,6 @@ export function createPartialUpdater(
|
|
|
119
109
|
|
|
120
110
|
/**
|
|
121
111
|
* Fetch partial update and trigger UI update
|
|
122
|
-
* Returns a promise that resolves when the RSC stream is fully consumed
|
|
123
112
|
*
|
|
124
113
|
* @param tx - Transaction for committing segment state (required)
|
|
125
114
|
* @param signal - AbortSignal to check if navigation is stale (not for aborting fetch)
|
|
@@ -130,39 +119,26 @@ export function createPartialUpdater(
|
|
|
130
119
|
isRetry: boolean,
|
|
131
120
|
signal: AbortSignal | undefined,
|
|
132
121
|
tx: BoundTransaction,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
staleRevalidation?: boolean;
|
|
136
|
-
interceptSourceUrl?: string;
|
|
137
|
-
targetCacheSegments?: ResolvedSegment[];
|
|
138
|
-
targetCacheHandleData?: Record<string, Record<string, unknown[]>>;
|
|
139
|
-
leavingIntercept?: boolean;
|
|
140
|
-
},
|
|
141
|
-
): Promise<Promise<void>> {
|
|
142
|
-
const {
|
|
143
|
-
isAction = false,
|
|
144
|
-
staleRevalidation = false,
|
|
145
|
-
interceptSourceUrl,
|
|
146
|
-
targetCacheSegments,
|
|
147
|
-
targetCacheHandleData,
|
|
148
|
-
leavingIntercept = false,
|
|
149
|
-
} = options || {};
|
|
122
|
+
mode: UpdateMode = { type: "navigate" },
|
|
123
|
+
): Promise<void> {
|
|
150
124
|
const segmentState = store.getSegmentState();
|
|
151
125
|
const url = targetUrl || window.location.href;
|
|
152
126
|
|
|
153
127
|
// Capture history key at start for stale revalidation consistency check
|
|
154
128
|
const historyKeyAtStart = store.getHistoryKey();
|
|
155
129
|
|
|
156
|
-
//
|
|
157
|
-
|
|
130
|
+
// Derive interceptSourceUrl from modes that carry it
|
|
131
|
+
const interceptSourceUrl =
|
|
132
|
+
mode.type === "stale-revalidation" ||
|
|
133
|
+
mode.type === "action" ||
|
|
134
|
+
mode.type === "navigate"
|
|
135
|
+
? mode.interceptSourceUrl
|
|
136
|
+
: undefined;
|
|
137
|
+
|
|
138
|
+
// When leaving intercept, filter out intercept-specific segments
|
|
158
139
|
let segments: string[];
|
|
159
|
-
if (
|
|
160
|
-
// When leaving intercept, only send segments that aren't intercept-specific
|
|
161
|
-
// The server will return the non-intercept version of the route
|
|
140
|
+
if (mode.type === "leave-intercept") {
|
|
162
141
|
const currentSegments = segmentIds ?? segmentState.currentSegmentIds;
|
|
163
|
-
// Use cached segment metadata (namespace) to identify intercept segments.
|
|
164
|
-
// Only intercept segments have namespace starting with "intercept:" —
|
|
165
|
-
// regular parallel segments like @sidebar are preserved.
|
|
166
142
|
const currentCached = getCurrentCachedSegments();
|
|
167
143
|
const interceptIds = new Set(
|
|
168
144
|
currentCached
|
|
@@ -178,7 +154,6 @@ export function createPartialUpdater(
|
|
|
178
154
|
}
|
|
179
155
|
|
|
180
156
|
// For intercept revalidation, use the intercept source URL as previousUrl
|
|
181
|
-
// This tells the server the route should be treated as an intercept
|
|
182
157
|
const previousUrl =
|
|
183
158
|
interceptSourceUrl || tx.currentUrl || segmentState.currentUrl;
|
|
184
159
|
|
|
@@ -191,36 +166,31 @@ export function createPartialUpdater(
|
|
|
191
166
|
}
|
|
192
167
|
|
|
193
168
|
// Get cached segments for merging with server diff.
|
|
194
|
-
// When
|
|
195
|
-
// to ensure consistency - we use the same segments we told the server we have.
|
|
169
|
+
// When navigating with targetCacheSegments, use those for consistency.
|
|
196
170
|
// Otherwise fall back to current page's segments (for same-route revalidation).
|
|
171
|
+
const targetCache =
|
|
172
|
+
mode.type === "navigate" ? mode.targetCacheSegments : undefined;
|
|
197
173
|
const cachedSegs =
|
|
198
|
-
|
|
199
|
-
?
|
|
174
|
+
targetCache && targetCache.length > 0
|
|
175
|
+
? targetCache
|
|
200
176
|
: getCurrentCachedSegments();
|
|
201
|
-
|
|
202
|
-
// The token is ended when the stream completes
|
|
203
|
-
const streamingToken = tx.startStreaming();
|
|
177
|
+
|
|
204
178
|
// Fetch partial payload (no abort signal - RSC doesn't support it well)
|
|
205
|
-
// Wrapped in try/catch to ensure streamingToken.end() is called if fetch throws,
|
|
206
|
-
// preventing isStreaming from being permanently stuck as true.
|
|
207
179
|
let fetchResult: Awaited<ReturnType<NavigationClient["fetchPartial"]>>;
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
throw err;
|
|
223
|
-
}
|
|
180
|
+
fetchResult = await client.fetchPartial({
|
|
181
|
+
targetUrl: url,
|
|
182
|
+
segmentIds: segments,
|
|
183
|
+
previousUrl,
|
|
184
|
+
// Mark stale when explicitly requested OR when no segments are sent
|
|
185
|
+
// (action redirect sends empty segments for a fresh render).
|
|
186
|
+
staleRevalidation:
|
|
187
|
+
mode.type === "stale-revalidation" || segments.length === 0,
|
|
188
|
+
version,
|
|
189
|
+
});
|
|
190
|
+
// Mark navigation as streaming (response received, now parsing RSC).
|
|
191
|
+
// Called after fetchPartial so pendingUrl stays set during the network wait,
|
|
192
|
+
// allowing useLinkStatus to show per-link pending indicators.
|
|
193
|
+
const streamingToken = tx.startStreaming();
|
|
224
194
|
const { payload, streamComplete: rawStreamComplete } = fetchResult;
|
|
225
195
|
debugLog("payload.metadata", payload.metadata);
|
|
226
196
|
|
|
@@ -228,17 +198,20 @@ export function createPartialUpdater(
|
|
|
228
198
|
streamingToken.end();
|
|
229
199
|
});
|
|
230
200
|
|
|
231
|
-
// Handle server-side redirect with state
|
|
232
|
-
// a redirect payload instead of a 3xx so that location state is preserved.
|
|
233
|
-
// Throw ServerRedirect to let navigate() catch it and re-navigate with state.
|
|
234
|
-
// Check signal.aborted first — a newer navigation may have started, and we
|
|
235
|
-
// must not redirect from a stale response.
|
|
201
|
+
// Handle server-side redirect with state
|
|
236
202
|
if (payload.metadata?.redirect) {
|
|
237
203
|
if (signal?.aborted) {
|
|
238
|
-
|
|
239
|
-
return
|
|
204
|
+
debugLog("[Browser] Ignoring stale redirect (aborted)");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const redirectUrl = validateRedirectOrigin(
|
|
208
|
+
payload.metadata.redirect.url,
|
|
209
|
+
window.location.origin,
|
|
210
|
+
);
|
|
211
|
+
if (!redirectUrl) {
|
|
212
|
+
debugLog("[Browser] Ignoring blocked redirect payload");
|
|
213
|
+
return;
|
|
240
214
|
}
|
|
241
|
-
const { url: redirectUrl } = payload.metadata.redirect;
|
|
242
215
|
const serverState = payload.metadata.locationState;
|
|
243
216
|
throw new ServerRedirect(redirectUrl, serverState);
|
|
244
217
|
}
|
|
@@ -249,15 +222,13 @@ export function createPartialUpdater(
|
|
|
249
222
|
// Check if this navigation is stale (a newer one started)
|
|
250
223
|
if (signal?.aborted) {
|
|
251
224
|
debugLog("[Browser] Ignoring stale navigation (aborted)");
|
|
252
|
-
return
|
|
225
|
+
return;
|
|
253
226
|
}
|
|
254
227
|
|
|
255
228
|
debugLog(`[Browser] Partial update - matched: ${matched?.join(", ")}`);
|
|
256
229
|
debugLog(`[Browser] Diff: ${diff?.join(", ")}`);
|
|
257
230
|
|
|
258
231
|
// If diff is empty, nothing changed on server side.
|
|
259
|
-
// However, if we're navigating with targetCacheSegments (to a different route),
|
|
260
|
-
// we still need to render those segments since the UI is showing the old route.
|
|
261
232
|
if (!diff || diff.length === 0) {
|
|
262
233
|
const matchedIds = matched || [];
|
|
263
234
|
const cacheMap = new Map(cachedSegs.map((s) => [s.id, s]));
|
|
@@ -266,8 +237,7 @@ export function createPartialUpdater(
|
|
|
266
237
|
.filter(Boolean) as ResolvedSegment[];
|
|
267
238
|
|
|
268
239
|
// When navigating with cached segments to a different route, render them.
|
|
269
|
-
|
|
270
|
-
if (targetCacheSegments && targetCacheSegments.length > 0) {
|
|
240
|
+
if (mode.type === "navigate" && targetCache && targetCache.length > 0) {
|
|
271
241
|
debugLog(
|
|
272
242
|
"[Browser] No diff but navigating with cached segments - rendering target route",
|
|
273
243
|
);
|
|
@@ -280,16 +250,15 @@ export function createPartialUpdater(
|
|
|
280
250
|
|
|
281
251
|
// Include cachedHandleData in metadata so NavigationProvider can restore
|
|
282
252
|
// breadcrumbs and other handle data from cache.
|
|
283
|
-
//
|
|
253
|
+
// Remove `handles` from metadata to prevent NavigationProvider from
|
|
284
254
|
// processing an empty handles stream, which would clear the cached breadcrumbs.
|
|
285
|
-
// When rendering from cache with empty diff, we want to use cachedHandleData instead.
|
|
286
255
|
const { handles: _unusedHandles, ...metadataWithoutHandles } =
|
|
287
256
|
payload.metadata!;
|
|
288
257
|
const cachedUpdate = {
|
|
289
258
|
root: newTree,
|
|
290
259
|
metadata: {
|
|
291
260
|
...metadataWithoutHandles,
|
|
292
|
-
cachedHandleData: targetCacheHandleData,
|
|
261
|
+
cachedHandleData: mode.targetCacheHandleData,
|
|
293
262
|
},
|
|
294
263
|
};
|
|
295
264
|
|
|
@@ -308,13 +277,11 @@ export function createPartialUpdater(
|
|
|
308
277
|
}
|
|
309
278
|
|
|
310
279
|
debugLog("[Browser] Navigation complete (rendered from cache)");
|
|
311
|
-
return
|
|
280
|
+
return;
|
|
312
281
|
}
|
|
313
282
|
|
|
314
283
|
// When leaving intercept, force re-render even with empty diff
|
|
315
|
-
|
|
316
|
-
// to remove the modal from the UI
|
|
317
|
-
if (leavingIntercept) {
|
|
284
|
+
if (mode.type === "leave-intercept") {
|
|
318
285
|
debugLog(
|
|
319
286
|
"[Browser] Leaving intercept - forcing re-render to remove modal",
|
|
320
287
|
);
|
|
@@ -331,7 +298,7 @@ export function createPartialUpdater(
|
|
|
331
298
|
});
|
|
332
299
|
|
|
333
300
|
debugLog("[Browser] Navigation complete (left intercept)");
|
|
334
|
-
return
|
|
301
|
+
return;
|
|
335
302
|
}
|
|
336
303
|
|
|
337
304
|
// Same route revalidation with no changes - skip UI update
|
|
@@ -340,13 +307,15 @@ export function createPartialUpdater(
|
|
|
340
307
|
);
|
|
341
308
|
tx.commit(matchedIds, existingSegments);
|
|
342
309
|
debugLog("[Browser] Navigation complete (no re-render)");
|
|
343
|
-
return
|
|
310
|
+
return;
|
|
344
311
|
}
|
|
345
312
|
|
|
346
313
|
// Reconcile server segments with cached segments (single source of truth)
|
|
347
314
|
const matchedIds = matched || [];
|
|
348
315
|
const actor: ReconcileActor =
|
|
349
|
-
|
|
316
|
+
mode.type === "stale-revalidation" || mode.type === "action"
|
|
317
|
+
? "stale-revalidation"
|
|
318
|
+
: "navigation";
|
|
350
319
|
|
|
351
320
|
const reconciled = reconcileSegments({
|
|
352
321
|
actor,
|
|
@@ -376,31 +345,28 @@ export function createPartialUpdater(
|
|
|
376
345
|
debugLog(
|
|
377
346
|
"[Browser] Ignoring stale navigation (aborted during HMR retry)",
|
|
378
347
|
);
|
|
379
|
-
return
|
|
348
|
+
return;
|
|
380
349
|
}
|
|
381
|
-
if (
|
|
382
|
-
return
|
|
350
|
+
if (mode.type === "action") {
|
|
351
|
+
return;
|
|
383
352
|
}
|
|
384
353
|
console.warn(
|
|
385
354
|
`[Browser] HMR detected: Missing ${missingCount} segments. Refetching all...`,
|
|
386
355
|
);
|
|
387
356
|
|
|
388
357
|
// Refetch with empty segments = server sends everything
|
|
389
|
-
return fetchPartialUpdate(url, [], true, signal, tx,
|
|
358
|
+
return fetchPartialUpdate(url, [], true, signal, tx, mode);
|
|
390
359
|
}
|
|
391
360
|
|
|
392
361
|
if (signal?.aborted) {
|
|
393
362
|
debugLog("[Browser] Ignoring stale navigation (aborted before render)");
|
|
394
|
-
return
|
|
363
|
+
return;
|
|
395
364
|
}
|
|
396
365
|
|
|
397
366
|
// Rebuild tree on client (await for loader data resolution)
|
|
398
|
-
// Race against abort signal to allow cancellation during loader awaiting
|
|
399
|
-
// Pass intercept segments separately for explicit handling
|
|
400
|
-
// For stale revalidation, use forceAwait to ensure no loading fallbacks
|
|
401
367
|
const renderOptions = {
|
|
402
|
-
isAction,
|
|
403
|
-
forceAwait:
|
|
368
|
+
isAction: mode.type === "action",
|
|
369
|
+
forceAwait: mode.type === "stale-revalidation",
|
|
404
370
|
interceptSegments:
|
|
405
371
|
reconciled.interceptSegments.length > 0
|
|
406
372
|
? reconciled.interceptSegments
|
|
@@ -423,36 +389,36 @@ export function createPartialUpdater(
|
|
|
423
389
|
// Final abort check before committing - another navigation may have started
|
|
424
390
|
if (signal?.aborted) {
|
|
425
391
|
debugLog("[Browser] Ignoring stale navigation (aborted before commit)");
|
|
426
|
-
return
|
|
392
|
+
return;
|
|
427
393
|
}
|
|
428
394
|
|
|
429
395
|
// Check if this is an intercept response (any slot is active)
|
|
430
|
-
// If so, disable scroll to keep the current scroll position
|
|
431
396
|
const isInterceptResponse = hasActiveInterceptSlots(
|
|
432
397
|
payload.metadata?.slots,
|
|
433
398
|
);
|
|
434
399
|
|
|
435
|
-
// Track intercept context
|
|
436
|
-
|
|
400
|
+
// Track intercept context (only on navigation, not actions or stale revalidation)
|
|
401
|
+
// Use the authoritative source from mode/history state when restoring an
|
|
402
|
+
// intercept via popstate cache miss; fall back to the current URL for fresh
|
|
403
|
+
// intercept navigations.
|
|
404
|
+
const effectiveInterceptSource =
|
|
405
|
+
interceptSourceUrl || segmentState.currentUrl;
|
|
406
|
+
if (mode.type !== "action" && mode.type !== "stale-revalidation") {
|
|
437
407
|
if (isInterceptResponse) {
|
|
438
|
-
|
|
439
|
-
store.setInterceptSourceUrl(segmentState.currentUrl);
|
|
408
|
+
store.setInterceptSourceUrl(effectiveInterceptSource);
|
|
440
409
|
} else {
|
|
441
|
-
// Clear intercept context when navigating to a non-intercept route
|
|
442
410
|
store.setInterceptSourceUrl(null);
|
|
443
411
|
}
|
|
444
412
|
}
|
|
445
413
|
|
|
446
414
|
// Commit navigation - transaction handles all store mutations atomically
|
|
447
|
-
// For intercept responses: disable scroll, mark as intercept, include source URL
|
|
448
|
-
// Use reconciled.segments (which includes inserted diff segments) instead of matchedIds
|
|
449
415
|
const allSegmentIds = reconciled.segments.map((s) => s.id);
|
|
450
416
|
const serverLocationState = payload.metadata?.locationState;
|
|
451
417
|
const overrides: CommitOverrides | undefined = isInterceptResponse
|
|
452
418
|
? {
|
|
453
419
|
scroll: false,
|
|
454
420
|
intercept: true,
|
|
455
|
-
interceptSourceUrl:
|
|
421
|
+
interceptSourceUrl: effectiveInterceptSource,
|
|
456
422
|
...(serverLocationState && { serverState: serverLocationState }),
|
|
457
423
|
}
|
|
458
424
|
: serverLocationState
|
|
@@ -461,26 +427,22 @@ export function createPartialUpdater(
|
|
|
461
427
|
tx.commit(allSegmentIds, reconciled.segments, overrides);
|
|
462
428
|
|
|
463
429
|
// For stale revalidation: verify history key hasn't changed before updating UI
|
|
464
|
-
|
|
465
|
-
if (staleRevalidation) {
|
|
430
|
+
if (mode.type === "stale-revalidation") {
|
|
466
431
|
const historyKeyNow = store.getHistoryKey();
|
|
467
432
|
if (historyKeyNow !== historyKeyAtStart) {
|
|
468
433
|
debugLog(
|
|
469
434
|
`[Browser] Stale revalidation: history key changed (${historyKeyAtStart} -> ${historyKeyNow}), skipping UI update`,
|
|
470
435
|
);
|
|
471
|
-
return
|
|
436
|
+
return;
|
|
472
437
|
}
|
|
473
438
|
}
|
|
474
439
|
|
|
475
440
|
debugLog("[partial-update] updating document");
|
|
476
441
|
|
|
477
442
|
// Emit update to trigger React render
|
|
478
|
-
// For stale revalidation: wait for stream to complete (loaders resolved), then update
|
|
479
|
-
// For actions: wrap in startTransition to avoid UI flickering
|
|
480
|
-
// For transitions: wrap in startTransition + addTransitionType for ViewTransition
|
|
481
443
|
const hasTransition = reconciled.mainSegments.some((s) => s.transition);
|
|
482
444
|
|
|
483
|
-
if (
|
|
445
|
+
if (mode.type === "action" || mode.type === "stale-revalidation") {
|
|
484
446
|
startTransition(() => {
|
|
485
447
|
if (hasTransition && addTransitionType) {
|
|
486
448
|
addTransitionType("action");
|
|
@@ -508,33 +470,27 @@ export function createPartialUpdater(
|
|
|
508
470
|
}
|
|
509
471
|
|
|
510
472
|
debugLog("[Browser] Navigation complete");
|
|
511
|
-
return
|
|
473
|
+
return;
|
|
512
474
|
} else {
|
|
513
475
|
// Full update (fallback)
|
|
514
|
-
// Reconstruct the tree client-side from segments via renderSegments
|
|
515
|
-
// to ensure consistent component references with action revalidation.
|
|
516
476
|
console.warn(`[Browser] Full update (fallback)`);
|
|
517
477
|
|
|
518
478
|
const segments = payload.metadata?.segments || [];
|
|
519
479
|
|
|
520
|
-
// Check if this navigation is stale (a newer one started)
|
|
521
480
|
if (signal?.aborted) {
|
|
522
481
|
debugLog("[Browser] Ignoring stale navigation (aborted)");
|
|
523
|
-
return
|
|
482
|
+
return;
|
|
524
483
|
}
|
|
525
484
|
|
|
526
485
|
const segmentIds = segments.map((s: ResolvedSegment) => s.id);
|
|
527
486
|
|
|
528
|
-
// Render on client for consistent component references
|
|
529
487
|
const newTree = await renderSegments(segments);
|
|
530
488
|
|
|
531
|
-
// Final abort check before committing - another navigation may have started
|
|
532
489
|
if (signal?.aborted) {
|
|
533
490
|
debugLog("[Browser] Ignoring stale navigation (aborted before commit)");
|
|
534
|
-
return
|
|
491
|
+
return;
|
|
535
492
|
}
|
|
536
493
|
|
|
537
|
-
// Commit navigation - transaction handles all store mutations atomically
|
|
538
494
|
const fullUpdateServerState = payload.metadata?.locationState;
|
|
539
495
|
if (fullUpdateServerState) {
|
|
540
496
|
tx.commit(segmentIds, segments, { serverState: fullUpdateServerState });
|
|
@@ -542,15 +498,11 @@ export function createPartialUpdater(
|
|
|
542
498
|
tx.commit(segmentIds, segments);
|
|
543
499
|
}
|
|
544
500
|
|
|
545
|
-
// Emit update to trigger React render
|
|
546
|
-
// For stale revalidation: wait for stream to complete, then update
|
|
547
|
-
// For actions: wrap in startTransition to avoid UI flickering
|
|
548
|
-
// For transitions: wrap in startTransition + addTransitionType
|
|
549
501
|
const fullHasTransition = segments.some(
|
|
550
502
|
(s: ResolvedSegment) => s.transition,
|
|
551
503
|
);
|
|
552
504
|
|
|
553
|
-
if (
|
|
505
|
+
if (mode.type === "stale-revalidation") {
|
|
554
506
|
await rawStreamComplete;
|
|
555
507
|
startTransition(() => {
|
|
556
508
|
if (fullHasTransition && addTransitionType) {
|
|
@@ -561,7 +513,7 @@ export function createPartialUpdater(
|
|
|
561
513
|
metadata: payload.metadata!,
|
|
562
514
|
});
|
|
563
515
|
});
|
|
564
|
-
} else if (
|
|
516
|
+
} else if (mode.type === "action") {
|
|
565
517
|
startTransition(async () => {
|
|
566
518
|
if (fullHasTransition && addTransitionType) {
|
|
567
519
|
addTransitionType("action");
|
|
@@ -588,7 +540,7 @@ export function createPartialUpdater(
|
|
|
588
540
|
});
|
|
589
541
|
}
|
|
590
542
|
|
|
591
|
-
return
|
|
543
|
+
return;
|
|
592
544
|
}
|
|
593
545
|
}
|
|
594
546
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prefetch Tracking
|
|
3
|
+
*
|
|
4
|
+
* Tracks in-flight and completed prefetches for deduplication.
|
|
5
|
+
* The actual response caching is handled by the browser's HTTP cache
|
|
6
|
+
* via Vary: X-Rango-State.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { cancelAllPrefetches } from "./queue.js";
|
|
10
|
+
import { invalidateRangoState } from "../rango-state.js";
|
|
11
|
+
|
|
12
|
+
const inflight = new Set<string>();
|
|
13
|
+
const prefetched = new Set<string>();
|
|
14
|
+
|
|
15
|
+
// Generation counter incremented on each clearPrefetchCache(). Fetches that
|
|
16
|
+
// started before a clear carry a stale generation and must not re-add their
|
|
17
|
+
// key to the prefetched set (the browser HTTP cache entry is already invalid
|
|
18
|
+
// due to Rango-State rotation).
|
|
19
|
+
let generation = 0;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if a prefetch is already in-flight or completed for the given key.
|
|
23
|
+
*/
|
|
24
|
+
export function hasPrefetch(key: string): boolean {
|
|
25
|
+
return prefetched.has(key) || inflight.has(key);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Capture the current generation. The returned value is passed to
|
|
30
|
+
* markPrefetched so it can detect stale completions.
|
|
31
|
+
*/
|
|
32
|
+
export function currentGeneration(): number {
|
|
33
|
+
return generation;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Mark a key as successfully prefetched (response is in browser HTTP cache).
|
|
38
|
+
* Skips if the generation has changed since the fetch started (cache was
|
|
39
|
+
* invalidated mid-flight, so the response uses a stale X-Rango-State).
|
|
40
|
+
*/
|
|
41
|
+
export function markPrefetched(key: string, fetchGeneration: number): void {
|
|
42
|
+
if (fetchGeneration === generation) {
|
|
43
|
+
prefetched.add(key);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function markPrefetchInflight(key: string): void {
|
|
48
|
+
inflight.add(key);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function clearPrefetchInflight(key: string): void {
|
|
52
|
+
inflight.delete(key);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Invalidate prefetch state. Called when server actions mutate data.
|
|
57
|
+
* Updates the localStorage state key so next fetch has a different
|
|
58
|
+
* X-Rango-State value, causing Vary mismatch in browser HTTP cache.
|
|
59
|
+
* Also cancels any in-flight or queued speculative prefetches.
|
|
60
|
+
*/
|
|
61
|
+
export function clearPrefetchCache(): void {
|
|
62
|
+
generation++;
|
|
63
|
+
inflight.clear();
|
|
64
|
+
prefetched.clear();
|
|
65
|
+
cancelAllPrefetches();
|
|
66
|
+
invalidateRangoState();
|
|
67
|
+
}
|