@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
@@ -40,8 +40,10 @@ Common reasons to migrate:
40
40
  build-time rendering instead of mixing rendering and caching behind conventions.
41
41
  See: `/prerender`
42
42
  - **Partial prerendering, shipped** — the `ppr` path option caches a page's
43
- HTML shell and resumes only the live holes on each request; loaders stay
44
- fresh. The equivalent of Next's `experimental_ppr`, stable and per-route.
43
+ HTML shell and resumes its holes per request: loaders without `ssr: false`,
44
+ read under `loading()` or an inline `<Suspense>`. Handler output bakes into
45
+ the shell. It maps to Next's `experimental_ppr`, stable and per-route, but
46
+ holes come from loaders, not from any `<Suspense>`.
45
47
  See: `/ppr`
46
48
  - **Composable route tree** — layouts, includes, middleware, parallels, and
47
49
  intercepts compose directly in the route definition.
@@ -93,6 +95,13 @@ its call site with the real Rango API:
93
95
  | `next/font` | see `/fonts` |
94
96
  | `next/script` `Script` | see `/scripts` |
95
97
  | `next-themes` | `theme: true` in `createRouter` (see §10) |
98
+ | `next/dynamic` `dynamic(() => import(...), { ssr: false })` | render it after mount in a `"use client"` component (see the note below) |
99
+
100
+ `next/dynamic`'s `{ ssr: false }` keeps a component out of server rendering:
101
+ render it only after mount in a `"use client"` component (a `useEffect` flag,
102
+ with `React.lazy()` if its code should split out). Do not map it to
103
+ `loader(L, { ssr: false })`, which does the opposite: the server settles that
104
+ loader before the first flush, and under `ppr` it bakes into the shell.
96
105
 
97
106
  If an import has no row here and no obvious Rango equivalent, stop and surface
98
107
  it to the user — do not mock it to keep the build green.
@@ -418,9 +427,12 @@ Next.js route segment config maps onto Rango's explicit primitives:
418
427
  ### Partial prerendering → the `ppr` path option
419
428
 
420
429
  Next.js PPR statically prerenders a shell at build time and streams the parts
421
- inside `<Suspense>` at request time. Rango ships the same model as a path
430
+ inside `<Suspense>` at request time. Rango ships a similar model as a path
422
431
  option — the shell is captured at runtime into the app cache store and resumed
423
- on later requests, with the holes rendered fresh per request:
432
+ on later requests, with the holes rendered fresh per request. The difference is
433
+ what counts as a hole: in Rango everything a handler produces is shell
434
+ material, as it is under `cache()`, including a promise it hands to a component
435
+ under `<Suspense>`. A live hole is loader data:
424
436
 
425
437
  ```typescript
426
438
  // Next.js: app/products/[id]/page.tsx
@@ -435,14 +447,14 @@ export default async function Page({ params }) {
435
447
  );
436
448
  }
437
449
 
438
- // Rango, step 1 — direct carry-over. Your Suspense tree IS the hole model:
439
- // hand the un-awaited promise down, keep the boundary, add the ppr option.
440
- // No loader, no loading(), no restructuring.
450
+ // Rango: the direct carry-over BAKES. Under ppr the capture awaits a promise
451
+ // the handler hands down (bounded by ppr.captureTimeout), and every shell HIT
452
+ // shows the captured price.
441
453
  import { Suspense } from "react";
442
454
  import type { HandlerContext } from "@rangojs/router";
443
455
 
444
456
  function ProductPage(ctx: HandlerContext) {
445
- const price = fetchPrice(ctx.params.id); // pending promise — NOT awaited
457
+ const price = fetchPrice(ctx.params.id); // awaited at capture, then frozen
446
458
  return (
447
459
  <ProductShell>
448
460
  <Suspense fallback={<PriceSkeleton />}>
@@ -451,41 +463,54 @@ function ProductPage(ctx: HandlerContext) {
451
463
  </ProductShell>
452
464
  );
453
465
  }
454
- path("/products/:id", ProductPage, {
455
- name: "product",
456
- ppr: { ttl: 600, swr: 120 }, // or ppr: true (default ttl 300s)
457
- });
458
466
 
459
- // Rango, step 2 (optional refinement) — promote the fetch to a live loader
460
- // (no ssr: false) for a GUARANTEED hole, even when the value resolves
461
- // instantly; loading() is its boundary (/ppr → The loader lane rule).
467
+ // Rango, live price: a loader without ssr: false, read with
468
+ // useLoader(LivePriceLoader) in a client component. loading() (or an inline
469
+ // <Suspense> around the reader) is the hole boundary (/ppr → The loader lane
470
+ // rule).
471
+ function ProductPageLive() {
472
+ return (
473
+ <ProductShell>
474
+ <LivePriceFromLoader /> {/* "use client": useLoader(LivePriceLoader) */}
475
+ </ProductShell>
476
+ );
477
+ }
462
478
  path(
463
479
  "/products/:id",
464
- ProductPage,
465
- { name: "product", ppr: { ttl: 600, swr: 120 } },
480
+ ProductPageLive,
481
+ { name: "product", ppr: { ttl: 600, swr: 120 } }, // or ppr: true (ttl 300s)
466
482
  () => [loader(LivePriceLoader), loading(<PriceSkeleton />)],
467
483
  ),
468
484
  ```
469
485
 
470
486
  Differences that matter during migration:
471
487
 
472
- - **The Suspense/promise model carries over.** As in Next, a still-pending
473
- promise handed to a component that suspends under its own `<Suspense>`
474
- postpones at capture and becomes a hole — existing Next PPR trees keep
475
- working as-is, no `loading()` required. One container rule everywhere
476
- (handlers, handles, loaders): awaited/settled data bakes into the shell; a
477
- promise nested inside your data stays a live hole. For loaders, `ssr: false`
478
- (not `loading()`) selects the lane — see `/ppr` → The loader lane rule.
479
- Identity reads (`cookies()`/`headers()`) where the value would bake refuse
480
- the capture by construction.
488
+ - **Handler output bakes; loader data is the hole.** Unlike Next, a pending
489
+ promise the handler hands to a component under `<Suspense>` does NOT make a
490
+ hole. Everything a handler produces — that promise, an async server
491
+ component, a nested promise in a handle it pushes, a loader it awaits — is
492
+ awaited at capture (bounded by `ppr.captureTimeout`) and served frozen for
493
+ the shell's lifetime; a shell HIT never runs a handler. Keep the parts that
494
+ must be fresh in loaders without `ssr: false`, read with `useLoader` under
495
+ `loading()` or an inline `<Suspense>`; inside an `ssr: false` loader, return
496
+ them as nested promises. For loaders, `ssr: false` (not `loading()`) selects
497
+ the lane — see `/ppr` → The loader lane rule.
481
498
  - **Shell freshness is explicit.** Next's PPR shell is fixed until the next
482
499
  build; Rango's has `ttl`/`swr`/`tags` per route, and `updateTag()` /
483
500
  `revalidateTag()` drop the shell (`revalidate()` does not — it is a data
484
- lever and never touches shell HTML).
485
- - **`cookies()`/`headers()` in shell material THROW during capture** (in Next
486
- they silently force dynamic rendering). Per-user reads must move into a live
487
- loader (no `ssr: false`; a nested promise does not help). The refusal
488
- surfaces at migration time, which is the point.
501
+ lever and never touches shell HTML). Under a route `cache()`, the shell
502
+ lives no longer than that entry: its ttl/swr are capped by the entry's (dev
503
+ warns when an explicit `ppr` window is reduced). A route `cache({ key })`
504
+ gives one shell per key value, and `cache(false)` or a false `condition()`
505
+ means no shell.
506
+ - **Request-scoped reads in shell material refuse the capture** (in Next they
507
+ silently force dynamic rendering). `cookies()`, `headers()`, a
508
+ `{ cache: false }` variable, and `ctx.dynamic()` refuse it anywhere the
509
+ capture waits: a handler, a promise it passes or pushes, an async component,
510
+ a loader it awaits. Per-user reads must move into a live loader (no
511
+ `ssr: false`; a nested promise does not help). A normal `ctx.get()` value is
512
+ not guarded and bakes as the capturing request's value. The refusal surfaces
513
+ at migration time, which is the point.
489
514
  - **A store is required.** PPR needs the app-level `createRouter({ cache })`
490
515
  store to implement the shell family (`MemorySegmentCacheStore`,
491
516
  `CFCacheStore`, `VercelCacheStore`). Without one the route quietly stays
@@ -506,19 +531,25 @@ tag-based cache invalidation now maps directly.
506
531
  with `cache({ tags })` or runtime `cacheTag(...tags)`. `cacheTag()` works inside a
507
532
  `"use cache"` function (tags that entry) AND render-callable in a plain server
508
533
  component (no `"use cache"` needed — it tags the document / PPR shell the component
509
- renders into). Then invalidate by tag:
534
+ renders into, and the route `cache()` entry it renders inside). Then invalidate by tag:
510
535
 
511
536
  ```typescript
512
537
  // Next.js Rango
513
538
  // revalidateTag("products") → await updateTag("products") // in a server action: awaitable,
514
539
  // // read-your-own-writes (next render is fresh)
515
540
  // or revalidateTag("products") // in a route handler / webhook:
516
- // // background, non-blocking (hard-purge)
541
+ // // background, non-blocking (evicts)
517
542
  ```
518
543
 
519
544
  `updateTag` is awaitable and immediate; `revalidateTag` is fire-and-forget. Both
520
- hard-purge (the next read re-renders fresh); the only difference is awaitability —
521
- despite the Next.js name, `revalidateTag` here is NOT stale-while-revalidate.
545
+ evict (despite the Next.js name, `revalidateTag` here is NOT
546
+ stale-while-revalidate), and the request that calls either reads its own
547
+ writes. `updateTag()` also waits for the durable write and rejects when it
548
+ fails; `revalidateTag()` runs it in the background. The mutating user's next
549
+ requests skip the stores' per-isolate PPR shell memos via the fresh-reads
550
+ cookie (`rango-state-fresh`); other users, and other locations on
551
+ `CFCacheStore`, see the invalidation once the memo refreshes and KV
552
+ propagates (`/caching` → "The fresh-reads cookie").
522
553
  Built-in stores (`MemorySegmentCacheStore`, `CFCacheStore`, `VercelCacheStore`)
523
554
  index by tag. Next's
524
555
  `revalidatePath` has no path-based equivalent — tag the relevant entries instead.
@@ -756,7 +787,7 @@ Server actions work the same way — `"use server"` directive, `useActionState`,
756
787
 
757
788
  Key difference: in Rango, route middleware does NOT wrap action execution. Actions only see global middleware context. Use `getRequestContext()` in actions to access `ctx.set()`/`ctx.get()`.
758
789
 
759
- Next.js's `revalidateTag()` maps directly: tag entries via `cache({ tags })` / `cacheTag(...)`, then invalidate. **In a server action use `await updateTag(tag)`** — it is read-your-own-writes, so the action's own re-render sees fresh data; `revalidateTag(tag)` is a background (non-blocking) hard-purge and is NOT read-your-own-writes, so reserve it for route handlers / webhooks (calling it from an action can leave that action's re-render stale). `revalidatePath()` has no path-based equivalent — tag the route's entries instead. Separately, the route, the segments inside its `path()`, and all loaders already re-run after every action; layouts and parallels above the route are skipped. To re-render one of those skipped segments after an action, attach `revalidate((ctx) => ctx.isAction(updateBlog) || undefined)` to it; to narrow a loader or route segment to one action, use `revalidate((ctx) => (ctx.isAction() ? ctx.isAction(updateBlog) : undefined))` (match the imported action by reference, as in §3). See `/server-actions` for the full pattern (validation, error handling, file uploads), `/caching` for tag invalidation, and `/loader` for revalidation rule semantics.
790
+ Next.js's `revalidateTag()` maps directly: tag entries via `cache({ tags })` / `cacheTag(...)`, then invalidate. **In a server action use `await updateTag(tag)`** — it is read-your-own-writes, so the action's own re-render sees fresh data, and it rejects when the store's durable write fails; `revalidateTag(tag)` is a background (non-blocking) hard-purge for route handlers / webhooks. The request that calls `revalidateTag()` also reads its own writes, but a failed durable write only reaches `onError`. `revalidatePath()` has no path-based equivalent — tag the route's entries instead. Separately, the route, the segments inside its `path()`, and all loaders already re-run after every action; layouts and parallels above the route are skipped. To re-render one of those skipped segments after an action, attach `revalidate((ctx) => ctx.isAction(updateBlog) || undefined)` to it; to narrow a loader or route segment to one action, use `revalidate((ctx) => (ctx.isAction() ? ctx.isAction(updateBlog) : undefined))` (match the imported action by reference, as in §3). See `/server-actions` for the full pattern (validation, error handling, file uploads), `/caching` for tag invalidation, and `/loader` for revalidation rule semantics.
760
791
 
761
792
  ## 8. Metadata / Head
762
793
 
@@ -33,7 +33,8 @@ Common reasons to migrate:
33
33
  See: `/middleware`, `/loader`
34
34
  - **Live data layer** — `createLoader()` and `loader()` keep data fresh
35
35
  independently of cached UI. A route can serve cached segments while loaders
36
- still resolve live on every request.
36
+ still resolve live on every request (on a `ppr` route, a loader registered
37
+ with `ssr: false` bakes into the cached HTML shell instead; see `/ppr`).
37
38
  See: `/loader`, `/caching`, `/cache-guide`
38
39
  - **Explicit caching model** — `cache()` DSL, `revalidate()`, `use cache`, and
39
40
  custom cache stores make data and render caching a first-class part of the router.
@@ -154,7 +154,9 @@ const HomePage: Handler<"home"> = (ctx) => {
154
154
  RR's data-derived `meta({ data })` maps to the same push from the LOADER that
155
155
  owns the data — `ctx.use(Meta)({ title: data.name })` in the loader body, with
156
156
  `loader(Def, { ssr: false })` when the title must be in the SSR'd
157
- head. See `/loader` → "Writing Handles from Loaders".
157
+ head. See `/loader` → "Writing Handles from Loaders". The loader flag is
158
+ unrelated to RR7's `ssr: false` (SPA mode), which has no route-wide Rango
159
+ equivalent: it makes the server settle that loader before the first flush.
158
160
 
159
161
  Add `<Html.Meta />` in the Document component's `<head>`:
160
162
 
@@ -48,7 +48,9 @@ Use it only when you need capabilities beyond what the handler provides:
48
48
  document loads, never an HTTP 302 — pre-stream 302s move to middleware)
49
49
  - **`meta({ data })` / `handle` exports** — data-derived page metadata becomes
50
50
  a handle push from the loader body (`ctx.use(Meta)({ title: data.name })`),
51
- with `loader(L, { ssr: false })` when it must be in the SSR'd head
51
+ with `loader(L, { ssr: false })` when it must be in the SSR'd head (not
52
+ RR7's `ssr: false` SPA mode, which has no route-wide Rango equivalent: the
53
+ loader flag makes the server settle that loader before the first flush)
52
54
 
53
55
  If the React Router loader just fetches data for its page component AND the
54
56
  component can become a server component, merge it into the handler. If the
@@ -142,10 +142,13 @@ the loader carried authority. A `createLoader()` body can throw `notFound()`
142
142
  for the missing product AND push the data-derived meta itself
143
143
  (`ctx.use(Meta)({ title: product.name })`); register it with
144
144
  `loader(ProductLoader, { ssr: false })` when the 404 status and
145
- title must be in the document deterministically. See `/loader` → "Loader
146
- Authority" and "Writing Handles from Loaders". (One RR habit that does NOT
147
- carry over: a loader `throw redirect()` is a client-side navigate on document
148
- loads, never an HTTP 302 — pre-stream 302s belong in middleware.)
145
+ title must be in the document deterministically (the flag makes the server
146
+ settle that loader before the first flush; it is unrelated to RR7's
147
+ `ssr: false` SPA mode, which has no route-wide Rango equivalent). See
148
+ `/loader` → "Loader Authority" and "Writing Handles from Loaders". (One RR
149
+ habit that does NOT carry over: a loader `throw redirect()` is a client-side
150
+ navigate on document loads, never an HTTP 302 — pre-stream 302s belong in
151
+ middleware.)
149
152
 
150
153
  ### RR7 file routing → urls() DSL
151
154
 
@@ -63,6 +63,70 @@ Read the timeline as intervals:
63
63
  actions (`action:<id>`), cache, RSC serialization, and SSR setup appear as
64
64
  separate rows, so the slow phase is visible without guessing.
65
65
 
66
+ ### Reading a PPR shell HIT
67
+
68
+ A `ppr` route serving from its shell cache commits the response as soon as the
69
+ stored prelude is read, so its waterfall ends at `ppr:shell-commit`; what runs
70
+ after the commit (the capture snapshot, the seeded tail, the resumed holes)
71
+ prints as one `shell tail` line when it finishes, and rides the NEXT request's
72
+ `Server-Timing` as `ppr-tail`. The store-read rows under `ppr:shell-read` come
73
+ from the built-in stores (`CFCacheStore`'s prelude-first read shown here;
74
+ `VercelCacheStore` reports `hit store`, one `ppr:shell-match (store)` row, and
75
+ a `serial` marker read); other stores show `ppr:shell-read`,
76
+ `ppr:shell-open`, and `ppr:shell-commit` only. From `vite preview` of
77
+ `tests/cloudflare-basic` `/ppr-large/holes` (614 KB prelude, 1.0 MB capture
78
+ snapshot), with the timeline column dropped:
79
+
80
+ ```
81
+ [RSC Perf] GET /ppr-large/holes (4.00ms)
82
+ start dur span
83
+ 1.00ms 0.00ms middleware:*#2
84
+ 1.00ms 3.00ms ppr:shell-read (hit l1)
85
+ 1.00ms 1.00ms ppr:shell-match (l1)
86
+ 1.00ms 3.00ms render:total:pprLargeHoles
87
+ 2.00ms 0.00ms ppr:shell-head (bytes=27943)
88
+ 2.00ms 2.00ms ppr:shell-prelude (bytes=628949)
89
+ 2.00ms 0.00ms ppr:shell-marker (tags=0 parallel commit-wait=0.00ms)
90
+ 4.00ms 0.00ms ppr:shell-open (cpu raw prelude=628949b)
91
+ 4.00ms 0.00ms ppr:shell-commit (cpu chunks=20 prelude=628949b)
92
+ 0.00ms 4.00ms handler:total
93
+ ```
94
+
95
+ and, when the tail finishes, one console line (wrapped here):
96
+
97
+ ```
98
+ [RSC Perf] GET /ppr-large/holes shell tail: complete snapshot=4ms
99
+ snapshot-read=2ms snapshot-bytes=1019145b snapshot-parse-cpu=2ms
100
+ records=segment:1 pruned=item:5 seed=4ms seed-cpu=0ms match=4ms handover=4ms
101
+ first-html=5ms complete=55ms prelude=628949b tail=1033762b
102
+ ```
103
+
104
+ - `ppr:shell-match` then `head` then `prelude`: the store read. Only the head
105
+ and the prelude are read before the first byte; the snapshot is read off the
106
+ commit path (`snapshot=` in the tail line). A KV hit after a Cache API miss
107
+ starts with a `ppr:shell-l1-miss` row (the L1 attempt and why it missed). A
108
+ `ppr:shell-memo (hit …)` row means the shell came from the isolate's memo:
109
+ no store read, only the marker check.
110
+ - `ppr:shell-marker` is the tag-marker check, on every `CFCacheStore` read; it
111
+ runs alongside `ppr:shell-prelude`, and `commit-wait` is how much it held the
112
+ commit back (with `tags=0` it resolves at once). For a tagged shell it adds
113
+ `memo=` (how the per-isolate marker memo answered: `fresh`, `stale` served
114
+ while it refreshes, `read` from the store, `bypass` for a request carrying
115
+ the fresh-reads cookie), `hint=<shell tags hinted>/<tags hinted>` with `lead=` (how long the
116
+ hinted marker reads ran before the entry named its tags), and `fresh-reads`
117
+ when the request carried the cookie.
118
+ - `records=` counts the snapshot records the tail was seeded with, by
119
+ family; `pruned=` counts the ones the capture dropped because no reader of
120
+ the entry consumes them. Here every HIT tail replays the handler layer from
121
+ the one segment record, so the five `"use cache"` item records that produced
122
+ it were not stored.
123
+ - `snapshot=`, `seed=`, `match=`, `first-html=` are offsets from the commit,
124
+ not durations; `snapshot-read=`, `snapshot-parse-cpu=` and `seed-cpu=` are
125
+ durations.
126
+ - On a deployed worker the clock only moves on I/O, so the CPU-only numbers
127
+ (`ppr:shell-open`, `ppr:shell-commit`, `snapshot-parse-cpu`, `seed-cpu`)
128
+ read 0 there; the byte counts are the cost to watch.
129
+
66
130
  The console waterfall uses these labels as written. In the `Server-Timing`
67
131
  header, colons become hyphens and other non-alphanumeric characters are
68
132
  dropped (`ssr:render-html` → `ssr-render-html`, `handler:total` →
@@ -148,13 +148,18 @@ route overrides via its own `@meta` slot. The route's slot pushes after the
148
148
  layout, and `Meta` keeps the last value per key, so the override wins.
149
149
 
150
150
  > **Loader-derived metadata: push from the loader instead.** This slot's
151
- > `await ctx.use(ProductLoader)` routes the data through HANDLER consumption —
152
- > the baked lane under `cache()`/PPR — just to reach a `ctx.use(Meta)` call.
153
- > The loader can now push `Meta` itself (`/loader` → "Writing Handles from
154
- > Loaders"), keeping the data on the live lane; add
155
- > `loader(Def, { ssr: false })` when the meta must be in the SSR'd
156
- > head. Keep the `@meta` slot for metadata that is NOT loader-derived
157
- > (templates, static descriptions, structured data with independent inputs).
151
+ > `await ctx.use(ProductLoader)` routes the data through HANDLER consumption
152
+ > just to reach a `ctx.use(Meta)` call: under `cache()` the slot's output is
153
+ > stored with the segment, and on a `ppr` route it bakes into the shell and a
154
+ > HIT replays it without running the slot. The loader can push `Meta` itself
155
+ > (`/loader` → "Writing Handles from Loaders"). Without `ssr: false` the
156
+ > loader runs per request, but on a `ppr` route its push is not in the
157
+ > shell's `<head>` (it applies after hydration). Add
158
+ > `loader(Def, { ssr: false })` when the meta must be in the SSR'd head; on a
159
+ > `ppr` route that bakes the push into the shell, and a HIT serves it without
160
+ > running the loader when its value has no promises. Keep the `@meta` slot for
161
+ > metadata that is NOT loader-derived (templates, static descriptions,
162
+ > structured data with independent inputs).
158
163
 
159
164
  ```typescript
160
165
  // Layout sets defaults
@@ -275,7 +280,7 @@ layout(<AccountLayout />, () => [
275
280
 
276
281
  A slot's `loading()` (whether from `handler.use` or explicit) makes that slot an independent streaming unit, exactly as in the **Streaming Behavior** section above.
277
282
 
278
- Under a shared artifact (`cache()`, `"use cache"`, a PPR shell), the server-side `await ctx.use(CartLoader)` above is the BAKED lane — the capture-time value (identity reads included) freezes into the artifact; consume the loader client-side (`useLoader` in a `"use client"` component) to keep the slot live per request. One rule, stated once: `/rango` → Invariants ("the consumption-lane rule").
283
+ Under a shared artifact (`cache()`, `"use cache"`, a PPR shell), the server-side `await ctx.use(CartLoader)` above is the BAKED lane — the capture-time value freezes into the artifact. Under `cache()`/`"use cache"` an identity read (`cookies()`, `headers()`) inside the loader bakes too; under a PPR shell it refuses the capture, so the route stays uncached. Consume the loader client-side (`useLoader` in a `"use client"` component, the slot's `loading()` as its boundary) to keep the slot live per request. One rule, stated once: `/rango` → Invariants ("the consumption-lane rule").
279
284
 
280
285
  The `parallel` mount site has the narrowest allow-list for `handler.use` items — slots cannot bring their own middleware or layout, only `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, and `transition`. See [skills/handler-use](../handler-use/SKILL.md) for the full table and merge rules.
281
286