@rangojs/router 0.0.0-experimental.140 → 0.0.0-experimental.141

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.
@@ -109,7 +109,7 @@ stated, greppable contract.
109
109
  | modal / soft navigation | `intercept()` | /intercept |
110
110
  | pre-render a route at build time | `Prerender(...)` wrapper | /prerender |
111
111
  | feed live loaders from a cached shell | replayed handle + `ctx.rendered()` | /shell-manifest |
112
- | cache the HTML shell, keep loaders live | `createShellCacheMiddleware()` | /ppr |
112
+ | cache the HTML shell, keep loaders live | `ppr` path option | /ppr |
113
113
  | stream SSE / upgrade a WebSocket | `path.stream()` / `path.any()` | /streams-and-websockets |
114
114
 
115
115
  ## Invariants
@@ -153,7 +153,7 @@ Same words, different jobs — this is the most common source of the
153
153
  | Next.js `revalidateTag` / `updateTag` | **Axis 1** (cache) | Cache busting by tag. Tag via `cache({ tags })` / `cacheTag(...tags)`; invalidate with `updateTag(...tags)` (awaitable, read-your-own-writes) or `revalidateTag(...tags)` (background, non-blocking). Built-in stores index by tag. No `revalidatePath` (path-based busting); use tags. |
154
154
  | React Router / Remix `shouldRevalidate` | **Axis 2** | This is the correct mental model for Rango's `revalidate()`. |
155
155
  | HTTP `Cache-Control` / ISR | **Axis 1** | Edge/document layer — see `/document-cache`. Separate from both `cache()` and `revalidate()`. |
156
- | Next.js PPR (partial prerendering) | HTML shell layer | Same idea, different wiring: opt-in `createShellCacheMiddleware()` captures at runtime (no build-time default), and holes are route-level `loading()` boundaries — a hand-rolled `<Suspense>` is not a hole. See `/ppr`. |
156
+ | Next.js PPR (partial prerendering) | HTML shell layer | Same idea, different wiring: the opt-in `ppr` path option captures at runtime (no build-time default); holes are render-defined — `loading()` subtrees plus pending promises under a consumer's own `<Suspense>`. See `/ppr`. |
157
157
  | Remix/RR `loader` | live data | Like Rango loaders, fresh per request — but Rango loaders run in parallel and stream (latency overlaps first paint), and can opt into caching on demand. |
158
158
 
159
159
  See `/cache-guide` for the axis-1 decision guide, `/loader` and `/route` for
@@ -211,6 +211,10 @@ interface KVShellEnvelope {
211
211
  t?: string[];
212
212
  /** Timestamp when tags were attached (ms epoch) */
213
213
  ta?: number;
214
+ /** initialTheme the capture render was built with (resume theme fidelity) */
215
+ i?: string;
216
+ /** Capture data snapshot: recorded cache-store hits/writes for HIT parity */
217
+ sn?: import("../types.js").ShellSnapshotRecord[];
214
218
  }
215
219
 
216
220
  /**
@@ -1651,6 +1655,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
1651
1655
  prelude: envelope.p,
1652
1656
  postponed: envelope.po,
1653
1657
  reactVersion: envelope.rv,
1658
+ initialTheme: envelope.i,
1659
+ snapshot: envelope.sn,
1654
1660
  createdAt: envelope.c,
1655
1661
  },
1656
1662
  shouldRevalidate,
@@ -1715,6 +1721,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
1715
1721
  e: staleAt + swrWindow * 1000,
1716
1722
  t: tags,
1717
1723
  ta: taggedAt,
1724
+ i: entry.initialTheme,
1725
+ sn: entry.snapshot,
1718
1726
  };
1719
1727
  return this.kv!.put(kvKey, JSON.stringify(envelope), {
1720
1728
  expirationTtl: totalTtl,
@@ -44,9 +44,4 @@ export {
44
44
  type DocumentCacheOptions,
45
45
  } from "./document-cache.js";
46
46
 
47
- export {
48
- createShellCacheMiddleware,
49
- type ShellCacheOptions,
50
- } from "./shell-cache.js";
51
-
52
47
  export type { CacheErrorCategory } from "./cache-error.js";
@@ -0,0 +1,368 @@
1
+ /**
2
+ * Capture data snapshot: recording + seeding stores for PPR shell parity.
3
+ *
4
+ * The scar tissue this fixes: a PPR HIT serves frozen prelude bytes, then a
5
+ * FULL FRESH Flight render for hydration. Any shell-baked (non-hole) content
6
+ * that drifts between capture time and hit time — a cache() segment with a
7
+ * shorter ttl than the shell, a tag-invalidated item — makes the fresh payload
8
+ * disagree with the prelude, so React throws a hydration text mismatch and
9
+ * regenerates the tree client-side (wiping the FOUC theme class, flashing
10
+ * content). See docs/design/ppr-shell-resume.md.
11
+ *
12
+ * The fix (Next.js resume-data-cache analog, adapted to Rango's cache rings):
13
+ * the CAPTURE render records every cache-store read-hit and write it performed
14
+ * (the {@link RecordingShellStore}); the record rides inside the ShellCacheEntry
15
+ * as its `snapshot`; on a HIT the tail render reads through a
16
+ * {@link SeededShellStore} overlay that serves those recorded values AS FRESH,
17
+ * so the shell region reproduces byte-identically while everything NOT recorded
18
+ * (the holes — masked loaders were never executed at capture, so their reads
19
+ * were never recorded) stays live.
20
+ *
21
+ * The invariant, verbatim: the snapshot is exactly the set of cache-store reads
22
+ * the capture render performed; replaying them on a HIT reproduces the shell
23
+ * content byte-identically; everything not recorded stays live.
24
+ */
25
+
26
+ import type {
27
+ SegmentCacheStore,
28
+ CacheGetResult,
29
+ CacheItemResult,
30
+ CacheItemOptions,
31
+ CachedEntryData,
32
+ ShellCacheEntry,
33
+ ShellSnapshotRecord,
34
+ ShellSnapshotItemValue,
35
+ ShellSnapshotResponseValue,
36
+ } from "./types.js";
37
+ import { bufferToBase64, base64ToBuffer } from "./cf/cf-base64.js";
38
+ import { isPerClientSignalHeader } from "../browser/cookie-name.js";
39
+
40
+ /** Compose the last-write-wins map key. NUL (`\u0000`) cannot appear in a cache key. */
41
+ function recordKey(family: ShellSnapshotRecord["family"], key: string): string {
42
+ return `${family}\u0000${key}`;
43
+ }
44
+
45
+ /** Serialize a Response to the snapshot's stored shape (base64 body). */
46
+ async function serializeResponse(
47
+ response: Response,
48
+ ): Promise<ShellSnapshotResponseValue> {
49
+ const body = await response.clone().arrayBuffer();
50
+ const headers: [string, string][] = [];
51
+ response.headers.forEach((value, name) => {
52
+ // Mirror putResponse: per-client signal headers never enter a shared entry.
53
+ if (isPerClientSignalHeader(name)) return;
54
+ headers.push([name, value]);
55
+ });
56
+ return { status: response.status, headers, body: bufferToBase64(body) };
57
+ }
58
+
59
+ /** Rebuild a live Response from a snapshot's stored response shape. */
60
+ function deserializeResponse(value: ShellSnapshotResponseValue): Response {
61
+ return new Response(base64ToBuffer(value.body), {
62
+ status: value.status,
63
+ headers: new Headers(value.headers),
64
+ });
65
+ }
66
+
67
+ /**
68
+ * A store wrapper the CAPTURE render reads through. Every call passes through to
69
+ * the underlying store unchanged; for the item/segment/response families it also
70
+ * RECORDS, last-write-wins per (family, key):
71
+ * - read-hits (get/getItem/getResponse returning non-null) — the value that
72
+ * fed the shell,
73
+ * - writes (set/setItem/putResponse) — the value a MISS computed and baked.
74
+ * The shell family (getShell/putShell) is never recorded (the snapshot rides
75
+ * inside a shell entry). Reads that MISS are not recorded (a miss produced no
76
+ * shell content; if the render then computed and wrote, that write is recorded).
77
+ *
78
+ * Deferred writes: cache writes run under waitUntil (fire-and-forget on Node,
79
+ * executionContext on workerd), so their setItem/set calls — hence their records
80
+ * — may land after the shell has quiesced. The capture collects those write
81
+ * promises via {@link trackWrite} and awaits them ({@link settleWrites}) before
82
+ * draining, so a MISS-at-capture value is still pinned.
83
+ */
84
+ export class RecordingShellStore<
85
+ TEnv = unknown,
86
+ > implements SegmentCacheStore<TEnv> {
87
+ private readonly records = new Map<string, ShellSnapshotRecord>();
88
+ private readonly writes: Promise<unknown>[] = [];
89
+
90
+ constructor(private readonly inner: SegmentCacheStore<TEnv>) {}
91
+
92
+ get defaults(): SegmentCacheStore<TEnv>["defaults"] {
93
+ return this.inner.defaults;
94
+ }
95
+ get keyGenerator(): SegmentCacheStore<TEnv>["keyGenerator"] {
96
+ return this.inner.keyGenerator;
97
+ }
98
+
99
+ private record(
100
+ family: ShellSnapshotRecord["family"],
101
+ key: string,
102
+ value: ShellSnapshotRecord["value"],
103
+ ): void {
104
+ this.records.set(recordKey(family, key), { family, key, value });
105
+ }
106
+
107
+ /** Track a deferred cache-write promise so the capture can await it pre-drain. */
108
+ trackWrite(p: Promise<unknown>): void {
109
+ this.writes.push(p);
110
+ }
111
+
112
+ /**
113
+ * Await the tracked deferred writes so their records are present before drain.
114
+ * Drains ITERATIVELY: a write task can schedule a NESTED write (the ring-3
115
+ * cacheRoute path schedules its actual store.set in a second waitUntil while the
116
+ * first is running), so each awaited batch may enqueue more. Loop until the
117
+ * queue empties or the deadline passes. Bounded: a pathologically slow write
118
+ * must never stall the capture task, so a key that does not settle in time is
119
+ * left unpinned (it drifts, the pre-snapshot behavior) rather than hanging.
120
+ */
121
+ async settleWrites(timeoutMs: number): Promise<void> {
122
+ const deadline = Date.now() + timeoutMs;
123
+ while (this.writes.length > 0) {
124
+ const remaining = deadline - Date.now();
125
+ if (remaining <= 0) return;
126
+ // Take the current batch; new writes scheduled while awaiting accumulate in
127
+ // this.writes and are drained on the next iteration.
128
+ const batch = this.writes.splice(0);
129
+ let timer: ReturnType<typeof setTimeout> | undefined;
130
+ const guard = new Promise<void>((resolve) => {
131
+ timer = setTimeout(resolve, remaining);
132
+ (timer as { unref?: () => void }).unref?.();
133
+ });
134
+ await Promise.race([Promise.allSettled(batch).then(() => {}), guard]);
135
+ if (timer) clearTimeout(timer);
136
+ }
137
+ }
138
+
139
+ /** The recorded snapshot (last-write-wins per family+key), or undefined if empty. */
140
+ drainSnapshot(): ShellSnapshotRecord[] | undefined {
141
+ return this.records.size > 0 ? [...this.records.values()] : undefined;
142
+ }
143
+
144
+ async get(key: string): Promise<CacheGetResult | null> {
145
+ const result = await this.inner.get(key);
146
+ if (result) this.record("segment", key, result.data);
147
+ return result;
148
+ }
149
+
150
+ async set(
151
+ key: string,
152
+ data: CachedEntryData,
153
+ ttl: number,
154
+ swr?: number,
155
+ ): Promise<void> {
156
+ this.record("segment", key, data);
157
+ return this.inner.set(key, data, ttl, swr);
158
+ }
159
+
160
+ async delete(key: string): Promise<boolean> {
161
+ return this.inner.delete(key);
162
+ }
163
+
164
+ async clear(): Promise<void> {
165
+ return this.inner.clear?.();
166
+ }
167
+
168
+ async getResponse(
169
+ key: string,
170
+ ): Promise<{ response: Response; shouldRevalidate: boolean } | null> {
171
+ if (!this.inner.getResponse) return null;
172
+ const result = await this.inner.getResponse(key);
173
+ if (result)
174
+ this.record("response", key, await serializeResponse(result.response));
175
+ return result;
176
+ }
177
+
178
+ async putResponse(
179
+ key: string,
180
+ response: Response,
181
+ ttl: number,
182
+ swr?: number,
183
+ tags?: string[],
184
+ ): Promise<void> {
185
+ if (!this.inner.putResponse) return;
186
+ this.record("response", key, await serializeResponse(response));
187
+ return this.inner.putResponse(key, response, ttl, swr, tags);
188
+ }
189
+
190
+ async getItem(key: string): Promise<CacheItemResult | null> {
191
+ if (!this.inner.getItem) return null;
192
+ const result = await this.inner.getItem(key);
193
+ if (result) {
194
+ const value: ShellSnapshotItemValue = {
195
+ value: result.value,
196
+ handles: result.handles,
197
+ tags: result.tags,
198
+ };
199
+ this.record("item", key, value);
200
+ }
201
+ return result;
202
+ }
203
+
204
+ async setItem(
205
+ key: string,
206
+ value: string,
207
+ options?: CacheItemOptions,
208
+ ): Promise<void> {
209
+ if (!this.inner.setItem) return;
210
+ const stored: ShellSnapshotItemValue = {
211
+ value,
212
+ handles: options?.handles,
213
+ tags: options?.tags,
214
+ };
215
+ this.record("item", key, stored);
216
+ return this.inner.setItem(key, value, options);
217
+ }
218
+
219
+ async getShell(
220
+ key: string,
221
+ ): Promise<{ entry: ShellCacheEntry; shouldRevalidate?: boolean } | null> {
222
+ return this.inner.getShell ? this.inner.getShell(key) : null;
223
+ }
224
+
225
+ async putShell(
226
+ key: string,
227
+ entry: ShellCacheEntry,
228
+ ttlSeconds?: number,
229
+ swrSeconds?: number,
230
+ tags?: string[],
231
+ ): Promise<void> {
232
+ return this.inner.putShell?.(key, entry, ttlSeconds, swrSeconds, tags);
233
+ }
234
+
235
+ async invalidateTags(tags: string[]): Promise<void> {
236
+ return this.inner.invalidateTags?.(tags);
237
+ }
238
+ }
239
+
240
+ /** True iff `store` is a RecordingShellStore (duck-typed across module copies). */
241
+ export function getRecordingStore<TEnv>(
242
+ store: SegmentCacheStore<TEnv> | undefined,
243
+ ): RecordingShellStore<TEnv> | undefined {
244
+ return store instanceof RecordingShellStore ? store : undefined;
245
+ }
246
+
247
+ /**
248
+ * A read-through overlay the HIT tail render reads through. For a key present in
249
+ * the snapshot it serves the recorded value AS FRESH (shouldRevalidate: false —
250
+ * a pinned key must NOT kick SWR background revalidation) so the tail's payload
251
+ * matches the frozen prelude. Every other read falls through to the real store
252
+ * (the holes — masked loaders were never recorded — stay live). ALL writes pass
253
+ * through unchanged: a live hole's loader may legitimately write. The shell
254
+ * family always passes through.
255
+ */
256
+ export class SeededShellStore<
257
+ TEnv = unknown,
258
+ > implements SegmentCacheStore<TEnv> {
259
+ private readonly items = new Map<string, ShellSnapshotItemValue>();
260
+ private readonly segments = new Map<string, CachedEntryData>();
261
+ private readonly responses = new Map<string, ShellSnapshotResponseValue>();
262
+
263
+ constructor(
264
+ private readonly inner: SegmentCacheStore<TEnv>,
265
+ snapshot: ShellSnapshotRecord[],
266
+ ) {
267
+ for (const rec of snapshot) {
268
+ if (rec.family === "item") {
269
+ this.items.set(rec.key, rec.value as ShellSnapshotItemValue);
270
+ } else if (rec.family === "segment") {
271
+ this.segments.set(rec.key, rec.value as CachedEntryData);
272
+ } else {
273
+ this.responses.set(rec.key, rec.value as ShellSnapshotResponseValue);
274
+ }
275
+ }
276
+ }
277
+
278
+ get defaults(): SegmentCacheStore<TEnv>["defaults"] {
279
+ return this.inner.defaults;
280
+ }
281
+ get keyGenerator(): SegmentCacheStore<TEnv>["keyGenerator"] {
282
+ return this.inner.keyGenerator;
283
+ }
284
+
285
+ async get(key: string): Promise<CacheGetResult | null> {
286
+ const seeded = this.segments.get(key);
287
+ if (seeded) return { data: seeded, shouldRevalidate: false };
288
+ return this.inner.get(key);
289
+ }
290
+
291
+ async set(
292
+ key: string,
293
+ data: CachedEntryData,
294
+ ttl: number,
295
+ swr?: number,
296
+ ): Promise<void> {
297
+ return this.inner.set(key, data, ttl, swr);
298
+ }
299
+
300
+ async delete(key: string): Promise<boolean> {
301
+ return this.inner.delete(key);
302
+ }
303
+
304
+ async clear(): Promise<void> {
305
+ return this.inner.clear?.();
306
+ }
307
+
308
+ async getResponse(
309
+ key: string,
310
+ ): Promise<{ response: Response; shouldRevalidate: boolean } | null> {
311
+ const seeded = this.responses.get(key);
312
+ if (seeded) {
313
+ return { response: deserializeResponse(seeded), shouldRevalidate: false };
314
+ }
315
+ return this.inner.getResponse ? this.inner.getResponse(key) : null;
316
+ }
317
+
318
+ async putResponse(
319
+ key: string,
320
+ response: Response,
321
+ ttl: number,
322
+ swr?: number,
323
+ tags?: string[],
324
+ ): Promise<void> {
325
+ return this.inner.putResponse?.(key, response, ttl, swr, tags);
326
+ }
327
+
328
+ async getItem(key: string): Promise<CacheItemResult | null> {
329
+ const seeded = this.items.get(key);
330
+ if (seeded) {
331
+ return {
332
+ value: seeded.value,
333
+ handles: seeded.handles,
334
+ tags: seeded.tags,
335
+ shouldRevalidate: false,
336
+ };
337
+ }
338
+ return this.inner.getItem ? this.inner.getItem(key) : null;
339
+ }
340
+
341
+ async setItem(
342
+ key: string,
343
+ value: string,
344
+ options?: CacheItemOptions,
345
+ ): Promise<void> {
346
+ return this.inner.setItem?.(key, value, options);
347
+ }
348
+
349
+ async getShell(
350
+ key: string,
351
+ ): Promise<{ entry: ShellCacheEntry; shouldRevalidate?: boolean } | null> {
352
+ return this.inner.getShell ? this.inner.getShell(key) : null;
353
+ }
354
+
355
+ async putShell(
356
+ key: string,
357
+ entry: ShellCacheEntry,
358
+ ttlSeconds?: number,
359
+ swrSeconds?: number,
360
+ tags?: string[],
361
+ ): Promise<void> {
362
+ return this.inner.putShell?.(key, entry, ttlSeconds, swrSeconds, tags);
363
+ }
364
+
365
+ async invalidateTags(tags: string[]): Promise<void> {
366
+ return this.inner.invalidateTags?.(tags);
367
+ }
368
+ }
@@ -240,10 +240,76 @@ export interface ShellCacheEntry {
240
240
  postponed: string | null;
241
241
  /** React.version captured at prerender time; the read-time invalidation gate. */
242
242
  reactVersion: string;
243
+ /**
244
+ * The initialTheme the CAPTURE render was built with (the derived context's
245
+ * reqCtx.theme). The resume tail must render ThemeProvider with the SAME
246
+ * initialTheme the frozen prelude was rendered with: React resume requires the
247
+ * tree above the holes to match the prerendered tree, and initialTheme is
248
+ * per-request METADATA, not part of the cached segments — a visitor whose
249
+ * theme differs from the capturer's would otherwise produce a divergent resume
250
+ * tree (broken stitching/hydration). The visitor's real theme is applied
251
+ * pre-paint by the FOUC script and re-synced from the cookie post-mount by
252
+ * ThemeProvider.
253
+ */
254
+ initialTheme?: string;
255
+ /**
256
+ * The CAPTURE DATA SNAPSHOT: every cache-store read-hit and write the capture
257
+ * render performed, in stored/serialized form. Replaying these on a HIT (via
258
+ * the SeededShellStore overlay, for the tail render only) reproduces the
259
+ * shell's cached content byte-identically, so the freshly rendered hydration
260
+ * payload matches the frozen prelude even after the underlying cache entries
261
+ * have drifted (expired, been recomputed, or been tag-invalidated).
262
+ *
263
+ * Optional: an entry captured before this field existed simply has no
264
+ * snapshot and keeps the pre-snapshot behavior (the tail reads live, so any
265
+ * shell-baked cached value that drifted mismatches the prelude). Recapture
266
+ * heals it. See docs/design/ppr-shell-resume.md ("the capture data snapshot").
267
+ */
268
+ snapshot?: ShellSnapshotRecord[];
243
269
  /** Epoch ms when the shell was captured. */
244
270
  createdAt: number;
245
271
  }
246
272
 
273
+ /**
274
+ * The cache-store families a shell snapshot pins. Excludes the shell family
275
+ * itself (getShell/putShell) — the snapshot rides INSIDE a shell entry, so
276
+ * recording it would be self-referential.
277
+ */
278
+ export type ShellSnapshotFamily = "item" | "segment" | "response";
279
+
280
+ /** A serialized cached Response for the response family of a shell snapshot. */
281
+ export interface ShellSnapshotResponseValue {
282
+ status: number;
283
+ /** Client-facing header pairs (per-client signal headers excluded at record). */
284
+ headers: [string, string][];
285
+ /** base64-encoded response body (binary-safe, JSON-serializable). */
286
+ body: string;
287
+ }
288
+
289
+ /** The stored form of an item-family (use cache / loader cache) snapshot value. */
290
+ export interface ShellSnapshotItemValue {
291
+ /** RSC-serialized return value. */
292
+ value: string;
293
+ /** RSC-encoded handle data, if any. */
294
+ handles?: string;
295
+ /** The entry's cache tags. */
296
+ tags?: string[];
297
+ }
298
+
299
+ /**
300
+ * One recorded cache-store read-hit or write from the capture render. `value`
301
+ * carries the entry in its stored/serialized shape so it round-trips through a
302
+ * JSON-serializing store (KV, CF, Vercel) with the rest of the ShellCacheEntry:
303
+ * - `item` -> {@link ShellSnapshotItemValue}
304
+ * - `segment` -> {@link CachedEntryData} (already JSON-able)
305
+ * - `response`-> {@link ShellSnapshotResponseValue}
306
+ */
307
+ export interface ShellSnapshotRecord {
308
+ family: ShellSnapshotFamily;
309
+ key: string;
310
+ value: ShellSnapshotItemValue | CachedEntryData | ShellSnapshotResponseValue;
311
+ }
312
+
247
313
  /**
248
314
  * Options for setItem() for function-level caching ("use cache").
249
315
  */
@@ -39,6 +39,7 @@ import type {
39
39
  CacheItemResult,
40
40
  CacheItemOptions,
41
41
  ShellCacheEntry,
42
+ ShellSnapshotRecord,
42
43
  } from "../types.js";
43
44
  import type { RequestContext } from "../../server/request-context.js";
44
45
  import { isPerClientSignalHeader } from "../../browser/cookie-name.js";
@@ -175,6 +176,10 @@ interface VercelShellEnvelope {
175
176
  e: number;
176
177
  /** Tags, preserved so a stale re-stamp keeps them. */
177
178
  t?: string[];
179
+ /** initialTheme the capture render was built with (resume theme fidelity). */
180
+ i?: string;
181
+ /** Capture data snapshot: recorded cache-store hits/writes for HIT parity. */
182
+ sn?: ShellSnapshotRecord[];
178
183
  }
179
184
 
180
185
  /** Read-path outcome for the debug sink. */
@@ -796,6 +801,8 @@ export class VercelCacheStore<
796
801
  prelude: env.p,
797
802
  postponed: env.po,
798
803
  reactVersion: env.rv,
804
+ initialTheme: env.i,
805
+ snapshot: env.sn,
799
806
  createdAt: env.c,
800
807
  },
801
808
  shouldRevalidate: isStale,
@@ -827,6 +834,8 @@ export class VercelCacheStore<
827
834
  s: staleAt,
828
835
  e: expiresAt,
829
836
  t: safeTags.length > 0 ? safeTags : undefined,
837
+ i: entry.initialTheme,
838
+ sn: entry.snapshot,
830
839
  };
831
840
  // write() enforces the 2 MB per-item ceiling (withinSizeLimit): an
832
841
  // oversized shell prelude is reported and skipped (fail-open to a full
@@ -1068,7 +1077,7 @@ export class VercelCacheStore<
1068
1077
 
1069
1078
  private asShellEnvelope(raw: unknown): VercelShellEnvelope | null {
1070
1079
  if (!isRecord(raw)) return null;
1071
- const { p, po, rv, c, s, e, t } = raw;
1080
+ const { p, po, rv, c, s, e, t, i, sn } = raw;
1072
1081
  if (typeof p !== "string" || typeof rv !== "string") return null;
1073
1082
  if (po !== null && typeof po !== "string") return null;
1074
1083
  if (typeof c !== "number") return null;
@@ -1081,6 +1090,8 @@ export class VercelCacheStore<
1081
1090
  s,
1082
1091
  e,
1083
1092
  t: Array.isArray(t) ? (t as string[]) : undefined,
1093
+ i: typeof i === "string" ? i : undefined,
1094
+ sn: Array.isArray(sn) ? (sn as ShellSnapshotRecord[]) : undefined,
1084
1095
  };
1085
1096
  }
1086
1097
 
package/src/index.rsc.ts CHANGED
@@ -157,6 +157,7 @@ export {
157
157
  urls,
158
158
  type PathHelpers,
159
159
  type PathOptions,
160
+ type PartialPrerenderProps,
160
161
  type UrlPatterns,
161
162
  type IncludeOptions,
162
163
  type IncludeItem,
@@ -219,11 +220,6 @@ export {
219
220
  type ReadonlyHeaders,
220
221
  } from "./server/cookie-store.js";
221
222
 
222
- // live() — the deterministic PPR hole primitive. Real (capture-aware)
223
- // implementation under the react-server condition; index.ts ships a client/SSR
224
- // passthrough (there is no shell capture off this condition).
225
- export { live } from "./server/live.js";
226
-
227
223
  // Cache tag APIs (server-only)
228
224
  // cacheTag: tag the current "use cache" entry at runtime.
229
225
  // updateTag: read-your-own-writes invalidation (awaitable, for Server Actions).
package/src/index.ts CHANGED
@@ -110,6 +110,7 @@ export type {
110
110
  TextResponsePathFn,
111
111
  RouteResponse,
112
112
  ProblemDetails,
113
+ PartialPrerenderProps,
113
114
  } from "./urls.js";
114
115
 
115
116
  // Middleware context types
@@ -228,23 +229,6 @@ export function headers(): never {
228
229
  throw serverOnlyStubError("headers");
229
230
  }
230
231
 
231
- /**
232
- * Client/SSR passthrough for `live()` (the PPR hole primitive). Unlike the
233
- * cookies()/headers() stubs this is a REAL function: there is no shell capture
234
- * off the react-server condition, so live() simply runs the thunk (or returns
235
- * the promise). The capture-aware implementation lives in index.rsc.ts
236
- * (./server/live.js). See docs/design/ppr-shell-resume.md.
237
- */
238
- export function live<T>(fn: () => Promise<T> | T): Promise<T>;
239
- export function live<T>(promise: Promise<T>): Promise<T>;
240
- export function live<T>(
241
- input: (() => Promise<T> | T) | Promise<T>,
242
- ): Promise<T> {
243
- return typeof input === "function"
244
- ? Promise.resolve((input as () => Promise<T> | T)())
245
- : input;
246
- }
247
-
248
232
  /**
249
233
  * Client implementation of `invalidateClientCache()`. Unlike the server-only
250
234
  * stubs above this is a REAL function under the `default` condition (it marks