@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2
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/AGENTS.md +6 -10
- package/README.md +289 -938
- package/dist/bin/rango.js +271 -46
- package/dist/vite/index.js +673 -193
- package/package.json +10 -8
- package/skills/api-client/SKILL.md +1 -1
- package/skills/breadcrumbs/SKILL.md +31 -14
- package/skills/cache-guide/SKILL.md +5 -2
- package/skills/caching/SKILL.md +59 -4
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +83 -2
- package/skills/debug-manifest/SKILL.md +1 -1
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +9 -1
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +8 -8
- package/skills/hooks/SKILL.md +54 -892
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +4 -4
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +46 -14
- package/skills/layout/SKILL.md +27 -10
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +23 -1
- package/skills/middleware/SKILL.md +7 -3
- package/skills/migrate-nextjs/SKILL.md +167 -6
- package/skills/migrate-react-router/SKILL.md +59 -677
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/observability/SKILL.md +9 -1
- package/skills/parallel/SKILL.md +23 -4
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +28 -18
- package/skills/rango/SKILL.md +84 -25
- package/skills/response-routes/SKILL.md +15 -1
- package/skills/route/SKILL.md +71 -4
- package/skills/router-setup/SKILL.md +14 -3
- package/skills/scripts/SKILL.md +1 -1
- package/skills/server-actions/SKILL.md +3 -2
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +1 -1
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/handles.md +4 -2
- package/skills/testing/render-handler.md +15 -14
- package/skills/testing/reverse-and-types.md +8 -7
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -919
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +30 -3
- package/skills/vercel/SKILL.md +1 -1
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/browser/event-controller.ts +62 -10
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +65 -16
- package/src/browser/navigation-client.ts +32 -2
- package/src/browser/navigation-store.ts +128 -14
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +76 -17
- package/src/browser/prefetch/cache.ts +51 -11
- package/src/browser/prefetch/fetch.ts +59 -21
- package/src/browser/prefetch/queue.ts +19 -4
- package/src/browser/react/Link.tsx +13 -3
- package/src/browser/react/NavigationProvider.tsx +108 -4
- package/src/browser/response-adapter.ts +38 -9
- package/src/browser/rsc-router.tsx +54 -4
- package/src/browser/scroll-restoration.ts +7 -5
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +22 -10
- package/src/browser/types.ts +54 -1
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/index.ts +3 -1
- package/src/build/route-trie.ts +35 -7
- package/src/build/route-types/include-resolution.ts +347 -47
- package/src/build/runtime-discovery.ts +4 -1
- package/src/cache/cache-key-utils.ts +29 -0
- package/src/cache/cache-runtime.ts +262 -71
- package/src/cache/cache-scope.ts +2 -17
- package/src/cache/cache-tag.ts +60 -14
- package/src/cache/cf/cf-cache-store.ts +243 -20
- package/src/cache/document-cache.ts +54 -21
- package/src/cache/index.ts +1 -0
- package/src/cache/memory-segment-store.ts +110 -3
- package/src/cache/profile-registry.ts +15 -0
- package/src/cache/read-through-swr.ts +15 -1
- package/src/cache/segment-codec.ts +4 -4
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/types.ts +158 -0
- package/src/cache/vercel/vercel-cache-store.ts +401 -124
- package/src/client.rsc.tsx +0 -3
- package/src/client.tsx +0 -3
- package/src/cloudflare/tracing.ts +7 -8
- package/src/defer.ts +11 -22
- package/src/handle.ts +37 -15
- package/src/handles/MetaTags.tsx +16 -82
- package/src/handles/breadcrumbs.ts +12 -14
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +7 -8
- package/src/handles/meta.ts +7 -44
- package/src/host/errors.ts +15 -0
- package/src/host/index.ts +1 -0
- package/src/index.rsc.ts +8 -2
- package/src/index.ts +19 -13
- package/src/internal-debug.ts +11 -8
- package/src/prerender.ts +17 -4
- package/src/redirect-origin.ts +14 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/route-content-wrapper.tsx +12 -5
- package/src/route-definition/dsl-helpers.ts +21 -32
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +43 -43
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/resolve-handler-use.ts +0 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -4
- package/src/route-types.ts +0 -5
- package/src/router/find-match.ts +86 -8
- package/src/router/instrument.ts +9 -4
- package/src/router/lazy-includes.ts +72 -12
- package/src/router/loader-resolution.ts +14 -2
- package/src/router/manifest.ts +56 -11
- package/src/router/match-api.ts +76 -32
- package/src/router/match-handlers.ts +181 -135
- package/src/router/match-middleware/background-revalidation.ts +40 -23
- package/src/router/match-middleware/cache-store.ts +39 -24
- package/src/router/match-result.ts +35 -15
- package/src/router/middleware.ts +64 -38
- package/src/router/navigation-snapshot.ts +7 -5
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +53 -64
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +11 -5
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +23 -8
- package/src/router/route-snapshot.ts +14 -2
- package/src/router/router-context.ts +3 -1
- package/src/router/router-interfaces.ts +32 -1
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +39 -3
- package/src/router/segment-resolution/loader-cache.ts +93 -2
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +3 -0
- package/src/router/segment-resolution/view-transition-default.ts +35 -15
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/telemetry-otel.ts +6 -8
- package/src/router/telemetry.ts +9 -1
- package/src/router/tracing.ts +14 -5
- package/src/router/trie-matching.ts +19 -11
- package/src/router/url-params.ts +13 -0
- package/src/router.ts +47 -16
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler.ts +60 -33
- package/src/rsc/manifest-init.ts +1 -1
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/progressive-enhancement.ts +61 -4
- package/src/rsc/redirect-guard.ts +2 -1
- package/src/rsc/rsc-rendering.ts +429 -37
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +30 -0
- package/src/segment-loader-promise.ts +18 -0
- package/src/segment-system.tsx +149 -14
- package/src/server/context.ts +67 -9
- package/src/server/cookie-store.ts +73 -1
- package/src/server/loader-registry.ts +13 -1
- package/src/server/request-context.ts +169 -10
- package/src/ssr/index.tsx +462 -178
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/testing/collect-handle.ts +14 -8
- package/src/testing/dispatch.ts +152 -40
- package/src/testing/generated-routes.ts +27 -11
- package/src/testing/index.ts +6 -0
- package/src/testing/render-handler.ts +14 -0
- package/src/testing/render-route.tsx +13 -10
- package/src/testing/run-transition-when.ts +164 -0
- package/src/theme/ThemeProvider.tsx +36 -26
- package/src/types/handler-context.ts +1 -1
- package/src/types/index.ts +2 -0
- package/src/types/route-config.ts +19 -7
- package/src/types/segments.ts +100 -0
- package/src/urls/include-helper.ts +10 -8
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +44 -12
- package/src/urls/path-helper.ts +5 -0
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +43 -18
- package/src/urls/urls-function.ts +0 -1
- package/src/vercel/tracing.ts +7 -7
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/index.ts +7 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugins/vercel-output.ts +114 -25
- package/src/vite/plugins/version-injector.ts +22 -7
- package/src/vite/plugins/virtual-entries.ts +80 -22
- package/src/vite/rango.ts +29 -19
- package/src/vite/router-discovery.ts +171 -43
- package/src/vite/utils/prerender-utils.ts +17 -4
- package/src/vite/utils/shared-utils.ts +47 -0
- package/src/network-error-thrower.tsx +0 -18
|
@@ -2,6 +2,16 @@ import { validateRedirectOrigin } from "./validate-redirect-origin.js";
|
|
|
2
2
|
|
|
3
3
|
type HeaderResult = { url: string } | "blocked" | null;
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Null-body statuses: the Fetch spec forbids pairing these with a body, so
|
|
7
|
+
* `new Response(body, { status })` throws ("Response with null body status
|
|
8
|
+
* cannot have body"). fetch() can still surface one WITH a body straight from
|
|
9
|
+
* the network layer (never the JS constructor): a 304 stale-while-revalidate
|
|
10
|
+
* prefetch revalidated to Not Modified (body from cache), or a 204 soft
|
|
11
|
+
* redirect. teeWithCompletion must not re-run those through `new Response`.
|
|
12
|
+
*/
|
|
13
|
+
const NULL_BODY_STATUS = new Set([101, 204, 205, 304]);
|
|
14
|
+
|
|
5
15
|
/**
|
|
6
16
|
* Extract and validate an RSC response header URL (X-RSC-Reload, X-RSC-Redirect).
|
|
7
17
|
* Returns { url } if valid, "blocked" if present but invalid origin, null if absent.
|
|
@@ -74,7 +84,13 @@ export function handleReloadHeader(
|
|
|
74
84
|
* Returns a new Response with one branch; the other is consumed to detect
|
|
75
85
|
* end-of-stream, calling onComplete when done.
|
|
76
86
|
*
|
|
77
|
-
*
|
|
87
|
+
* `onComplete` receives `endedCleanly`: true only on a normal EOF drain, false
|
|
88
|
+
* on a read error or an abort (the signal cancelled the reader). Callers that
|
|
89
|
+
* gate a "fully complete" fast path (e.g. prefetch's `entry.complete`) must
|
|
90
|
+
* treat a non-clean end as incomplete — a broken stream is not complete data.
|
|
91
|
+
*
|
|
92
|
+
* If the response has no body, onComplete fires synchronously with `true`
|
|
93
|
+
* (an empty body is a clean, complete stream).
|
|
78
94
|
* If signal is provided, an abort cancels the tracking reader.
|
|
79
95
|
*
|
|
80
96
|
* `silent` suppresses the stream-error log. Prefetch passes it: a speculative,
|
|
@@ -84,7 +100,7 @@ export function handleReloadHeader(
|
|
|
84
100
|
*/
|
|
85
101
|
export function teeWithCompletion(
|
|
86
102
|
response: Response,
|
|
87
|
-
onComplete: () => void,
|
|
103
|
+
onComplete: (endedCleanly: boolean) => void,
|
|
88
104
|
signal?: AbortSignal,
|
|
89
105
|
silent = false,
|
|
90
106
|
): Response {
|
|
@@ -92,15 +108,18 @@ export function teeWithCompletion(
|
|
|
92
108
|
// rejection's .catch, so onComplete must be settled exactly once across all
|
|
93
109
|
// paths (no-body early return, finally, catch).
|
|
94
110
|
let settled = false;
|
|
95
|
-
const settle = () => {
|
|
111
|
+
const settle = (endedCleanly: boolean) => {
|
|
96
112
|
if (!settled) {
|
|
97
113
|
settled = true;
|
|
98
|
-
onComplete();
|
|
114
|
+
onComplete(endedCleanly);
|
|
99
115
|
}
|
|
100
116
|
};
|
|
101
117
|
|
|
102
|
-
|
|
103
|
-
|
|
118
|
+
// Empty body, or a null-body status fetch() paired with a body: either way the
|
|
119
|
+
// body can't be re-attached via `new Response` below. Settle and pass the
|
|
120
|
+
// original response through; its body (when present) stays readable downstream.
|
|
121
|
+
if (!response.body || NULL_BODY_STATUS.has(response.status)) {
|
|
122
|
+
settle(true);
|
|
104
123
|
return response;
|
|
105
124
|
}
|
|
106
125
|
|
|
@@ -110,21 +129,31 @@ export function teeWithCompletion(
|
|
|
110
129
|
const reader = trackingStream.getReader();
|
|
111
130
|
const onAbort = signal ? reader.cancel.bind(reader) : undefined;
|
|
112
131
|
if (onAbort) signal!.addEventListener("abort", onAbort, { once: true });
|
|
132
|
+
// Only a loop that reaches `done` is a clean EOF. A read error rejects out
|
|
133
|
+
// of the try and still runs the finally, so the finally must NOT assume
|
|
134
|
+
// clean — it gates on this flag (false on error) AND on signal.aborted
|
|
135
|
+
// (an abort cancels the reader so read() resolves { done: true } and the
|
|
136
|
+
// loop breaks here normally, NOT in the .catch — re-check the signal to
|
|
137
|
+
// catch that case).
|
|
138
|
+
let cleanEof = false;
|
|
113
139
|
try {
|
|
114
140
|
while (true) {
|
|
115
141
|
const { done } = await reader.read();
|
|
116
|
-
if (done)
|
|
142
|
+
if (done) {
|
|
143
|
+
cleanEof = true;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
117
146
|
}
|
|
118
147
|
} finally {
|
|
119
148
|
if (onAbort) signal!.removeEventListener("abort", onAbort);
|
|
120
149
|
reader.releaseLock();
|
|
121
|
-
settle();
|
|
150
|
+
settle(cleanEof && !signal?.aborted);
|
|
122
151
|
}
|
|
123
152
|
})().catch((error) => {
|
|
124
153
|
if (!silent && !signal?.aborted) {
|
|
125
154
|
console.error("[Browser] Error reading tracking stream:", error);
|
|
126
155
|
}
|
|
127
|
-
settle();
|
|
156
|
+
settle(false);
|
|
128
157
|
});
|
|
129
158
|
|
|
130
159
|
return new Response(rscStream, {
|
|
@@ -25,6 +25,7 @@ import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
|
|
|
25
25
|
import { initRangoState } from "./rango-state.js";
|
|
26
26
|
import { registerNavigationStore } from "./navigation-store-handle.js";
|
|
27
27
|
import { initPrefetchCache } from "./prefetch/cache.js";
|
|
28
|
+
import { setPrefetchConcurrency } from "./prefetch/queue.js";
|
|
28
29
|
import { setPrefetchDecoder } from "./prefetch/fetch.js";
|
|
29
30
|
import { setAppVersion } from "./app-version.js";
|
|
30
31
|
import {
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
splitInterceptSegments,
|
|
33
34
|
} from "./intercept-utils.js";
|
|
34
35
|
import { createAppShellRef } from "./app-shell.js";
|
|
36
|
+
import { bootLog, IS_BROWSER_DEBUG } from "./logging.js";
|
|
35
37
|
|
|
36
38
|
// Vite HMR types are provided by vite/client
|
|
37
39
|
|
|
@@ -155,6 +157,8 @@ export async function initBrowserApp(
|
|
|
155
157
|
initialTheme,
|
|
156
158
|
} = options;
|
|
157
159
|
|
|
160
|
+
bootLog("initBrowserApp start");
|
|
161
|
+
bootLog("flight decode: awaiting initial payload from document stream");
|
|
158
162
|
const initialPayload =
|
|
159
163
|
await deps.createFromReadableStream<RscPayload>(rscStream);
|
|
160
164
|
|
|
@@ -168,6 +172,14 @@ export async function initBrowserApp(
|
|
|
168
172
|
// Get initial segments and compute history key from current URL
|
|
169
173
|
const initialSegments = (initialPayload.metadata?.segments ??
|
|
170
174
|
[]) as ResolvedSegment[];
|
|
175
|
+
if (IS_BROWSER_DEBUG) {
|
|
176
|
+
bootLog("initial payload decoded", {
|
|
177
|
+
version: initialPayload.metadata?.version,
|
|
178
|
+
routerId: initialPayload.metadata?.routerId,
|
|
179
|
+
segments: initialSegments.map((s) => s.id),
|
|
180
|
+
matched: initialPayload.metadata?.matched,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
171
183
|
const initialHistoryKey = generateHistoryKey(window.location.href);
|
|
172
184
|
|
|
173
185
|
// Create navigation store with history-based caching
|
|
@@ -206,11 +218,24 @@ export async function initBrowserApp(
|
|
|
206
218
|
// This ensures useHandle returns correct data during hydration to avoid mismatch
|
|
207
219
|
// The handles property is an async generator that yields on each push
|
|
208
220
|
if (initialPayload.metadata?.handles) {
|
|
221
|
+
// This for-await consumes the handle generator to completion BEFORE
|
|
222
|
+
// hydrateRoot is called — on a streaming/PPR document the generator only
|
|
223
|
+
// ends when its stream side does, so the per-push logs below are the
|
|
224
|
+
// primary probe for "the document render is holding hydration".
|
|
225
|
+
bootLog("handles: consuming payload handle stream (pre-hydration await)");
|
|
209
226
|
const handlesGenerator = initialPayload.metadata.handles;
|
|
210
227
|
let lastHandleData: Record<string, Record<string, unknown[]>> = {};
|
|
228
|
+
let handlePushes = 0;
|
|
211
229
|
for await (const handleData of handlesGenerator) {
|
|
212
230
|
lastHandleData = handleData;
|
|
231
|
+
if (IS_BROWSER_DEBUG) {
|
|
232
|
+
handlePushes += 1;
|
|
233
|
+
bootLog(`handles: push #${handlePushes}`, {
|
|
234
|
+
segments: Object.keys(handleData),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
213
237
|
}
|
|
238
|
+
bootLog("handles: stream complete", { pushes: handlePushes });
|
|
214
239
|
// Initialize event controller with initial handle state before hydration.
|
|
215
240
|
eventController.setHandleData(
|
|
216
241
|
lastHandleData,
|
|
@@ -220,6 +245,8 @@ export async function initBrowserApp(
|
|
|
220
245
|
// Update the initial cache entry with the processed handleData
|
|
221
246
|
// The cache entry was created by createNavigationStore but without handleData
|
|
222
247
|
store.updateCacheHandleData(initialHistoryKey, lastHandleData);
|
|
248
|
+
} else {
|
|
249
|
+
bootLog("handles: none in payload");
|
|
223
250
|
}
|
|
224
251
|
|
|
225
252
|
// Create composable utilities
|
|
@@ -245,11 +272,17 @@ export async function initBrowserApp(
|
|
|
245
272
|
initRangoState(version ?? "0", initialPayload.metadata?.stateCookieName);
|
|
246
273
|
setAppVersion(version);
|
|
247
274
|
|
|
248
|
-
// Initialize the in-memory prefetch cache TTL
|
|
249
|
-
// A
|
|
275
|
+
// Initialize the in-memory prefetch cache (TTL + max size) and the prefetch
|
|
276
|
+
// queue concurrency from server config. A TTL of 0 disables the cache;
|
|
277
|
+
// undefined values fall back to the module defaults.
|
|
250
278
|
const prefetchCacheTTL = initialPayload.metadata?.prefetchCacheTTL;
|
|
251
|
-
|
|
252
|
-
|
|
279
|
+
const prefetchCacheSize = initialPayload.metadata?.prefetchCacheSize;
|
|
280
|
+
if (prefetchCacheTTL !== undefined || prefetchCacheSize !== undefined) {
|
|
281
|
+
initPrefetchCache(prefetchCacheTTL, prefetchCacheSize);
|
|
282
|
+
}
|
|
283
|
+
const prefetchConcurrency = initialPayload.metadata?.prefetchConcurrency;
|
|
284
|
+
if (prefetchConcurrency !== undefined) {
|
|
285
|
+
setPrefetchConcurrency(prefetchConcurrency);
|
|
253
286
|
}
|
|
254
287
|
|
|
255
288
|
// Wire the RSC decoder so prefetches decode eagerly and warm the route's
|
|
@@ -314,9 +347,17 @@ export async function initBrowserApp(
|
|
|
314
347
|
if (linkInterception) {
|
|
315
348
|
navigationBridge.registerLinkInterception();
|
|
316
349
|
}
|
|
350
|
+
bootLog("bridges registered (action + navigation)");
|
|
317
351
|
|
|
318
352
|
// Build initial tree with rootLayout
|
|
353
|
+
bootLog("building initial segment tree (renderSegments)");
|
|
319
354
|
const initialTree = renderSegments(initialPayload.metadata!.segments);
|
|
355
|
+
if (IS_BROWSER_DEBUG && initialTree instanceof Promise) {
|
|
356
|
+
initialTree.then(
|
|
357
|
+
() => bootLog("initial segment tree settled"),
|
|
358
|
+
(err: unknown) => bootLog("initial segment tree rejected", { err }),
|
|
359
|
+
);
|
|
360
|
+
}
|
|
320
361
|
|
|
321
362
|
// Setup HMR with debounce — burst saves (format-on-save, rapid edits)
|
|
322
363
|
// fire many rsc:update events in quick succession. Without debouncing,
|
|
@@ -484,9 +525,14 @@ export async function initBrowserApp(
|
|
|
484
525
|
};
|
|
485
526
|
browserAppContext = context;
|
|
486
527
|
|
|
528
|
+
bootLog("initBrowserApp complete -- handing off to hydrateRoot");
|
|
487
529
|
return context;
|
|
488
530
|
}
|
|
489
531
|
|
|
532
|
+
// Once-flag so the hydration-commit boot log fires a single time (StrictMode
|
|
533
|
+
// re-runs the root effect; the second flush is not a second hydration).
|
|
534
|
+
let hydrationCommitLogged = false;
|
|
535
|
+
|
|
490
536
|
/**
|
|
491
537
|
* Get the browser app context. Throws if initBrowserApp hasn't been called.
|
|
492
538
|
*/
|
|
@@ -554,6 +600,10 @@ export function Rango(_props: RangoProps): React.ReactElement {
|
|
|
554
600
|
// that does not depend on React internals like __reactFiber.
|
|
555
601
|
React.useEffect(() => {
|
|
556
602
|
document.documentElement.dataset.hydrated = "";
|
|
603
|
+
if (IS_BROWSER_DEBUG && !hydrationCommitLogged) {
|
|
604
|
+
hydrationCommitLogged = true;
|
|
605
|
+
bootLog("hydration commit (root effect flushed)");
|
|
606
|
+
}
|
|
557
607
|
}, []);
|
|
558
608
|
|
|
559
609
|
return (
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - Supports hash link scrolling
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { debugLog } from "./logging.js";
|
|
11
|
+
import { debugLog, IS_BROWSER_DEBUG } from "./logging.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Defers a callback to the next animation frame.
|
|
@@ -150,10 +150,12 @@ export function initScrollRestoration(options?: {
|
|
|
150
150
|
|
|
151
151
|
window.addEventListener("pagehide", handlePageHide);
|
|
152
152
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
if (IS_BROWSER_DEBUG) {
|
|
154
|
+
debugLog(
|
|
155
|
+
"[Scroll] Initialized, loaded positions:",
|
|
156
|
+
Object.keys(savedScrollPositions).length,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
157
159
|
|
|
158
160
|
return () => {
|
|
159
161
|
cancelScrollRestorationPolling();
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from "./merge-segment-loaders.js";
|
|
7
7
|
import { assertSegmentStructure } from "./segment-structure-assert.js";
|
|
8
8
|
import { splitInterceptSegments } from "./intercept-utils.js";
|
|
9
|
-
import { debugLog } from "./logging.js";
|
|
9
|
+
import { debugLog, IS_BROWSER_DEBUG } from "./logging.js";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Determines the merging behavior for segment reconciliation.
|
|
@@ -87,15 +87,17 @@ export function reconcileSegments(input: ReconcileInput): ReconcileResult {
|
|
|
87
87
|
input.cachedSegments.forEach((s) => cachedSegments.set(s.id, s));
|
|
88
88
|
|
|
89
89
|
const diffSet = new Set(diff);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
90
|
+
if (IS_BROWSER_DEBUG) {
|
|
91
|
+
debugLog(
|
|
92
|
+
`[reconcile] actor=${actor}, matched=${matched.length}, diff=${diff.length}`,
|
|
93
|
+
);
|
|
94
|
+
debugLog(
|
|
95
|
+
`[reconcile] server segments: ${[...serverSegments.keys()].join(", ")}`,
|
|
96
|
+
);
|
|
97
|
+
debugLog(
|
|
98
|
+
`[reconcile] cached segments: ${[...cachedSegments.keys()].join(", ")}`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
99
101
|
|
|
100
102
|
const segments = matched
|
|
101
103
|
.map((segId: string) => {
|
|
@@ -106,9 +108,11 @@ export function reconcileSegments(input: ReconcileInput): ReconcileResult {
|
|
|
106
108
|
const inDiff = diffSet.has(segId);
|
|
107
109
|
// Merge partial loader data when server returns fewer loaders than cached
|
|
108
110
|
if (shouldMergeLoaders && needsLoaderMerge(fromServer, fromCache)) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
if (IS_BROWSER_DEBUG) {
|
|
112
|
+
debugLog(
|
|
113
|
+
`[reconcile] ${segId}: MERGE loaders (server partial, ${inDiff ? "in diff" : "not in diff"})`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
112
116
|
return mergeSegmentLoaders(fromServer, fromCache);
|
|
113
117
|
}
|
|
114
118
|
|
|
@@ -159,14 +163,18 @@ export function reconcileSegments(input: ReconcileInput): ReconcileResult {
|
|
|
159
163
|
// above fails to preserve a value it should have.
|
|
160
164
|
assertSegmentStructure(fromCache, merged, context);
|
|
161
165
|
|
|
166
|
+
if (IS_BROWSER_DEBUG) {
|
|
167
|
+
debugLog(
|
|
168
|
+
`[reconcile] ${segId}: SERVER+CACHE merge (${inDiff ? "in diff" : "not in diff"}, type=${fromServer.type}, component=${fromServer.component === null ? "null→cached" : "server"})`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return merged;
|
|
172
|
+
}
|
|
173
|
+
if (IS_BROWSER_DEBUG) {
|
|
162
174
|
debugLog(
|
|
163
|
-
`[reconcile] ${segId}: SERVER
|
|
175
|
+
`[reconcile] ${segId}: SERVER only (${inDiff ? "in diff" : "not in diff"}, type=${fromServer.type}, no cache entry)`,
|
|
164
176
|
);
|
|
165
|
-
return merged;
|
|
166
177
|
}
|
|
167
|
-
debugLog(
|
|
168
|
-
`[reconcile] ${segId}: SERVER only (${inDiff ? "in diff" : "not in diff"}, type=${fromServer.type}, no cache entry)`,
|
|
169
|
-
);
|
|
170
178
|
return fromServer;
|
|
171
179
|
}
|
|
172
180
|
|
|
@@ -180,9 +188,11 @@ export function reconcileSegments(input: ReconcileInput): ReconcileResult {
|
|
|
180
188
|
return fromCache;
|
|
181
189
|
}
|
|
182
190
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
191
|
+
if (IS_BROWSER_DEBUG) {
|
|
192
|
+
debugLog(
|
|
193
|
+
`[reconcile] ${segId}: CACHE only (not from server, type=${fromCache.type}, component=${fromCache.component != null ? "yes" : "null"})`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
186
196
|
|
|
187
197
|
// Return the cached segment as-is, regardless of actor. We used to clear
|
|
188
198
|
// truthy `loading` here to prevent a stale Suspense fallback from
|
|
@@ -558,18 +558,15 @@ export function createServerActionBridge(
|
|
|
558
558
|
return undefined;
|
|
559
559
|
}
|
|
560
560
|
|
|
561
|
-
// Update UI with error boundary
|
|
562
|
-
startTransition(() => {
|
|
563
|
-
onUpdate({ root: errorTree, metadata: metadata! });
|
|
564
|
-
});
|
|
565
|
-
|
|
566
561
|
// Update segment tracking to exclude error segment IDs
|
|
567
562
|
const errorSegmentIds = new Set(diff);
|
|
568
563
|
const segmentIdsAfterError = segmentState.currentSegmentIds.filter(
|
|
569
564
|
(id) => !errorSegmentIds.has(id),
|
|
570
565
|
);
|
|
571
566
|
|
|
572
|
-
//
|
|
567
|
+
// Cache (and bump the nav instance) BEFORE the UI update so a deferred
|
|
568
|
+
// handle pushed by the error-boundary render still applies — see the
|
|
569
|
+
// "normal" case below for why caching after onUpdate dropped it.
|
|
573
570
|
store.setSegmentIds(segmentIdsAfterError);
|
|
574
571
|
const currentHandleData = eventController.getHandleState().data;
|
|
575
572
|
store.cacheSegmentsForHistory(
|
|
@@ -578,6 +575,11 @@ export function createServerActionBridge(
|
|
|
578
575
|
currentHandleData,
|
|
579
576
|
);
|
|
580
577
|
|
|
578
|
+
// Update UI with error boundary
|
|
579
|
+
startTransition(() => {
|
|
580
|
+
onUpdate({ root: errorTree, metadata: metadata! });
|
|
581
|
+
});
|
|
582
|
+
|
|
581
583
|
// Throw the error so the action promise rejects
|
|
582
584
|
if (returnValue && !returnValue.ok) {
|
|
583
585
|
throw returnValue.data;
|
|
@@ -779,10 +781,16 @@ export function createServerActionBridge(
|
|
|
779
781
|
break;
|
|
780
782
|
}
|
|
781
783
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
784
|
+
// Cache (and bump the nav instance) BEFORE the UI update, matching the
|
|
785
|
+
// navigation commit order (navigation-transaction.ts:157). processHandles
|
|
786
|
+
// is spawned by onUpdate and captures the current nav instance up front;
|
|
787
|
+
// a deferred handle value resolves asynchronously and is only applied
|
|
788
|
+
// while stillLive() (its captured instance still owns the page). Caching
|
|
789
|
+
// AFTER onUpdate bumped the instance out from under that in-flight
|
|
790
|
+
// resolve, so stillLive() turned false and the resolved handle snapshot
|
|
791
|
+
// was dropped on the action-revalidation path (sync siblings sharing the
|
|
792
|
+
// yield are held atomically, so they were dropped too).
|
|
793
|
+
//
|
|
786
794
|
// Location state already applied above (pre-switch). Update store.
|
|
787
795
|
store.setSegmentIds(matched);
|
|
788
796
|
const currentHandleData = eventController.getHandleState().data;
|
|
@@ -791,6 +799,10 @@ export function createServerActionBridge(
|
|
|
791
799
|
fullSegments,
|
|
792
800
|
currentHandleData,
|
|
793
801
|
);
|
|
802
|
+
|
|
803
|
+
startTransition(() => {
|
|
804
|
+
onUpdate({ root: newTree, metadata: metadata! });
|
|
805
|
+
});
|
|
794
806
|
// Invalidation deferred to finalizeAction() (runs after this caches
|
|
795
807
|
// the fresh segments), suppressed when the action called
|
|
796
808
|
// keepClientCache().
|
package/src/browser/types.ts
CHANGED
|
@@ -70,6 +70,16 @@ export interface RscMetadata {
|
|
|
70
70
|
* Sent on initial render so the browser can configure its cache duration.
|
|
71
71
|
*/
|
|
72
72
|
prefetchCacheTTL?: number;
|
|
73
|
+
/**
|
|
74
|
+
* Max entries in the client-side in-memory prefetch cache (FIFO eviction).
|
|
75
|
+
* Sent on initial render so the browser can configure its cache capacity.
|
|
76
|
+
*/
|
|
77
|
+
prefetchCacheSize?: number;
|
|
78
|
+
/**
|
|
79
|
+
* Max concurrent speculative prefetch requests on the client.
|
|
80
|
+
* Sent on initial render so the browser can configure its prefetch queue.
|
|
81
|
+
*/
|
|
82
|
+
prefetchConcurrency?: number;
|
|
73
83
|
/**
|
|
74
84
|
* Server-resolved rango state cookie name (`{prefix}_{routerId}`). The client
|
|
75
85
|
* reads it verbatim and binds the rango state cookie to it; composition
|
|
@@ -447,6 +457,10 @@ export interface NavigationStore {
|
|
|
447
457
|
// History-based segment cache (for back/forward navigation and partial merging)
|
|
448
458
|
getHistoryKey(): string;
|
|
449
459
|
setHistoryKey(key: string): void;
|
|
460
|
+
/** Monotonic token of the most recently committed navigation. */
|
|
461
|
+
getNavInstance(): number;
|
|
462
|
+
/** Nav-instance token recorded on a cache entry (undefined if absent). */
|
|
463
|
+
getCacheEntryInstance(historyKey: string): number | undefined;
|
|
450
464
|
cacheSegmentsForHistory(
|
|
451
465
|
historyKey: string,
|
|
452
466
|
segments: ResolvedSegment[],
|
|
@@ -458,10 +472,40 @@ export interface NavigationStore {
|
|
|
458
472
|
stale: boolean;
|
|
459
473
|
handleData?: HandleData;
|
|
460
474
|
routerId?: string;
|
|
475
|
+
/**
|
|
476
|
+
* True when the entry's handle data is incomplete (a deferred Meta was
|
|
477
|
+
* still pending at navigate-away). A popstate return must revalidate with
|
|
478
|
+
* a FULL re-render so the server re-streams handles.
|
|
479
|
+
*/
|
|
480
|
+
handlesPending?: boolean;
|
|
461
481
|
}
|
|
462
482
|
| undefined;
|
|
463
483
|
hasHistoryCache(historyKey: string): boolean;
|
|
464
|
-
|
|
484
|
+
/**
|
|
485
|
+
* Update only the handleData (and optionally the stale / handlesPending flags)
|
|
486
|
+
* of an existing cache entry. When a flag is omitted the entry's current value
|
|
487
|
+
* is preserved. `stale=true` marks a single entry stale so a popstate return
|
|
488
|
+
* revalidates it; `handlesPending=true` additionally forces that revalidation
|
|
489
|
+
* to be a full re-render (so a deferred Meta re-streams).
|
|
490
|
+
*/
|
|
491
|
+
updateCacheHandleData(
|
|
492
|
+
historyKey: string,
|
|
493
|
+
handleData: HandleData,
|
|
494
|
+
stale?: boolean,
|
|
495
|
+
handlesPending?: boolean,
|
|
496
|
+
): void;
|
|
497
|
+
/**
|
|
498
|
+
* Owner-guarded variant of updateCacheHandleData: writes only when the entry
|
|
499
|
+
* is still owned by `ownerInstance`. Folds the caller's separate ownership
|
|
500
|
+
* probe and write into one historyCache scan for the per-yield streaming path.
|
|
501
|
+
*/
|
|
502
|
+
updateCacheHandleDataIfOwned(
|
|
503
|
+
historyKey: string,
|
|
504
|
+
handleData: HandleData,
|
|
505
|
+
ownerInstance: number,
|
|
506
|
+
stale?: boolean,
|
|
507
|
+
handlesPending?: boolean,
|
|
508
|
+
): void;
|
|
465
509
|
markCacheAsStale(): void;
|
|
466
510
|
markHistoryCacheStale(): void;
|
|
467
511
|
markCacheAsStaleAndBroadcast(): void;
|
|
@@ -521,6 +565,15 @@ export interface FetchPartialResult {
|
|
|
521
565
|
payload: RscPayload;
|
|
522
566
|
/** Promise that resolves when the response stream is fully consumed */
|
|
523
567
|
streamComplete: Promise<void>;
|
|
568
|
+
/**
|
|
569
|
+
* True only when this payload came from a prefetch-cache hit whose stream had
|
|
570
|
+
* ALREADY fully drained at fetch time (the route was fully prefetched). The
|
|
571
|
+
* commit then runs in a startTransition so loading()/Suspense content — already
|
|
572
|
+
* resolved — swaps in directly without flashing a fallback. A partially-warmed
|
|
573
|
+
* (still-streaming) prefetch hit and a cold fetch leave this false so their
|
|
574
|
+
* fallbacks stream as usual.
|
|
575
|
+
*/
|
|
576
|
+
fullyPrefetched?: boolean;
|
|
524
577
|
}
|
|
525
578
|
|
|
526
579
|
/**
|