@rangojs/router 0.0.0-experimental.148 → 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 +1 -1
- package/package.json +1 -1
- package/src/rsc/shell-capture-constants.ts +27 -0
- package/src/rsc/shell-capture.ts +7 -5
- package/src/rsc/shell-serve.ts +1 -1
- package/src/ssr/index.tsx +42 -12
- package/src/urls/pattern-types.ts +12 -10
package/dist/vite/index.js
CHANGED
|
@@ -2520,7 +2520,7 @@ import { resolve } from "node:path";
|
|
|
2520
2520
|
// package.json
|
|
2521
2521
|
var package_default = {
|
|
2522
2522
|
name: "@rangojs/router",
|
|
2523
|
-
version: "0.0.0-experimental.
|
|
2523
|
+
version: "0.0.0-experimental.149",
|
|
2524
2524
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
2525
2525
|
keywords: [
|
|
2526
2526
|
"react",
|
package/package.json
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default upper bound on the capture prerender wait before forcing the abort
|
|
3
|
+
* that freezes the shell — the ONLY wall-clock on the capture path, and a
|
|
4
|
+
* pathological guard: it should never fire once the caller's `quiesce` is a
|
|
5
|
+
* task-quantized, frozen-byte signal (the capture gate in shell-capture.ts).
|
|
6
|
+
*
|
|
7
|
+
* Leaf module on purpose (imports nothing): shell-capture.ts re-exports it
|
|
8
|
+
* (single import site for the rsc graph — shell-build-manifest.ts sizes the
|
|
9
|
+
* dev fetch envelope from that re-export) and ssr/index.tsx uses it as
|
|
10
|
+
* captureShellHTML's own fallback; the ssr graph must not pull the capture
|
|
11
|
+
* orchestration module for one number.
|
|
12
|
+
*
|
|
13
|
+
* 15s, raised from 5s: captures are background work (waitUntil), so the budget
|
|
14
|
+
* costs latency-to-HIT only — never a served response — and 5s spuriously
|
|
15
|
+
* refused legitimately-slow deferred shell material (a real storefront's meta
|
|
16
|
+
* chains settle at ~7s). Ceiling math: workerd's waitUntil lifetime is ~30s
|
|
17
|
+
* past response completion, and a guaranteed two-attempt envelope needs
|
|
18
|
+
* 2x budget + the in-place retry delay + store I/O, i.e. budget <= ~14s. The
|
|
19
|
+
* 15s default deliberately sits just past that: attempt 1 always gets its
|
|
20
|
+
* full 15s, but when it consumed the whole budget the in-place RETRY may be
|
|
21
|
+
* truncated by the platform kill on workerd — degrading to the existing
|
|
22
|
+
* best-effort contract (the key stays MISS; a later request re-captures).
|
|
23
|
+
* Node/dev and build-time captures have no waitUntil ceiling. The per-route
|
|
24
|
+
* `ppr.captureTimeout` knob remains for tightening below the default. See
|
|
25
|
+
* docs/design/ppr-shell-resume.md (Cost model).
|
|
26
|
+
*/
|
|
27
|
+
export const SHELL_CAPTURE_MAX_WAIT_MS = 15_000;
|
package/src/rsc/shell-capture.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { bufferToBase64 } from "../cache/cf/cf-base64.js";
|
|
|
21
21
|
import { reportCacheError } from "../cache/cache-error.js";
|
|
22
22
|
import { runBackground } from "../cache/background-task.js";
|
|
23
23
|
import { enqueueSerializedCapture } from "./capture-queue.js";
|
|
24
|
+
import { SHELL_CAPTURE_MAX_WAIT_MS } from "./shell-capture-constants.js";
|
|
24
25
|
import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
25
26
|
import { observePhase, PHASES } from "../router/instrument.js";
|
|
26
27
|
import {
|
|
@@ -80,11 +81,12 @@ import { resolveDeferredHandleValues } from "../handles/deferred-resolution.js";
|
|
|
80
81
|
const FLIGHT_QUIET_HOPS = 2;
|
|
81
82
|
|
|
82
83
|
/**
|
|
83
|
-
* Default
|
|
84
|
-
*
|
|
85
|
-
*
|
|
84
|
+
* Default capture budget. Canonical value, raise rationale, and ceiling math
|
|
85
|
+
* live on the leaf module (shell-capture-constants.ts, importable from the ssr
|
|
86
|
+
* graph too). Re-exported here so shell-build-manifest.ts's envelope math
|
|
87
|
+
* keeps its existing import site and cannot drift from the capture.
|
|
86
88
|
*/
|
|
87
|
-
export
|
|
89
|
+
export { SHELL_CAPTURE_MAX_WAIT_MS };
|
|
88
90
|
|
|
89
91
|
/**
|
|
90
92
|
* Upper bound on waiting for the capture's DEFERRED cache writes to settle before
|
|
@@ -807,7 +809,7 @@ export interface ShellCaptureDescriptor {
|
|
|
807
809
|
* bounding the whole capture, so it covers BOTH the fizz prerender AND the
|
|
808
810
|
* deferred-material settle window (the handlesBaked/loader-container
|
|
809
811
|
* holdUntil that keeps the gate from freezing while top-level pushes are
|
|
810
|
-
* pending). Undefined = SHELL_CAPTURE_MAX_WAIT_MS (
|
|
812
|
+
* pending). Undefined = SHELL_CAPTURE_MAX_WAIT_MS (15_000).
|
|
811
813
|
*/
|
|
812
814
|
captureTimeout?: number;
|
|
813
815
|
store?: SegmentCacheStore<any>;
|
package/src/rsc/shell-serve.ts
CHANGED
|
@@ -52,7 +52,7 @@ export interface ResolvedPprConfig {
|
|
|
52
52
|
maxSnapshotBytes?: number;
|
|
53
53
|
/**
|
|
54
54
|
* Capture settle budget in ms (`ppr.captureTimeout`). Undefined = the
|
|
55
|
-
* capture default (SHELL_CAPTURE_MAX_WAIT_MS,
|
|
55
|
+
* capture default (SHELL_CAPTURE_MAX_WAIT_MS, 15_000) — the default's single
|
|
56
56
|
* owner stays shell-capture.ts so build/runtime producers cannot drift.
|
|
57
57
|
*/
|
|
58
58
|
captureTimeout?: number;
|
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.
|
|
@@ -74,19 +74,21 @@ export interface PartialPrerenderProps {
|
|
|
74
74
|
*/
|
|
75
75
|
maxSnapshotBytes?: number;
|
|
76
76
|
/**
|
|
77
|
-
* Capture settle budget in MILLISECONDS (default
|
|
77
|
+
* Capture settle budget in MILLISECONDS (default 15000). Bounds the whole
|
|
78
78
|
* background capture: the wait for deferred shell material — top-level
|
|
79
79
|
* pushed handle promises (`ctx.use(Meta)(promise.then(...))` and friends)
|
|
80
80
|
* are AWAITED and their settled values baked into the stored shell — AND
|
|
81
|
-
* the fizz prerender deadline. Declare it
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
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.
|
|
90
92
|
*/
|
|
91
93
|
captureTimeout?: number;
|
|
92
94
|
}
|