@rangojs/router 0.17.0 → 0.18.0

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 (152) hide show
  1. package/README.md +3 -2
  2. package/dist/types/browser/cookie-name.d.ts +6 -0
  3. package/dist/types/browser/history-state.d.ts +16 -0
  4. package/dist/types/cache/cache-key-utils.d.ts +17 -2
  5. package/dist/types/cache/cache-policy.d.ts +17 -0
  6. package/dist/types/cache/cache-scope.d.ts +98 -14
  7. package/dist/types/cache/cache-tag.d.ts +92 -7
  8. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  9. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  10. package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
  11. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  12. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  13. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
  14. package/dist/types/cache/handle-snapshot.d.ts +4 -3
  15. package/dist/types/cache/index.d.ts +1 -0
  16. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  17. package/dist/types/cache/shell-memo.d.ts +178 -0
  18. package/dist/types/cache/shell-snapshot.d.ts +110 -21
  19. package/dist/types/cache/tag-invalidation.d.ts +30 -3
  20. package/dist/types/cache/types.d.ts +207 -43
  21. package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
  22. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  23. package/dist/types/router/instrument.d.ts +8 -0
  24. package/dist/types/router/middleware-types.d.ts +6 -1
  25. package/dist/types/router/segment-resolution/loader-cache.d.ts +40 -11
  26. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +21 -10
  27. package/dist/types/router/segment-resolution/mask-nested.d.ts +65 -24
  28. package/dist/types/rsc/full-payload.d.ts +14 -0
  29. package/dist/types/rsc/handler.d.ts +1 -1
  30. package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
  31. package/dist/types/rsc/shell-capture-constants.d.ts +53 -6
  32. package/dist/types/rsc/shell-capture.d.ts +146 -15
  33. package/dist/types/rsc/shell-serve.d.ts +140 -17
  34. package/dist/types/rsc/types.d.ts +7 -0
  35. package/dist/types/server/context.d.ts +109 -15
  36. package/dist/types/server/cookie-store.d.ts +21 -0
  37. package/dist/types/server/request-context.d.ts +159 -33
  38. package/dist/types/ssr/index.d.ts +9 -0
  39. package/dist/types/ssr/ssr-root.d.ts +9 -8
  40. package/dist/types/testing/flight.entry.d.ts +7 -0
  41. package/dist/types/testing/run-loader.d.ts +18 -3
  42. package/dist/types/testing/serve-shell-request.d.ts +132 -0
  43. package/dist/types/testing/shell-status.d.ts +23 -10
  44. package/dist/types/types/handler-context.d.ts +14 -1
  45. package/dist/types/urls/pattern-types.d.ts +22 -18
  46. package/dist/types/vite/router-discovery.d.ts +29 -2
  47. package/dist/vite/index.js +46 -9
  48. package/package.json +1 -1
  49. package/skills/breadcrumbs/SKILL.md +17 -7
  50. package/skills/cache-guide/SKILL.md +123 -51
  51. package/skills/caching/SKILL.md +253 -23
  52. package/skills/catalog.json +1 -1
  53. package/skills/client-urls/SKILL.md +11 -6
  54. package/skills/cloudflare/SKILL.md +15 -0
  55. package/skills/comparison/references/framework-comparison.md +44 -32
  56. package/skills/defer-hydration/SKILL.md +50 -30
  57. package/skills/deployment-caching/SKILL.md +5 -2
  58. package/skills/document-cache/SKILL.md +8 -2
  59. package/skills/handler-use/SKILL.md +2 -0
  60. package/skills/hooks/handle-and-actions.md +3 -1
  61. package/skills/hooks/state.md +3 -1
  62. package/skills/loader/SKILL.md +161 -39
  63. package/skills/middleware/SKILL.md +16 -9
  64. package/skills/migrate-nextjs/SKILL.md +67 -36
  65. package/skills/migrate-react-router/SKILL.md +2 -1
  66. package/skills/migrate-react-router/component-migration.md +3 -1
  67. package/skills/migrate-react-router/data-and-actions.md +3 -1
  68. package/skills/migrate-react-router/route-mapping.md +7 -4
  69. package/skills/observability/SKILL.md +64 -0
  70. package/skills/parallel/SKILL.md +13 -8
  71. package/skills/ppr/SKILL.md +655 -363
  72. package/skills/prerender/SKILL.md +27 -14
  73. package/skills/rango/SKILL.md +52 -33
  74. package/skills/route/SKILL.md +12 -7
  75. package/skills/router-setup/SKILL.md +4 -2
  76. package/skills/scripts/SKILL.md +8 -1
  77. package/skills/server-actions/SKILL.md +11 -0
  78. package/skills/shell-manifest/SKILL.md +21 -12
  79. package/skills/testing/SKILL.md +25 -21
  80. package/skills/testing/cache-prerender.md +15 -14
  81. package/skills/testing/loader.md +18 -1
  82. package/skills/testing/serve-shell-request.md +188 -0
  83. package/skills/theme/SKILL.md +52 -4
  84. package/skills/typesafety/env-and-bindings.md +10 -2
  85. package/skills/use-cache/SKILL.md +39 -16
  86. package/skills/vercel/SKILL.md +14 -2
  87. package/skills/view-transitions/SKILL.md +1 -1
  88. package/src/browser/cookie-name.ts +23 -0
  89. package/src/browser/history-state.ts +23 -0
  90. package/src/browser/rsc-router.tsx +5 -0
  91. package/src/cache/cache-key-utils.ts +22 -2
  92. package/src/cache/cache-policy.ts +22 -0
  93. package/src/cache/cache-runtime.ts +34 -7
  94. package/src/cache/cache-scope.ts +345 -33
  95. package/src/cache/cache-tag.ts +375 -11
  96. package/src/cache/cf/cf-base64.ts +36 -4
  97. package/src/cache/cf/cf-cache-constants.ts +12 -0
  98. package/src/cache/cf/cf-cache-store.ts +990 -348
  99. package/src/cache/cf/cf-cache-types.ts +32 -4
  100. package/src/cache/cf/cf-shell-frame.ts +258 -0
  101. package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
  102. package/src/cache/handle-snapshot.ts +4 -3
  103. package/src/cache/index.ts +2 -0
  104. package/src/cache/isolate-tag-memo.ts +308 -0
  105. package/src/cache/shell-memo.ts +341 -0
  106. package/src/cache/shell-snapshot.ts +217 -27
  107. package/src/cache/tag-invalidation.ts +104 -19
  108. package/src/cache/types.ts +211 -43
  109. package/src/cache/vercel/vercel-cache-store.ts +545 -61
  110. package/src/prerender/build-shell-capture.ts +65 -11
  111. package/src/route-definition/dsl-helpers.ts +4 -3
  112. package/src/router/handler-context.ts +38 -4
  113. package/src/router/instrument.ts +54 -2
  114. package/src/router/intercept-resolution.ts +19 -7
  115. package/src/router/loader-resolution.ts +63 -32
  116. package/src/router/match-api.ts +28 -9
  117. package/src/router/match-handlers.ts +5 -0
  118. package/src/router/match-middleware/cache-lookup.ts +17 -0
  119. package/src/router/match-middleware/cache-store.ts +31 -11
  120. package/src/router/middleware-types.ts +6 -1
  121. package/src/router/middleware.ts +23 -4
  122. package/src/router/prerender-match.ts +6 -0
  123. package/src/router/segment-resolution/fresh.ts +18 -9
  124. package/src/router/segment-resolution/helpers.ts +4 -2
  125. package/src/router/segment-resolution/loader-cache.ts +227 -56
  126. package/src/router/segment-resolution/loader-snapshot.ts +203 -51
  127. package/src/router/segment-resolution/mask-nested.ts +136 -42
  128. package/src/router/segment-resolution/revalidation.ts +27 -10
  129. package/src/rsc/full-payload.ts +25 -1
  130. package/src/rsc/handler.ts +14 -2
  131. package/src/rsc/progressive-enhancement.ts +3 -2
  132. package/src/rsc/response-cache-serve.ts +9 -6
  133. package/src/rsc/rsc-rendering.ts +601 -181
  134. package/src/rsc/shell-build-manifest.ts +27 -9
  135. package/src/rsc/shell-capture-constants.ts +60 -6
  136. package/src/rsc/shell-capture.ts +1001 -250
  137. package/src/rsc/shell-serve.ts +316 -34
  138. package/src/rsc/types.ts +7 -0
  139. package/src/server/context.ts +219 -28
  140. package/src/server/cookie-store.ts +87 -62
  141. package/src/server/request-context.ts +263 -38
  142. package/src/ssr/index.tsx +96 -37
  143. package/src/ssr/ssr-root.tsx +9 -8
  144. package/src/testing/flight.entry.ts +14 -0
  145. package/src/testing/run-loader.ts +63 -5
  146. package/src/testing/serve-shell-request.ts +378 -0
  147. package/src/testing/shell-status.ts +34 -12
  148. package/src/theme/ThemeProvider.tsx +7 -6
  149. package/src/types/handler-context.ts +14 -1
  150. package/src/urls/pattern-types.ts +22 -18
  151. package/src/vite/discovery/shell-prerender-phase.ts +9 -0
  152. package/src/vite/router-discovery.ts +14 -4
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import { type EntryData } from "../server/context.js";
19
19
  import type { SearchParamsFilter } from "../cache/search-params-filter.js";
20
- import type { DocumentShellCacheEntry, ShellCacheEntry, SegmentCacheStore } from "../cache/types.js";
20
+ import type { DocumentShellCacheEntry, ShellCacheEntry, ShellDocumentRead, ShellReadStats, ShellSnapshotFailure, ShellSnapshotRecord, SegmentCacheStore } from "../cache/types.js";
21
21
  /** Debug/status header the browser (and e2e assertions) can read: HIT | MISS. */
22
22
  export declare const SHELL_STATUS_HEADER = "x-rango-shell";
23
23
  /** Partial-navigation status header set only after captured PPR segments are consumed. */
@@ -94,6 +94,41 @@ export declare function resolvePprConfig(entry: EntryData | undefined | null): R
94
94
  * and lookup makes every shell request a permanent miss.
95
95
  */
96
96
  export declare function buildShellKey(url: URL, filter?: SearchParamsFilter): string;
97
+ export { navigationShellKey, partitionShellKey, } from "./shell-capture-constants.js";
98
+ /**
99
+ * The forced-MISS query marker. A document HIT that cannot finish its tail
100
+ * reloads the page with it (shellReloadScript); the serve gate renders a
101
+ * request carrying it on axis 1, with no shell read and no capture, so the
102
+ * reload can never be a HIT that degrades again. The handler drops it from
103
+ * the request on entry (withoutShellMissMarker) and keeps only the flag
104
+ * (RequestContext._shellForcedMiss), so nothing else sees it.
105
+ */
106
+ export declare const SHELL_MISS_PARAM: string;
107
+ /**
108
+ * The request without the forced-MISS marker, or undefined when it carries
109
+ * none. The handler calls it before anything reads the request: left in, the
110
+ * marker reached `ctx.request.url`, `originalUrl`, middleware, and the SSR
111
+ * search seed (so `useSearchParams` rendered it), and a `"use cache"` key
112
+ * built from the request URL was never read again. GET and HEAD only: the
113
+ * reload is a navigation, and a request with a body cannot be re-created
114
+ * without re-streaming it. The substring test keeps the URL parse off every
115
+ * other request.
116
+ */
117
+ export declare function withoutShellMissMarker(request: Request): Request | undefined;
118
+ /**
119
+ * The inline script a degraded document HIT ends with: reload once into a
120
+ * forced MISS. The marker in the URL is the loop bound — a request carrying
121
+ * it is never a HIT, so this script never runs on the reload, and if it ever
122
+ * did (a server that ignores the marker) it would return without touching
123
+ * the page. window.stop() runs only when it reloads: it keeps the half-sent
124
+ * Flight stream from being closed by DOMContentLoaded, which would throw
125
+ * React's "Connection closed" (#412) before the reload lands. `nonce` is the
126
+ * request's CSP nonce; a ppr route with an active nonce is axis 1
127
+ * (rsc-rendering.ts shellServePlan), so on a HIT it is normally absent.
128
+ */
129
+ export declare function shellReloadScript(nonce?: string): string;
130
+ /** An inline script a shell HIT's tail appends, carrying the CSP nonce if any. */
131
+ export declare function inlineShellScript(body: string, nonce?: string): string;
97
132
  /**
98
133
  * The shell key's search portion (`?`-prefixed sorted search with the
99
134
  * cache.searchParams filter applied, or "") — ALSO the string the capture and
@@ -117,16 +152,11 @@ export declare function shellSearchSeed(url: URL, filter?: SearchParamsFilter):
117
152
  */
118
153
  export declare function isValidShellHit(entry: ShellCacheEntry, buildVersion: string): boolean;
119
154
  /**
120
- * DOCUMENT-half integrity gate and type narrowing, run BEFORE the HIT response
121
- * commits: a stored entry whose prelude is not decodable base64 or whose
122
- * postponed blob is not parseable JSON would otherwise throw AFTER the 200 +
123
- * full static prelude flushed (`serveShellHit` decodes at stream construction,
124
- * `resumeShellHTML` parses in the tail) — the client gets a visually complete
125
- * page that never hydrates, re-served on every request until the entry ages
126
- * out (no eviction path exists; failure schedules no recapture by itself).
127
- * Checking here turns a corrupt entry (store-layer fault) into a plain MISS
128
- * the recapture overwrites. Cost: one duplicate decode/parse per HIT, sub-ms
129
- * against a prelude flush that dominates the path.
155
+ * DOCUMENT-half structural gate and type narrowing, without decoding the
156
+ * prelude: the prelude is a string and the postponed blob parses. Partial
157
+ * replay (which never serves the prelude) and the build-manifest read-through
158
+ * use it as-is; the document HIT path runs it inside
159
+ * {@link openShellDocument}, whose single decode is the prelude's check.
130
160
  *
131
161
  * navigationOnly entries store no document half (prelude/postponed absent) and
132
162
  * therefore never pass. The partial-replay path skips this gate for them
@@ -135,8 +165,46 @@ export declare function isValidShellHit(entry: ShellCacheEntry, buildVersion: st
135
165
  * capture).
136
166
  */
137
167
  export declare function hasIntactShellPayload(entry: ShellCacheEntry): entry is DocumentShellCacheEntry;
138
- /** Decode a base64 prelude back into bytes for stream composition. */
139
- export declare function base64ToBytes(b64: string): Uint8Array;
168
+ /** A document shell ready to serve: its prelude decoded exactly once. */
169
+ export interface ShellDocument {
170
+ entry: ShellCacheEntry;
171
+ prelude: Uint8Array;
172
+ /** React's postponed state, parse-checked (null = DATA variant). */
173
+ postponed: string | null;
174
+ /**
175
+ * The capture snapshot: on the entry, or still arriving on its own promise
176
+ * after a prelude-first read (SegmentCacheStore.readShellDocument), which
177
+ * only the tail awaits.
178
+ */
179
+ snapshot: ShellSnapshotRecord[] | undefined | Promise<ShellSnapshotRecord[] | undefined>;
180
+ /** Why a prelude-first read's snapshot is missing (ShellDocumentRead). */
181
+ snapshotFailure?: Promise<ShellSnapshotFailure | undefined>;
182
+ /** The prelude-first read's stats, when perf metrics are on. */
183
+ stats?: ShellReadStats;
184
+ }
185
+ /**
186
+ * Pre-commit integrity gate for a document HIT, and its only prelude decode.
187
+ * A stored entry whose prelude is not decodable base64 or whose postponed blob
188
+ * is not parseable JSON would otherwise throw AFTER the 200 + full static
189
+ * prelude flushed (`resumeShellHTML` parses in the tail) — the client gets a
190
+ * visually complete page that never hydrates, re-served on every request
191
+ * until the entry ages out. Returning null here turns a corrupt entry
192
+ * (store-layer fault) into a plain MISS the recapture overwrites. The decoded
193
+ * bytes are what serveShellHit enqueues, so the check costs no second decode.
194
+ * A prelude-first `read` (SegmentCacheStore.readShellDocument) already carries
195
+ * the raw bytes and a pending snapshot, so only its postponed blob is checked.
196
+ */
197
+ export declare function openShellDocument(entry: ShellCacheEntry, read?: Pick<ShellDocumentRead, "prelude" | "snapshot" | "snapshotFailure" | "stats">): ShellDocument | null;
198
+ /**
199
+ * Prelude enqueue granularity. A streaming compressor in front of the worker
200
+ * emits its first byte only after compressing the whole write it was handed:
201
+ * one 629 KB enqueue delayed the first compressed byte by the full prelude's
202
+ * compression time (brotli-6 5.1 ms, gzip-6 1.1 ms in Node zlib with a flush
203
+ * per write; workerd's CompressionStream emits nothing until the whole write is
204
+ * compressed), while 32 KB chunks emit within 0.1 ms and cost slightly more
205
+ * in total compression (0.55-0.70 ms) than one write (issue #941).
206
+ */
207
+ export declare const SHELL_PRELUDE_CHUNK_BYTES: number;
140
208
  /** True when the store implements the shell entry family. */
141
209
  export declare function hasShellFamily(store: SegmentCacheStore | undefined): store is SegmentCacheStore & {
142
210
  getShell: NonNullable<SegmentCacheStore["getShell"]>;
@@ -152,8 +220,22 @@ export declare function hasShellFamily(store: SegmentCacheStore | undefined): st
152
220
  export interface ShellTailTiming {
153
221
  key: string;
154
222
  outcome: "complete" | "redirect" | "error";
223
+ /** The capture snapshot available to the tail (read and parsed). */
224
+ snapshotMs?: number;
225
+ /** Snapshot bytes read after the commit (prelude-first stores). */
226
+ snapshotBytes?: number;
227
+ /** Snapshot bytes read after the commit, excluding the parse. */
228
+ snapshotReadMs?: number;
229
+ /** Snapshot JSON.parse CPU (reads 0 on a deployed worker: see bytes). */
230
+ snapshotParseMs?: number;
231
+ /** Snapshot records by family, e.g. `segment:1/item:5` (no commas: it rides a Server-Timing desc). */
232
+ snapshotRecords?: string;
233
+ /** Records the capture pruned from the snapshot, same format (ShellCacheEntry.prunedRecords). */
234
+ snapshotPruned?: string;
155
235
  /** Loader-family seed decode (only when the entry carried a snapshot). */
156
236
  seedMs?: number;
237
+ /** The seed decode's own duration: Flight deserialization, CPU only. */
238
+ seedCpuMs?: number;
157
239
  /** Tail router.match() settled. */
158
240
  matchMs?: number;
159
241
  /** Tail stream (resume output) handed to the response stream. */
@@ -168,12 +250,13 @@ export interface ShellTailTiming {
168
250
  tailBytes?: number;
169
251
  }
170
252
  /**
171
- * Compact single-line form for the console log and the dev Server-Timing
172
- * mirror's `desc`. Plain alphanumerics/`=`/`-` only — no quoted-string
173
- * escaping needed.
253
+ * Compact single-line form for the console log and the Server-Timing mirror's
254
+ * `desc`: alphanumerics and `=`, `-`, `:`, `/` only (records=segment:1 pruned=item:5),
255
+ * so no quoted-string escaping is needed. Offsets are from the commit; the
256
+ * `-cpu` fields are CPU-only durations.
174
257
  */
175
258
  export declare function describeShellTailTiming(timing: ShellTailTiming): string;
176
- /** Buffer one terminal tail timing for the dev Server-Timing mirror. */
259
+ /** Buffer one terminal tail timing for the Server-Timing mirror. */
177
260
  export declare function publishShellTailTiming(timing: ShellTailTiming): void;
178
261
  /**
179
262
  * Consume (read-and-clear) the buffered tail timing for `key`, so one tail
@@ -198,3 +281,43 @@ export declare function warnShellStoreMissingOnce(key: string): void;
198
281
  * the missing-store warning above (an undeclared route stays silent).
199
282
  */
200
283
  export declare function warnPprNonceActiveOnce(key: string): void;
284
+ /**
285
+ * Dev only (the caller gates on NODE_ENV): warn once per route that the
286
+ * route's cache() entry reduces an explicit `ppr.ttl` or `ppr.swr`. `shell`
287
+ * is what the capture stores for a record written just now
288
+ * (shell-capture.ts capShellWindow), so the message states the values the
289
+ * shell actually gets. A ppr value the cap leaves whole (or raises: the
290
+ * cap can move a record's stale time into the shell's swr) is silent, and
291
+ * so is a ppr config that sets no ttl/swr.
292
+ */
293
+ export declare function warnPprWindowCappedOnce(routeName: string, ppr: {
294
+ ttl?: number;
295
+ swr?: number;
296
+ }, shell: {
297
+ ttl: number;
298
+ swr: number;
299
+ }, cache: {
300
+ ttl: number;
301
+ swr: number;
302
+ }): void;
303
+ /**
304
+ * Whether a partitioned request needs no build-shell probe: its route was
305
+ * already warned, or its path was already found without one.
306
+ */
307
+ export declare function partitionBuildShellCheckDone(pathname: string, routeName: string | undefined): boolean;
308
+ /**
309
+ * Record a partitioned request's build-shell probe, and warn once per route
310
+ * when it found one: the route has a build-time shell its requests cannot
311
+ * read. The route's request partition (a `cache({ key })` enclosing it, at
312
+ * any depth (#970), or the store's keyGenerator) keys the shell per
313
+ * partition, and the build captured only
314
+ * the default one, so each partition captures at runtime. Same
315
+ * declared-intent-cannot-be-honored doctrine as the warnings above; a path
316
+ * with no build shell stays silent.
317
+ */
318
+ export declare function notePartitionBuildShellCheck(pathname: string, routeName: string | undefined, found: boolean): void;
319
+ /**
320
+ * @internal Reset the serve path's once-per-key warnings and buffered tail
321
+ * timings. Tests only (testing/serve-shell-request.ts resetShellTestState).
322
+ */
323
+ export declare function resetShellServeStateForTests(): void;
@@ -208,6 +208,13 @@ export interface SSRModule {
208
208
  origin?: string;
209
209
  /** Each component error the prerender reports (never the capture's own abort). */
210
210
  onError?: (error: unknown) => void;
211
+ /**
212
+ * React's errorInfo for each task still pending at the capture's abort
213
+ * (its componentStack is computed when read).
214
+ */
215
+ onAbortedTask?: (errorInfo: {
216
+ componentStack?: string;
217
+ } | undefined) => void;
211
218
  }) => Promise<{
212
219
  prelude: Uint8Array;
213
220
  postponed: string | null;
@@ -106,6 +106,15 @@ export type LoaderEntry = {
106
106
  * appears on navigation-lane entries.
107
107
  */
108
108
  awaitBeforeFlush?: true;
109
+ /**
110
+ * loader(Def, { ssr: false }) on every DSL evaluation, document and
111
+ * navigation: the loader's PPR bake lane (loader-cache.ts
112
+ * resolveLoaderData). The bake/seed key rides on it, so a client
113
+ * navigation's shell replay pins exactly the loaders a document HIT pins,
114
+ * whatever the entry's loading(). awaitBeforeFlush cannot carry that: a
115
+ * navigation evaluation never has it.
116
+ */
117
+ bake?: true;
109
118
  };
110
119
  /**
111
120
  * Segments state for intercept context
@@ -408,15 +417,64 @@ export declare function getParallelSlotCount(parallels: ParallelEntries | EntryD
408
417
  export declare function track(label: string, depth?: number): () => void;
409
418
  /**
410
419
  * Check if the current execution is inside a cache() DSL boundary.
411
- * Returns false inside loader execution — loaders are always fresh
412
- * (never cached), so non-cacheable reads are safe.
420
+ * Returns false inside loader execution: a route cache() does not store
421
+ * loader values, so non-cacheable reads are safe. A loader bound with its own
422
+ * cache() stores its value; recordLoaderIdentityRead guards that.
413
423
  */
414
424
  export declare function isInsideCacheScope(): boolean;
425
+ /**
426
+ * Throw if called during the ACTIVE background shell-capture render
427
+ * (`_shellCaptureRun` true on the derived request context built by
428
+ * shell-capture.ts). The captured shell is shared across every user hitting
429
+ * the URL, so a request-scoped read here would bake one user's cookies,
430
+ * headers, or per-request variables into markup served to others — same
431
+ * hazard as the cache scopes above, at the document tier. DSL segment loaders
432
+ * need no exemption: the live lane is masked (never executed) during capture,
433
+ * and the bake lane is exactly what this guard exists for.
434
+ *
435
+ * It covers everything the capture waits for, not only synchronous handler
436
+ * code: a promise the handler passes or pushes, an async server component,
437
+ * and a loader the handler awaits (`await ctx.use(Loader)`) all run under the
438
+ * capture context and all bake. The last one used to be exempt (the
439
+ * consumption-lane rule, #672/#674): while a HIT re-ran handlers, a slot
440
+ * handler's `ctx.use` of an identity loader rendered per visitor on the HIT.
441
+ * Now every HIT replays the capture's copy, so the exemption would bake the
442
+ * capturing request's identity into every visitor's page. The cache()
443
+ * guards above keep it (a cache() HIT is a separate tier).
444
+ *
445
+ * Keys off `_shellCaptureRun`, NOT the `_shellCapture` descriptor: the descriptor
446
+ * is also present during the FOREGROUND render (it means "a capture is wanted"),
447
+ * and the foreground must read cookies/headers normally to serve the real user.
448
+ * Only the derived capture context sets `_shellCaptureRun`.
449
+ *
450
+ * Applies only to the READ surfaces (cookies(), headers(), a { cache: false }
451
+ * variable, and the theme reads through tripShellCaptureGuard) whose values
452
+ * become markup. Response directives
453
+ * (invalidateClientCache(), keepClientCache()) stay callable: during capture
454
+ * they are header effects on a discarded response, and on the live HIT path
455
+ * the full pipeline runs so their headers flow to the client normally.
456
+ *
457
+ * The throw can be caught by the code that made the read, so the guard also
458
+ * flags the capture context; the capture refuses on the flag
459
+ * (shell-capture.ts refuseOnCaptureGuard), nothing is stored, and every
460
+ * request keeps getting the normal axis-1 render.
461
+ */
462
+ export declare function assertNotInsideShellCapture(ctx: unknown, fnName: string): void;
463
+ /**
464
+ * True when `ctx` is the active capture render (assertNotInsideShellCapture
465
+ * above). On true the capture context is flagged with the read (`surface`,
466
+ * e.g. "cookies()", "ctx.theme") and the fix the refusal warning gives, plus
467
+ * the loader body (if any) that made the read, and the caller throws.
468
+ */
469
+ export declare function tripShellCaptureGuard(ctx: unknown, surface: string, fix: string): boolean;
415
470
  /**
416
471
  * Read guard for a non-cacheable variable (`createVar({ cache: false })` or a
417
472
  * `ctx.set(..., { cache: false })` write). Callers check isNonCacheable() first
418
473
  * so ordinary reads never reach the scope lookups.
419
474
  *
475
+ * Throws during a PPR shell capture (`requestCtx` is the ambient request
476
+ * context; assertNotInsideShellCapture): the shell is shared per host+URL.
477
+ *
420
478
  * Throws inside a "use cache" body: the key does not include the value, so the
421
479
  * first caller's value would be stored and served to later callers (#925). A
422
480
  * loader body entered INSIDE the cached function is exempt, the same baked-copy
@@ -425,7 +483,47 @@ export declare function isInsideCacheScope(): boolean;
425
483
  * records the exec scope it was entered in, so the exemption holds only while
426
484
  * that is still the innermost one.
427
485
  */
428
- export declare function assertNonCacheableReadAllowed(keyOrVar: string | ContextVar<unknown>): void;
486
+ export declare function assertNonCacheableReadAllowed(keyOrVar: string | ContextVar<unknown>, requestCtx?: unknown): void;
487
+ /** How the identity error words a read: a function call or a property read. */
488
+ export type LoaderIdentityReadVerb = "called" | "read";
489
+ /** A request-identity read a loader execution made (recordLoaderIdentityRead). */
490
+ export interface LoaderIdentityRead {
491
+ /**
492
+ * The read as the error names it: "cookies()", "headers()",
493
+ * `ctx.get() for a non-cacheable variable "x"`, "ctx.theme" or
494
+ * "getRequestContext().theme".
495
+ */
496
+ surface: string;
497
+ /** "called" for a function, "read" for a property (the theme getters). */
498
+ verb: LoaderIdentityReadVerb;
499
+ /** The loader body that made the read. */
500
+ bodyId: string | undefined;
501
+ /**
502
+ * The cached loader whose value carries a read another loader made
503
+ * (loader-cache.ts identity mark): its readers read it through `via`.
504
+ */
505
+ via?: string;
506
+ }
507
+ /**
508
+ * Record a request-identity read (cookies(), headers(), a non-cacheable
509
+ * ctx.get()) on the current loader execution (#972).
510
+ *
511
+ * isInsideCacheScope() exempts loader bodies because a route cache() never
512
+ * stores their values. A loader bound with its own cache() does: with no key()
513
+ * and no store keyGenerator its entry is keyed by loader, host, path and
514
+ * params only, and an enclosing route cache() key does not partition it
515
+ * (#974). A cookies() read there stored the first visitor's session and served
516
+ * it to everyone for the TTL. The read is recorded on the execution, not
517
+ * thrown at the call: a reader that starts the loader before its binding does
518
+ * runs it outside any fill, and the binding's MISS then reuses that run. The
519
+ * fill checks what its execution recorded, through the same links as its tags
520
+ * (cache-tag.ts recordedIdentityRead), so both orders fail the same way.
521
+ * Response directives do not record: a key cannot make a skipped body's side
522
+ * effect reach a HIT.
523
+ */
524
+ export declare function recordLoaderIdentityRead(surface: string, verb?: LoaderIdentityReadVerb): void;
525
+ /** The error a loader cache() fill with no declared key fails with (#972). */
526
+ export declare function loaderCacheIdentityError(read: LoaderIdentityRead, cachedLoaderId: string): Error;
429
527
  /**
430
528
  * Check if the current execution is inside a DSL loader scope
431
529
  * (wrapped by runInsideLoaderScope). Used by rendered() barrier
@@ -439,6 +537,10 @@ export declare function isInsideLoaderScope(): boolean;
439
537
  * Takes scalars so the already-latched path allocates nothing.
440
538
  */
441
539
  export declare function latchCachedHeaderScope(kind: CachedHeaderScope["kind"], routeKey?: string): void;
540
+ /** True inside ANY loader execution — DSL loader scope or a loader body
541
+ * (however invoked). The "loaders always re-run fresh" exemptions key off
542
+ * this. */
543
+ export declare function isInsideAnyLoaderScope(): boolean;
442
544
  /**
443
545
  * The one ppr opt-in predicate: a page route entry that DECLARED `ppr`
444
546
  * (`false` and undefined mean plain axis 1). Shared by the serve path
@@ -511,11 +613,13 @@ export declare function runInsideLoaderScope<T>(fn: () => T): T;
511
613
  * and handler-invoked via ctx.use) so request-scoped reads inside a loader
512
614
  * never trip the cache-scope guards — loaders always run fresh.
513
615
  */
514
- export declare function runInsideLoaderBodyScope<T>(fn: () => T, loaderId?: string, handlerInvoked?: boolean): T;
616
+ export declare function runInsideLoaderBodyScope<T>(fn: () => T, loaderId?: string, tags?: Set<string>): T;
617
+ /** The innermost loader body's execution tag set (runInsideLoaderBodyScope). */
618
+ export declare function getLoaderBodyTags(): Set<string> | undefined;
515
619
  /**
516
620
  * The $$id of the loader whose body is currently executing, or undefined
517
621
  * outside any loader body. Used by the shell-capture identity guard
518
- * (cookie-store.ts) so its refusal warning can name the loader that read
622
+ * (assertNotInsideShellCapture) so its refusal warning can name the loader that read
519
623
  * cookies()/headers() instead of blaming a lane it cannot see — the old
520
624
  * hardcoded "bake-lane loader" text misled a live-lane debugging session
521
625
  * (issue #672, secondary).
@@ -528,16 +632,6 @@ export declare function getCurrentLoaderBodyId(): string | undefined;
528
632
  * reader runs in that request only reaches the page through the replay.
529
633
  */
530
634
  export declare function isInsideLoaderBody(loaderId: string): boolean;
531
- /**
532
- * True while a HANDLER-invoked loader body (`await ctx.use(Loader)` from a
533
- * handler, not the DSL segment funnel) is executing. The consumption-lane
534
- * rule keys off this: handler consumption yields a BAKED copy in every shared
535
- * artifact — cache(), "use cache", and the PPR shell — so the shell-capture
536
- * identity guard (cookie-store.ts) permits cookies()/headers() here, exactly
537
- * like the cache-purity guards do. DSL segment loaders (live lane masked at
538
- * capture, bake lane guarded) never set the flag.
539
- */
540
- export declare function isInsideHandlerInvokedLoaderBody(): boolean;
541
635
  /**
542
636
  * Check if the current execution is inside a handle push callback (sync or an
543
637
  * async callback's continuation). Used by the handler-to-loader deadlock guard
@@ -63,6 +63,27 @@ export interface ReadonlyHeaders {
63
63
  [Symbol.iterator](): HeadersIterator<[string, string]>;
64
64
  }
65
65
  type HeadersIterator<T> = IterableIterator<T>;
66
+ /** The public reads of the visitor's theme cookie that assertThemeReadAllowed guards. */
67
+ export type ThemeReadSurface = "ctx.theme" | "getRequestContext().theme";
68
+ /**
69
+ * Guard for a read of the visitor's theme: the handler and middleware
70
+ * `ctx.theme` (handler-context.ts, middleware.ts) and `getRequestContext().theme`
71
+ * (request-context.ts). The theme is the visitor's theme cookie, so it is an
72
+ * identity read like cookies(): it refuses in the same scopes, in the same
73
+ * order (refusingCacheScope, then tripShellCaptureGuard in context.ts), with
74
+ * the same exemption (a loader body under cache(); at capture there is none).
75
+ * Before #971 each read was a plain cookie read, so a ppr shell, a cache()
76
+ * entry or a "use cache" entry stored the first visitor's theme and served it
77
+ * to every later visitor.
78
+ *
79
+ * A read the guards allow is recorded on the current loader execution, as a
80
+ * cookies() read method records it (#972, recordLoaderIdentityRead).
81
+ *
82
+ * `ctx` is the request context at read time, as cookies() reads it. The
83
+ * router's own payload read (payloadInitialTheme, rsc/full-payload.ts) goes
84
+ * through the unguarded `_readTheme()`.
85
+ */
86
+ export declare function assertThemeReadAllowed(ctx: unknown, surface: ThemeReadSurface): void;
66
87
  /**
67
88
  * Get the original request headers (read-only).
68
89
  *