@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.
- package/dist/vite/index.js +23 -6
- package/package.json +2 -2
- package/skills/cache-guide/SKILL.md +3 -1
- package/skills/caching/SKILL.md +23 -2
- package/skills/catalog.json +6 -0
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/loader/SKILL.md +5 -0
- package/skills/migrate-nextjs/SKILL.md +4 -2
- package/skills/parallel/SKILL.md +2 -0
- package/skills/ppr/SKILL.md +34 -10
- package/skills/rango/SKILL.md +10 -0
- package/skills/use-cache/SKILL.md +12 -2
- package/src/browser/partial-update.ts +7 -0
- package/src/cache/cache-key-utils.ts +29 -0
- package/src/cache/cache-scope.ts +2 -17
- package/src/cache/cache-tag.ts +60 -14
- package/src/cache/cf/cf-cache-store.ts +54 -20
- package/src/cache/document-cache.ts +17 -11
- package/src/cache/vercel/vercel-cache-store.ts +9 -19
- package/src/redirect-origin.ts +14 -0
- package/src/route-map-builder.ts +17 -3
- package/src/router/lazy-includes.ts +8 -2
- package/src/router/loader-resolution.ts +14 -2
- package/src/router/match-handlers.ts +11 -6
- package/src/router/middleware.ts +4 -1
- package/src/router/telemetry.ts +9 -1
- package/src/router.ts +7 -8
- package/src/rsc/handler.ts +9 -2
- package/src/rsc/redirect-guard.ts +2 -1
- package/src/rsc/rsc-rendering.ts +35 -2
- package/src/rsc/shell-capture.ts +93 -20
- package/src/server/context.ts +47 -9
- package/src/server/cookie-store.ts +26 -5
- package/src/server/request-context.ts +22 -0
- package/src/ssr/index.tsx +145 -107
- package/src/testing/dispatch.ts +7 -0
- package/src/vite/inject-client-debug.ts +64 -12
- package/src/vite/router-discovery.ts +9 -1
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -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(
|
|
574
|
+
`<script>location.replace(${JSON.stringify(safeTarget)})</script>`,
|
|
542
575
|
),
|
|
543
576
|
);
|
|
544
577
|
}
|
package/src/rsc/shell-capture.ts
CHANGED
|
@@ -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).
|
|
491
|
-
*
|
|
492
|
-
*
|
|
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
|
-
//
|
|
780
|
-
//
|
|
781
|
-
//
|
|
782
|
-
//
|
|
783
|
-
//
|
|
784
|
-
|
|
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
|
-
|
|
884
|
-
"
|
|
885
|
-
"(the live lane,
|
|
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
|
-
|
|
1120
|
+
shellTags,
|
|
1048
1121
|
);
|
|
1049
1122
|
} catch (error) {
|
|
1050
1123
|
// Best-effort: a failed put must never throw out of the background task.
|
package/src/server/context.ts
CHANGED
|
@@ -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
|
|
774
|
-
// request-scoped reads
|
|
775
|
-
//
|
|
776
|
-
//
|
|
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<{
|
|
779
|
-
|
|
780
|
-
|
|
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>(
|
|
826
|
-
|
|
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 {
|
|
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.
|
|
143
|
-
* masked (never executed) during capture and
|
|
144
|
-
*
|
|
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,
|