@rangojs/router 0.0.0-experimental.147 → 0.0.0-experimental.149
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/vite/index.js +19 -3
- package/package.json +1 -1
- package/skills/mime-routes/SKILL.md +25 -17
- package/skills/ppr/SKILL.md +20 -10
- package/src/cache/cf/cf-cache-store.ts +37 -2
- package/src/index.rsc.ts +6 -0
- package/src/prerender/build-shell-capture.ts +17 -1
- package/src/router/content-negotiation.ts +47 -5
- package/src/router/metrics.ts +17 -2
- package/src/router/router-interfaces.ts +7 -0
- package/src/router/router-options.ts +13 -0
- package/src/router.ts +5 -0
- package/src/rsc/handler.ts +4 -2
- package/src/rsc/rsc-rendering.ts +49 -0
- package/src/rsc/shell-build-manifest.ts +40 -10
- package/src/rsc/shell-capture-constants.ts +27 -0
- package/src/rsc/shell-capture.ts +388 -24
- package/src/rsc/shell-serve.ts +44 -0
- package/src/rsc/ssr-setup.ts +54 -22
- package/src/server/context.ts +1 -0
- package/src/ssr/index.tsx +42 -12
- package/src/urls/pattern-types.ts +29 -0
- package/src/vite/discovery/shell-prerender-phase.ts +2 -0
- package/src/vite/discovery/state.ts +3 -1
- package/src/vite/router-discovery.ts +20 -2
package/src/rsc/shell-serve.ts
CHANGED
|
@@ -30,11 +30,47 @@ export const SHELL_STATUS_HEADER = "x-rango-shell";
|
|
|
30
30
|
*/
|
|
31
31
|
export const DEFAULT_PPR_TTL_SECONDS = 300;
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Timeout for the dev /__rsc_shell endpoint's sequential /__rsc_prerender
|
|
35
|
+
* pre-flight probe (vite/router-discovery.ts). Hoisted here so the client-side
|
|
36
|
+
* fetch bound (shell-build-manifest.ts devShellFetchTimeoutMs) enumerates the
|
|
37
|
+
* SAME term of the endpoint's worst-case envelope — the two cannot drift.
|
|
38
|
+
*/
|
|
39
|
+
export const DEV_SHELL_PROBE_TIMEOUT_MS: number = 10_000;
|
|
40
|
+
|
|
33
41
|
/** The route's ppr option normalized to a concrete policy. */
|
|
34
42
|
export interface ResolvedPprConfig {
|
|
35
43
|
ttl: number;
|
|
36
44
|
swr?: number;
|
|
37
45
|
tags?: string[];
|
|
46
|
+
/**
|
|
47
|
+
* Snapshot size cap, passed through undefaulted (like swr/tags): the single
|
|
48
|
+
* defaulting site is captureAndStoreShell (DEFAULT_PPR_MAX_SNAPSHOT_BYTES in
|
|
49
|
+
* shell-capture.ts), so direct descriptor callers and resolved configs
|
|
50
|
+
* cannot drift.
|
|
51
|
+
*/
|
|
52
|
+
maxSnapshotBytes?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Capture settle budget in ms (`ppr.captureTimeout`). Undefined = the
|
|
55
|
+
* capture default (SHELL_CAPTURE_MAX_WAIT_MS, 15_000) — the default's single
|
|
56
|
+
* owner stays shell-capture.ts so build/runtime producers cannot drift.
|
|
57
|
+
*/
|
|
58
|
+
captureTimeout?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validate the raw `ppr.captureTimeout` option: a finite number >= 1ms passes
|
|
63
|
+
* through; anything else (including 0/negative/NaN/Infinity/non-number)
|
|
64
|
+
* resolves to undefined, which means "use the capture default" downstream.
|
|
65
|
+
* Mirrors the prefetch-limit option policy: invalid values silently fall back
|
|
66
|
+
* to the default rather than throwing at request time. Also the boundary
|
|
67
|
+
* re-normalizer for the dev /__rsc_shell endpoint (vite/router-discovery.ts),
|
|
68
|
+
* whose param crossed an HTTP query string.
|
|
69
|
+
*/
|
|
70
|
+
export function normalizeCaptureTimeout(value: unknown): number | undefined {
|
|
71
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 1
|
|
72
|
+
? value
|
|
73
|
+
: undefined;
|
|
38
74
|
}
|
|
39
75
|
|
|
40
76
|
/**
|
|
@@ -42,6 +78,12 @@ export interface ResolvedPprConfig {
|
|
|
42
78
|
* route does not declare `ppr` (or declares `ppr: false`) — the caller then does
|
|
43
79
|
* NOTHING: no store read, no capture, no logs. Pure axis 1, zero cost.
|
|
44
80
|
*
|
|
81
|
+
* The route's NAME is irrelevant here (and everywhere on the shell lane):
|
|
82
|
+
* nameless `path()` routes register their EntryData under a synthesized
|
|
83
|
+
* `$path_*` manifest key with the `ppr` option intact (urls/path-helper.ts),
|
|
84
|
+
* so a nameless entry resolves exactly like a named one — pinned by the
|
|
85
|
+
* nameless-ppr e2e in both apps (issue #714).
|
|
86
|
+
*
|
|
45
87
|
* PPR is a DOCUMENT-level property of the page route; there is no subtree
|
|
46
88
|
* inheritance (declaring it on a layout is not supported — a follow-up).
|
|
47
89
|
*/
|
|
@@ -56,6 +98,8 @@ export function resolvePprConfig(
|
|
|
56
98
|
ttl: ppr.ttl ?? DEFAULT_PPR_TTL_SECONDS,
|
|
57
99
|
swr: ppr.swr,
|
|
58
100
|
tags: ppr.tags,
|
|
101
|
+
maxSnapshotBytes: ppr.maxSnapshotBytes,
|
|
102
|
+
captureTimeout: normalizeCaptureTimeout(ppr.captureTimeout),
|
|
59
103
|
};
|
|
60
104
|
}
|
|
61
105
|
|
package/src/rsc/ssr-setup.ts
CHANGED
|
@@ -11,6 +11,11 @@ import type { SSRModule } from "./types.js";
|
|
|
11
11
|
import type { SSRStreamMode } from "../router/router-options.js";
|
|
12
12
|
import type { MetricsStore } from "../server/context.js";
|
|
13
13
|
import { appendMetric } from "../router/metrics.js";
|
|
14
|
+
import {
|
|
15
|
+
parseAcceptTypes,
|
|
16
|
+
prefersFlightRepresentation,
|
|
17
|
+
RSC_WIRE_MIME,
|
|
18
|
+
} from "../router/content-negotiation.js";
|
|
14
19
|
import { _getRequestContext } from "../server/request-context.js";
|
|
15
20
|
|
|
16
21
|
export type SSRSetup = readonly [SSRModule, SSRStreamMode];
|
|
@@ -90,12 +95,42 @@ export function getSSRSetup<TEnv>(
|
|
|
90
95
|
);
|
|
91
96
|
}
|
|
92
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Accept-based flight opt-in: the client explicitly listed the RSC wire
|
|
100
|
+
* format (text/x-component) in Accept, ranked above the HTML document, and
|
|
101
|
+
* did not override with __html.
|
|
102
|
+
*
|
|
103
|
+
* The flight stream is an internal transport representation — it is served
|
|
104
|
+
* ONLY on explicit opt-in (this Accept value, or the _rsc_ / __rsc transport
|
|
105
|
+
* params). Everything else (missing Accept, wildcards, application/json,
|
|
106
|
+
* browser Accept strings) gets the HTML document, per RFC 9110: a missing
|
|
107
|
+
* Accept is equivalent to a full wildcard, and a wildcard gets the server's
|
|
108
|
+
* canonical representation. The old rule ("no text/html substring → flight")
|
|
109
|
+
* handed the wire format to every generic client — curl, health checks,
|
|
110
|
+
* link unfurlers.
|
|
111
|
+
*
|
|
112
|
+
* The includes() guard is a parse-skipping fast path: the bulk of traffic
|
|
113
|
+
* (browsers, curl, monitors) never mentions the wire format and pays no
|
|
114
|
+
* parseAcceptTypes allocation. Ranking lives in prefersFlightRepresentation
|
|
115
|
+
* (router/content-negotiation.ts), co-located with the candidate MIME set.
|
|
116
|
+
*/
|
|
117
|
+
function acceptsFlightExplicitly(request: Request, url: URL): boolean {
|
|
118
|
+
if (url.searchParams.has("__html")) return false;
|
|
119
|
+
const accept = request.headers.get("accept");
|
|
120
|
+
if (accept === null || !accept.includes(RSC_WIRE_MIME)) return false;
|
|
121
|
+
return prefersFlightRepresentation(parseAcceptTypes(accept));
|
|
122
|
+
}
|
|
123
|
+
|
|
93
124
|
/**
|
|
94
125
|
* Classify whether a request may require SSR (HTML rendering).
|
|
95
126
|
*
|
|
96
|
-
* Returns false for requests that are definitively RSC-only
|
|
97
|
-
* prerender collection, or
|
|
98
|
-
*
|
|
127
|
+
* Returns false for requests that are definitively RSC-only: transport
|
|
128
|
+
* params (partial/action/loader/__rsc), prerender collection, or an explicit
|
|
129
|
+
* Accept: text/x-component. Must never return false for a request whose
|
|
130
|
+
* render-time decision (isRscRequest) will be HTML — the two share
|
|
131
|
+
* acceptsFlightExplicitly so the Accept rule cannot drift. document-cache.ts
|
|
132
|
+
* keys its HTML/RSC response slots off this function, so any divergence from
|
|
133
|
+
* the render decision poisons a cache slot with the wrong representation.
|
|
99
134
|
*
|
|
100
135
|
* Note: response/mime routes are excluded by the caller — this function
|
|
101
136
|
* runs after classifyRequest() determines the request mode.
|
|
@@ -112,24 +147,21 @@ export function mayNeedSSR(request: Request, url: URL): boolean {
|
|
|
112
147
|
return false;
|
|
113
148
|
}
|
|
114
149
|
|
|
115
|
-
|
|
116
|
-
// if Accept is present and does not include text/html (and no __html override),
|
|
117
|
-
// the response will be RSC, not HTML.
|
|
118
|
-
const accept = request.headers.get("accept");
|
|
119
|
-
if (
|
|
120
|
-
accept &&
|
|
121
|
-
!accept.includes("text/html") &&
|
|
122
|
-
!url.searchParams.has("__html")
|
|
123
|
-
) {
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return true;
|
|
150
|
+
return !acceptsFlightExplicitly(request, url);
|
|
128
151
|
}
|
|
129
152
|
|
|
130
|
-
// Final render-time decision: is the response an RSC stream (vs HTML)?
|
|
131
|
-
//
|
|
132
|
-
// Accept
|
|
153
|
+
// Final render-time decision: is the response an RSC stream (vs HTML)?
|
|
154
|
+
// Flight requires explicit opt-in: the partial transport param, __rsc, or
|
|
155
|
+
// Accept: text/x-component. mayNeedSSR is the coarse pre-filter over the
|
|
156
|
+
// transport params; both delegate the Accept call to acceptsFlightExplicitly.
|
|
157
|
+
//
|
|
158
|
+
// _rsc_partial is read from the URL in addition to the plan-derived isPartial
|
|
159
|
+
// flag: the 404 fallback plan hardcodes mode "full-render" even for partial
|
|
160
|
+
// navigations (handler.ts RouteNotFoundError catch), so a partial 404 reaches
|
|
161
|
+
// this decision with isPartial=false. The old Accept rule masked that by
|
|
162
|
+
// classifying */* as flight; without the URL check a client-side navigation
|
|
163
|
+
// to a missing route received an HTML 404 it cannot apply, and the
|
|
164
|
+
// navigation never committed (multi-router soft-404, popstate not-found).
|
|
133
165
|
export function isRscRequest(
|
|
134
166
|
request: Request,
|
|
135
167
|
url: URL,
|
|
@@ -137,8 +169,8 @@ export function isRscRequest(
|
|
|
137
169
|
): boolean {
|
|
138
170
|
return (
|
|
139
171
|
isPartial ||
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
url
|
|
172
|
+
url.searchParams.has("_rsc_partial") ||
|
|
173
|
+
url.searchParams.has("__rsc") ||
|
|
174
|
+
acceptsFlightExplicitly(request, url)
|
|
143
175
|
);
|
|
144
176
|
}
|
package/src/server/context.ts
CHANGED
|
@@ -27,6 +27,7 @@ export interface PerformanceMetric {
|
|
|
27
27
|
duration: number; // milliseconds
|
|
28
28
|
startTime: number; // relative to request start
|
|
29
29
|
depth?: number; // nesting level for hierarchical display (0 = top-level)
|
|
30
|
+
desc?: string; // free-form outcome detail, emitted as Server-Timing desc="..."
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
/**
|
package/src/ssr/index.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import React from "react";
|
|
|
2
2
|
import { createSsrRootComponent } from "./ssr-root.js";
|
|
3
3
|
import { injectRSCPayloadEager } from "./inject-rsc-eager.js";
|
|
4
4
|
import { runWithPreinitNonce } from "./preinit-client-references.js";
|
|
5
|
+
import { SHELL_CAPTURE_MAX_WAIT_MS } from "../rsc/shell-capture-constants.js";
|
|
5
6
|
import type { ErrorPhase } from "../types.js";
|
|
6
7
|
import type { HeadScriptsOption } from "../vite/plugin-types.js";
|
|
7
8
|
|
|
@@ -181,15 +182,6 @@ export interface SSRDependencies<TEnv = unknown> {
|
|
|
181
182
|
onError?: (error: Error, context: { phase: ErrorPhase }) => void;
|
|
182
183
|
}
|
|
183
184
|
|
|
184
|
-
/**
|
|
185
|
-
* Default guard for how long capture waits on the caller's `quiesce` signal
|
|
186
|
-
* before forcing the abort that freezes the shell. This is the ONLY wall-clock
|
|
187
|
-
* on the capture path and it is a pathological guard — it should never fire once
|
|
188
|
-
* the caller's `quiesce` is a task-quantized, frozen-byte signal (the capture
|
|
189
|
-
* gate in shell-capture.ts). See docs/design/ppr-shell-resume.md.
|
|
190
|
-
*/
|
|
191
|
-
const DEFAULT_SHELL_CAPTURE_MAX_WAIT_MS = 5000;
|
|
192
|
-
|
|
193
185
|
/**
|
|
194
186
|
* Fixed number of macrotask hops between `quiesce` resolving and the abort. These
|
|
195
187
|
* give React's fizz worker turns to flush the settled shell into the prelude and
|
|
@@ -258,6 +250,38 @@ function createCancelableTimeout(ms: number): {
|
|
|
258
250
|
return { promise, cancel: () => clearTimeout(id) };
|
|
259
251
|
}
|
|
260
252
|
|
|
253
|
+
/**
|
|
254
|
+
* True when a debugger is attached outside a production build: a breakpoint
|
|
255
|
+
* pauses script execution but not wall-clock, so the capture deadline would
|
|
256
|
+
* expire mid-inspection and push the key into refusal/backoff while debugging.
|
|
257
|
+
* Next.js precedent (packages/next/src/export/worker.ts): its static-page
|
|
258
|
+
* timeout race is disabled when NODE_OPTIONS contains --inspect. The gate is
|
|
259
|
+
* the bare `process.env.NODE_ENV !== "production"` token — the build define
|
|
260
|
+
* folds exactly that token to a literal (same dev signal as shell-capture.ts's
|
|
261
|
+
* isDevMode), and it covers dev servers that never set NODE_ENV, the common
|
|
262
|
+
* debug case. A production worker launched with --inspect in NODE_OPTIONS
|
|
263
|
+
* keeps the capture's safety bound unconditionally. Every probe is guarded:
|
|
264
|
+
* `process` reads are optional-chained and the node:inspector import is
|
|
265
|
+
* try/caught, so non-Node runtimes (workerd) resolve to false instead of
|
|
266
|
+
* breaking.
|
|
267
|
+
*/
|
|
268
|
+
export async function isDebuggerAttached(): Promise<boolean> {
|
|
269
|
+
if (process.env.NODE_ENV === "production") return false;
|
|
270
|
+
try {
|
|
271
|
+
// Substring match also catches --inspect-brk / --inspect=PORT.
|
|
272
|
+
if (globalThis.process?.env?.NODE_OPTIONS?.includes("--inspect")) {
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
if (globalThis.process?.execArgv?.some((a) => a.startsWith("--inspect"))) {
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
const inspector = await import("node:inspector");
|
|
279
|
+
return inspector.url() !== undefined;
|
|
280
|
+
} catch {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
261
285
|
/**
|
|
262
286
|
* Drain a ReadableStream fully into a single Uint8Array. Capture buffers the
|
|
263
287
|
* whole prelude so it can be stored and later prepended byte-for-byte.
|
|
@@ -321,7 +345,7 @@ function createDataVariantHtmlStream(): ReadableStream<Uint8Array> {
|
|
|
321
345
|
interface ShellCaptureOptions {
|
|
322
346
|
/** Caller-provided promise that resolves once the cached content settled. */
|
|
323
347
|
quiesce: Promise<void>;
|
|
324
|
-
/** Upper bound on how long to wait for `quiesce`. Default
|
|
348
|
+
/** Upper bound on how long to wait for `quiesce`. Default SHELL_CAPTURE_MAX_WAIT_MS. */
|
|
325
349
|
maxWaitMs?: number;
|
|
326
350
|
}
|
|
327
351
|
|
|
@@ -509,7 +533,7 @@ export function createShellCaptureHandler<TEnv = unknown>(
|
|
|
509
533
|
rscStream: ReadableStream<Uint8Array>,
|
|
510
534
|
opts: ShellCaptureOptions,
|
|
511
535
|
): Promise<ShellCaptureResult | null> {
|
|
512
|
-
const maxWaitMs = opts.maxWaitMs ??
|
|
536
|
+
const maxWaitMs = opts.maxWaitMs ?? SHELL_CAPTURE_MAX_WAIT_MS;
|
|
513
537
|
|
|
514
538
|
// Arm the maxWaitMs deadline BEFORE the first await so it bounds the ENTIRE
|
|
515
539
|
// capture, the bootstrap-script load included. loadBootstrapScriptContent()
|
|
@@ -517,7 +541,13 @@ export function createShellCaptureHandler<TEnv = unknown>(
|
|
|
517
541
|
// captureShellHTML with no upper bound and held the background capture task
|
|
518
542
|
// open. One deadline, shared by the bootstrap race below and the quiesce
|
|
519
543
|
// race, keeps the whole path "bounded by maxWaitMs like every quiesce input".
|
|
520
|
-
|
|
544
|
+
// Debugger attached (non-production, see isDebuggerAttached): a paused process
|
|
545
|
+
// must not burn the budget, so the deadline becomes a never-resolving
|
|
546
|
+
// promise (the Next.js approach) — no timer, so it cannot hold the event
|
|
547
|
+
// loop open either.
|
|
548
|
+
const deadline = (await isDebuggerAttached())
|
|
549
|
+
? { promise: new Promise<void>(() => {}), cancel: () => {} }
|
|
550
|
+
: createCancelableTimeout(maxWaitMs);
|
|
521
551
|
try {
|
|
522
552
|
// No nonce (nonce'd requests never reach capture); no formState.
|
|
523
553
|
// payloadSettled: fires when the Flight payload root settles — i.e.
|
|
@@ -62,6 +62,35 @@ export interface PartialPrerenderProps {
|
|
|
62
62
|
* capture render auto-collects (the shell's own non-loader request tags).
|
|
63
63
|
*/
|
|
64
64
|
tags?: string[];
|
|
65
|
+
/**
|
|
66
|
+
* Upper bound (serialized UTF-8 bytes) on the capture data snapshot riding
|
|
67
|
+
* inside the shell entry. The snapshot duplicates every cache-store value
|
|
68
|
+
* the capture pinned, so a page over a large cache() segment can push the
|
|
69
|
+
* entry toward store value limits (Cloudflare KV caps a value at 25 MiB).
|
|
70
|
+
* Over the cap the snapshot is skipped: the shell is still stored and
|
|
71
|
+
* served, but pinned reads fall back to the live store, so drifted cached
|
|
72
|
+
* content can hydration-mismatch and be repaired client-side (the
|
|
73
|
+
* pre-snapshot behavior). Reported once per key. Defaults to 8 MiB.
|
|
74
|
+
*/
|
|
75
|
+
maxSnapshotBytes?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Capture settle budget in MILLISECONDS (default 15000). Bounds the whole
|
|
78
|
+
* background capture: the wait for deferred shell material — top-level
|
|
79
|
+
* pushed handle promises (`ctx.use(Meta)(promise.then(...))` and friends)
|
|
80
|
+
* are AWAITED and their settled values baked into the stored shell — AND
|
|
81
|
+
* the fizz prerender deadline. Declare it to tighten the budget below the
|
|
82
|
+
* default or when a route's shell material takes longer than 15s to
|
|
83
|
+
* settle. A budget that expires with pushes still pending REFUSES the
|
|
84
|
+
* capture (the route stays MISS with the once-per-key warning) — a shell
|
|
85
|
+
* with missing head material is never stored. Capture is background work
|
|
86
|
+
* (waitUntil), so a longer budget costs latency-to-HIT only, never a served
|
|
87
|
+
* response; the platform waitUntil lifetime (workerd: ~30s past response
|
|
88
|
+
* completion) is the physical ceiling — the default's derivation lives in
|
|
89
|
+
* docs/design/ppr-shell-resume.md (Cost model). Build-time captures
|
|
90
|
+
* (Prerender+ppr, producer B) honor the same budget with no platform
|
|
91
|
+
* ceiling. Non-finite or sub-1ms values fall back to the default.
|
|
92
|
+
*/
|
|
93
|
+
captureTimeout?: number;
|
|
65
94
|
}
|
|
66
95
|
|
|
67
96
|
export interface PathOptions<
|
|
@@ -243,6 +243,8 @@ export async function runShellPrerenderPhase(
|
|
|
243
243
|
ttl: policy.ttl,
|
|
244
244
|
swr: policy.swr,
|
|
245
245
|
tags: policy.tags,
|
|
246
|
+
maxSnapshotBytes: policy.maxSnapshotBytes,
|
|
247
|
+
captureTimeout: policy.captureTimeout,
|
|
246
248
|
buildEnv: s.resolvedBuildEnv,
|
|
247
249
|
buildVersion,
|
|
248
250
|
captureShellHTML,
|
|
@@ -73,7 +73,9 @@ export interface ShellPrerenderCandidate {
|
|
|
73
73
|
urlPath: string;
|
|
74
74
|
routeName: string;
|
|
75
75
|
paramHash: string;
|
|
76
|
-
ppr:
|
|
76
|
+
ppr:
|
|
77
|
+
| true
|
|
78
|
+
| { ttl?: number; swr?: number; tags?: string[]; captureTimeout?: number };
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
export interface DiscoveryState {
|
|
@@ -19,6 +19,10 @@ import {
|
|
|
19
19
|
createScanFilter,
|
|
20
20
|
} from "../build/generate-route-types.js";
|
|
21
21
|
import { firstCodeMatchIndex } from "../build/route-types/source-scan.js";
|
|
22
|
+
import {
|
|
23
|
+
DEV_SHELL_PROBE_TIMEOUT_MS,
|
|
24
|
+
normalizeCaptureTimeout,
|
|
25
|
+
} from "../rsc/shell-serve.js";
|
|
22
26
|
import {
|
|
23
27
|
injectClientDebugFlag,
|
|
24
28
|
internalDebugNoCacheMiddleware,
|
|
@@ -1144,6 +1148,18 @@ export function createRouterDiscoveryPlugin(
|
|
|
1144
1148
|
const swr = swrRaw === null ? undefined : Number(swrRaw);
|
|
1145
1149
|
const tagsRaw = url.searchParams.get("tags");
|
|
1146
1150
|
const tags = tagsRaw ? tagsRaw.split(",") : undefined;
|
|
1151
|
+
const maxSnapshotBytesRaw = url.searchParams.get("maxSnapshotBytes");
|
|
1152
|
+
const maxSnapshotBytes =
|
|
1153
|
+
maxSnapshotBytesRaw === null
|
|
1154
|
+
? undefined
|
|
1155
|
+
: Number(maxSnapshotBytesRaw);
|
|
1156
|
+
// Boundary revalidation via the SHARED normalizer (shell-serve.ts):
|
|
1157
|
+
// the param crossed an HTTP query string, and a garbage value must
|
|
1158
|
+
// fall back to the capture default, never reach setTimeout as NaN
|
|
1159
|
+
// (which Node clamps to ~1ms — an instant abort).
|
|
1160
|
+
const captureTimeout = normalizeCaptureTimeout(
|
|
1161
|
+
Number(url.searchParams.get("captureTimeout")),
|
|
1162
|
+
);
|
|
1147
1163
|
|
|
1148
1164
|
// Resolve the capture realms: main-server envs (Node preset) or the
|
|
1149
1165
|
// shared temp Node server (Cloudflare preset — no main RSC runner).
|
|
@@ -1205,7 +1221,7 @@ export function createRouterDiscoveryPlugin(
|
|
|
1205
1221
|
// (this fetch blocks a foreground document request), and the memoized
|
|
1206
1222
|
// body needs neither the pre-flight round-trip nor a capture. Keyed
|
|
1207
1223
|
// per router instance (= HMR generation) like the prerender memo.
|
|
1208
|
-
const cacheKey = `shell|${pathname}|r=${routeName}|t=${ttl}|s=${swr ?? ""}|g=${(tags ?? []).join("+")}|v=${version}`;
|
|
1224
|
+
const cacheKey = `shell|${pathname}|r=${routeName}|t=${ttl}|s=${swr ?? ""}|g=${(tags ?? []).join("+")}|c=${captureTimeout ?? ""}|v=${version}`;
|
|
1209
1225
|
for (const [, routerInstance] of registry) {
|
|
1210
1226
|
if (typeof routerInstance.match !== "function") continue;
|
|
1211
1227
|
const cached = devPrerenderCache.get(routerInstance, cacheKey);
|
|
@@ -1224,7 +1240,7 @@ export function createRouterDiscoveryPlugin(
|
|
|
1224
1240
|
try {
|
|
1225
1241
|
const probe = await fetch(
|
|
1226
1242
|
`${s.devServerOrigin}/__rsc_prerender?pathname=${encodeURIComponent(pathname)}&routeName=${encodeURIComponent(routeName)}`,
|
|
1227
|
-
{ signal: AbortSignal.timeout(
|
|
1243
|
+
{ signal: AbortSignal.timeout(DEV_SHELL_PROBE_TIMEOUT_MS) },
|
|
1228
1244
|
);
|
|
1229
1245
|
if (!probe.ok) {
|
|
1230
1246
|
res.statusCode = 404;
|
|
@@ -1258,6 +1274,8 @@ export function createRouterDiscoveryPlugin(
|
|
|
1258
1274
|
ttl,
|
|
1259
1275
|
swr,
|
|
1260
1276
|
tags,
|
|
1277
|
+
maxSnapshotBytes,
|
|
1278
|
+
captureTimeout,
|
|
1261
1279
|
buildEnv: s.resolvedBuildEnv,
|
|
1262
1280
|
buildVersion: version,
|
|
1263
1281
|
captureShellHTML: ssrModule.captureShellHTML,
|