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

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.
Files changed (38) hide show
  1. package/dist/vite/index.js +23 -6
  2. package/package.json +2 -2
  3. package/skills/cache-guide/SKILL.md +3 -1
  4. package/skills/caching/SKILL.md +23 -2
  5. package/skills/catalog.json +6 -0
  6. package/skills/defer-hydration/SKILL.md +235 -0
  7. package/skills/loader/SKILL.md +5 -0
  8. package/skills/migrate-nextjs/SKILL.md +4 -2
  9. package/skills/parallel/SKILL.md +2 -0
  10. package/skills/ppr/SKILL.md +34 -10
  11. package/skills/rango/SKILL.md +10 -0
  12. package/skills/use-cache/SKILL.md +12 -2
  13. package/src/browser/partial-update.ts +7 -0
  14. package/src/cache/cache-key-utils.ts +29 -0
  15. package/src/cache/cache-scope.ts +2 -17
  16. package/src/cache/cache-tag.ts +60 -14
  17. package/src/cache/cf/cf-cache-store.ts +54 -20
  18. package/src/cache/document-cache.ts +17 -11
  19. package/src/cache/vercel/vercel-cache-store.ts +9 -19
  20. package/src/redirect-origin.ts +14 -0
  21. package/src/route-map-builder.ts +17 -3
  22. package/src/router/lazy-includes.ts +8 -2
  23. package/src/router/loader-resolution.ts +14 -2
  24. package/src/router/match-handlers.ts +11 -6
  25. package/src/router/middleware.ts +4 -1
  26. package/src/router/telemetry.ts +9 -1
  27. package/src/router.ts +7 -8
  28. package/src/rsc/handler.ts +9 -2
  29. package/src/rsc/redirect-guard.ts +2 -1
  30. package/src/rsc/rsc-rendering.ts +35 -2
  31. package/src/rsc/shell-capture.ts +93 -20
  32. package/src/server/context.ts +47 -9
  33. package/src/server/cookie-store.ts +26 -5
  34. package/src/server/request-context.ts +22 -0
  35. package/src/ssr/index.tsx +145 -107
  36. package/src/testing/dispatch.ts +7 -0
  37. package/src/vite/inject-client-debug.ts +64 -12
  38. package/src/vite/router-discovery.ts +9 -1
@@ -44,6 +44,10 @@ import {
44
44
  warnPprNonceActiveOnce,
45
45
  } from "./shell-serve.js";
46
46
  import { contextGet } from "../context-var.js";
47
+ import {
48
+ resolveSameOriginRedirect,
49
+ safeSameOriginLanding,
50
+ } from "../redirect-origin.js";
47
51
  import { nonce as nonceToken } from "./nonce.js";
48
52
  import { reportCacheError } from "../cache/cache-error.js";
49
53
  import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
@@ -413,6 +417,29 @@ async function handleRscRenderingInner<TEnv>(
413
417
  return response;
414
418
  }
415
419
 
420
+ /**
421
+ * Neutralize the shell-HIT degradation redirect target.
422
+ *
423
+ * The inline `location.replace` emitted by serveShellHit when a shell HIT lands
424
+ * on a URL whose route became redirecting mid-TTL is a document-native redirect
425
+ * exit that BYPASSES the 3xx chokepoint (guardOutgoingRedirect acts only on 3xx
426
+ * + Location responses, never a committed 200 body). So it reuses the ONE
427
+ * same-origin resolver directly: a cross-origin/unparseable/unsafe target
428
+ * neutralizes to the same safe same-origin landing as redirect-guard.ts
429
+ * (basename root, or "/" when unset) rather than navigating the user off-host.
430
+ * A safe same-origin/relative target passes through as its normalized href.
431
+ */
432
+ export function resolveShellHitRedirectTarget(
433
+ rawTarget: string,
434
+ requestOrigin: string,
435
+ basename: string | undefined,
436
+ ): string {
437
+ return (
438
+ resolveSameOriginRedirect(rawTarget, requestOrigin) ??
439
+ safeSameOriginLanding(basename)
440
+ );
441
+ }
442
+
416
443
  /**
417
444
  * Serve a validated shell HIT: commit the composed response NOW — the stored
418
445
  * prelude bytes are the first thing on the wire — and run the live tail
@@ -535,10 +562,16 @@ function serveShellHit(
535
562
  // a shell (capture bails on redirects), so a HIT on a redirecting URL
536
563
  // requires the route to have BECOME redirecting within the shell TTL.
537
564
  // The 200 + prelude are already committed; degrade to a client-side
538
- // replace so the user still lands on the target.
565
+ // replace so the user still lands on the target. The target is
566
+ // neutralized first (see resolveShellHitRedirectTarget).
567
+ const safeTarget = resolveShellHitRedirectTarget(
568
+ tail.redirect,
569
+ url.origin,
570
+ ctx.router.basename,
571
+ );
539
572
  controller.enqueue(
540
573
  new TextEncoder().encode(
541
- `<script>location.replace(${JSON.stringify(tail.redirect)})</script>`,
574
+ `<script>location.replace(${JSON.stringify(safeTarget)})</script>`,
542
575
  ),
543
576
  );
544
577
  }
@@ -327,6 +327,39 @@ function warnCaptureRefusedOnce(key: string, reason: string): void {
327
327
  );
328
328
  }
329
329
 
330
+ /** Keys already warned about untagged bake-lane data baked into the shell. */
331
+ const warnedUntaggedShellBakes = new Set<string>();
332
+
333
+ /**
334
+ * Warn once per shell key that a bake-lane loader baked material into the shell
335
+ * but the capture recorded ZERO tags (no render-collected _requestTags, no
336
+ * static ppr.tags). Such data is frozen in the shared shell until TTL and is
337
+ * un-evictable by tag: a server action refreshes the CLIENT only (rotates Rango
338
+ * state, busts the browser HTTP cache) and never touches the server shell store,
339
+ * and updateTag()/revalidateTag() cannot drop data that was baked WITHOUT a tag.
340
+ *
341
+ * Coarse per-shell-key signal, not per-loader attribution — the tag set is
342
+ * unioned globally at the write barrier, not tracked per loader, so we cannot
343
+ * name the offending loader without adding attribution plumbing (deliberately
344
+ * not done). Dev-only + once-per-key so it never spams production or fires on
345
+ * every capture.
346
+ */
347
+ function warnUntaggedShellBakeOnce(key: string): void {
348
+ if (warnedUntaggedShellBakes.has(key)) return;
349
+ warnedUntaggedShellBakes.add(key);
350
+ console.warn(
351
+ `[rango] Shell capture for "${key}" baked bake-lane loader data into the ` +
352
+ "shell with NO cache tag. That data is frozen in the shared shell until " +
353
+ "TTL and cannot be tag-invalidated: a server action refresh touches the " +
354
+ "client only (not the server shell store), and updateTag() cannot evict " +
355
+ "data that was baked without a tag.\n" +
356
+ 'Fix: tag the data (cacheTag() / "use cache" / cache({ tags })) so ' +
357
+ "updateTag() drops the shell, or move the volatile read under a loading() " +
358
+ "hole so it stays on the live lane and is never baked. See the /ppr skill " +
359
+ "(node_modules/@rangojs/router/skills/ppr/SKILL.md).",
360
+ );
361
+ }
362
+
330
363
  export interface FlightCaptureGate {
331
364
  /** Identity passthrough of the source stream; feed this to captureShellHTML. */
332
365
  stream: ReadableStream<Uint8Array>;
@@ -487,9 +520,12 @@ export function gateFlightForCapture(
487
520
  * store, and passed to scheduleShellCapture directly — it is NOT threaded through
488
521
  * the request context. `tags` carries the route's OPERATIONAL `ppr.tags`; the
489
522
  * capture UNIONS them with the shell's own auto-collected (non-loader) request
490
- * tags from its derived render (the collected set stays authoritative). `store`
491
- * is the same store the serve path resolved for its getShell read
492
- * (requestCtx._cacheStore), so the capture writes where the serve reads.
523
+ * tags from its derived render (the collected set stays authoritative). That
524
+ * union happens at the putShell WRITE BARRIER in captureAndStoreShell — after the
525
+ * capture quiesces — not at stream construction, so a tag recorded after an await
526
+ * in async shell content is still collected (issue #676). `store` is the same
527
+ * store the serve path resolved for its getShell read (requestCtx._cacheStore),
528
+ * so the capture writes where the serve reads.
493
529
  */
494
530
  export interface ShellCaptureDescriptor {
495
531
  key: string;
@@ -776,24 +812,18 @@ async function attemptCapture(
776
812
  },
777
813
  });
778
814
 
779
- // Shell tags = the non-loader request tags the capture render recorded on its
780
- // own fresh _requestTags (loaders are masked, so loader cache tags — which
781
- // belong to the holes, not the shell — are correctly excluded), UNIONED with
782
- // the middleware's operational `tags` option (descriptor.tags). The collected
783
- // set is authoritative; the option only adds tags the render cannot know.
784
- const collected = [...derivedCtx._requestTags];
785
- const union = new Set<string>([...(descriptor.tags ?? []), ...collected]);
786
- const tags = union.size > 0 ? [...union] : undefined;
787
-
815
+ // Pass the descriptor with its STATIC ppr.tags unchanged. The shell's own
816
+ // render-recorded tags are snapshotted at the putShell WRITE BARRIER inside
817
+ // captureAndStoreShell, not here: a tag recorded AFTER an await in async shell
818
+ // content (and tags propagated by async cache()/"use cache" reads) lands after
819
+ // this synchronous construction point, so snapshotting here dropped it — the
820
+ // shell-tag snapshot must sit behind the quiesce gate (issue #676).
788
821
  return captureAndStoreShell(
789
822
  ssrModule,
790
823
  rscStream,
791
824
  freshHandleStore,
792
825
  derivedCtx,
793
- {
794
- ...descriptor,
795
- tags,
796
- },
826
+ descriptor,
797
827
  );
798
828
  });
799
829
  }
@@ -878,11 +908,25 @@ async function captureAndStoreShell(
878
908
  const refuseOnGuardTrip = (): "refused" | undefined => {
879
909
  const fnName = reqCtx._shellCaptureGuardTripped;
880
910
  if (!fnName) return undefined;
911
+ // Name the recorded source instead of hardcoding a lane. Under the
912
+ // consumption-lane rule, handler-INVOKED loader bodies are exempt from
913
+ // the guard (their value is a baked shared copy, mirroring cache()), so a
914
+ // trip can only come from a bake-lane SEGMENT loader or from non-loader
915
+ // handler/render code — the old blanket "bake-lane loader" attribution
916
+ // sent a live-lane debugging session down the wrong lane (issue #672).
917
+ const loaderId = reqCtx._shellCaptureGuardTrippedLoaderId;
918
+ const origin = loaderId
919
+ ? `segment loader "${loaderId}"`
920
+ : "handler/render code (no loader body was executing)";
881
921
  warnCaptureRefusedOnce(
882
922
  capture.key,
883
- `a bake-lane loader (a loader on an entry with no loading()) called ${fnName}() during capture. ` +
884
- "Identity must not bake into a shared shell. Give that loader's entry a loading() boundary " +
885
- "(the live lane, exempt from the guard) or move the identity-dependent part into a nested promise.",
923
+ `${origin} called ${fnName}() during capture. Identity must not bake into a shared shell. ` +
924
+ "For a segment loader (bake lane, no loading()): give its entry a loading() boundary " +
925
+ "(the live lane, masked at capture) or move the identity-dependent part into a nested " +
926
+ "promise. For handler/render code: keep the value live by consuming a loader " +
927
+ 'client-side (useLoader in a "use client" component). Note: `await ctx.use(loader)` ' +
928
+ "inside a HANDLER is exempt from this guard — its value bakes into the shared shell " +
929
+ "as a capture-time copy, mirroring cache() semantics (the consumption-lane rule).",
886
930
  );
887
931
  return "refused";
888
932
  };
@@ -981,6 +1025,10 @@ async function captureAndStoreShell(
981
1025
  // gate above already returned no-shell) or postponed under an ANCESTOR
982
1026
  // boundary (it is a hole; omitting the record keeps it live).
983
1027
  const loaderRecords = reqCtx._shellCaptureLoaderRecords;
1028
+ // Set once a bake-lane loader settles with real (non-hole) material: its data
1029
+ // is frozen into the shell prelude regardless of whether snapshot
1030
+ // serialization succeeds. Drives the untagged-bake dev warning below.
1031
+ let bakedLoaderMaterial = false;
984
1032
  if (loaderRecords && loaderRecords.size > 0) {
985
1033
  // The codec import is deferred past the elide probes: a rejected record
986
1034
  // refuses and a never-settled record is omitted WITHOUT touching Flight
@@ -1001,6 +1049,9 @@ async function captureAndStoreShell(
1001
1049
  // The container itself never settled: it is a hole (under an ancestor
1002
1050
  // boundary) or the trivial-prelude gate already fired. Omit — no pin.
1003
1051
  if (isLoaderHoleMarker(elided.value)) continue;
1052
+ // Past the hole check: this container settled with real material that
1053
+ // bakes into the shell prelude (independent of the snapshot pin below).
1054
+ bakedLoaderMaterial = true;
1004
1055
  try {
1005
1056
  // serializeResult (not rscSerialize): null is a valid container and
1006
1057
  // must round-trip; serializeResult preserves it through Flight.
@@ -1021,6 +1072,28 @@ async function captureAndStoreShell(
1021
1072
  }
1022
1073
  }
1023
1074
 
1075
+ // Shell tags snapshot at the WRITE BARRIER, not at stream construction: by
1076
+ // here the capture has quiesced and the deferred cache writes were awaited, so
1077
+ // tags recorded AFTER an await in async shell content (and by async
1078
+ // cache()/"use cache" reads propagating through recordRequestTags) are
1079
+ // included — issue #676. Loaders are masked, so loader cache tags — which
1080
+ // belong to the holes, not the shell — never execute during capture and cannot
1081
+ // contribute. Union with the route's static ppr.tags (capture.tags); the
1082
+ // collected set is authoritative, the option only adds what the render cannot
1083
+ // know.
1084
+ const collected = [...reqCtx._requestTags];
1085
+ const union = new Set<string>([...(capture.tags ?? []), ...collected]);
1086
+ const shellTags = union.size > 0 ? [...union] : undefined;
1087
+
1088
+ // Untagged-bake diagnostic: a bake-lane loader froze mutable data into the
1089
+ // shell but nothing tags the entry, so it is un-invalidatable except by TTL —
1090
+ // a read-your-own-writes gap on the document channel (an action refresh skips
1091
+ // the server shell; updateTag cannot drop untagged data). Coarse per-shell-key
1092
+ // signal; dev-only and once-per-key so it never spams production.
1093
+ if (isDevMode() && bakedLoaderMaterial && shellTags === undefined) {
1094
+ warnUntaggedShellBakeOnce(capture.key);
1095
+ }
1096
+
1024
1097
  const store = capture.store ?? reqCtx._cacheStore;
1025
1098
  if (store?.putShell) {
1026
1099
  try {
@@ -1044,7 +1117,7 @@ async function captureAndStoreShell(
1044
1117
  entry,
1045
1118
  capture.ttl,
1046
1119
  capture.swr,
1047
- capture.tags,
1120
+ shellTags,
1048
1121
  );
1049
1122
  } catch (error) {
1050
1123
  // Best-effort: a failed put must never throw out of the background task.
@@ -770,14 +770,23 @@ const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
770
770
 
771
771
  // Purity-only scope: marks that a loader FUNCTION BODY is executing, regardless
772
772
  // of how the loader was invoked (DSL via runInsideLoaderScope, or handler-
773
- // invoked via ctx.use). Consulted ONLY by isInsideCacheScope() to exempt
774
- // request-scoped reads. It deliberately does NOT affect isInsideLoaderScope(),
775
- // so rendered()/barrier/deadlock gating (which must distinguish DSL from
776
- // handler-invoked loaders) is unchanged.
773
+ // invoked via ctx.use). Consulted by isInsideCacheScope() to exempt
774
+ // request-scoped reads, by getCurrentLoaderBodyId() for guard-warning
775
+ // attribution, and by isInsideHandlerInvokedLoaderBody() for the
776
+ // consumption-lane rule (the shell-capture guard exemption). It deliberately
777
+ // does NOT affect isInsideLoaderScope(), so rendered()/barrier/deadlock
778
+ // gating (which must distinguish DSL from handler-invoked loaders) is
779
+ // unchanged.
777
780
  const LOADER_BODY_SCOPE_KEY = Symbol.for("rangojs-router:loader-body-scope");
778
- const loaderBodyScopeALS: AsyncLocalStorage<{ active: true }> = ((
779
- globalThis as any
780
- )[LOADER_BODY_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
781
+ const loaderBodyScopeALS: AsyncLocalStorage<{
782
+ active: true;
783
+ loaderId?: string;
784
+ handlerInvoked?: boolean;
785
+ }> = ((globalThis as any)[LOADER_BODY_SCOPE_KEY] ??= new AsyncLocalStorage<{
786
+ active: true;
787
+ loaderId?: string;
788
+ handlerInvoked?: boolean;
789
+ }>());
781
790
 
782
791
  /**
783
792
  * Check if the current execution is inside a cache() DSL boundary.
@@ -822,8 +831,37 @@ export function runInsideLoaderScope<T>(fn: () => T): T {
822
831
  * and handler-invoked via ctx.use) so request-scoped reads inside a loader
823
832
  * never trip the cache-scope guards — loaders always run fresh.
824
833
  */
825
- export function runInsideLoaderBodyScope<T>(fn: () => T): T {
826
- return loaderBodyScopeALS.run({ active: true }, fn);
834
+ export function runInsideLoaderBodyScope<T>(
835
+ fn: () => T,
836
+ loaderId?: string,
837
+ handlerInvoked?: boolean,
838
+ ): T {
839
+ return loaderBodyScopeALS.run({ active: true, loaderId, handlerInvoked }, fn);
840
+ }
841
+
842
+ /**
843
+ * The $$id of the loader whose body is currently executing, or undefined
844
+ * outside any loader body. Used by the shell-capture identity guard
845
+ * (cookie-store.ts) so its refusal warning can name the loader that read
846
+ * cookies()/headers() instead of blaming a lane it cannot see — the old
847
+ * hardcoded "bake-lane loader" text misled a live-lane debugging session
848
+ * (issue #672, secondary).
849
+ */
850
+ export function getCurrentLoaderBodyId(): string | undefined {
851
+ return loaderBodyScopeALS.getStore()?.loaderId;
852
+ }
853
+
854
+ /**
855
+ * True while a HANDLER-invoked loader body (`await ctx.use(Loader)` from a
856
+ * handler, not the DSL segment funnel) is executing. The consumption-lane
857
+ * rule keys off this: handler consumption yields a BAKED copy in every shared
858
+ * artifact — cache(), "use cache", and the PPR shell — so the shell-capture
859
+ * identity guard (cookie-store.ts) permits cookies()/headers() here, exactly
860
+ * like the cache-purity guards do. DSL segment loaders (live lane masked at
861
+ * capture, bake lane guarded) never set the flag.
862
+ */
863
+ export function isInsideHandlerInvokedLoaderBody(): boolean {
864
+ return loaderBodyScopeALS.getStore()?.handlerInvoked === true;
827
865
  }
828
866
 
829
867
  // Scope for handle PUSH CALLBACKS (push(() => ...), including async ones).
@@ -9,7 +9,11 @@
9
9
 
10
10
  import type { CookieOptions } from "../router/middleware-types.js";
11
11
  import { getRequestContext, _getRequestContext } from "./request-context.js";
12
- import { isInsideCacheScope } from "./context.js";
12
+ import {
13
+ isInsideCacheScope,
14
+ getCurrentLoaderBodyId,
15
+ isInsideHandlerInvokedLoaderBody,
16
+ } from "./context.js";
13
17
  import { INSIDE_CACHE_EXEC } from "../cache/taint.js";
14
18
 
15
19
  /**
@@ -139,9 +143,19 @@ function assertNotInsideCacheContext(ctx: unknown, fnName: string): void {
139
143
  * shell-capture.ts). The captured shell prelude is shared across every user
140
144
  * hitting the URL, so a request-scoped read here would bake one user's
141
145
  * cookies/headers into markup served to others — same hazard as the cache
142
- * scopes above, at the document tier. Loaders need no exemption: they are
143
- * masked (never executed) during capture and remain the per-request holes of
144
- * the shell.
146
+ * scopes above, at the document tier. DSL segment loaders need no exemption:
147
+ * the live lane is masked (never executed) during capture, and the bake lane
148
+ * is exactly what this guard exists for.
149
+ *
150
+ * HANDLER-INVOKED loader bodies (`await ctx.use(Loader)` from a handler) are
151
+ * EXEMPT — the consumption-lane rule: handler consumption yields a BAKED
152
+ * shared copy in every artifact tier, and the cache-purity guards above
153
+ * already permit identity reads there (cache()/"use cache" bake the same
154
+ * reads today). Guarding only the PPR tier made the same code legal under
155
+ * cache() but capture-refusing under ppr (issue #672 / #674). The trade is
156
+ * documented: an identity read in a handler-consumed loader bakes the CAPTURE
157
+ * request's value into the shared shell; client-side consumption (useLoader)
158
+ * is the live lane.
145
159
  *
146
160
  * Keys off `_shellCaptureRun`, NOT the `_shellCapture` descriptor: the descriptor
147
161
  * is also present during the FOREGROUND render (it means "a capture is wanted"),
@@ -164,12 +178,19 @@ function assertNotInsideShellCapture(ctx: unknown, fnName: string): void {
164
178
  typeof ctx === "object" &&
165
179
  (ctx as { _shellCaptureRun?: unknown })._shellCaptureRun === true
166
180
  ) {
181
+ if (isInsideHandlerInvokedLoaderBody()) return;
167
182
  // Flag the capture context BEFORE throwing: inside an executing bake-lane
168
183
  // loader this throw is swallowed by wrapLoaderPromise into per-loader error
169
184
  // UI, which would bake silently into the shared shell. The capture checks
170
- // the flag after the render and refuses (shell-capture.ts).
185
+ // the flag after the render and refuses (shell-capture.ts). Also record
186
+ // WHICH loader body (if any) made the read, so the refusal warning can
187
+ // name the real source instead of hardcoding a lane — the read may come
188
+ // from a bake-lane loader OR from handler/render code (issue #672).
171
189
  (ctx as { _shellCaptureGuardTripped?: string })._shellCaptureGuardTripped =
172
190
  fnName;
191
+ (
192
+ ctx as { _shellCaptureGuardTrippedLoaderId?: string }
193
+ )._shellCaptureGuardTrippedLoaderId = getCurrentLoaderBodyId();
173
194
  throw new Error(
174
195
  `${fnName}() cannot be called while capturing a shared shell ` +
175
196
  `(shell-cache middleware). The captured shell is served to every user ` +
@@ -220,6 +220,16 @@ export interface RequestContext<
220
220
  */
221
221
  _shellCaptureGuardTripped?: string;
222
222
 
223
+ /**
224
+ * @internal The loader $$id whose BODY was executing when the capture guard
225
+ * tripped (read off the loader-body ALS scope at trip time), or undefined
226
+ * when the read came from handler/render code. Only used to make the
227
+ * once-per-key refusal warning name the real source — the old text
228
+ * hardcoded "a bake-lane loader", which misattributed handler-land reads
229
+ * and sent users debugging the wrong lane (issue #672, secondary).
230
+ */
231
+ _shellCaptureGuardTrippedLoaderId?: string;
232
+
223
233
  /**
224
234
  * @internal Handler-owned registry of explicit per-scope stores from
225
235
  * cache({ store }). Created once per createRSCHandler() and threaded into
@@ -468,6 +478,15 @@ export interface RequestContext<
468
478
  /** @internal Request-scoped performance metrics store */
469
479
  _metricsStore?: MetricsStore;
470
480
 
481
+ /**
482
+ * @internal True request entry timestamp (performance.now() at handler entry).
483
+ * Set once at request-context creation (rsc/handler.ts) so a metrics store
484
+ * created MID-request — ctx.debugPerformance() or the getMetricsStore wrapper —
485
+ * anchors its timeline to the real request start instead of the opt-in moment,
486
+ * keeping phases that began before the opt-in at their true (non-negative) offset.
487
+ */
488
+ _handlerStart?: number;
489
+
471
490
  /** @internal Resolved platform phase-span tracing for this request (Cloudflare or OTel) */
472
491
  _tracing?: ResolvedTracing;
473
492
 
@@ -509,6 +528,7 @@ export type PublicRequestContext<
509
528
  | "_transitionWhen"
510
529
  | "_cacheStore"
511
530
  | "_shellCaptureRun"
531
+ | "_shellCaptureGuardTrippedLoaderId"
512
532
  | "_explicitTaggedStores"
513
533
  | "_requestTags"
514
534
  | "_cacheProfiles"
@@ -536,6 +556,7 @@ export type PublicRequestContext<
536
556
  | "_reportBackgroundError"
537
557
  | "_debugPerformance"
538
558
  | "_metricsStore"
559
+ | "_handlerStart"
539
560
  | "_basename"
540
561
  | "_setStatus"
541
562
  | "_rotateStateCookie"
@@ -1028,6 +1049,7 @@ export function createRequestContext<TEnv>(
1028
1049
 
1029
1050
  _reportedErrors: new WeakSet<object>(),
1030
1051
  _metricsStore: undefined,
1052
+ _handlerStart: undefined,
1031
1053
 
1032
1054
  _renderBarrier: null as any,
1033
1055
  _resolveRenderBarrier: null as any,