@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
@@ -22,12 +22,14 @@ declare global {
22
22
  * - Non-blocking writes via waitUntil
23
23
  * - KV L2 for cross-colo cache persistence
24
24
  */
25
- import type { SegmentCacheStore, CachedEntryData, CacheDefaults, CacheGetResult, CacheItemResult, CacheItemOptions, ShellCacheEntry, CacheReadError } from "../types.js";
25
+ import type { SegmentCacheStore, CachedEntryData, CacheDefaults, CacheGetResult, CacheItemResult, CacheItemOptions, ShellCacheEntry, ShellDocumentRead, ShellDocumentReadOptions, CacheReadError } from "../types.js";
26
26
  import { type RequestContext } from "../../server/request-context.js";
27
27
  import { CACHE_STALE_AT_HEADER, CACHE_STATUS_HEADER, CACHE_TAGS_HEADER, CACHE_TAGGED_AT_HEADER, TAG_MARKER_PREFIX, CACHE_REVALIDATING_AT_HEADER, MAX_REVALIDATION_INTERVAL, EDGE_LOOKUP_TIMEOUT_MS, EDGE_READ_TIMEOUT_MS, KV_READ_TIMEOUT_MS } from "./cf-cache-constants.js";
28
28
  export { CACHE_STALE_AT_HEADER, CACHE_STATUS_HEADER, CACHE_TAGS_HEADER, CACHE_TAGGED_AT_HEADER, TAG_MARKER_PREFIX, CACHE_REVALIDATING_AT_HEADER, MAX_REVALIDATION_INTERVAL, EDGE_LOOKUP_TIMEOUT_MS, EDGE_READ_TIMEOUT_MS, KV_READ_TIMEOUT_MS, };
29
29
  import type { KVNamespace, CFCacheReadDebugEvent, CFCacheDebug, CFCacheStoreOptions } from "./cf-cache-types.js";
30
30
  export type { KVNamespace, CFCacheReadDebugEvent, CFCacheDebug, CFCacheStoreOptions, };
31
+ /** @internal Reset the per-isolate shell and marker memos (tests). */
32
+ export declare function resetCFShellMemoForTests(): void;
31
33
  export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
32
34
  readonly supportsPassiveShellReads: true;
33
35
  readonly defaults?: CacheDefaults;
@@ -39,6 +41,19 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
39
41
  private readonly edgeLookupTimeoutMs;
40
42
  private readonly edgeReadTimeoutMs;
41
43
  private readonly kvReadTimeoutMs;
44
+ /**
45
+ * The shell snapshot read's budgets. `read` (getShell: partial replay, the
46
+ * testing helpers) has nothing committed, so a slow snapshot is a quick
47
+ * miss at kvReadTimeoutMs. `document` (readShellDocument, the serve path)
48
+ * has sent the prelude and cannot finish without the snapshot's doc
49
+ * record; a timeout there reloads the page, so it waits at least
50
+ * SHELL_SNAPSHOT_READ_MIN_TIMEOUT_MS. `kvReadTimeoutMs <= 0` keeps both
51
+ * unbounded.
52
+ */
53
+ private readonly snapshotBudgets;
54
+ private readonly memo;
55
+ /** @internal SegmentCacheStore.freshReadsWindowMs */
56
+ readonly freshReadsWindowMs: number;
42
57
  private readonly debug?;
43
58
  private readonly kv?;
44
59
  /** True when constructed without KV: no durable tag history (see ctor). */
@@ -72,7 +87,11 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
72
87
  * @internal
73
88
  */
74
89
  private emitDebug;
75
- /** Build-time-gated shell tier trace for deployed cross-colo diagnostics. */
90
+ /**
91
+ * Build-time-gated shell tier trace for deployed cross-colo diagnostics.
92
+ * A read's `stats` print as its timings, after the other details and before
93
+ * `expiresAt`.
94
+ */
76
95
  private debugShell;
77
96
  /**
78
97
  * Resolve the cache-key base URL for the current cache operation.
@@ -216,7 +235,9 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
216
235
  get(key: string): Promise<CacheGetResult | null | CacheReadError>;
217
236
  /**
218
237
  * Store entry data with TTL and optional SWR window.
219
- * Uses waitUntil for non-blocking write when available.
238
+ * Resolves once the L1 write lands, per the SegmentCacheStore.set contract
239
+ * (also held by waitUntil when available); the KV write stays in the
240
+ * background.
220
241
  * When KV is configured, also persists to L2.
221
242
  */
222
243
  set(key: string, data: CachedEntryData, ttl: number, swr?: number): Promise<void>;
@@ -278,31 +299,121 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
278
299
  */
279
300
  private isShellGenerationInvalidated;
280
301
  /**
281
- * Get a cached PPR shell entry from Cache API, falling through to KV and
282
- * promoting a valid KV hit. Both tiers store one envelope so the prelude,
283
- * postponed state, snapshot, versions, and generation metadata cannot mix.
284
- * SWR remains a plain staleness flag; the capture scheduler's module-level
285
- * in-flight set is the recapture stampede guard.
302
+ * Get a cached PPR shell entry: the public custom-store contract, built on
303
+ * readShellDocument. It waits for the snapshot and re-encodes the prelude to
304
+ * the base64 `ShellCacheEntry.prelude` the contract carries (native
305
+ * `toBase64` in workerd). Partial replay and the testing helpers read
306
+ * through here; the document serve path reads readShellDocument directly.
307
+ * A snapshot that failed to read is a miss here: with no committed response
308
+ * to protect, an entry without its pins is worse than none.
286
309
  */
287
310
  getShell(key: string): Promise<{
288
311
  entry: ShellCacheEntry;
289
312
  shouldRevalidate?: boolean;
290
313
  } | null>;
291
314
  /**
292
- * Store a PPR shell envelope in Cache API and, when KV is configured and
293
- * the retention meets its 60-second floor, KV. The shared write is
294
- * registered with waitUntil and awaited so invalidation rejection can be
295
- * acknowledged to the capture scheduler. Short-lived shells remain useful
296
- * in L1 even though KV rejects them; a KV-less store is L1-only by design
297
- * (edge-only ppr — see the section comment).
315
+ * @internal Prelude-first read of a shell (SegmentCacheStore
316
+ * .readShellDocument). Cache API first, then KV; both tiers hold one
317
+ * cf-shell-frame body, so the prelude, postponed state, snapshot, versions,
318
+ * and generation metadata cannot mix. Before it resolves it reads only the
319
+ * head and the prelude's bytes, and the tag-marker read runs alongside the
320
+ * prelude read (it starts as soon as the head is parsed, and is awaited
321
+ * before this resolves, so an invalidated shell is never returned). The
322
+ * snapshot is the rest of the same body, read on its own promise, bounded
323
+ * by the document read's budget (`snapshotBudgets.document`; getShell uses
324
+ * `snapshotBudgets.read`). SWR is a plain staleness flag; the capture
325
+ * scheduler's in-flight set is the recapture stampede guard.
326
+ */
327
+ readShellDocument(key: string, options?: ShellDocumentReadOptions): Promise<ShellDocumentRead | null>;
328
+ private readShellDocumentWithin;
329
+ /** @internal SegmentCacheStore.dropShellMemo */
330
+ dropShellMemo(key: string): void;
331
+ /**
332
+ * Start the marker reads of the tags this shell key carried last time (and
333
+ * the route's static tags) alongside the entry read (readShellTagMarker),
334
+ * so the marker check after the head reuses them. With KV only: without it
335
+ * there are no markers to read. A wrong hint costs a wasted read; the check
336
+ * itself uses the head's tags.
337
+ */
338
+ private prefetchShellMarkers;
339
+ /**
340
+ * A HIT: remember the entry's tags for the next read's hints, and fill the
341
+ * marker row. The shell-read marker values stay in their own record: the
342
+ * request's data reads (the tail's, or a partial navigation's matchPartial
343
+ * on the same context) read their markers as before.
344
+ */
345
+ private recordShellHit;
346
+ /** The isolate marker memo's key for a tag: markers are per namespace and version. */
347
+ private markerMemoKey;
348
+ /** The memo key: one per namespace, build version, base URL, and shell key. */
349
+ private shellMemoKey;
350
+ /**
351
+ * Serve a memoized shell (shell-memo.ts). The tag-marker check is the only
352
+ * store I/O left, so it is started first and awaited before anything is
353
+ * returned: nothing of an invalidated shell reaches the response. A shell
354
+ * that went stale since it was memoized, or that predates a tag this
355
+ * isolate is invalidating or just invalidated (RecentTagInvalidations), is
356
+ * dropped and read from the store (SWR recapture scheduling and each mode's
357
+ * invalidation semantics stay with the store read). That read keeps the
358
+ * caller's `tagHints`: cfTagHints may have evicted the key while the memo
359
+ * still holds it.
360
+ */
361
+ private readMemoizedShell;
362
+ /**
363
+ * Stats for one shell read, allocated only when the request collects
364
+ * `debugPerformance` metrics or the build has the internal shell trace on,
365
+ * so a normal HIT takes no timestamps for them.
366
+ */
367
+ private shellReadStats;
368
+ /**
369
+ * Read a frame's head and prelude under one budget, starting the
370
+ * tag-marker read (`isInvalidated`) the moment the head is parsed so it
371
+ * overlaps the prelude read, then await it. Expired heads are rejected
372
+ * before the marker read starts.
373
+ */
374
+ private openShellFrame;
375
+ /**
376
+ * The ShellDocumentRead for an opened frame. Its snapshot promise starts
377
+ * reading the rest of the body now (off the commit path, not after it),
378
+ * bounded by `budget` ({@link snapshotReadBudget}), and is registered with
379
+ * waitUntil so the KV tier's L1 promotion and a corrupt entry's eviction
380
+ * still run when nothing awaits it (a read that is not served). A snapshot
381
+ * whose length differs from the head's `sl`, or that fails to parse, evicts
382
+ * the entry from both tiers and reports cache-corrupt. Every failure
383
+ * resolves undefined and names itself on `snapshotFailure` (`unavailable`
384
+ * for a timeout or read error, `corrupt` otherwise): a document HIT then
385
+ * degrades (rsc-rendering.ts serveShellHit), and getShell misses. Only
386
+ * a read whose snapshot parsed goes into the per-isolate memo (when still
387
+ * fresh), and `promote` receives its snapshot bytes (the KV tier's L1
388
+ * promotion). `headers` are the L1 response's, kept for the KV-less
389
+ * memo-hit marker check.
390
+ */
391
+ private shellDocumentRead;
392
+ /** Delete a shell from both tiers (non-blocking). */
393
+ private evictShell;
394
+ /**
395
+ * Store a PPR shell in Cache API and, when KV is configured, KV — the same
396
+ * cf-shell-frame bytes in both. The shared write is registered with
397
+ * waitUntil and awaited so invalidation rejection can be acknowledged to
398
+ * the capture scheduler. A KV-less store is L1-only by design (edge-only
399
+ * ppr — see the section comment).
400
+ *
401
+ * KV rejects an expirationTtl below KV_MIN_EXPIRATION_TTL, so a shorter
402
+ * retention (a `ppr` ttl + swr under 60, or a shell capped to a short or
403
+ * aging route cache() entry) is written with that floor: the frame head's deadlines (`s`, `e`) are the
404
+ * entry's, and every read checks `e` (openShellFrame), so the extra KV
405
+ * lifetime is never served, and the shell is readable from every colo, not
406
+ * only from the capturing colo's L1.
298
407
  */
299
408
  putShell(key: string, entry: ShellCacheEntry, ttlSeconds?: number, swrSeconds?: number, tags?: string[]): Promise<"stored" | "invalidated" | "uncacheable" | void>;
300
- /** Rebuild the public shell entry from its validated storage envelope. */
301
- private shellEnvelopeToEntry;
302
- /** Build the Cache API representation of the coupled shell envelope. */
303
- private shellEnvelopeResponse;
304
- /** KV shell fallback with corruption checks and background L1 promotion. */
305
- private kvGetShell;
409
+ /** The Cache API representation of a shell frame. */
410
+ private shellFrameResponse;
411
+ /**
412
+ * KV shell fallback: the same prelude-first read, then L1 promotion. After
413
+ * an L1 attempt, `stats` is that read's and gains the L1 attempt's time
414
+ * (`l1MissMs`) before its per-tier fields restart for KV.
415
+ */
416
+ private kvReadShellDocument;
306
417
  /** Promote a valid KV shell into the per-colo Cache API tier. */
307
418
  private promoteShellToL1;
308
419
  /**
@@ -450,7 +561,8 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
450
561
  * entry Cache-Tag tokens (`headers`): an entry a purge cannot reach — one
451
562
  * written before the tokens existed, or whose tag set overflowed the
452
563
  * Cache-Tag header limit — keeps the full marker check, or purge mode
453
- * would serve it stale until TTL with no eviction path.
564
+ * would serve it stale until TTL with no eviction path. KV-less there are
565
+ * no markers, so it gets the per-request memo check like any hit (#973).
454
566
  *
455
567
  * Without tagPurge this is the full marker cascade. KV-tier reads and
456
568
  * shells always use isGloballyInvalidated directly: purge cannot reach KV,
@@ -475,6 +587,18 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
475
587
  * @internal
476
588
  */
477
589
  private readTagMarker;
590
+ /**
591
+ * A PPR shell read's marker for `tag`: this request's own value if it has
592
+ * one (its invalidateTags() writes, or a store read), else through the
593
+ * per-isolate marker memo (isolate-tag-memo.ts, stale-while-revalidate
594
+ * under `markerFreshMs` / `markerMaxStaleMs`; a request carrying the
595
+ * fresh-reads cookie reads L1/KV). The value is kept in the request's
596
+ * shell-read record, never in the per-request memo the data families read,
597
+ * HIT or MISS. A store read still lands in the per-request memo, as any
598
+ * marker read does; it is not stale. A timed-out read fails open for this
599
+ * request and is not memoized.
600
+ */
601
+ private readShellTagMarker;
478
602
  /**
479
603
  * Uncached body of readTagMarker: L1 (per-colo Cache API, opt-in via
480
604
  * tagCacheTtl) -> KV. Writes the resolved value back into the memo.
@@ -554,29 +678,6 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
554
678
  * @internal
555
679
  */
556
680
  private putTagMarkerL1;
557
- /**
558
- * Invalidate every entry tagged with any of `tags`. Receives the whole batch
559
- * from one updateTag()/revalidateTag() call so the eager-purge hook fires
560
- * ONCE (one CDN purge request, not one per tag). For each tag: records the KV
561
- * marker (the durable cross-colo truth that reads compare taggedAt against),
562
- * writes the fresh marker straight into this colo's L1 (write-through, NOT
563
- * delete - a delete would let the next read re-read a not-yet-converged KV
564
- * value and re-arm the stale window), and memoizes it for same-request
565
- * read-your-own-writes. In purge mode (tagPurge) it then AWAITS the
566
- * consumer's purge-by-tag call with the entry Cache-Tags — the eviction the
567
- * per-read marker skip on L1 hits relies on. Finally fires onRevalidateTag
568
- * with the namespaced lookup Cache-Tags so a consumer purge evicts the
569
- * cached lookups in other colos promptly (otherwise they converge within
570
- * tagCacheTtl).
571
- *
572
- * Durable-write integrity: the in-memory write-through (memo + L1) for a tag
573
- * runs ONLY after that tag's KV marker write is confirmed. If any KV write
574
- * fails (transient error; over-limit keys are normalized by toKVKey rather
575
- * than rejected), this rejects with the
576
- * failed tags so an awaiting updateTag() surfaces the failure instead of
577
- * silently reporting success while other requests/colos serve stale data. The
578
- * eager purge still fires for the whole batch first (it is additive).
579
- */
580
681
  /**
581
682
  * Shell tag-generation gate (SegmentCacheStore.isTagsInvalidatedSince): the
582
683
  * SAME KV markers used by runtime envelopes also evict immutable build shells
@@ -586,7 +687,47 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
586
687
  * isShellGenerationInvalidated.
587
688
  */
588
689
  isTagsInvalidatedSince(tags: string[], sinceMs: number): Promise<boolean>;
690
+ /**
691
+ * Invalidate every entry tagged with any of `tags`. Receives the whole batch
692
+ * from one updateTag()/revalidateTag() call so the eager-purge hook fires
693
+ * ONCE (one CDN purge request, not one per tag). Before its first await it
694
+ * masks the tags for the rest of the request (maskTagsForRequest), which is
695
+ * all revalidateTag() waits for. For each tag it then records the KV marker
696
+ * (the durable cross-colo truth that reads compare taggedAt against) and
697
+ * writes the fresh marker straight into this colo's L1 (write-through, NOT
698
+ * delete - a delete would let the next read re-read a not-yet-converged KV
699
+ * value and re-arm the stale window). In purge mode (tagPurge) it then
700
+ * AWAITS the consumer's purge-by-tag call with the entry Cache-Tags — the
701
+ * eviction the per-read marker skip on L1 hits relies on. Finally fires
702
+ * onRevalidateTag with the namespaced lookup Cache-Tags so a consumer purge
703
+ * evicts the cached lookups in other colos promptly (otherwise they converge
704
+ * within tagCacheTtl).
705
+ *
706
+ * Durable-write integrity: the write-through other requests read (the
707
+ * isolate marker memo and L1) for a tag runs ONLY after that tag's KV marker
708
+ * write is confirmed. If any KV write fails (transient error; over-limit
709
+ * keys are normalized by toKVKey rather than rejected), this rejects with
710
+ * the failed tags so an awaiting updateTag() surfaces the failure instead of
711
+ * silently reporting success while other requests/colos serve stale data.
712
+ * The purge and onRevalidateTag still fire for the whole batch once the
713
+ * puts settle, whether or not one failed (they are additive); the rejection
714
+ * follows them.
715
+ */
589
716
  invalidateTags(tags: string[]): Promise<void>;
717
+ /**
718
+ * The request-local half of invalidateTags(), run before its first await
719
+ * (#973): this request's marker memo takes the new marker, so every later
720
+ * read in the request (data families, purge-mode L1 hits, shell reads)
721
+ * treats the tags as invalidated at `invalidatedAt` without waiting for the
722
+ * KV put or the purge that revalidateTag() leaves to waitUntil. Written
723
+ * before the durable write is confirmed: if that write fails, this request
724
+ * pays extra misses, never a stale read, and later requests read the
725
+ * durable state. Skipped where no read consults the memo (neither KV nor
726
+ * tagPurge).
727
+ */
728
+ private maskTagsForRequest;
729
+ /** invalidateTags' marker writes, tag purge, and hooks. */
730
+ private writeTagInvalidation;
590
731
  /**
591
732
  * KV fallback for segment cache reads.
592
733
  * Returns null if KV is not configured, entry is missing, or expired.
@@ -2,6 +2,7 @@ import type { ExecutionContext } from "../../types/request-scope.js";
2
2
  import type { CacheDefaults } from "../types.js";
3
3
  import type { RequestContext } from "../../server/request-context.js";
4
4
  import type { CloudflareZonePurgeOptions } from "./cf-zone-purge.js";
5
+ import type { StoreMemoOptions } from "../shell-memo.js";
5
6
  /**
6
7
  * Minimal Cloudflare KV Namespace interface.
7
8
  * Avoids hard dependency on @cloudflare/workers-types.
@@ -294,9 +295,12 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
294
295
  edgeLookupTimeoutMs?: number;
295
296
  /**
296
297
  * Latency budget (ms) for reading the BODY of a matched L1 entry
297
- * (response.json()). CF streams the cache body lazily, so the multi-second
298
- * tail can appear after `match` already resolved; this bounds it. On timeout
299
- * the read is treated as a miss and falls through to L2/KV or render.
298
+ * (response.json(); for a PPR shell, only its head and prelude — the
299
+ * snapshot behind them is read after the HIT commits, under
300
+ * {@link kvReadTimeoutMs}). CF streams the cache body lazily, so the
301
+ * multi-second tail can appear after `match` already resolved; this bounds
302
+ * it. On timeout the read is treated as a miss and falls through to L2/KV or
303
+ * render.
300
304
  *
301
305
  * Separate from {@link edgeLookupTimeoutMs} because a healthy body read
302
306
  * (fetch + JSON parse of a potentially large Flight payload) takes a little
@@ -309,7 +313,11 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
309
313
  * Latency budget (ms) for an L2 (KV) read. KV is the last cache tier before a
310
314
  * full render and is a global store (~50ms healthy, seconds when degraded);
311
315
  * this bounds it so a slow namespace cannot pin the request. On timeout the
312
- * read is treated as a miss (no L1 promote) and falls through to render.
316
+ * read is treated as a miss (no L1 promote) and falls through to render. For
317
+ * a PPR shell it covers opening the KV value and reading its head and
318
+ * prelude; separately, it bounds the shell's snapshot read (either tier),
319
+ * which runs off the commit path: a timeout there lets the tail run without
320
+ * the snapshot's pins.
313
321
  *
314
322
  * Defaults to {@link KV_READ_TIMEOUT_MS} (170) -- a few multiples above the
315
323
  * ~50ms healthy read, with headroom for legitimate tails (large payloads / far
@@ -319,6 +327,25 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
319
327
  * <= 0) to disable and always await KV.
320
328
  */
321
329
  kvReadTimeoutMs?: number;
330
+ /**
331
+ * Per-isolate memos ({@link StoreMemoOptions}), one set per isolate and
332
+ * shared by every CFCacheStore in it. `shellMs` (default 2000) and
333
+ * `shellMaxBytes` (default 16 MiB) size the PPR shell memo;
334
+ * `markerFreshMs` (default 1000) and `markerMaxStaleMs` (default 10000)
335
+ * the tag-marker memo PPR shell reads use with KV bound (served
336
+ * stale-while-revalidate). A memo HIT still runs the tag-marker check; the
337
+ * isolate that invalidates drops its own shell copies (and keeps reads in
338
+ * flight from memoizing them again) and writes the new marker into its
339
+ * marker memo, and its own `putShell` replaces its copy. Another isolate
340
+ * rejects a memoized shell once its marker memo sees the invalidation (up
341
+ * to `markerMaxStaleMs`). Without KV, in purge mode, the purge does not
342
+ * reach other isolates' memos: they serve the purged shell until their
343
+ * window passes. The response of a request that ran `updateTag()` /
344
+ * `revalidateTag()` sets the fresh-reads cookie, and the same user's
345
+ * requests carrying it skip both memos. `{ shellMs: 0 }` turns the shell
346
+ * memo off, `{ markerFreshMs: 0 }` the marker memo.
347
+ */
348
+ memo?: StoreMemoOptions;
322
349
  /**
323
350
  * Emit a {@link CFCacheReadDebugEvent} per L1 read. `true` logs to console
324
351
  * (visible via `wrangler tail`); pass a function to capture events directly.
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Prelude-first binary layout of a CFCacheStore PPR shell entry, shared by the
3
+ * Cache API body and the KV value (issue #941; docs/design/shell-entry-layout.md):
4
+ *
5
+ * "RSH1" | head length, 8 hex digits | head JSON | prelude bytes | snapshot JSON
6
+ *
7
+ * A document HIT needs the head (versions, freshness, tags, postponed) and the
8
+ * raw prelude before its first byte, and the capture snapshot only in the
9
+ * tail. Laying them out in that order lets the serve path read the head,
10
+ * start the tag-marker read, read exactly `pl` prelude bytes, and commit while
11
+ * the snapshot bytes are still arriving. One record keeps the prelude and the
12
+ * snapshot generation-coupled by construction: a Cache API entry and a KV
13
+ * value are written and read whole, so a split read can never pair one
14
+ * capture's prelude with another's pins.
15
+ *
16
+ * Every byte is UTF-8 text when the prelude is (React's HTML through
17
+ * TextEncoder always is), so the KV value is stored as a string (the
18
+ * KVNamespace contract's `put(key, string)`) and read back as the same bytes
19
+ * with `{ type: "stream" }`.
20
+ */
21
+ import type { ShellSnapshotRecord } from "../types.js";
22
+ /** The frame head: every entry field except the prelude and the snapshot. */
23
+ export interface ShellFrameHead {
24
+ /** ShellCacheEntry.reactVersion */
25
+ rv: string;
26
+ /** ShellCacheEntry.buildVersion */
27
+ bv?: string;
28
+ /** Capture-generation start time (ms epoch), used by tag marker checks. */
29
+ c: number;
30
+ /** When the entry becomes stale (ms epoch). */
31
+ s: number;
32
+ /** When the entry hard-expires (ms epoch). */
33
+ e: number;
34
+ /** Cache tags (for distributed tag invalidation). */
35
+ t?: string[];
36
+ /** Timestamp when tags were attached (ms epoch). */
37
+ ta?: number;
38
+ /** ShellCacheEntry.initialTheme */
39
+ i?: string;
40
+ /** ShellCacheEntry.docKey; navigation replay needs it after any round trip. */
41
+ dk?: string;
42
+ /** ShellCacheEntry.prunedRecords (diagnostic). */
43
+ pr?: string;
44
+ /** ShellCacheEntry.navigationOnly; its partial-context prelude is not document-safe. */
45
+ no?: true;
46
+ /** ShellCacheEntry.postponed. Absent iff `no` (no document half is stored). */
47
+ po?: string | null;
48
+ /** Prelude byte length (0 for a navigationOnly entry). */
49
+ pl: number;
50
+ /**
51
+ * Snapshot byte length (0 when the entry has none). A body whose snapshot
52
+ * part is any other length was truncated or corrupted.
53
+ */
54
+ sl: number;
55
+ }
56
+ /**
57
+ * Serialize a shell entry into one prelude-first frame. Throws when the head's
58
+ * byte lengths (`pl`, `sl`) do not describe `prelude` and `snapshot`.
59
+ */
60
+ export declare function encodeShellFrame(head: ShellFrameHead, prelude: Uint8Array, snapshot?: Uint8Array): Uint8Array<ArrayBuffer>;
61
+ /**
62
+ * The frame as the string KV stores. Throws when the prelude is not valid
63
+ * UTF-8, which would not round-trip through a string.
64
+ */
65
+ export declare function shellFrameToText(frame: Uint8Array): string;
66
+ /** UTF-8 JSON of a snapshot, or undefined when there is none to store. */
67
+ export declare function encodeShellSnapshot(snapshot: ShellSnapshotRecord[] | undefined): Uint8Array | undefined;
68
+ /**
69
+ * Parse the snapshot bytes that follow the prelude. Zero bytes means the entry
70
+ * carries no snapshot. Throws on anything else that is not a JSON array: the
71
+ * caller treats that as corruption.
72
+ */
73
+ export declare function parseShellSnapshot(bytes: Uint8Array): ShellSnapshotRecord[] | undefined;
74
+ /**
75
+ * Incremental reader over a streamed frame (a Cache API response body or a KV
76
+ * `{ type: "stream" }` value). The serve path reads the head and exactly the
77
+ * prelude's bytes before it commits; the snapshot is whatever remains.
78
+ */
79
+ export declare class ShellFrameReader {
80
+ private readonly reader;
81
+ private chunks;
82
+ private buffered;
83
+ private done;
84
+ /** Bytes of the prefix and head, once readHead has read them. */
85
+ headBytes: number;
86
+ constructor(body: ReadableStream<Uint8Array>);
87
+ /** Exactly `n` bytes, or null when the stream ends first. */
88
+ take(n: number): Promise<Uint8Array | null>;
89
+ /** The head, or null when the magic, length, or JSON is malformed or short. */
90
+ readHead(): Promise<ShellFrameHead | null>;
91
+ /** Everything after what was taken, to the end of the stream. */
92
+ readRest(): Promise<Uint8Array>;
93
+ /** Stop reading and release the body. */
94
+ cancel(): void;
95
+ }
@@ -1,3 +1,4 @@
1
+ import type { MarkerMemoOutcome } from "../isolate-tag-memo.js";
1
2
  /**
2
3
  * Cache-API path prefix for the optional per-colo L1 cache of tag-invalidation
3
4
  * markers (enabled by tagCacheTtl). Distinct from data keys (doc:/fn:/segment)
@@ -11,5 +12,38 @@ export declare const TAG_MARKER_CACHE_PREFIX = "__tagmarker__/";
11
12
  * never "no marker" - absence is only ever represented by this cached sentinel.
12
13
  */
13
14
  export declare const TAG_MARKER_ABSENT = "none";
15
+ /**
16
+ * Request context -> store instance -> T, both levels weakly held. Also
17
+ * VercelCacheStore's request tag mask (#973).
18
+ */
19
+ export type PerRequestStoreMap<T> = WeakMap<object, WeakMap<object, T>>;
20
+ /** The value for (ctx, store), created on first use. */
21
+ export declare function perRequestStoreValue<T>(map: PerRequestStoreMap<T>, ctx: object, store: object, create: () => T): T;
14
22
  export declare function getTagMarkerMemo(ctx: object, store: object): Map<string, number | null>;
23
+ /**
24
+ * Mask `tags` at `at` for the rest of the request: in the request's mask and
25
+ * in every memo the request's contexts hold for `store` (a memo created later
26
+ * starts from the mask).
27
+ */
28
+ export declare function maskRequestTags(ctx: object, store: object, tags: readonly string[], at: number): void;
29
+ /** Whether this request masked `tag` through `store` (maskRequestTags). */
30
+ export declare function isRequestMasked(ctx: object, store: object, tag: string): boolean;
31
+ /**
32
+ * A request's PPR shell-read marker reads (issue #941), kept apart from the
33
+ * per-request memo above, HIT or MISS. A shell read may answer a marker from
34
+ * the isolate marker memo (isolate-tag-memo.ts), up to `markerMaxStaleMs`
35
+ * old; the data families must never see that value. Scar tissue: first the
36
+ * shell read wrote into the per-request memo, so a MISS request rendered and
37
+ * stored its `"use cache"` data under a stale marker; then a HIT copied its
38
+ * entry's tags across, which a partial navigation's matchPartial (on the same
39
+ * context as its replay gate's getShell) read, and a `cache()` segment it
40
+ * wrote kept the stale item past `markerMaxStaleMs` under a fresh taggedAt.
41
+ */
42
+ export interface ShellMarkerReads {
43
+ /** The read of each tag, settled or in flight (collapses concurrent reads). */
44
+ reads: Map<string, Promise<number | null>>;
45
+ /** How the isolate memo answered each tag (the marker row's `memo=`). */
46
+ outcomes: Map<string, MarkerMemoOutcome>;
47
+ }
48
+ export declare function getShellMarkerReads(ctx: object, store: object): ShellMarkerReads;
15
49
  export declare function getTagMarkerInflight(ctx: object, store: object): Map<string, Promise<number | null>>;
@@ -50,9 +50,10 @@ export declare function captureHandleOwners(segments: ResolvedSegment[], handleS
50
50
  * cache() record owns its segments' arrays, so it REPLACES them.
51
51
  *
52
52
  * A segment with `owners` is replaced with empty arrays and re-pushed in
53
- * recorded order, an owned value through pushReplayed: the owning loader
54
- * re-runs on the HIT (loaders stay live), and its live pushes replace the
55
- * recorded ones in place instead of appending a second copy. `claim`
53
+ * recorded order, an owned value through pushReplayed: if the owning loader
54
+ * runs on the HIT (a live loader, or a promise-carrying `ssr: false` one;
55
+ * a promise-free one is served from the shell and does not), its live pushes
56
+ * replace the recorded ones in place instead of appending a second copy. `claim`
56
57
  * (setupLoaderAccess _claimLoaderPushes) is asked once per owner, so the
57
58
  * loader's own cache() HIT does not replay its pushes a second time
58
59
  * (loader-cache.ts replayLoaderHandles); an owner it refuses is skipped. A
@@ -1,6 +1,7 @@
1
1
  export type { SegmentCacheStore, CachedEntryData, CacheGetResult, CacheItemResult, CacheItemOptions, ShellCacheEntry, SerializedSegmentData, SegmentHandleData, CacheReadError, } from "./types.js";
2
2
  export { CACHE_READ_ERROR } from "./types.js";
3
3
  export { MemorySegmentCacheStore } from "./memory-segment-store.js";
4
+ export type { StoreMemoOptions } from "./shell-memo.js";
4
5
  export { CFCacheStore, type CFCacheStoreOptions, type CFCacheDebug, type CFCacheReadDebugEvent, type KVNamespace, CACHE_STALE_AT_HEADER, CACHE_STATUS_HEADER, CACHE_REVALIDATING_AT_HEADER, EDGE_LOOKUP_TIMEOUT_MS, EDGE_READ_TIMEOUT_MS, KV_READ_TIMEOUT_MS, createCloudflareZonePurge, type CloudflareZonePurgeOptions, } from "./cf/index.js";
5
6
  export { VercelCacheStore, type VercelCacheStoreOptions, type VercelRuntimeCache, type VercelCacheDebug, type VercelCacheReadDebugEvent, type VercelCacheReadOutcome, VERCEL_MAX_ITEM_BYTES, VERCEL_MAX_TAGS_PER_ITEM, VERCEL_MAX_TAG_BYTES, } from "./vercel/index.js";
6
7
  export { TRACKING_SEARCH_PARAMS, type CacheSearchParams, } from "./search-params-filter.js";