@rangojs/router 0.1.1 → 0.2.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.
- package/README.md +2 -1
- package/dist/types/cache/document-cache.d.ts +3 -1
- package/dist/types/cache/types.d.ts +4 -3
- package/dist/types/route-definition/helpers-types.d.ts +6 -6
- package/dist/types/router/match-handlers.d.ts +2 -3
- package/dist/types/router/segment-resolution/view-transition-default.d.ts +3 -4
- package/dist/types/router/transition-when.d.ts +13 -0
- package/dist/types/rsc/shell-serve.d.ts +2 -0
- package/dist/types/rsc/transition-gate.d.ts +10 -14
- package/dist/types/server/request-context.d.ts +5 -1
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/index.d.ts +2 -2
- package/dist/types/testing/run-transition-when.d.ts +6 -5
- package/dist/types/testing/shell-status.d.ts +23 -3
- package/dist/types/types/segments.d.ts +27 -22
- package/dist/types/urls/path-helper-types.d.ts +4 -3
- package/dist/vite/index.js +1 -1
- package/package.json +20 -21
- package/skills/cache-guide/SKILL.md +6 -3
- package/skills/catalog.json +7 -1
- package/skills/deployment-caching/SKILL.md +176 -0
- package/skills/document-cache/SKILL.md +30 -3
- package/skills/ppr/SKILL.md +57 -25
- package/skills/prerender/SKILL.md +15 -8
- package/skills/rango/SKILL.md +20 -17
- package/skills/testing/SKILL.md +1 -1
- package/skills/testing/cache-prerender.md +5 -1
- package/skills/vercel/SKILL.md +22 -1
- package/skills/view-transitions/SKILL.md +12 -8
- package/src/cache/cache-scope.ts +6 -1
- package/src/cache/document-cache.ts +4 -2
- package/src/cache/types.ts +4 -3
- package/src/route-definition/helpers-types.ts +6 -6
- package/src/router/match-handlers.ts +44 -6
- package/src/router/match-middleware/cache-lookup.ts +10 -3
- package/src/router/segment-resolution/view-transition-default.ts +9 -5
- package/src/router/transition-when.ts +76 -0
- package/src/rsc/progressive-enhancement.ts +9 -14
- package/src/rsc/rsc-rendering.ts +142 -31
- package/src/rsc/shell-serve.ts +3 -0
- package/src/rsc/transition-gate.ts +37 -40
- package/src/server/request-context.ts +6 -0
- package/src/testing/e2e/index.ts +5 -0
- package/src/testing/index.ts +9 -1
- package/src/testing/run-transition-when.ts +42 -9
- package/src/testing/shell-status.ts +92 -3
- package/src/types/segments.ts +27 -22
- package/src/urls/path-helper-types.ts +4 -3
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deployment-caching
|
|
3
|
+
description: "Choose the deployment cache boundary for a Rango app: in-function segment/prerender/PPR caches, store-backed whole-response caching, or an external CDN cache. Use when comparing Cloudflare, Vercel, and Node deployments; deciding whether Cache-Control can reduce origin work; or reasoning about middleware, live loaders, PPR, and CDN behavior."
|
|
4
|
+
argument-hint: [cloudflare|vercel|node]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Deployment caching boundaries
|
|
8
|
+
|
|
9
|
+
Start with one question: **does the request reach Rango before shared bytes are
|
|
10
|
+
served?** That boundary decides whether middleware runs, whether loaders stay
|
|
11
|
+
live, and which invalidation system owns the result.
|
|
12
|
+
|
|
13
|
+
## The execution matrix
|
|
14
|
+
|
|
15
|
+
| Mechanism | Stored artifact | Function/worker runs on a hit? | Rango middleware on a hit? | What stays live? |
|
|
16
|
+
| --------------------------------- | ----------------------------------------------- | -----------------------------: | -----------------------------------------------------------: | ------------------------------------ |
|
|
17
|
+
| `"use cache"` | one function result | yes | yes | caller, handlers, loaders, rendering |
|
|
18
|
+
| `cache()` | serialized Flight segments | yes | yes | middleware, loaders, HTML rendering |
|
|
19
|
+
| `Prerender()` | build-time Flight segments in the server bundle | yes | yes | middleware, loaders, HTML rendering |
|
|
20
|
+
| `ppr` | HTML prelude + React postponed state | yes | **yes, before shell commit** | middleware, holes, hydration payload |
|
|
21
|
+
| `createDocumentCacheMiddleware()` | complete response in the app cache store | yes | outer middleware runs; the hit skips its downstream pipeline | nothing downstream |
|
|
22
|
+
| HTTP CDN cache (`s-maxage`) | complete HTTP response outside the app | **no** | **no** | nothing |
|
|
23
|
+
|
|
24
|
+
The first five rows enter the app. The first four preserve the complete request
|
|
25
|
+
model; a document-cache middleware hit intentionally short-circuits its
|
|
26
|
+
downstream pipeline. The last row is a platform cache: on a hit it serves bytes
|
|
27
|
+
without invoking Rango at all.
|
|
28
|
+
|
|
29
|
+
## Rango PPR is in-function PPR
|
|
30
|
+
|
|
31
|
+
Rango's `ppr` path option does not make the HTML shell a public static asset.
|
|
32
|
+
The worker or function handles every document request:
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
request
|
|
36
|
+
-> global middleware
|
|
37
|
+
-> route middleware
|
|
38
|
+
-> shell lookup
|
|
39
|
+
-> flush stored prelude
|
|
40
|
+
-> run live loaders + Flight + React resume
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
That ordering is the security contract. A redirect, 401, tenant decision,
|
|
44
|
+
`ctx.dynamic()`, cookie, or response header from middleware wins before a shell
|
|
45
|
+
byte is committed.
|
|
46
|
+
|
|
47
|
+
There are two shell producers:
|
|
48
|
+
|
|
49
|
+
- An ordinary `ppr` route captures after a runtime MISS.
|
|
50
|
+
- A route that combines `Prerender()` with the `ppr` path option captures during
|
|
51
|
+
`vite build`, so its first production request can already be a shell HIT.
|
|
52
|
+
|
|
53
|
+
Both producers feed the same in-function serve path. Build-time shells are
|
|
54
|
+
content-hashed modules in the server bundle, not CDN-served HTML files.
|
|
55
|
+
|
|
56
|
+
`ppr.ttl`, `ppr.swr`, and `ppr.tags` govern that shell entry. They do **not**
|
|
57
|
+
emit HTTP `Cache-Control` and do not configure a platform CDN.
|
|
58
|
+
|
|
59
|
+
## Platform deployment shapes
|
|
60
|
+
|
|
61
|
+
### Cloudflare
|
|
62
|
+
|
|
63
|
+
The Cloudflare preset runs the RSC app in a Worker. The request reaches the
|
|
64
|
+
Worker at the edge, middleware runs there, and PPR shell lookup/resume stays in
|
|
65
|
+
that Worker. `CFCacheStore` supplies the app cache families; client assets are
|
|
66
|
+
served separately as assets.
|
|
67
|
+
|
|
68
|
+
### Vercel
|
|
69
|
+
|
|
70
|
+
The Vercel preset emits static client assets plus one streaming Node Function.
|
|
71
|
+
HTML, Flight, prerender payloads, and PPR shells are served from that function.
|
|
72
|
+
`VercelCacheStore` uses Runtime Cache inside the function; Runtime Cache is not
|
|
73
|
+
Vercel's CDN/ISR cache.
|
|
74
|
+
|
|
75
|
+
The preset deliberately emits no Vercel `.prerender-config.json`, response
|
|
76
|
+
`chain`, or CDN-stitched resume function. Vercel's open-source Build Output
|
|
77
|
+
parser accepts a generic `chain`, but the production CDN stitching protocol is
|
|
78
|
+
not a documented third-party Build Output contract. More importantly, a CDN
|
|
79
|
+
that emits the shell before invoking Rango cannot preserve the middleware
|
|
80
|
+
ordering above. Parser support does not solve that semantic mismatch.
|
|
81
|
+
|
|
82
|
+
### Generic Node
|
|
83
|
+
|
|
84
|
+
The Node preset runs the same in-function model behind your server or reverse
|
|
85
|
+
proxy. A CDN in front may cache complete responses when you emit shared-cache
|
|
86
|
+
headers, but that CDN is outside Rango and follows the HTTP CDN row in the
|
|
87
|
+
matrix.
|
|
88
|
+
|
|
89
|
+
## Cache-Control is the full-response mitigation
|
|
90
|
+
|
|
91
|
+
If a response is completely public and shared, HTTP caching can eliminate more
|
|
92
|
+
origin work than CDN-stitched PPR:
|
|
93
|
+
|
|
94
|
+
```http
|
|
95
|
+
Cache-Control: public, s-maxage=300, stale-while-revalidate=3600
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
On a CDN hit the function does not run and no shell or dynamic tail crosses the
|
|
99
|
+
origin boundary. The tradeoff is exact: the CDN stores the **completed** HTML
|
|
100
|
+
response, including loader output, resumed holes, and hydration payload. It
|
|
101
|
+
does not cache only the PPR prelude.
|
|
102
|
+
|
|
103
|
+
Use shared HTTP caching only when all of these are true:
|
|
104
|
+
|
|
105
|
+
- the complete response is identical for every request sharing the cache key;
|
|
106
|
+
- no authorization, redirect, rate-limit, tenant, or preview middleware must
|
|
107
|
+
run on every request;
|
|
108
|
+
- no loader or rendered value contains session, cart, account, experiment, or
|
|
109
|
+
other per-user data;
|
|
110
|
+
- replaying the response headers is safe, with no per-client `Set-Cookie`;
|
|
111
|
+
- TTL/SWR freshness for the whole response is acceptable.
|
|
112
|
+
|
|
113
|
+
Do not use `Vary: Cookie` as a general escape hatch. It creates a variant for
|
|
114
|
+
every cookie combination, destroys cache reuse, and makes the safety contract
|
|
115
|
+
hard to audit.
|
|
116
|
+
|
|
117
|
+
### One header, two possible consumers
|
|
118
|
+
|
|
119
|
+
`createDocumentCacheMiddleware()` parses `Cache-Control: s-maxage` as policy for
|
|
120
|
+
the configured app store's response family. The deployment platform may also
|
|
121
|
+
interpret the same header and cache the response at its CDN.
|
|
122
|
+
|
|
123
|
+
These are independent caches:
|
|
124
|
+
|
|
125
|
+
```text
|
|
126
|
+
CDN HIT
|
|
127
|
+
-> function never runs
|
|
128
|
+
|
|
129
|
+
CDN MISS
|
|
130
|
+
-> function
|
|
131
|
+
-> Rango document-cache middleware HIT or MISS
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Consequences:
|
|
135
|
+
|
|
136
|
+
- `skipPaths`, `isEnabled`, and `keyGenerator` only control the Rango
|
|
137
|
+
middleware. They cannot guard a response already served by the CDN.
|
|
138
|
+
- `x-document-cache-status` reports the Rango store only when the function
|
|
139
|
+
executes. A CDN may replay an old status header; use the platform's own cache
|
|
140
|
+
header or logs to identify CDN hits.
|
|
141
|
+
- Platform CDN invalidation and the app store's tags/TTL are separate systems.
|
|
142
|
+
Do not assume `updateTag()` purges an external CDN response.
|
|
143
|
+
|
|
144
|
+
Use a platform-targeted header such as `Vercel-CDN-Cache-Control` when you need
|
|
145
|
+
to keep CDN policy out of browser/downstream `Cache-Control`, but the complete
|
|
146
|
+
response and middleware-bypass rules are unchanged. Rango's document middleware
|
|
147
|
+
does not parse that platform-specific header; use it when the CDN, rather than
|
|
148
|
+
the app store, should own the complete response.
|
|
149
|
+
|
|
150
|
+
## Decision guide
|
|
151
|
+
|
|
152
|
+
| Requirement | Choose |
|
|
153
|
+
| --------------------------------------------------- | ------------------------------------------ |
|
|
154
|
+
| Per-request auth or request shaping | in-function caching; never shared CDN HTML |
|
|
155
|
+
| Stable shell with cart/session/live prices | `ppr` with live holes |
|
|
156
|
+
| Build-known segments with live loaders | `Prerender()` |
|
|
157
|
+
| Fully public response, whole-page TTL is acceptable | HTTP `s-maxage` + SWR |
|
|
158
|
+
| Whole response reused inside the app store | `createDocumentCacheMiddleware()` |
|
|
159
|
+
| One query or component is expensive | `"use cache"` |
|
|
160
|
+
| One route subtree is expensive | `cache()` |
|
|
161
|
+
|
|
162
|
+
For a large dynamic app, CDN-stitched PPR would mainly improve shell first-byte
|
|
163
|
+
latency and avoid sending the prelude from the function. It would not remove the
|
|
164
|
+
per-request function invocation, live loaders, Flight payload, or React resume.
|
|
165
|
+
If the current in-function path is already fast, preserve middleware semantics
|
|
166
|
+
and apply full-response CDN caching only to the smaller set of routes that are
|
|
167
|
+
provably public and shared.
|
|
168
|
+
|
|
169
|
+
## Related skills
|
|
170
|
+
|
|
171
|
+
- `/ppr` — shell capture, holes, middleware commit point, invalidation
|
|
172
|
+
- `/prerender` — build-time Flight segments and `Prerender + ppr`
|
|
173
|
+
- `/document-cache` — store-backed complete-response middleware
|
|
174
|
+
- `/vercel` — Vercel Build Output preset and Runtime Cache wiring
|
|
175
|
+
- `/cloudflare` — Worker deployment and bindings
|
|
176
|
+
- `/cache-guide` — function, loader, and segment cache selection
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: document-cache
|
|
3
|
-
description: Cache
|
|
3
|
+
description: Cache complete HTTP responses in Rango's configured app store with createDocumentCacheMiddleware, using Cache-Control s-maxage as policy. Use when reusing a whole HTML/RSC response, comparing the store-backed middleware with a platform CDN cache, or deciding whether full-response caching is safe.
|
|
4
4
|
argument-hint: [setup]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
# Document Cache
|
|
7
|
+
# Store-backed Document Cache
|
|
8
8
|
|
|
9
|
-
Caches complete HTTP responses (HTML/RSC)
|
|
9
|
+
Caches complete HTTP responses (HTML/RSC) in the app-level cache store based on
|
|
10
|
+
`Cache-Control`. Routes opt in by setting `s-maxage`.
|
|
11
|
+
|
|
12
|
+
This middleware runs **inside** the worker/function. It is not itself a platform
|
|
13
|
+
CDN cache. With `CFCacheStore` the response family can use Cloudflare's edge/KV
|
|
14
|
+
tiers; with `VercelCacheStore` it uses Vercel Runtime Cache. The request still
|
|
15
|
+
reaches Rango before the middleware can return a store hit.
|
|
10
16
|
|
|
11
17
|
## Not this skill if…
|
|
12
18
|
|
|
@@ -15,6 +21,8 @@ Caches complete HTTP responses (HTML/RSC) at the edge based on Cache-Control hea
|
|
|
15
21
|
is `cache()`: see `/caching`.
|
|
16
22
|
- You want a cached HTML shell with per-request live holes — see `/ppr`.
|
|
17
23
|
- You are unsure which cache layer you need — start at `/cache-guide`.
|
|
24
|
+
- You mean a platform CDN that serves a complete response without invoking the
|
|
25
|
+
app — see `/deployment-caching` first.
|
|
18
26
|
|
|
19
27
|
## Setup
|
|
20
28
|
|
|
@@ -55,6 +63,13 @@ Routes opt-in to document caching by setting a `Cache-Control` response header
|
|
|
55
63
|
with `s-maxage`. The middleware caches responses whose `Cache-Control` includes
|
|
56
64
|
`s-maxage`; `stale-while-revalidate` enables background revalidation (SWR).
|
|
57
65
|
|
|
66
|
+
The deployment platform may independently interpret the same `s-maxage` header
|
|
67
|
+
and cache the completed response at its CDN. A CDN hit bypasses the function,
|
|
68
|
+
all Rango middleware, handlers, and loaders. Therefore these headers are safe
|
|
69
|
+
only when the **complete** response is public and identical for every request
|
|
70
|
+
sharing the cache key. The middleware's `skipPaths`, `isEnabled`, and
|
|
71
|
+
`keyGenerator` cannot protect a response once an outer CDN serves it.
|
|
72
|
+
|
|
58
73
|
```typescript
|
|
59
74
|
// Cache full page for 5 min, serve stale for 1 hour
|
|
60
75
|
function BlogIndexHandler(ctx) {
|
|
@@ -116,6 +131,11 @@ Request → Check Cache
|
|
|
116
131
|
background (SWR)
|
|
117
132
|
```
|
|
118
133
|
|
|
134
|
+
This diagram starts after the request reaches the Rango middleware. A store hit
|
|
135
|
+
short-circuits the middleware's downstream pipeline; global middleware that
|
|
136
|
+
wraps it can still run. Route middleware, handlers, and loaders below it do not.
|
|
137
|
+
An external CDN hit is different: the function never runs at all.
|
|
138
|
+
|
|
119
139
|
## Cache Status Header
|
|
120
140
|
|
|
121
141
|
Response includes `x-document-cache-status`:
|
|
@@ -124,6 +144,10 @@ Response includes `x-document-cache-status`:
|
|
|
124
144
|
- `STALE` - Served stale, revalidating in background
|
|
125
145
|
- `MISS` - Cache miss, response was generated fresh
|
|
126
146
|
|
|
147
|
+
This header reports the Rango store-backed middleware, not the platform CDN. A
|
|
148
|
+
CDN may replay a previously cached status header, so use the platform's cache
|
|
149
|
+
header or logs to identify an actual CDN hit.
|
|
150
|
+
|
|
127
151
|
## Cache Key Generation
|
|
128
152
|
|
|
129
153
|
Default keys differentiate:
|
|
@@ -211,3 +235,6 @@ function BlogPost(ctx) {
|
|
|
211
235
|
| Key includes | URL + segment hash | Route params |
|
|
212
236
|
|
|
213
237
|
Use document cache for mostly-static pages. Use segment cache when different parts of a page have different cache requirements.
|
|
238
|
+
|
|
239
|
+
See `/deployment-caching` for the full in-function versus CDN execution matrix,
|
|
240
|
+
middleware implications, and the shared-response safety checklist.
|
package/skills/ppr/SKILL.md
CHANGED
|
@@ -7,16 +7,21 @@ argument-hint: "[setup]"
|
|
|
7
7
|
# PPR Shell Caching
|
|
8
8
|
|
|
9
9
|
Caches the rendered HTML **shell** of a page route (React `prerender` prelude
|
|
10
|
-
bytes plus `postponed` state) and, on a later request, flushes those bytes
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
bytes plus `postponed` state) and, on a later request, flushes those bytes after
|
|
11
|
+
route classification and the complete middleware chain, but before downstream
|
|
12
|
+
tail rendering. It then resumes fizz for just the live holes. The browser sees
|
|
13
|
+
one ordinary streamed document; loaders stay fresh on every request. This is
|
|
14
|
+
the second render axis — the default axis-1 path is untouched, and every
|
|
15
|
+
ineligible request falls open to it.
|
|
15
16
|
|
|
16
17
|
Compare `/document-cache`, which freezes the WHOLE response including loader
|
|
17
18
|
output. Shell caching is for pages that mix a stable shell with live data: the
|
|
18
19
|
shell is shared per host+URL, the holes are per request.
|
|
19
20
|
|
|
21
|
+
This is in-function PPR on every deployment. The worker/function serves the
|
|
22
|
+
prelude; it is not a CDN static file. See `/deployment-caching` before combining
|
|
23
|
+
PPR with HTTP shared-cache headers.
|
|
24
|
+
|
|
20
25
|
## Not this skill if…
|
|
21
26
|
|
|
22
27
|
- You want the WHOLE response frozen, loader output included — see
|
|
@@ -188,21 +193,31 @@ and loader-container pins are NOT replayed on this path, so loader reads stay
|
|
|
188
193
|
live. A route's own `cache()` scope still resolves its normal store, key, TTL,
|
|
189
194
|
SWR, tags, and condition; only the implicit document scope sees the replay
|
|
190
195
|
overlay, and fresh segment writes there stay request-local rather than polluting
|
|
191
|
-
the canonical `doc:` namespace.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
cannot recapture
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
196
|
+
the canonical `doc:` namespace. `transition({ when })` is evaluated from the
|
|
197
|
+
matched manifest before route handlers on every PPR match, so it can vary by
|
|
198
|
+
URL/params/action or middleware context without disabling replay; handler-set
|
|
199
|
+
context is unavailable by design. Intercepts, handler-live holes, an active
|
|
200
|
+
nonce, and an absent/corrupt segment snapshot fall open to the ordinary partial
|
|
201
|
+
path when encountered by the shell capture. A transition already replayed from
|
|
202
|
+
an explicit cache tier remains frozen by that tier's normal semantics.
|
|
203
|
+
|
|
204
|
+
Fresh and stale-within-SWR runtime shells replay. The stale read is passive: it
|
|
205
|
+
uses `getShell(key, { claimRevalidation: false })`, does not claim SWR ownership,
|
|
206
|
+
and cannot recapture HTML. A later document request owns the background
|
|
207
|
+
recapture; hard-expired entries fall open. Production may also use a fresh local
|
|
208
|
+
build manifest; development stays runtime-only because probing `/__rsc_shell`
|
|
209
|
+
would block navigation on an on-demand capture. Custom `SegmentCacheStore`
|
|
210
|
+
implementations must set `supportsPassiveShellReads: true` and honor the
|
|
211
|
+
non-claiming read option.
|
|
212
|
+
|
|
213
|
+
Partial responses expose the actual decision as `x-rango-ppr-replay`:
|
|
214
|
+
`HIT; freshness=fresh|stale` or `BYPASS; reason=<bounded-token>`. With
|
|
215
|
+
performance metrics enabled, the same decision appears as
|
|
216
|
+
`ppr-navigation-replay` in `Server-Timing`. `HIT` means matching consumed the
|
|
217
|
+
seeded segment record after it decoded successfully, not merely that a snapshot
|
|
218
|
+
existed. An explicit `cache()` scope that supplies the match cannot produce a
|
|
219
|
+
false HIT. There is still no Flight resume API; this is segment replay followed
|
|
220
|
+
by normal Flight streaming, not reuse of the HTML `prelude`/`postponed` bytes.
|
|
206
221
|
|
|
207
222
|
### Capture-generation invalidation
|
|
208
223
|
|
|
@@ -252,7 +267,12 @@ sees the header directly; only an explicit Flight request shape lacks it:
|
|
|
252
267
|
curl -s -D - -o /dev/null https://app.example.com/products/1 | grep -i x-rango-shell
|
|
253
268
|
```
|
|
254
269
|
|
|
255
|
-
-
|
|
270
|
+
- Runtime-captured route: first document GET is `MISS`, plus a background
|
|
271
|
+
capture; a later request becomes a `HIT`.
|
|
272
|
+
- `Prerender + ppr` route: the shell is produced during `vite build`, so the
|
|
273
|
+
first production document request can already be a `HIT`. In dev, producer B
|
|
274
|
+
runs on demand and can return a `HIT`; if capture outlasts the bounded
|
|
275
|
+
foreground wait, the request falls open to `MISS` and runtime capture.
|
|
256
276
|
- Production (workerd/node): the SECOND request is a `HIT`.
|
|
257
277
|
- Dev: expect a few extra MISSes — cold module transforms abort the capture
|
|
258
278
|
window (per-attempt breadcrumbs: start the server with
|
|
@@ -284,6 +304,8 @@ Import from `@rangojs/router/testing` (Vitest) or `@rangojs/router/testing/e2e`
|
|
|
284
304
|
| Helper | Use for |
|
|
285
305
|
| ------------------------------------------------- | ------------------------------------------------------------ |
|
|
286
306
|
| `assertShellStatus(res, "HIT" \| "MISS")` | Document Response from a real RSC serve / e2e `page.request` |
|
|
307
|
+
| `assertPprReplayStatus(res, expected)` | Partial response fresh/stale replay or bounded bypass |
|
|
308
|
+
| `parsePprReplayStatus(res)` | Read structured replay/bypass status or null |
|
|
287
309
|
| `shellCacheKey(url)` | Production store key for `store.getShell` / custom stores |
|
|
288
310
|
| `MemorySegmentCacheStore` + `getShell`/`putShell` | Custom store contract / tag eviction (no faked HIT) |
|
|
289
311
|
|
|
@@ -639,11 +661,18 @@ path(
|
|
|
639
661
|
| `swr` | — | stale window: serve the stale shell + background recapture |
|
|
640
662
|
| `tags` | — | operational tags UNIONED with the tags the capture render auto-collects — see "Invalidation" below |
|
|
641
663
|
| `maxSnapshotBytes` | 8 MiB | cap on the entry's capture data snapshot; over it the snapshot is skipped (shell still stored, warned once per key) so the entry stays under store limits |
|
|
664
|
+
| `captureTimeout` | 15000ms | capture settle budget; a timed-out capture is refused rather than storing a partial shell |
|
|
642
665
|
|
|
643
666
|
The shell store is always the app-level `createRouter({ cache })` store; the
|
|
644
667
|
default key is `${host}${pathname}${sortedSearch}:shell` (host-scoped so
|
|
645
668
|
multi-tenant shells never collide).
|
|
646
669
|
|
|
670
|
+
These options control the in-function shell entry only. They do not emit HTTP
|
|
671
|
+
`Cache-Control`. Adding `s-maxage` separately allows a platform CDN to cache the
|
|
672
|
+
completed response, including the live-hole output, and CDN hits bypass Rango
|
|
673
|
+
middleware entirely. Only do that for a fully public response whose complete
|
|
674
|
+
output is shared; see `/deployment-caching`.
|
|
675
|
+
|
|
647
676
|
## Invalidation: tags vs revalidate()
|
|
648
677
|
|
|
649
678
|
`updateTag()`/`revalidateTag()` is the ONLY lever that changes the frozen shell
|
|
@@ -676,8 +705,9 @@ evicted by tag at all — move always-fresh data under a `loading()` hole.
|
|
|
676
705
|
- **A bake-lane container that must be fresh per document GET**: it is
|
|
677
706
|
snapshot-pinned for the shell's lifetime by design. Use the live lane
|
|
678
707
|
(`loading()`) or a nested promise instead.
|
|
679
|
-
- **A bake-lane loader slower than
|
|
680
|
-
|
|
708
|
+
- **A bake-lane loader slower than `ppr.captureTimeout` (15s by default)**: the
|
|
709
|
+
capture is refused rather than storing a partial shell. Increase the route's
|
|
710
|
+
budget only when the deployment can keep the background/build work alive.
|
|
681
711
|
- **Per-user value in shell material**: baked into the shared shell —
|
|
682
712
|
deterministically, not by race (handler promises deep-settle at the ring-3
|
|
683
713
|
write on cached chains; awaited/resolved values bake everywhere). Put
|
|
@@ -728,8 +758,10 @@ cache"` value baked into the shell is PINNED at capture (the capture data
|
|
|
728
758
|
prelude and detonate hydration. Wrap it in `cache()`/`"use cache"` (then it is
|
|
729
759
|
pinned) or move it under a hole (`loading()`, or a pending-promise
|
|
730
760
|
`<Suspense>` region).
|
|
731
|
-
- **Stacking with `/document-cache
|
|
732
|
-
|
|
761
|
+
- **Stacking with `/document-cache` or HTTP CDN caching**: both cache the
|
|
762
|
+
completed composite, including live-hole output, so PPR becomes redundant on
|
|
763
|
+
a hit. A platform CDN also bypasses every Rango middleware. Restrict this to
|
|
764
|
+
fully public, shared responses; see `/deployment-caching`.
|
|
733
765
|
- **Dev + HMR**: works, but edits produce stale shells until TTL/recapture.
|
|
734
766
|
- **Dev cold-start cadence**: expect `MISS -> (in-place retry) -> HIT`. A
|
|
735
767
|
refused capture is negatively cached with an exponential window (1s doubling
|
|
@@ -326,18 +326,25 @@ This only applies when `__PRERENDER_DEV_URL` is set by the plugin.
|
|
|
326
326
|
|
|
327
327
|
## Storage Layout
|
|
328
328
|
|
|
329
|
-
Pre-rendered Flight payloads are stored
|
|
329
|
+
Pre-rendered Flight payloads are stored as content-hashed modules behind a lazy
|
|
330
|
+
manifest in the RSC server bundle. They are not public `.rsc` files:
|
|
330
331
|
|
|
331
332
|
```
|
|
332
|
-
dist/
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
333
|
+
dist/rsc/
|
|
334
|
+
__prerender-manifest.js
|
|
335
|
+
assets/
|
|
336
|
+
__pr-<content-hash>.js
|
|
337
|
+
|
|
338
|
+
# When a Prerender route also declares ppr:
|
|
339
|
+
__shell-manifest.js
|
|
340
|
+
assets/
|
|
341
|
+
__ps-<content-hash>.js
|
|
339
342
|
```
|
|
340
343
|
|
|
344
|
+
The worker/function handles every request and reads these modules as build-time
|
|
345
|
+
cache entries. See `/deployment-caching` for how this differs from a platform
|
|
346
|
+
CDN static or prerender output.
|
|
347
|
+
|
|
341
348
|
## Concurrency
|
|
342
349
|
|
|
343
350
|
Prerender handlers can specify how many param sets render in parallel:
|
package/skills/rango/SKILL.md
CHANGED
|
@@ -110,6 +110,7 @@ stated, greppable contract.
|
|
|
110
110
|
| pre-render a route at build time | `Prerender(...)` wrapper | /prerender |
|
|
111
111
|
| feed live loaders from a cached shell | replayed handle + `ctx.rendered()` | /shell-manifest |
|
|
112
112
|
| cache the HTML shell, keep loaders live | `ppr` path option | /ppr |
|
|
113
|
+
| choose in-function vs CDN caching | deployment cache boundary | /deployment-caching |
|
|
113
114
|
| stream SSE / upgrade a WebSocket | `path.stream()` / `path.any()` | /streams-and-websockets |
|
|
114
115
|
|
|
115
116
|
## Invariants
|
|
@@ -161,8 +162,8 @@ Same words, different jobs — this is the most common source of the
|
|
|
161
162
|
| Next.js `export const revalidate = N` | **Axis 1** (cache) | Same word, opposite meaning. Next's `revalidate` is time-based cache expiry; Rango's `revalidate()` is **axis 2**. Use `cache({ ttl })` for the Next behavior. |
|
|
162
163
|
| Next.js `revalidateTag` / `updateTag` | **Axis 1** (cache) | Cache busting by tag. Tag via `cache({ tags })` / `cacheTag(...tags)`; invalidate with `updateTag(...tags)` (awaitable, read-your-own-writes) or `revalidateTag(...tags)` (background, non-blocking). Built-in stores index by tag. No `revalidatePath` (path-based busting); use tags. |
|
|
163
164
|
| React Router / Remix `shouldRevalidate` | **Axis 2** | This is the correct mental model for Rango's `revalidate()`. |
|
|
164
|
-
| HTTP `Cache-Control` / ISR |
|
|
165
|
-
| Next.js PPR (partial prerendering) | HTML shell layer | Same
|
|
165
|
+
| HTTP `Cache-Control` / ISR | Deployment layer | Complete-response deployment layer. A CDN hit bypasses Rango entirely; the store-backed middleware does not. See `/deployment-caching` and `/document-cache`. |
|
|
166
|
+
| Next.js PPR (partial prerendering) | HTML shell layer | Same React primitive, different transport: Rango serves shells in-function after middleware. Ordinary `ppr` captures at runtime; `Prerender + ppr` captures at build. See `/ppr`, `/prerender`, and `/deployment-caching`. |
|
|
166
167
|
| Remix/RR `loader` | live data | Like Rango loaders, fresh per request — but Rango loaders run in parallel and stream (latency overlaps first paint), and can opt into caching on demand. |
|
|
167
168
|
|
|
168
169
|
See `/cache-guide` for the axis-1 decision guide, `/loader` and `/route` for
|
|
@@ -257,17 +258,18 @@ Grouped by concern — read when you need to…
|
|
|
257
258
|
|
|
258
259
|
**Data & caching** — fetch, mutate, and cache:
|
|
259
260
|
|
|
260
|
-
| Skill
|
|
261
|
-
|
|
|
262
|
-
| `/loader`
|
|
263
|
-
| `/server-actions`
|
|
264
|
-
| `/caching`
|
|
265
|
-
| `/use-cache`
|
|
266
|
-
| `/cache-guide`
|
|
267
|
-
| `/document-cache`
|
|
268
|
-
| `/
|
|
269
|
-
| `/
|
|
270
|
-
| `/
|
|
261
|
+
| Skill | Description |
|
|
262
|
+
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
263
|
+
| `/loader` | Data loaders with `createLoader()` and `revalidate()` |
|
|
264
|
+
| `/server-actions` | Mutations with `"use server"`, useActionState, validation, revalidation |
|
|
265
|
+
| `/caching` | Segment caching with memory or KV stores |
|
|
266
|
+
| `/use-cache` | Function-level caching with `"use cache"` directive |
|
|
267
|
+
| `/cache-guide` | When to use `cache()` vs `"use cache"` — differences and decision guide |
|
|
268
|
+
| `/document-cache` | Store-backed complete-response middleware using Cache-Control policy |
|
|
269
|
+
| `/deployment-caching` | Choose between in-function caches, store-backed responses, and an external CDN cache |
|
|
270
|
+
| `/ppr` | PPR shell caching: cached shell served instantly, live holes resumed — a hole is a `loading()` subtree OR a pending promise under `<Suspense>` (no loader needed) |
|
|
271
|
+
| `/prerender` | Pre-render route segments at build time (Passthrough live fallback) |
|
|
272
|
+
| `/shell-manifest` | Replayed handles as cache metadata read by live loaders (frozen shell, batched live holes) |
|
|
271
273
|
|
|
272
274
|
**Client & presentation** — build the client-side UX:
|
|
273
275
|
|
|
@@ -295,10 +297,11 @@ Grouped by concern — read when you need to…
|
|
|
295
297
|
|
|
296
298
|
**Deployment**:
|
|
297
299
|
|
|
298
|
-
| Skill
|
|
299
|
-
|
|
|
300
|
-
| `/cloudflare`
|
|
301
|
-
| `/vercel`
|
|
300
|
+
| Skill | Description |
|
|
301
|
+
| --------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
302
|
+
| `/cloudflare` | Deploy to Cloudflare Workers with the Vite plugin, typed D1/KV bindings, migrations, secrets, and preview parity |
|
|
303
|
+
| `/vercel` | Deploy to Vercel Functions (`preset: "vercel"`), Runtime Cache, and `createVercelTracing` |
|
|
304
|
+
| `/deployment-caching` | Compare deployment cache boundaries, middleware execution, PPR transport, and HTTP CDN caching |
|
|
302
305
|
|
|
303
306
|
**Testing**:
|
|
304
307
|
|
package/skills/testing/SKILL.md
CHANGED
|
@@ -88,7 +88,7 @@ Each primitive links to its sub-file (API + recipe + caveats).
|
|
|
88
88
|
| a loader's cookie / header / redirect output (auth-loader pattern) | unit (node) | [`runLoaderResult`](./loader.md) | `@rangojs/router/testing` |
|
|
89
89
|
| one middleware's ordering / short-circuit / cookie+header merge | unit (node) | [`runMiddleware`](./middleware.md) | `@rangojs/router/testing` |
|
|
90
90
|
| a `"use server"` action's cookie / header / flash output (even on `throw redirect()`) | unit (node) | [`runInRequestContext`](./server-actions.md) | `@rangojs/router/testing` |
|
|
91
|
-
| a `transition({ when })` gate (keep/drop) against nav source / target / action metadata | unit (node) | `runTransitionWhen` (`{ kept, whenContext }`)
|
|
91
|
+
| a `transition({ when })` gate (keep/drop) against nav source / target / action metadata | unit (node) | `runTransitionWhen` (`{ kept, whenContext }`; pass `{ ppr: true }` for pre-handler timing) | `@rangojs/router/testing` |
|
|
92
92
|
| a handle's `collect`/accumulator, or a seeded handle read | unit | [`collectHandle` / seeded `handles`](./handles.md) | `@rangojs/router/testing` |
|
|
93
93
|
| a CLIENT component reading router context (`useParams`/`useReverse`/`Outlet`/`useNavigation`/`useLoader`) | unit (DOM) | [`renderRoute`](./client-components.md) | `@rangojs/router/testing/dom` |
|
|
94
94
|
| a redirect / status / headers / cookies / **response route** (json/text/html/xml/md), no Flight | integration | [`dispatch`](./response-routes.md) | `@rangojs/router/testing` |
|
|
@@ -113,7 +113,7 @@ expect(decision.segments?.[0].shouldRevalidate).toBe(true);
|
|
|
113
113
|
|
|
114
114
|
`events` accumulates across requests, so the FIRST matching segment for a `routeKey` wins — slice or recreate the sink between requests for the same route.
|
|
115
115
|
|
|
116
|
-
## PPR shell
|
|
116
|
+
## PPR shell and navigation replay
|
|
117
117
|
|
|
118
118
|
**DSL:** `ppr: true | PartialPrerenderProps` on a page route (see `/ppr`). **Not** the same header as `X-Rango-Cache` — shell is a second render axis.
|
|
119
119
|
|
|
@@ -123,6 +123,9 @@ expect(decision.segments?.[0].shouldRevalidate).toBe(true);
|
|
|
123
123
|
| `parseShellStatus(res)` | same | `"HIT" \| "MISS" \| null` (null = header absent / unrecognized) |
|
|
124
124
|
| `shellCacheKey(url)` | same | Production shell store key (`host+pathname+sorted search+:shell`) for `store.getShell` / custom stores |
|
|
125
125
|
| `SHELL_STATUS_HEADER` | same | `"x-rango-shell"` constant |
|
|
126
|
+
| `assertPprReplayStatus(res, expected)` | same | Assert fresh/stale replay or a bounded bypass decision |
|
|
127
|
+
| `parsePprReplayStatus(res)` | same | Structured replay/bypass status, or null for an absent/unrecognized header |
|
|
128
|
+
| `PPR_REPLAY_STATUS_HEADER` | same | `"x-rango-ppr-replay"` constant |
|
|
126
129
|
|
|
127
130
|
### What unit can prove vs e2e
|
|
128
131
|
|
|
@@ -131,6 +134,7 @@ expect(decision.segments?.[0].shouldRevalidate).toBe(true);
|
|
|
131
134
|
| **Unit** | store family + key identity | `MemorySegmentCacheStore` + `shellCacheKey(url)` + `putShell`/`getShell` / tag eviction — dogfood in `e2e/mini/test/shell-store-family.test.ts` and `src/testing/__tests__/shell-status.test.ts` |
|
|
132
135
|
| **Unit** | header helper contract | `assertShellStatus` on a Response that already carries the header (characterizes the helper; **never** invent a HIT to claim capture worked) |
|
|
133
136
|
| **E2E** | live MISS → capture → HIT | document GET, poll until `x-rango-shell: HIT` (background capture) — `e2e/shell-cache.test.ts` |
|
|
137
|
+
| **E2E** | partial replay decision | soft navigation to a warmed shell and `assertPprReplayStatus(response, { outcome: "HIT", freshness: "fresh" })` |
|
|
134
138
|
|
|
135
139
|
`dispatch` is RSC-free: it never runs shell serve/capture. `renderHandler` only surfaces `ctx.dynamic()` opt-out, not bake/serve.
|
|
136
140
|
|
package/skills/vercel/SKILL.md
CHANGED
|
@@ -8,6 +8,25 @@ argument-hint:
|
|
|
8
8
|
|
|
9
9
|
The `vercel` preset builds like the `node` preset (Vercel runs Node Functions, not Workers): rango owns the RSC entry, folds `process.env.NODE_ENV` for the SSR/RSC build, and after `vite build` assembles a `.vercel/output` directory (Build Output API v3) from `dist/` — a single streaming Node Function plus the static client assets.
|
|
10
10
|
|
|
11
|
+
## Deployment boundary
|
|
12
|
+
|
|
13
|
+
Only client JS, CSS, and public assets are emitted under
|
|
14
|
+
`.vercel/output/static`. HTML, Flight, prerender payloads, and PPR shells are
|
|
15
|
+
served from the streaming Node Function. `VercelCacheStore` is an in-function
|
|
16
|
+
Runtime Cache backend; it is separate from Vercel's CDN/ISR cache.
|
|
17
|
+
|
|
18
|
+
The preset does not emit `.prerender-config.json`, a response `chain`, or a
|
|
19
|
+
CDN-stitched PPR resume function. Rango PPR intentionally runs the whole global
|
|
20
|
+
and route middleware chain before committing shell bytes. A CDN-first shell
|
|
21
|
+
cannot preserve that contract because the resume function is invoked after the
|
|
22
|
+
shell starts streaming.
|
|
23
|
+
|
|
24
|
+
For fully public responses, HTTP `s-maxage`/`stale-while-revalidate` can cache
|
|
25
|
+
the completed response at Vercel's CDN and avoid the function on a hit. That is
|
|
26
|
+
whole-response caching: it freezes loader output and bypasses all Rango
|
|
27
|
+
middleware. Use `/deployment-caching` for the execution matrix and safety
|
|
28
|
+
checklist before adding shared-cache headers.
|
|
29
|
+
|
|
11
30
|
## Setup
|
|
12
31
|
|
|
13
32
|
```bash
|
|
@@ -48,7 +67,9 @@ rango({
|
|
|
48
67
|
|
|
49
68
|
## Runtime Cache
|
|
50
69
|
|
|
51
|
-
`VercelCacheStore` wraps the Vercel Runtime Cache
|
|
70
|
+
`VercelCacheStore` wraps the Vercel Runtime Cache for segment, item, response,
|
|
71
|
+
and PPR shell families. Locally (no `process.env.VERCEL`) fall back to an
|
|
72
|
+
in-memory store so dev/preview work without the platform:
|
|
52
73
|
|
|
53
74
|
```typescript
|
|
54
75
|
import {
|