@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.
@@ -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.148",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.0.0-experimental.148",
3
+ "version": "0.0.0-experimental.149",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -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;
@@ -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 upper bound on the capture prerender wait before forcing the abort.
84
- * Single owner of the default budget shell-build-manifest.ts imports it so
85
- * the dev fetch bound's envelope math cannot drift from the capture.
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 const SHELL_CAPTURE_MAX_WAIT_MS = 5000;
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 (5000).
812
+ * pending). Undefined = SHELL_CAPTURE_MAX_WAIT_MS (15_000).
811
813
  */
812
814
  captureTimeout?: number;
813
815
  store?: SegmentCacheStore<any>;
@@ -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, 5000) — the default's single
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 5000ms. */
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 ?? DEFAULT_SHELL_CAPTURE_MAX_WAIT_MS;
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
- const deadline = createCancelableTimeout(maxWaitMs);
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 5000). Bounds the whole
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 when a route's shell material
82
- * takes longer than 5s to settle. A budget that expires with pushes still
83
- * pending REFUSES the capture (the route stays MISS with the once-per-key
84
- * warning) a shell with missing head material is never stored. Capture
85
- * is background work (waitUntil), so a longer budget costs latency-to-HIT
86
- * only, never a served response; the platform waitUntil lifetime (workerd:
87
- * ~30s past response completion) is the physical ceiling. Build-time
88
- * captures (Prerender+ppr, producer B) honor the same budget with no
89
- * platform ceiling. Non-finite or sub-1ms values fall back to the default.
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
  }