@rangojs/router 0.0.0-experimental.142 → 0.0.0-experimental.143

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.
@@ -41,6 +41,106 @@ import {
41
41
  } from "./telemetry.js";
42
42
  import { _getRequestContext } from "../server/request-context.js";
43
43
 
44
+ /**
45
+ * Per-call telemetry lifecycle emitter for match()/matchPartial(). Each method
46
+ * reproduces the exact event object the two functions used to emit inline and is
47
+ * gated on the same per-call flag (`enabled` = the request's `emitTelemetry`), so
48
+ * a PPR shell-capture run (enabled=false) emits nothing while a foreground run
49
+ * emits byte-identical events. Extracted so the two transactions can't drift;
50
+ * pinned by thrown-response-telemetry.test.ts and
51
+ * shell-capture-telemetry-suppression.test.ts.
52
+ */
53
+ interface LifecycleEmitter {
54
+ start(): void;
55
+ end(segmentCount: number, cacheHit: boolean): void;
56
+ cacheDecision(
57
+ routeKey: string,
58
+ state: {
59
+ cacheHit: boolean;
60
+ cacheSource?: "runtime" | "prerender";
61
+ shouldRevalidate?: boolean;
62
+ },
63
+ segments: CacheSegmentSignal[],
64
+ ): void;
65
+ error(error: Error, phase: string): void;
66
+ }
67
+
68
+ function createLifecycleEmitter(args: {
69
+ enabled: boolean;
70
+ sink: TelemetrySink;
71
+ requestId: string | undefined;
72
+ method: string;
73
+ pathname: string;
74
+ transaction: "match" | "matchPartial";
75
+ isPartial: boolean;
76
+ matchStart: number;
77
+ }): LifecycleEmitter {
78
+ return {
79
+ start(): void {
80
+ if (!args.enabled) return;
81
+ safeEmit(args.sink, {
82
+ type: "request.start",
83
+ timestamp: args.matchStart,
84
+ requestId: args.requestId,
85
+ method: args.method,
86
+ pathname: args.pathname,
87
+ transaction: args.transaction,
88
+ isPartial: args.isPartial,
89
+ });
90
+ },
91
+ end(segmentCount: number, cacheHit: boolean): void {
92
+ if (!args.enabled) return;
93
+ safeEmit(args.sink, {
94
+ type: "request.end",
95
+ timestamp: performance.now(),
96
+ requestId: args.requestId,
97
+ method: args.method,
98
+ pathname: args.pathname,
99
+ transaction: args.transaction,
100
+ durationMs: performance.now() - args.matchStart,
101
+ segmentCount,
102
+ cacheHit,
103
+ });
104
+ },
105
+ cacheDecision(
106
+ routeKey: string,
107
+ state: {
108
+ cacheHit: boolean;
109
+ cacheSource?: "runtime" | "prerender";
110
+ shouldRevalidate?: boolean;
111
+ },
112
+ segments: CacheSegmentSignal[],
113
+ ): void {
114
+ if (!args.enabled) return;
115
+ safeEmit(args.sink, {
116
+ type: "cache.decision",
117
+ timestamp: performance.now(),
118
+ requestId: args.requestId,
119
+ pathname: args.pathname,
120
+ routeKey,
121
+ hit: state.cacheHit,
122
+ shouldRevalidate: !!state.shouldRevalidate,
123
+ source: state.cacheSource,
124
+ segments,
125
+ });
126
+ },
127
+ error(error: Error, phase: string): void {
128
+ if (!args.enabled) return;
129
+ safeEmit(args.sink, {
130
+ type: "request.error",
131
+ timestamp: performance.now(),
132
+ requestId: args.requestId,
133
+ method: args.method,
134
+ pathname: args.pathname,
135
+ transaction: args.transaction,
136
+ error,
137
+ phase,
138
+ durationMs: performance.now() - args.matchStart,
139
+ });
140
+ },
141
+ };
142
+ }
143
+
44
144
  export interface MatchHandlerDeps<TEnv = any> {
45
145
  buildRouterContext: () => RouterContext<TEnv>;
46
146
  callOnError: (error: unknown, phase: ErrorPhase, context: any) => void;
@@ -155,9 +255,20 @@ export function createMatchHandlers<TEnv = any>(
155
255
  }
156
256
 
157
257
  async function match(request: Request, env: TEnv): Promise<MatchResult> {
158
- const requestId = hasTelemetry ? getRequestId(request) : undefined;
258
+ // Silence telemetry for the PPR background shell capture: it re-runs match()
259
+ // under a derived request context flagged _shellCaptureRun (shell-capture.ts
260
+ // attemptCapture), re-using the foreground Request — a second request.start/
261
+ // cache.decision/request.end stamped with the same WeakMap-keyed requestId
262
+ // would double-count dashboards. Derived here (inside the capture's active
263
+ // request-context ALS) so the read sees the derived context, not module state.
264
+ const emitTelemetry =
265
+ hasTelemetry && !_getRequestContext()?._shellCaptureRun;
266
+ const requestId = emitTelemetry ? getRequestId(request) : undefined;
159
267
  return runWithRouterLogContext({ request, transaction: "match" }, () => {
160
268
  const routerCtx = buildRouterContext();
269
+ // Also mute in-pipeline observeEvent emitters (revalidation.decision,
270
+ // cache-lookup's cache.decision) which read routerCtx.telemetry.
271
+ if (!emitTelemetry) routerCtx.telemetry = undefined;
161
272
  routerCtx.requestId = requestId;
162
273
  return runWithRouterContext(routerCtx, async () =>
163
274
  withRouterLogScope("match", async () => {
@@ -165,34 +276,22 @@ export function createMatchHandlers<TEnv = any>(
165
276
  const pathname =
166
277
  _getRequestContext()?.url?.pathname ??
167
278
  new URL(request.url).pathname;
168
- if (hasTelemetry) {
169
- safeEmit(telemetry, {
170
- type: "request.start",
171
- timestamp: matchStart,
172
- requestId,
173
- method: request.method,
174
- pathname,
175
- transaction: "match",
176
- isPartial: false,
177
- });
178
- }
279
+ const emitter = createLifecycleEmitter({
280
+ enabled: emitTelemetry,
281
+ sink: telemetry,
282
+ requestId,
283
+ method: request.method,
284
+ pathname,
285
+ transaction: "match",
286
+ isPartial: false,
287
+ matchStart,
288
+ });
289
+ emitter.start();
179
290
 
180
291
  const result = await createMatchContextForFull(request, env);
181
292
 
182
293
  if ("type" in result && result.type === "redirect") {
183
- if (hasTelemetry) {
184
- safeEmit(telemetry, {
185
- type: "request.end",
186
- timestamp: performance.now(),
187
- requestId,
188
- method: request.method,
189
- pathname,
190
- transaction: "match",
191
- durationMs: performance.now() - matchStart,
192
- segmentCount: 0,
193
- cacheHit: false,
194
- });
195
- }
294
+ emitter.end(0, false);
196
295
  return {
197
296
  segments: [],
198
297
  matched: [],
@@ -212,51 +311,25 @@ export function createMatchHandlers<TEnv = any>(
212
311
  if (hasTelemetry || cacheSignalEnabled) {
213
312
  const signalSegments = buildSignal(ctx.routeKey, state);
214
313
  recordSignalIfEnabled(signalSegments);
215
- if (hasTelemetry) {
216
- safeEmit(telemetry, {
217
- type: "cache.decision",
218
- timestamp: performance.now(),
219
- requestId,
220
- pathname,
221
- routeKey: ctx.routeKey,
222
- hit: state.cacheHit,
223
- shouldRevalidate: !!state.shouldRevalidate,
224
- source: state.cacheSource,
225
- segments: signalSegments,
226
- });
227
- }
228
- }
229
- if (hasTelemetry) {
230
- safeEmit(telemetry, {
231
- type: "request.end",
232
- timestamp: performance.now(),
233
- requestId,
234
- method: request.method,
235
- pathname,
236
- transaction: "match",
237
- durationMs: performance.now() - matchStart,
238
- segmentCount: matchResult.segments.length,
239
- cacheHit: state.cacheHit,
240
- });
314
+ emitter.cacheDecision(ctx.routeKey, state, signalSegments);
241
315
  }
316
+ emitter.end(matchResult.segments.length, state.cacheHit);
242
317
  return matchResult;
243
318
  } catch (error) {
244
- if (hasTelemetry) {
245
- const errorObj =
246
- error instanceof Error ? error : new Error(String(error));
247
- safeEmit(telemetry, {
248
- type: "request.error",
249
- timestamp: performance.now(),
250
- requestId,
251
- method: request.method,
252
- pathname,
253
- transaction: "match",
254
- error: errorObj,
255
- phase: error instanceof Response ? "redirect" : "routing",
256
- durationMs: performance.now() - matchStart,
257
- });
319
+ if (error instanceof Response) {
320
+ // A thrown Response (middleware short-circuit — redirect / auth
321
+ // gate) is a COMPLETED request from the consumer's seat, not an
322
+ // error: emit request.end (the same shape the non-thrown redirect
323
+ // result above already emits), never request.error with a
324
+ // synthetic "[object Response]" error. Rethrow so the caller
325
+ // drives the redirect.
326
+ emitter.end(0, false);
327
+ throw error;
258
328
  }
259
- if (error instanceof Response) throw error;
329
+ emitter.error(
330
+ error instanceof Error ? error : new Error(String(error)),
331
+ "routing",
332
+ );
260
333
  callOnError(error, "routing", {
261
334
  request,
262
335
  url: ctx.url,
@@ -296,11 +369,16 @@ export function createMatchHandlers<TEnv = any>(
296
369
  context: TEnv,
297
370
  actionContext?: ActionContext,
298
371
  ): Promise<MatchResult | null> {
299
- const partialRequestId = hasTelemetry ? getRequestId(request) : undefined;
372
+ // See match() above: the PPR shell capture re-runs matchPartial() under a
373
+ // _shellCaptureRun context and must stay invisible to the sink.
374
+ const emitTelemetry =
375
+ hasTelemetry && !_getRequestContext()?._shellCaptureRun;
376
+ const partialRequestId = emitTelemetry ? getRequestId(request) : undefined;
300
377
  return runWithRouterLogContext(
301
378
  { request, transaction: "matchPartial" },
302
379
  () => {
303
380
  const routerCtx = buildRouterContext();
381
+ if (!emitTelemetry) routerCtx.telemetry = undefined;
304
382
  routerCtx.requestId = partialRequestId;
305
383
  return runWithRouterContext(routerCtx, async () =>
306
384
  withRouterLogScope("matchPartial", async () => {
@@ -308,17 +386,17 @@ export function createMatchHandlers<TEnv = any>(
308
386
  const pathname =
309
387
  _getRequestContext()?.url?.pathname ??
310
388
  new URL(request.url).pathname;
311
- if (hasTelemetry) {
312
- safeEmit(telemetry, {
313
- type: "request.start",
314
- timestamp: matchStart,
315
- requestId: partialRequestId,
316
- method: request.method,
317
- pathname,
318
- transaction: "matchPartial",
319
- isPartial: true,
320
- });
321
- }
389
+ const emitter = createLifecycleEmitter({
390
+ enabled: emitTelemetry,
391
+ sink: telemetry,
392
+ requestId: partialRequestId,
393
+ method: request.method,
394
+ pathname,
395
+ transaction: "matchPartial",
396
+ isPartial: true,
397
+ matchStart,
398
+ });
399
+ emitter.start();
322
400
 
323
401
  const ctx = await createMatchContextForPartial(
324
402
  request,
@@ -326,19 +404,7 @@ export function createMatchHandlers<TEnv = any>(
326
404
  actionContext,
327
405
  );
328
406
  if (!ctx) {
329
- if (hasTelemetry) {
330
- safeEmit(telemetry, {
331
- type: "request.end",
332
- timestamp: performance.now(),
333
- requestId: partialRequestId,
334
- method: request.method,
335
- pathname,
336
- transaction: "matchPartial",
337
- durationMs: performance.now() - matchStart,
338
- segmentCount: 0,
339
- cacheHit: false,
340
- });
341
- }
407
+ emitter.end(0, false);
342
408
  return null;
343
409
  }
344
410
 
@@ -365,53 +431,24 @@ export function createMatchHandlers<TEnv = any>(
365
431
  if (hasTelemetry || cacheSignalEnabled) {
366
432
  const signalSegments = buildSignal(ctx.routeKey, state);
367
433
  recordSignalIfEnabled(signalSegments);
368
- if (hasTelemetry) {
369
- safeEmit(telemetry, {
370
- type: "cache.decision",
371
- timestamp: performance.now(),
372
- requestId: partialRequestId,
373
- pathname,
374
- routeKey: ctx.routeKey,
375
- hit: state.cacheHit,
376
- shouldRevalidate: !!state.shouldRevalidate,
377
- source: state.cacheSource,
378
- segments: signalSegments,
379
- });
380
- }
381
- }
382
- if (hasTelemetry) {
383
- safeEmit(telemetry, {
384
- type: "request.end",
385
- timestamp: performance.now(),
386
- requestId: partialRequestId,
387
- method: request.method,
388
- pathname,
389
- transaction: "matchPartial",
390
- durationMs: performance.now() - matchStart,
391
- segmentCount: matchResult.segments.length,
392
- cacheHit: state.cacheHit,
393
- });
434
+ emitter.cacheDecision(ctx.routeKey, state, signalSegments);
394
435
  }
436
+ emitter.end(matchResult.segments.length, state.cacheHit);
395
437
  return matchResult;
396
438
  } catch (error) {
397
439
  flushRevalidationTrace();
398
- if (hasTelemetry) {
399
- const errorObj =
400
- error instanceof Error ? error : new Error(String(error));
401
- const phase = actionContext ? "action" : "revalidation";
402
- safeEmit(telemetry, {
403
- type: "request.error",
404
- timestamp: performance.now(),
405
- requestId: partialRequestId,
406
- method: request.method,
407
- pathname,
408
- transaction: "matchPartial",
409
- error: errorObj,
410
- phase: error instanceof Response ? "redirect" : phase,
411
- durationMs: performance.now() - matchStart,
412
- });
440
+ if (error instanceof Response) {
441
+ // A thrown Response (middleware short-circuit — redirect / auth
442
+ // gate) is a COMPLETED request, not an error: emit request.end
443
+ // (parity with match()), never request.error. Rethrow so the
444
+ // caller drives the redirect.
445
+ emitter.end(0, false);
446
+ throw error;
413
447
  }
414
- if (error instanceof Response) throw error;
448
+ emitter.error(
449
+ error instanceof Error ? error : new Error(String(error)),
450
+ actionContext ? "action" : "revalidation",
451
+ );
415
452
  callOnError(error, actionContext ? "action" : "revalidation", {
416
453
  request,
417
454
  url: ctx.url,
@@ -550,22 +550,28 @@ export async function executeMiddleware<TEnv>(
550
550
  // when neither surface is active.
551
551
  let result: Response | void;
552
552
  try {
553
- result = await observePhase(PHASES.middleware(metricLabel), () =>
554
- entry.handler(ctx, wrappedNext),
555
- );
556
- } catch (error) {
557
- // Thrown Response is short-circuit control flow, not an error.
558
- // Fall through to the `if (result instanceof Response)` branch below
559
- // so stub headers and request-context cookies merge as they do for
560
- // an explicit `return new Response(...)`. Real errors propagate.
561
- if (error instanceof Response) {
562
- result = error;
563
- } else {
564
- finishMiddleware();
565
- throw error;
566
- }
553
+ result = await observePhase(PHASES.middleware(metricLabel), async () => {
554
+ try {
555
+ return await entry.handler(ctx, wrappedNext);
556
+ } catch (error) {
557
+ // Thrown Response is short-circuit control flow, not an error
558
+ // absorb it INSIDE the span so the tracing runner settles the
559
+ // rango.middleware span as success, not STATUS_ERROR (every auth
560
+ // redirect would otherwise inflate trace error rates). Returning it
561
+ // routes through the `if (result instanceof Response)` branch below,
562
+ // so stub headers and request-context cookies merge identically to an
563
+ // explicit `return new Response(...)`. Segment handlers already follow
564
+ // this convention (segment-resolution/helpers.ts keeps result handling
565
+ // outside the span). Real errors propagate past the span.
566
+ if (error instanceof Response) return error;
567
+ throw error;
568
+ }
569
+ });
570
+ } finally {
571
+ // Settle the middleware own-time metric once on both the success and
572
+ // error paths (idempotent guard in finishMiddleware).
573
+ finishMiddleware();
567
574
  }
568
- finishMiddleware();
569
575
 
570
576
  // Record post-next() processing time when middleware did work after
571
577
  // the downstream chain resolved (e.g. adding headers, logging).
@@ -725,20 +731,21 @@ export async function executeInterceptMiddleware<TEnv>(
725
731
  ordinal,
726
732
  );
727
733
 
728
- let result: Response | void;
729
- try {
730
- result = await observePhase(PHASES.middleware(label), () =>
731
- middleware(ctx, guardedNext),
732
- );
733
- } catch (error) {
734
- // Thrown Response is short-circuit control flow, parity with the
735
- // explicit-return path below. Real errors propagate.
736
- if (error instanceof Response) {
737
- result = error;
738
- } else {
739
- throw error;
740
- }
741
- }
734
+ const result: Response | void = await observePhase(
735
+ PHASES.middleware(label),
736
+ async () => {
737
+ try {
738
+ return await middleware(ctx, guardedNext);
739
+ } catch (error) {
740
+ // Thrown Response is short-circuit control flow, parity with the
741
+ // explicit-return path below. Absorb it INSIDE the span so the tracing
742
+ // runner settles rango.middleware as success, not STATUS_ERROR (same
743
+ // reasoning as executeMiddleware's main chain). Real errors propagate.
744
+ if (error instanceof Response) return error;
745
+ throw error;
746
+ }
747
+ },
748
+ );
742
749
 
743
750
  if (result instanceof Response) {
744
751
  earlyResponse = result;
@@ -16,6 +16,7 @@ import type { RangoOptions, RootLayoutProps } from "./router-options.js";
16
16
  import type { DefaultVars } from "../types/global-namespace.js";
17
17
  import type { ResolvedTimeouts, OnTimeoutCallback } from "./timeout.js";
18
18
  import type { ResolvedTracing } from "./tracing.js";
19
+ import type { TelemetrySink } from "./telemetry.js";
19
20
 
20
21
  /**
21
22
  * Options passed to router.fetch(), router.match(), and other request entrypoints.
@@ -350,6 +351,14 @@ export interface RangoInternal<
350
351
  */
351
352
  readonly tracing?: ResolvedTracing;
352
353
 
354
+ /**
355
+ * Raw telemetry sink from RangoOptions, exposed so handler-level emitters
356
+ * (rsc/handler.ts timeout/origin/late-handle) can emit WITHOUT the
357
+ * RouterContext ALS, which only match()/matchPartial() enter. See
358
+ * observeEvent's emitter list in router/instrument.ts.
359
+ */
360
+ readonly telemetry?: TelemetrySink;
361
+
353
362
  /**
354
363
  * Whether ?__debug_manifest is allowed in production.
355
364
  * Always enabled in development.
@@ -10,15 +10,20 @@
10
10
  *
11
11
  * - elide: deep-walk the settled container; a SETTLED nested promise baked
12
12
  * its value (physics: it won the quiet window), so it is pinned as
13
- * that value; a PENDING nested promise is a hole, replaced by
14
- * {@link LOADER_HOLE_MARKER}. The result is promise-free and
13
+ * that value wrapped in a SETTLED marker the wrapper remembers
14
+ * "this was a promise" so the overlay can rehydrate the shape; a
15
+ * PENDING nested promise is a hole, replaced by
16
+ * {@link LOADER_HOLE_KEY}. The result is promise-free and
15
17
  * Flight-serializable.
16
18
  * - overlay: on a HIT the loader runs fresh (only the loader body can mint
17
19
  * the live nested promises), then the recorded container is laid
18
20
  * over it: recorded paths win (they are what the prelude froze),
19
- * marker paths take the fresh run's value (the live hole), and
20
- * fresh-only paths pass through (they cannot contradict prelude
21
- * bytes that never rendered them).
21
+ * hole-marker paths take the fresh run's value (the live hole),
22
+ * SETTLED-marker paths become Promise.resolve(pinned) consumers
23
+ * wrote use(data.x) against a promise-shaped container, and
24
+ * handing them the raw value throws React #438 on every HIT (the
25
+ * storefront PDP regression) — and fresh-only paths pass through
26
+ * (they cannot contradict prelude bytes that never rendered them).
22
27
  */
23
28
 
24
29
  import { isThenable } from "../../handles/is-thenable.js";
@@ -42,6 +47,37 @@ export function isLoaderHoleMarker(value: unknown): value is LoaderHoleMarker {
42
47
  );
43
48
  }
44
49
 
50
+ /**
51
+ * Marker wrapping the inlined value of a NESTED promise that settled during
52
+ * capture. The value baked (physics), but the container key was a promise —
53
+ * the overlay must hand consumers a Promise.resolve(value), not the raw value,
54
+ * or an unconditional use(data.x) throws React #438 on every HIT. The ROOT
55
+ * container is never wrapped: loader-cache overlays against the awaited fresh
56
+ * container value.
57
+ */
58
+ export const LOADER_SETTLED_KEY = "$rangoLoaderSettled" as const;
59
+
60
+ export interface LoaderSettledMarker {
61
+ [LOADER_SETTLED_KEY]: 1;
62
+ value: unknown;
63
+ /**
64
+ * Present when the pinned subtree contains hole markers. Computed once at
65
+ * capture (elide already visits every node) so the per-HIT overlay never
66
+ * rescans the pinned structure to pick its rehydration path.
67
+ */
68
+ holes?: 1;
69
+ }
70
+
71
+ export function isLoaderSettledMarker(
72
+ value: unknown,
73
+ ): value is LoaderSettledMarker {
74
+ return (
75
+ typeof value === "object" &&
76
+ value !== null &&
77
+ (value as Record<string, unknown>)[LOADER_SETTLED_KEY] === 1
78
+ );
79
+ }
80
+
45
81
  function isPlainObject(value: unknown): value is Record<string, unknown> {
46
82
  if (typeof value !== "object" || value === null) return false;
47
83
  const proto = Object.getPrototypeOf(value);
@@ -77,17 +113,22 @@ async function probeSettled(
77
113
  }
78
114
 
79
115
  export type ElideResult =
80
- | { state: "ok"; value: unknown }
116
+ | { state: "ok"; value: unknown; hasHole: boolean }
81
117
  | { state: "rejected" };
82
118
 
83
119
  /**
84
120
  * Deep-elide a settled bake-lane container for recording. Settled nested
85
- * promises are inlined (they baked); pending ones become hole markers; a
86
- * REJECTED nested promise poisons the record (error UI must never bake into a
87
- * shared shell) the caller refuses the capture. Only plain objects/arrays are
121
+ * promises pin their value behind a settled marker (they baked, but consumers
122
+ * hold a promise-shaped key); pending ones become hole markers; a REJECTED
123
+ * nested promise poisons the record (error UI must never bake into a shared
124
+ * shell) — the caller refuses the capture. Only plain objects/arrays are
88
125
  * traversed; anything else (Date, Map, class instance) is a pinned leaf.
89
126
  * Cycles are cut as pinned references (best effort — Flight rejects true
90
127
  * cycles later regardless).
128
+ *
129
+ * The ROOT container promise-chain unwraps with NO marker: loader-cache
130
+ * overlays against the AWAITED fresh container, so the recorded root must be
131
+ * the unwrapped structure. Everything below it goes through elideNested.
91
132
  */
92
133
  export async function elideLoaderContainer(
93
134
  value: unknown,
@@ -96,37 +137,61 @@ export async function elideLoaderContainer(
96
137
  if (isThenable(value)) {
97
138
  const probed = await probeSettled(value);
98
139
  if (probed.state === "pending") {
99
- return { state: "ok", value: { [LOADER_HOLE_KEY]: 1 } };
140
+ return { state: "ok", value: { [LOADER_HOLE_KEY]: 1 }, hasHole: true };
100
141
  }
101
142
  if (probed.state === "rejected") return { state: "rejected" };
102
143
  return elideLoaderContainer(probed.value, seen);
103
144
  }
145
+ return elideNested(value, seen);
146
+ }
147
+
148
+ async function elideNested(
149
+ value: unknown,
150
+ seen: Set<object>,
151
+ ): Promise<ElideResult> {
152
+ if (isThenable(value)) {
153
+ const probed = await probeSettled(value);
154
+ if (probed.state === "pending") {
155
+ return { state: "ok", value: { [LOADER_HOLE_KEY]: 1 }, hasHole: true };
156
+ }
157
+ if (probed.state === "rejected") return { state: "rejected" };
158
+ const inner = await elideNested(probed.value, seen);
159
+ if (inner.state === "rejected") return inner;
160
+ const marker: LoaderSettledMarker = inner.hasHole
161
+ ? { [LOADER_SETTLED_KEY]: 1, value: inner.value, holes: 1 }
162
+ : { [LOADER_SETTLED_KEY]: 1, value: inner.value };
163
+ return { state: "ok", value: marker, hasHole: inner.hasHole };
164
+ }
104
165
 
105
166
  if (Array.isArray(value)) {
106
- if (seen.has(value)) return { state: "ok", value };
167
+ if (seen.has(value)) return { state: "ok", value, hasHole: false };
107
168
  seen.add(value);
108
169
  const out: unknown[] = new Array(value.length);
170
+ let hasHole = false;
109
171
  for (let i = 0; i < value.length; i++) {
110
- const r = await elideLoaderContainer(value[i], seen);
172
+ const r = await elideNested(value[i], seen);
111
173
  if (r.state === "rejected") return r;
112
174
  out[i] = r.value;
175
+ hasHole ||= r.hasHole;
113
176
  }
114
- return { state: "ok", value: out };
177
+ return { state: "ok", value: out, hasHole };
115
178
  }
116
179
 
117
180
  if (isPlainObject(value)) {
118
- if (seen.has(value)) return { state: "ok", value };
181
+ if (seen.has(value)) return { state: "ok", value, hasHole: false };
119
182
  seen.add(value);
120
183
  const out: Record<string, unknown> = {};
184
+ let hasHole = false;
121
185
  for (const key of Object.keys(value)) {
122
- const r = await elideLoaderContainer(value[key], seen);
186
+ const r = await elideNested(value[key], seen);
123
187
  if (r.state === "rejected") return r;
124
188
  out[key] = r.value;
189
+ hasHole ||= r.hasHole;
125
190
  }
126
- return { state: "ok", value: out };
191
+ return { state: "ok", value: out, hasHole };
127
192
  }
128
193
 
129
- return { state: "ok", value };
194
+ return { state: "ok", value, hasHole: false };
130
195
  }
131
196
 
132
197
  /**
@@ -142,6 +207,22 @@ export function overlayLoaderContainer(
142
207
  ): unknown {
143
208
  if (isLoaderHoleMarker(recorded)) return fresh;
144
209
 
210
+ if (isLoaderSettledMarker(recorded)) {
211
+ const pinned = recorded.value;
212
+ // Deep holes inside a settled container (capture-computed `holes` bit)
213
+ // need the fresh promise's resolved value to fill them; a fully-pinned
214
+ // container resolves immediately (the prelude already shows it — never
215
+ // gate it on fresh latency). A rejecting fresh run degrades holes to
216
+ // undefined instead of poisoning the pin.
217
+ if (recorded.holes === 1) {
218
+ return Promise.resolve(fresh).then(
219
+ (freshValue) => overlayLoaderContainer(freshValue, pinned),
220
+ () => overlayLoaderContainer(undefined, pinned),
221
+ );
222
+ }
223
+ return Promise.resolve(overlayLoaderContainer(undefined, pinned));
224
+ }
225
+
145
226
  if (Array.isArray(recorded)) {
146
227
  const freshArr = Array.isArray(fresh) ? fresh : [];
147
228
  return recorded.map((item, i) => overlayLoaderContainer(freshArr[i], item));