@rangojs/router 0.0.0-experimental.d7eeaa75 → 0.0.0-experimental.dc2bd2b4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -25
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +48 -0
- package/dist/vite/index.js +2151 -846
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +57 -11
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +220 -30
- package/skills/caching/SKILL.md +116 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +229 -20
- package/skills/host-router/SKILL.md +45 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +46 -4
- package/skills/layout/SKILL.md +28 -7
- package/skills/links/SKILL.md +247 -17
- package/skills/loader/SKILL.md +219 -9
- package/skills/middleware/SKILL.md +47 -12
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +71 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +242 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +66 -9
- package/skills/route/SKILL.md +57 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +647 -0
- package/skills/typesafety/SKILL.md +319 -27
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/event-controller.ts +86 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +84 -11
- package/src/browser/navigation-client.ts +76 -28
- package/src/browser/navigation-store.ts +32 -9
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +64 -26
- package/src/browser/prefetch/cache.ts +129 -21
- package/src/browser/prefetch/fetch.ts +148 -16
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +30 -2
- package/src/browser/react/NavigationProvider.tsx +72 -31
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +22 -2
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +64 -22
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +21 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-trie.ts +52 -25
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-scope.ts +28 -42
- package/src/cache/cf/cf-cache-store.ts +54 -13
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +92 -182
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +26 -13
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +9 -4
- package/src/index.ts +53 -15
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender.ts +4 -4
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -36
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +384 -257
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +100 -28
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +26 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/handler-context.ts +21 -38
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +8 -8
- package/src/router/loader-resolution.ts +19 -2
- package/src/router/manifest.ts +22 -13
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/cache-lookup.ts +44 -91
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +53 -32
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +15 -26
- package/src/router/middleware.ts +99 -84
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/prerender-match.ts +1 -1
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/router/revalidation.ts +58 -2
- package/src/router/router-interfaces.ts +45 -28
- package/src/router/router-options.ts +40 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +27 -6
- package/src/router/segment-resolution/revalidation.ts +147 -106
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +18 -13
- package/src/router/types.ts +8 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +38 -23
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +28 -69
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-route-handler.ts +46 -53
- package/src/rsc/rsc-rendering.ts +35 -51
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +17 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +8 -2
- package/src/search-params.ts +4 -4
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +132 -116
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +143 -53
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +20 -42
- package/src/ssr/index.tsx +5 -1
- package/src/static-handler.ts +1 -1
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +193 -0
- package/src/testing/render-route.tsx +536 -0
- package/src/testing/run-loader.ts +296 -0
- package/src/testing/run-middleware.ts +170 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +183 -0
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +68 -50
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +5 -6
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +11 -0
- package/src/types/segments.ts +35 -2
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +41 -7
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +101 -51
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +67 -26
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +54 -30
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +496 -486
- package/src/vite/plugins/performance-tracks.ts +29 -25
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +116 -29
- package/src/vite/router-discovery.ts +750 -100
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +21 -6
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -6,8 +6,138 @@ argument-hint:
|
|
|
6
6
|
|
|
7
7
|
# cache() vs "use cache" — When to Use Which
|
|
8
8
|
|
|
9
|
-
Both mechanisms share the same backing store
|
|
10
|
-
|
|
9
|
+
Both mechanisms share the same backing store and cache profiles, and both accept
|
|
10
|
+
an optional `tags` field (not yet honored by the built-in stores — see "Two axes"
|
|
11
|
+
below). They differ in scope, cache key, execution model, and runtime control.
|
|
12
|
+
|
|
13
|
+
## Two axes — do not conflate
|
|
14
|
+
|
|
15
|
+
Everything on this page is **axis 1: stored-value freshness** — _is a cached
|
|
16
|
+
value still good?_ There is a second, orthogonal axis it is easy to mistake for
|
|
17
|
+
caching:
|
|
18
|
+
|
|
19
|
+
1. **Stored-value freshness** — _is a cached value still good?_
|
|
20
|
+
→ `"use cache"` (fn/component), `cache()` (segment), loader `cache()` (loader data).
|
|
21
|
+
Entries expire by **TTL/SWR**. They accept an optional `tags` field, but the
|
|
22
|
+
built-in stores (`MemorySegmentCacheStore`, `CFCacheStore`) do not yet index or
|
|
23
|
+
invalidate by tag, so tag-based invalidation (`revalidateTag`) is a
|
|
24
|
+
forward-looking API requiring a custom store with secondary indices.
|
|
25
|
+
2. **Client-update selection** — _should this segment re-run and stream to the
|
|
26
|
+
client on this navigation/action?_
|
|
27
|
+
→ `revalidate()`. Covered in `/loader` and `/route`, **not here**.
|
|
28
|
+
|
|
29
|
+
They are orthogonal and compose: a segment selected by `revalidate()` still
|
|
30
|
+
consults its cache (hit → no recompute); a cache bust does **not** force a client
|
|
31
|
+
update, and `revalidate()` never reads, writes, or expires a cached value. If you
|
|
32
|
+
know React Router, `revalidate()` is `shouldRevalidate`, not `Cache-Control`. See
|
|
33
|
+
`/rango` → "Coming from another framework" for the cross-framework mapping.
|
|
34
|
+
|
|
35
|
+
## Fast choice
|
|
36
|
+
|
|
37
|
+
Read this first; use the rest of the page when the choice has edge cases.
|
|
38
|
+
|
|
39
|
+
1. Do you want to cache an entire route or group of routes?
|
|
40
|
+
**Yes** -> `cache()`
|
|
41
|
+
2. Do you need runtime conditions, such as skip for authed users or key by
|
|
42
|
+
locale?
|
|
43
|
+
**Yes** -> `cache()` with `condition` / `key`
|
|
44
|
+
3. Do you want to cache a data fetch or helper shared across routes?
|
|
45
|
+
**Yes** -> `"use cache"`
|
|
46
|
+
4. Do you need different cache entries for different function arguments?
|
|
47
|
+
**Yes** -> `"use cache"` (keyed by args)
|
|
48
|
+
5. Is the expensive part rendering a subtree?
|
|
49
|
+
**Yes** -> `cache()` (caches rendered segments)
|
|
50
|
+
6. Is the expensive part one query inside a larger live handler?
|
|
51
|
+
**Yes** -> `"use cache"` on the query function
|
|
52
|
+
|
|
53
|
+
## Correctness & invalidation
|
|
54
|
+
|
|
55
|
+
rango's caches are built so a hit can't serve wrong or stale-shaped data. These
|
|
56
|
+
guarantees are mostly automatic — worth knowing so you don't reimplement
|
|
57
|
+
protection the framework already gives you (or assume one it deliberately
|
|
58
|
+
doesn't).
|
|
59
|
+
|
|
60
|
+
There are two guard models to keep separate. Both block response side effects
|
|
61
|
+
(`ctx.header()`, cookie writes) that would be lost on a hit; they differ in what
|
|
62
|
+
else they allow:
|
|
63
|
+
|
|
64
|
+
- **`cache()` boundary guard** (route-level) — fires while the handler runs on a
|
|
65
|
+
miss. `cookies()` and `headers()` throw (request-scoped data would be baked into
|
|
66
|
+
the shared cached shell), `ctx.get(nonCacheableVar)` throws (a tainted value
|
|
67
|
+
would be baked in), and response side effects (`ctx.header()`, `ctx.setCookie()`,
|
|
68
|
+
`ctx.setStatus()`, `ctx.onResponse()`) throw. `ctx.set()` of a cacheable var is
|
|
69
|
+
**allowed** — children are cached too and can read it. **Loaders are exempt**
|
|
70
|
+
(they always run fresh) — read request data inside a loader.
|
|
71
|
+
- **`"use cache"` exec-guard** (function-level) — the same request-scoped APIs
|
|
72
|
+
throw inside the cached function (`cookies()`, `headers()`, `ctx.set()`,
|
|
73
|
+
`ctx.header()`); additionally, tainted `ctx`/`env`/`req` args are excluded from
|
|
74
|
+
the cache key.
|
|
75
|
+
|
|
76
|
+
### Cross-deploy safety: version-segmented store keys
|
|
77
|
+
|
|
78
|
+
`CFCacheStore` prefixes every **physical** store key (the CF Cache API URL and
|
|
79
|
+
the KV key) with the build version — auto-generated from the
|
|
80
|
+
`@rangojs/router:version` virtual module, overridable via the store's `version`
|
|
81
|
+
option. A new deploy reads under a new prefix, so it can **never** read a
|
|
82
|
+
previous build's entries: no cross-deploy shape drift, and no dead client-chunk
|
|
83
|
+
references baked into cached RSC.
|
|
84
|
+
|
|
85
|
+
The tradeoff to know: **loader/data caches use the same store**, so they're
|
|
86
|
+
version-segmented too. Every deploy is therefore a _cold data cache_ — SWR can't
|
|
87
|
+
soften it, because no stale entry exists under the new key. For high-traffic,
|
|
88
|
+
frequently-deploying, data-bound apps that's a deploy-time origin warm-up. Decide
|
|
89
|
+
deliberately: accept it (correctness over hit-rate), or split the policy — let
|
|
90
|
+
the render/edge cache auto-version while a separate data store gets a stable
|
|
91
|
+
`version` so its entries survive deploys. (Per-process stores like
|
|
92
|
+
`MemorySegmentCacheStore` are cold on every restart anyway; this matters for
|
|
93
|
+
persistent stores.) See `/caching` for store setup.
|
|
94
|
+
|
|
95
|
+
### Client cache: forward/back is mutation-aware
|
|
96
|
+
|
|
97
|
+
The browser keeps a history (forward/back) cache of rendered segments. Any
|
|
98
|
+
client-side mutation (a server action) marks those entries **stale** and
|
|
99
|
+
broadcasts it to other tabs. On back/forward (popstate) the router looks up the
|
|
100
|
+
entry, sees it's stale, and revalidates — so your `revalidate()` predicates re-run
|
|
101
|
+
and the segment refreshes (SWR: the stale view paints instantly, fresh data
|
|
102
|
+
streams in). It's the client-side analog of the server-cache correctness problem,
|
|
103
|
+
solved on the partial-render axis.
|
|
104
|
+
|
|
105
|
+
### Request-scoped data: the `cache: false` taint
|
|
106
|
+
|
|
107
|
+
`createVar({ cache: false })` (or a `ctx.set(var, v, { cache: false })` write)
|
|
108
|
+
taints a value as request-scoped; reading it **directly** with `ctx.get()` inside
|
|
109
|
+
a `cache()` boundary throws — the guard against the catastrophic "serve user A's
|
|
110
|
+
data to user B" bug. The guarantee is precise and intentionally narrow — see
|
|
111
|
+
"Context Variable Cache Safety" below for exactly what it does and does not catch.
|
|
112
|
+
|
|
113
|
+
## Stale-while-revalidate
|
|
114
|
+
|
|
115
|
+
SWR is a first-class cache behavior when the backing store supports it: while an
|
|
116
|
+
entry is within its SWR window the cache serves the **stale value instantly** and
|
|
117
|
+
refreshes it in the **background** (`waitUntil`), so users never wait on a
|
|
118
|
+
recompute for a merely-aging entry.
|
|
119
|
+
|
|
120
|
+
- **`"use cache"`** resolves to the `default` profile `{ ttl: 900, swr: 1800 }`,
|
|
121
|
+
so function/component caching gets a 30-minute SWR window **out of the box**.
|
|
122
|
+
Tune or add profiles via `createRouter({ cacheProfiles: { … } })`
|
|
123
|
+
(`"use cache: short"` → the `short` profile).
|
|
124
|
+
- **`cache()` DSL and loader caches** take an explicit `swr` in seconds (or
|
|
125
|
+
inherit `store.defaults.swr`): `cache({ ttl: 60, swr: 300 })` → fresh ≤60s,
|
|
126
|
+
stale-served 60–360s, miss after 360s in stores that implement SWR for that
|
|
127
|
+
layer.
|
|
128
|
+
- **Client forward/back** is SWR after a mutation — see "Correctness &
|
|
129
|
+
invalidation" → Client cache.
|
|
130
|
+
- **Edge / document layer** uses the HTTP `stale-while-revalidate` directive; see
|
|
131
|
+
`/document-cache`.
|
|
132
|
+
|
|
133
|
+
SWR softens normal TTL expiry, **not** a cross-deploy cold cache — a new build
|
|
134
|
+
has no stale entry to serve (see version-segmented store keys above).
|
|
135
|
+
|
|
136
|
+
Store support is layer-specific. `CFCacheStore` supports SWR for segment,
|
|
137
|
+
document/response, and `"use cache"` item entries. `MemorySegmentCacheStore`
|
|
138
|
+
supports SWR for response and `"use cache"` item entries, but its route-segment
|
|
139
|
+
entries expire at TTL and never background-revalidate. Use the memory store for
|
|
140
|
+
local/dev behavior, not as proof that segment SWR is active.
|
|
11
141
|
|
|
12
142
|
## Key Differences
|
|
13
143
|
|
|
@@ -18,7 +148,7 @@ invalidation. They differ in scope, cache key, execution model, and runtime cont
|
|
|
18
148
|
| **Cache key** | Request type + pathname + params (+ optional custom) | Function identity + serialized non-tainted args |
|
|
19
149
|
| **Execution on hit** | All-or-nothing: entire handler skipped | Partial: function body skipped, calling code runs |
|
|
20
150
|
| **Runtime control** | `condition` to disable, custom `key` function | None — if the directive is present, it caches |
|
|
21
|
-
| **Side effects** |
|
|
151
|
+
| **Side effects** | Response side effects throw inside the boundary | `ctx.header()`, `ctx.set()`, etc. throw at runtime |
|
|
22
152
|
| **Handle data** | Captured and replayed | Captured and replayed |
|
|
23
153
|
| **Loaders** | Always fresh — excluded from cache, opt-in per loader | Can be used inside loaders |
|
|
24
154
|
| **Nesting** | Nest `cache()` boundaries with different TTLs | Compose by calling cached functions from uncached |
|
|
@@ -144,13 +274,38 @@ On cache hit for the route, the handler doesn't run and `getProductData` is neve
|
|
|
144
274
|
called. On cache miss, the handler runs and `getProductData` may itself return a
|
|
145
275
|
cached value from a previous call with the same slug.
|
|
146
276
|
|
|
277
|
+
### Nesting rule: the outer window bounds the inner
|
|
278
|
+
|
|
279
|
+
A cache's window bounds everything rendered inside it (loaders excepted). An
|
|
280
|
+
inner shorter TTL only takes effect when the **enclosing** cache recomputes — it
|
|
281
|
+
does **not** keep a value fresher than its parent:
|
|
282
|
+
|
|
283
|
+
- Outer `cache()` **fresh hit** → the subtree is served from stored RSC, so inner
|
|
284
|
+
`"use cache"` functions are **not consulted** (frozen at the outer's age — no
|
|
285
|
+
code inside the boundary runs on a hit).
|
|
286
|
+
- Outer **miss / SWR revalidation** → inner caches are consulted, each per its own
|
|
287
|
+
ttl/swr. With SWR on the outer, a stale subtree serves instantly and refreshes
|
|
288
|
+
in the background, so under traffic it keeps refreshing rather than rotting to
|
|
289
|
+
the worst case.
|
|
290
|
+
- **Loaders are the exception** — excluded from the segment cache, re-resolved
|
|
291
|
+
live even on an outer hit.
|
|
292
|
+
|
|
293
|
+
So `"use cache: short"` (60s) inside `cache({ ttl: 600 })` yields ~600s freshness
|
|
294
|
+
on hits, **not** 60s. This is not a bug: setting `cache({ ttl: 600 })` declares
|
|
295
|
+
"this subtree may be ~600s stale." **If a value must be fresher than its
|
|
296
|
+
enclosing segment, put it in a loader** (always live). `debugPerformance` prints
|
|
297
|
+
cache hits per layer, so the actual per-request behavior is observable.
|
|
298
|
+
|
|
147
299
|
## Headers and Cookies
|
|
148
300
|
|
|
149
301
|
Neither mechanism caches response headers or cookies.
|
|
150
302
|
|
|
151
|
-
- **cache()**:
|
|
152
|
-
|
|
153
|
-
(
|
|
303
|
+
- **cache()**: Response-level side effects throw inside the cache boundary even
|
|
304
|
+
on a miss: `ctx.header()`, `ctx.setCookie()`, `ctx.deleteCookie()`,
|
|
305
|
+
`ctx.setStatus()`, `ctx.onResponse()`, and direct `ctx.headers` mutation. On a
|
|
306
|
+
hit the handler would be skipped, so allowing the write on a miss would produce
|
|
307
|
+
inconsistent responses. If you need headers or cookies on every response, set
|
|
308
|
+
them in middleware or a live segment outside the cache boundary.
|
|
154
309
|
- **"use cache"**: cookies() and headers() throw inside the cached function
|
|
155
310
|
(both reads and writes). ctx.header() also throws. Move them outside.
|
|
156
311
|
|
|
@@ -165,8 +320,9 @@ middleware(async (ctx, next) => {
|
|
|
165
320
|
## Context Variable Cache Safety
|
|
166
321
|
|
|
167
322
|
Context variables created with `createVar()` are cacheable by default and can
|
|
168
|
-
be read freely inside
|
|
169
|
-
|
|
323
|
+
be read freely inside cached scopes. A non-cacheable var throws when read
|
|
324
|
+
**directly** with `ctx.get()` inside a `cache()` boundary — where the value would
|
|
325
|
+
otherwise be serialized into the stored segment.
|
|
170
326
|
|
|
171
327
|
There are two ways to mark a value as non-cacheable:
|
|
172
328
|
|
|
@@ -181,19 +337,68 @@ ctx.set(Theme, derivedTheme, { cache: false });
|
|
|
181
337
|
"Least cacheable wins": if either the var definition or the `ctx.set()` call
|
|
182
338
|
specifies `cache: false`, the value is non-cacheable.
|
|
183
339
|
|
|
184
|
-
**Behavior inside cache
|
|
340
|
+
**Behavior inside a `cache()` boundary:**
|
|
341
|
+
|
|
342
|
+
| Operation | Inside a `cache()` boundary |
|
|
343
|
+
| ----------------------------------------- | ------------------------------------------------------ |
|
|
344
|
+
| `cookies()` / `headers()` (read or write) | Throws (request-scoped, would poison the shared entry) |
|
|
345
|
+
| `ctx.get(cacheableVar)` | Allowed |
|
|
346
|
+
| `ctx.get(nonCacheableVar)` | Throws (would be baked in) |
|
|
347
|
+
| `ctx.set(var, value)` (cacheable) | Allowed |
|
|
348
|
+
| `ctx.header()` / cookie writes | Throws (response side effect would be lost on hit) |
|
|
349
|
+
| Any of the above **inside a loader** | Allowed (loaders always run fresh) |
|
|
185
350
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
351
|
+
(Both scopes block the same request-scoped APIs — `cookies()`, `headers()`,
|
|
352
|
+
response side effects, and non-cacheable `ctx.get()` — because each would leak
|
|
353
|
+
per-request data into a shared cache entry. The `cache()` boundary tracks the
|
|
354
|
+
scope via `isInsideCacheScope()`; `"use cache"` uses the exec guard and also
|
|
355
|
+
excludes tainted `ctx`/`env`/`req` args from the cache key. Loaders are exempt in
|
|
356
|
+
both — see "Headers and Cookies" and the precise guarantee below.)
|
|
192
357
|
|
|
193
358
|
Write is dumb — `ctx.set()` stores the cache metadata but does not enforce.
|
|
194
359
|
Enforcement happens at read time (`ctx.get()`), where ALS detects the cache
|
|
195
360
|
scope and rejects non-cacheable reads.
|
|
196
361
|
|
|
362
|
+
### The guarantee is precise — a direct read inside `cache()`, not propagating
|
|
363
|
+
|
|
364
|
+
The guard fires on a **direct** `ctx.get(taintedVar)` **inside a `cache()`
|
|
365
|
+
boundary** (the scope `isInsideCacheScope` detects). The taint lives on the
|
|
366
|
+
variable; a value **derived** from it and read **outside** the boundary is not
|
|
367
|
+
tracked:
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
// CAUGHT — direct read of a tainted var inside a cache() boundary
|
|
371
|
+
cache({ ttl: 60 }, () => [
|
|
372
|
+
path("/dashboard", (ctx) => {
|
|
373
|
+
const user = ctx.get(User); // throws: non-cacheable read inside cache()
|
|
374
|
+
return <Dashboard user={user} />;
|
|
375
|
+
}, { name: "dashboard" }),
|
|
376
|
+
]);
|
|
377
|
+
|
|
378
|
+
// NOT CAUGHT — read outside the boundary, derived value cached
|
|
379
|
+
layout((ctx) => {
|
|
380
|
+
const name = ctx.get(User).name; // allowed — this layout is not cached
|
|
381
|
+
ctx.set(UserName, name); // now a plain (cacheable) string
|
|
382
|
+
return <Outlet />;
|
|
383
|
+
}, () => [
|
|
384
|
+
cache({ ttl: 60 }, () => [
|
|
385
|
+
// a child reads ctx.get(UserName) and silently caches user-derived data
|
|
386
|
+
]),
|
|
387
|
+
]);
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
So do **not** read this as "you can't cache user data" — that overstates it and
|
|
391
|
+
breeds the false confidence that makes the derived leak _more_ likely. The guard
|
|
392
|
+
is deliberately non-propagating (propagation would cost a wrapper per derivation
|
|
393
|
+
on the hot path), and it is scoped to the `cache()` segment boundary. `"use
|
|
394
|
+
cache"` functions block the same request-scoped reads (`cookies()` / `headers()`
|
|
395
|
+
throw inside them) and additionally exclude tainted `ctx`/`env`/`req` args from
|
|
396
|
+
the cache key. The pattern that stays safe is also the natural one:
|
|
397
|
+
**read tainted context at the point of use, in the path that needs it (a loader or
|
|
398
|
+
live segment) — never extract user data into a plain value and cache that.**
|
|
399
|
+
Loaders are exempt because they run outside the cache scope and resolve fresh
|
|
400
|
+
every request.
|
|
401
|
+
|
|
197
402
|
## Loaders Are Always Fresh
|
|
198
403
|
|
|
199
404
|
Loaders are **never cached** by route-level `cache()`. Even on a full cache hit
|
|
@@ -272,21 +477,6 @@ data is cached independently from the route's segment cache. Loader caching
|
|
|
272
477
|
supports custom keys, tags, SWR, conditional bypass, and per-loader store
|
|
273
478
|
overrides — see `/loader` for the full reference.
|
|
274
479
|
|
|
275
|
-
## Decision Flowchart
|
|
276
|
-
|
|
277
|
-
1. Do you want to cache an entire route or group of routes?
|
|
278
|
-
**Yes** -> `cache()`
|
|
279
|
-
2. Do you need runtime conditions (skip for auth users, key by locale)?
|
|
280
|
-
**Yes** -> `cache()` with `condition` / `key`
|
|
281
|
-
3. Do you want to cache a data fetch shared across routes?
|
|
282
|
-
**Yes** -> `"use cache"`
|
|
283
|
-
4. Do you need different cache entries for different arguments?
|
|
284
|
-
**Yes** -> `"use cache"` (keyed by args)
|
|
285
|
-
5. Is the expensive part rendering, not data fetching?
|
|
286
|
-
**Yes** -> `cache()` (caches rendered segments)
|
|
287
|
-
6. Is the expensive part a single query inside a larger handler?
|
|
288
|
-
**Yes** -> `"use cache"` on the query function
|
|
289
|
-
|
|
290
480
|
## See Also
|
|
291
481
|
|
|
292
482
|
- `/caching` — cache() DSL setup, stores, nested boundaries
|
package/skills/caching/SKILL.md
CHANGED
|
@@ -8,6 +8,46 @@ argument-hint: [setup]
|
|
|
8
8
|
|
|
9
9
|
@rangojs/router supports segment-level caching with stale-while-revalidate (SWR) for optimal performance.
|
|
10
10
|
|
|
11
|
+
> SWR support is store-specific. `CFCacheStore` revalidates segment, response,
|
|
12
|
+
> and `"use cache"` entries in the background. `MemorySegmentCacheStore`
|
|
13
|
+
> supports SWR for response and `"use cache"` item entries, but its
|
|
14
|
+
> route-segment entries expire at TTL with no background revalidation — use
|
|
15
|
+
> `CFCacheStore` for real segment SWR. See `/cache-guide`.
|
|
16
|
+
|
|
17
|
+
## cache() is Partial Prerendering (PPR)
|
|
18
|
+
|
|
19
|
+
`cache()` caches **everything except loaders**. On a cache hit, the cached
|
|
20
|
+
segments (layouts, route components, parallels — including any resolved
|
|
21
|
+
Suspense) are served from the store, and **loaders re-run fresh on every
|
|
22
|
+
request**, streaming their results into the same response. Loaders are the
|
|
23
|
+
dynamic "holes" of an otherwise-cached tree.
|
|
24
|
+
|
|
25
|
+
This means a `cache()` boundary at the document root **is** whole-document
|
|
26
|
+
Partial Prerendering: the static shell is cached and served instantly while
|
|
27
|
+
per-request/per-user data stays live — in one streamed response, no extra round
|
|
28
|
+
trip. The browser cannot tell the shell came from cache.
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
cache({ ttl: 60, swr: 300 }, () => [
|
|
32
|
+
layout(<RootLayout />), // cached shell
|
|
33
|
+
path("/dashboard", Dashboard, { name: "dashboard" }, () => [
|
|
34
|
+
loader(StatsLoader), // DYNAMIC HOLE — re-runs every request
|
|
35
|
+
]),
|
|
36
|
+
]);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The consumer rule: **want it cached? render it inline. want it dynamic? put it
|
|
40
|
+
in a loader and read it with `useLoader()` in a client component.** Anything
|
|
41
|
+
read with `cookies()`, `headers()`, or a non-cacheable variable belongs in a
|
|
42
|
+
loader (loaders always run fresh). Reading it directly in a cached handler
|
|
43
|
+
throws; awaiting it with `ctx.use()` and rendering the result in a cached
|
|
44
|
+
handler silently bakes per-request data into the shared shell (see "Cache purity
|
|
45
|
+
& tainted objects" below).
|
|
46
|
+
|
|
47
|
+
Pre-rendering (`/prerender`) is the build-time twin: it caches the same shell at
|
|
48
|
+
build time instead of on first request. Both feed the segment system
|
|
49
|
+
identically, and loaders always run fresh at request time.
|
|
50
|
+
|
|
11
51
|
## Route-Level Caching with cache()
|
|
12
52
|
|
|
13
53
|
Use the `cache()` DSL function to cache routes:
|
|
@@ -116,7 +156,6 @@ import { MemorySegmentCacheStore } from "@rangojs/router/cache";
|
|
|
116
156
|
|
|
117
157
|
const store = new MemorySegmentCacheStore({
|
|
118
158
|
defaults: { ttl: 60, swr: 300 },
|
|
119
|
-
maxSize: 1000, // Max entries
|
|
120
159
|
});
|
|
121
160
|
```
|
|
122
161
|
|
|
@@ -173,13 +212,82 @@ const router = createRouter<AppBindings>({
|
|
|
173
212
|
KV entries require `expirationTtl >= 60s`. Short-lived entries (< 60s total TTL)
|
|
174
213
|
are only cached in L1.
|
|
175
214
|
|
|
176
|
-
##
|
|
215
|
+
## Cache purity & tainted objects
|
|
216
|
+
|
|
217
|
+
A `cache()` boundary caches everything except loaders, so anything read inside a
|
|
218
|
+
cached handler is **frozen into the shared cache entry** and served to every
|
|
219
|
+
subsequent visitor. To stop one user's request-scoped data from leaking to
|
|
220
|
+
another, request-scoped APIs are guarded inside a cache scope:
|
|
221
|
+
|
|
222
|
+
| Inside a `cache()` boundary | Behavior |
|
|
223
|
+
| --------------------------------------------------------------- | --------------------------------------------------- |
|
|
224
|
+
| `cookies()` / `headers()` (read or write) | **throws** — request-scoped, would poison the entry |
|
|
225
|
+
| `ctx.header()` / `setCookie()` / `setStatus()` / `onResponse()` | **throws** — response side effects lost on a hit |
|
|
226
|
+
| `ctx.get(var)` where the var is `{ cache: false }` | **throws** on read |
|
|
227
|
+
| `ctx.set(var, value)` for a cacheable var | allowed (children are cached too) |
|
|
228
|
+
| Any of the above **inside a loader** | **allowed** — loaders always run fresh |
|
|
229
|
+
|
|
230
|
+
**Tainted objects.** Request-scoped objects (`ctx`, `env`, `request`) carry an
|
|
231
|
+
internal taint symbol so they are excluded from `"use cache"` cache keys, and
|
|
232
|
+
the cache scope is tracked via async-local state. Two flags back the guards:
|
|
233
|
+
`INSIDE_CACHE_EXEC` (set while a `"use cache"` function runs) and the `cache()`
|
|
234
|
+
DSL scope (`isInsideCacheScope()`). `isInsideCacheScope()` deliberately returns
|
|
235
|
+
`false` inside loaders — which is exactly why loaders are the dynamic holes:
|
|
236
|
+
they may read `cookies()`/`headers()` and re-run on every request.
|
|
237
|
+
|
|
238
|
+
The fix for "I need request data in a cached route": register a `loader()` and
|
|
239
|
+
**consume it with `useLoader()` in a client component**. The loader is the
|
|
240
|
+
dynamic hole — its data rides the fresh (never-cached) loader segment and is
|
|
241
|
+
rendered in the client component, so it never lands in the cached shell.
|
|
242
|
+
|
|
243
|
+
This is NOT the same as awaiting the loader in the handler. A cached handler
|
|
244
|
+
that does `await ctx.use(Loader)` and renders the result bakes that per-request
|
|
245
|
+
data straight into the shared cached segment — the loader running "fresh" does
|
|
246
|
+
not help, because its output was inlined into the cached parent, and `ctx.use()`
|
|
247
|
+
is **not** guarded. `ctx.use()` is a server-side escape hatch for non-rendered
|
|
248
|
+
uses (set a ctx var, make a routing decision); never render its result inside a
|
|
249
|
+
cached handler.
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// WRONG — throws: cookies() read directly in a cached handler
|
|
253
|
+
cache({ ttl: 60 }, () => [
|
|
254
|
+
path("/me", () => <Profile id={cookies().get("uid")?.value} />),
|
|
255
|
+
]);
|
|
256
|
+
|
|
257
|
+
// ALSO WRONG (unguarded, but leaks) — the awaited loader data is rendered into
|
|
258
|
+
// the cached handler, so the user's data is frozen into the shared shell.
|
|
259
|
+
cache({ ttl: 60 }, () => [
|
|
260
|
+
path(
|
|
261
|
+
"/me",
|
|
262
|
+
async (ctx) => {
|
|
263
|
+
const { user } = await ctx.use(MeLoader); // runs fresh…
|
|
264
|
+
return <Profile user={user} />; // …but inlined into the CACHED segment → leak
|
|
265
|
+
},
|
|
266
|
+
{ name: "me" },
|
|
267
|
+
() => [loader(MeLoader)],
|
|
268
|
+
),
|
|
269
|
+
]);
|
|
270
|
+
|
|
271
|
+
// RIGHT — consume the loader in a CLIENT component via useLoader(). The cached
|
|
272
|
+
// route segment holds only the <Profile/> reference; the user data rides the
|
|
273
|
+
// fresh loader segment and renders client-side.
|
|
274
|
+
|
|
275
|
+
// profile.tsx (client component)
|
|
276
|
+
"use client";
|
|
277
|
+
import { useLoader } from "@rangojs/router/client";
|
|
278
|
+
|
|
279
|
+
export function Profile() {
|
|
280
|
+
const { user } = useLoader(MeLoader); // fresh per request; never cached
|
|
281
|
+
return <span>{user.name}</span>;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// urls — register the loader; MeLoader reads cookies() inside the loader (allowed)
|
|
285
|
+
cache({ ttl: 60 }, () => [
|
|
286
|
+
path("/me", () => <Profile />, { name: "me" }, () => [loader(MeLoader)]),
|
|
287
|
+
]);
|
|
288
|
+
```
|
|
177
289
|
|
|
178
|
-
|
|
179
|
-
written inside `cache()` scopes. Variables marked with `{ cache: false }` (at
|
|
180
|
-
the var level or write level) throw when read inside a cache scope. Response
|
|
181
|
-
side effects (`ctx.header()`, `ctx.cookie()`) always throw inside cache
|
|
182
|
-
boundaries. See `/cache-guide` for the full cache safety table.
|
|
290
|
+
See `/cache-guide` for the full decision guide and the `cache()` vs `"use cache"` comparison.
|
|
183
291
|
|
|
184
292
|
## Nested Cache Boundaries
|
|
185
293
|
|
|
@@ -245,7 +353,7 @@ export const urlpatterns = urls(({ path, layout, cache, loader, revalidate }) =>
|
|
|
245
353
|
path("/shop/product/:slug", ProductPage, { name: "product" }, () => [
|
|
246
354
|
loader(ProductLoader, () => [cache({ ttl: 120 })]),
|
|
247
355
|
loader(CartLoader, () => [
|
|
248
|
-
revalidate(({ actionId }) => actionId?.includes("Cart")
|
|
356
|
+
revalidate(({ actionId }) => actionId?.includes("Cart") || undefined),
|
|
249
357
|
]),
|
|
250
358
|
]),
|
|
251
359
|
]),
|
|
@@ -55,7 +55,9 @@ import { cache, revalidate, loading, errorBoundary, middleware } from "@rangojs/
|
|
|
55
55
|
// Shared caching configuration
|
|
56
56
|
const withCaching = () => [
|
|
57
57
|
cache({ ttl: 600_000 }),
|
|
58
|
-
|
|
58
|
+
// Defer on navigation (|| undefined) so each route keeps its own param/search
|
|
59
|
+
// revalidation default; only force a re-run when an action ran.
|
|
60
|
+
revalidate(({ actionId }) => (actionId ? true : undefined)),
|
|
59
61
|
];
|
|
60
62
|
|
|
61
63
|
// Shared loading and error handling
|
|
@@ -71,6 +73,29 @@ const withAuth = () => [
|
|
|
71
73
|
];
|
|
72
74
|
```
|
|
73
75
|
|
|
76
|
+
> **Factories compose logic, not just values.** A `revalidate()` predicate in a
|
|
77
|
+
> shared factory applies its logic to _every_ route that composes it, so a
|
|
78
|
+
> footgun here is amplified across the app. Two rules:
|
|
79
|
+
>
|
|
80
|
+
> 1. Use `|| undefined` (defer), not `?? false` (hard short-circuit), in shared
|
|
81
|
+
> predicates — a hard `false` ends the chain and overrides each consuming
|
|
82
|
+
> route's own default, and a downstream revalidator never runs. See `/loader`
|
|
83
|
+
> → "`|| undefined` (defer) vs `?? false` (hard)".
|
|
84
|
+
> 2. Match actions with `ctx.isAction(Action)`, not an inline
|
|
85
|
+
> `actionId.includes("…")` buried in a factory: it resolves the action from an
|
|
86
|
+
> imported reference, so a rename is a compile error in one place instead of
|
|
87
|
+
> silent drift across every consumer.
|
|
88
|
+
>
|
|
89
|
+
> Remember the axis: a factory's `revalidate()` controls client-update
|
|
90
|
+
> selection, while its `cache()` controls stored-value freshness. They are
|
|
91
|
+
> independent even when bundled in the same factory (`/cache-guide` → "Two axes").
|
|
92
|
+
|
|
93
|
+
> **Keep factories small and intention-named.** The anti-pattern that kills
|
|
94
|
+
> readability is over-bundling — a `withDefaults()` that secretly adds five
|
|
95
|
+
> things — and factory-of-factories nesting (leaning on `.flat(3)`). Surprising
|
|
96
|
+
> config stays inline; extract only the boring, repeated parts; compose by
|
|
97
|
+
> _naming concerns_ (`withAuth()`, `withCaching()`), not by hiding them.
|
|
98
|
+
|
|
74
99
|
## Using Factories in Routes
|
|
75
100
|
|
|
76
101
|
Place factory calls inside `path()` or `layout()` use callbacks. The returned arrays are flattened automatically (up to 3 levels):
|
|
@@ -107,7 +132,7 @@ import { authMiddleware } from "./middleware/auth";
|
|
|
107
132
|
|
|
108
133
|
export const withPublicDefaults = () => [
|
|
109
134
|
cache({ ttl: 300 }),
|
|
110
|
-
revalidate(({ actionId }) =>
|
|
135
|
+
revalidate(({ actionId }) => (actionId ? true : undefined)),
|
|
111
136
|
];
|
|
112
137
|
|
|
113
138
|
export const withProtectedDefaults = () => [
|