@rangojs/router 0.21.0 → 0.22.1
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/dist/bin/rango.js +13 -1
- package/dist/types/browser/event-controller.d.ts +30 -13
- package/dist/types/browser/navigation-store.d.ts +9 -0
- package/dist/types/browser/partial-update.d.ts +2 -1
- package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
- package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
- package/dist/types/browser/react/context.d.ts +14 -0
- package/dist/types/browser/rsc-router.d.ts +13 -2
- package/dist/types/browser/types.d.ts +10 -0
- package/dist/types/build/generate-manifest.d.ts +2 -0
- package/dist/types/cache/background-task.d.ts +22 -0
- package/dist/types/cache/cache-key-utils.d.ts +50 -5
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +1 -1
- package/dist/types/cache/cf/cf-cache-store.d.ts +7 -1
- package/dist/types/cache/document-cache.d.ts +8 -5
- package/dist/types/cache/handle-snapshot.d.ts +23 -0
- package/dist/types/cache/index.d.ts +1 -1
- package/dist/types/cache/invalidation-order.d.ts +14 -0
- package/dist/types/cache/memory-segment-store.d.ts +33 -1
- package/dist/types/cache/shell-snapshot.d.ts +10 -1
- package/dist/types/cache/store-scope.d.ts +22 -0
- package/dist/types/cache/tag-invalidation.d.ts +7 -6
- package/dist/types/cache/types.d.ts +63 -14
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +7 -1
- package/dist/types/errors.d.ts +6 -0
- package/dist/types/handles/deferred-resolution.d.ts +7 -0
- package/dist/types/internal-suspense-audit.d.ts +1 -0
- package/dist/types/loader-store.d.ts +4 -8
- package/dist/types/outlet-provider.d.ts +2 -1
- package/dist/types/prerender/cloudflare.d.ts +49 -0
- package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
- package/dist/types/prerender/index.d.ts +12 -0
- package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
- package/dist/types/prerender/on-demand.d.ts +272 -0
- package/dist/types/prerender/producer-guard.d.ts +33 -0
- package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
- package/dist/types/prerender/warm-request.d.ts +65 -0
- package/dist/types/prerender/warm.d.ts +65 -0
- package/dist/types/prerender/writable-store.d.ts +170 -0
- package/dist/types/prerender.d.ts +27 -0
- package/dist/types/reverse.d.ts +1 -1
- package/dist/types/route-content-wrapper.d.ts +3 -1
- package/dist/types/router/error-handling.d.ts +2 -0
- package/dist/types/router/handler-context.d.ts +1 -1
- package/dist/types/router/match-middleware/cache-lookup.d.ts +14 -0
- package/dist/types/router/pattern-matching.d.ts +2 -0
- package/dist/types/router/prefetch-default.d.ts +4 -4
- package/dist/types/router/prerender-match.d.ts +23 -1
- package/dist/types/router/route-trie-builder.d.ts +8 -1
- package/dist/types/router/router-context.d.ts +1 -0
- package/dist/types/router/router-interfaces.d.ts +30 -0
- package/dist/types/router/router-options.d.ts +24 -0
- package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +2 -1
- package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
- package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
- package/dist/types/router/trie-matching.d.ts +2 -0
- package/dist/types/rsc/shell-build-manifest.d.ts +6 -4
- package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
- package/dist/types/rsc/shell-capture.d.ts +14 -1
- package/dist/types/rsc/shell-serve.d.ts +2 -2
- package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
- package/dist/types/rsc/types.d.ts +8 -0
- package/dist/types/segment-boundary-content.d.ts +17 -0
- package/dist/types/segment-loader-promise.d.ts +2 -6
- package/dist/types/server/build-version-table.d.ts +9 -0
- package/dist/types/server/context.d.ts +12 -4
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +63 -2
- package/dist/types/suspense-audit-react.d.ts +26 -0
- package/dist/types/suspense-audit.d.ts +157 -0
- package/dist/types/testing/action-form.d.ts +34 -0
- package/dist/types/testing/flight.entry.d.ts +2 -0
- package/dist/types/testing/index.d.ts +4 -0
- package/dist/types/testing/internal/server-modules.d.ts +8 -0
- package/dist/types/testing/serve-shell-request.d.ts +34 -4
- package/dist/types/testing/shell-status.d.ts +14 -6
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
- package/dist/types/thenable-status.d.ts +6 -0
- package/dist/types/types/handler-context.d.ts +7 -0
- package/dist/types/types/route-entry.d.ts +5 -0
- package/dist/types/urls/path-helper.d.ts +18 -0
- package/dist/types/vite/discovery/state.d.ts +11 -0
- package/dist/types/vite/inject-client-debug.d.ts +6 -25
- package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
- package/dist/vite/index.js +180 -497
- package/package.json +13 -3
- package/skills/cache-guide/SKILL.md +18 -17
- package/skills/caching/SKILL.md +40 -4
- package/skills/catalog.json +1 -1
- package/skills/cloudflare/SKILL.md +131 -0
- package/skills/comparison/references/framework-comparison.md +1 -1
- package/skills/deployment-caching/SKILL.md +33 -12
- package/skills/document-cache/SKILL.md +16 -14
- package/skills/hooks/SKILL.md +22 -0
- package/skills/hooks/handle-and-actions.md +3 -4
- package/skills/hooks/navigation.md +10 -0
- package/skills/hooks/state.md +5 -4
- package/skills/host-router/SKILL.md +18 -0
- package/skills/loader/SKILL.md +6 -4
- package/skills/middleware/SKILL.md +1 -1
- package/skills/migrate-nextjs/SKILL.md +82 -9
- package/skills/ppr/SKILL.md +31 -8
- package/skills/prerender/SKILL.md +645 -8
- package/skills/rango/SKILL.md +1 -0
- package/skills/response-routes/SKILL.md +5 -5
- package/skills/route/SKILL.md +29 -0
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/cache-prerender.md +18 -17
- package/skills/testing/serve-shell-request.md +104 -8
- package/skills/use-cache/SKILL.md +9 -5
- package/skills/vercel/SKILL.md +2 -0
- package/skills/view-transitions/SKILL.md +9 -4
- package/src/browser/event-controller.ts +69 -25
- package/src/browser/link-interceptor.ts +2 -2
- package/src/browser/navigation-bridge.ts +75 -35
- package/src/browser/navigation-store.ts +26 -0
- package/src/browser/navigation-transaction.ts +9 -2
- package/src/browser/network-error-handler.ts +2 -0
- package/src/browser/partial-update.ts +48 -12
- package/src/browser/prefetch/default-strategy.ts +22 -42
- package/src/browser/react/Link.tsx +37 -42
- package/src/browser/react/NavigationProvider.tsx +63 -3
- package/src/browser/react/context.ts +17 -0
- package/src/browser/react/location-state.ts +5 -19
- package/src/browser/react/use-action.ts +4 -1
- package/src/browser/react/use-handle.ts +4 -20
- package/src/browser/react/use-link-status.ts +4 -3
- package/src/browser/react/use-navigation.ts +4 -1
- package/src/browser/react/use-params.ts +4 -1
- package/src/browser/react/use-pathname.ts +4 -1
- package/src/browser/react/use-search-params.ts +5 -3
- package/src/browser/react/use-segments.ts +36 -54
- package/src/browser/rsc-router.tsx +79 -39
- package/src/browser/segment-reconciler.ts +3 -3
- package/src/browser/server-action-bridge.ts +26 -6
- package/src/browser/types.ts +11 -0
- package/src/build/generate-manifest.ts +17 -0
- package/src/build/merge-full-manifests.ts +5 -0
- package/src/build/route-trie.ts +1 -0
- package/src/cache/background-task.ts +61 -0
- package/src/cache/cache-key-utils.ts +91 -12
- package/src/cache/cache-policy.ts +11 -2
- package/src/cache/cache-runtime.ts +23 -23
- package/src/cache/cache-scope.ts +38 -36
- package/src/cache/cf/cf-cache-store.ts +16 -11
- package/src/cache/document-cache.ts +35 -14
- package/src/cache/handle-snapshot.ts +50 -0
- package/src/cache/index.ts +1 -0
- package/src/cache/invalidation-order.ts +53 -0
- package/src/cache/memory-segment-store.ts +52 -1
- package/src/cache/shell-snapshot.ts +17 -0
- package/src/cache/store-scope.ts +36 -0
- package/src/cache/tag-invalidation.ts +7 -5
- package/src/cache/types.ts +69 -14
- package/src/cache/vercel/vercel-cache-store.ts +20 -6
- package/src/client.tsx +40 -3
- package/src/errors.ts +13 -6
- package/src/handles/deferred-resolution.ts +7 -2
- package/src/internal-suspense-audit.ts +12 -0
- package/src/loader-store.ts +4 -9
- package/src/outlet-provider.tsx +25 -1
- package/src/prerender/cloudflare.ts +130 -0
- package/src/prerender/create-prerender-trigger.ts +780 -0
- package/src/prerender/index.ts +39 -0
- package/src/prerender/memory-prerender-store.ts +115 -0
- package/src/prerender/on-demand.ts +339 -0
- package/src/prerender/producer-guard.ts +66 -0
- package/src/prerender/shell-manifest-key.ts +12 -17
- package/src/prerender/warm-request.ts +154 -0
- package/src/prerender/warm.ts +193 -0
- package/src/prerender/writable-store.ts +278 -0
- package/src/prerender.ts +29 -0
- package/src/reverse.ts +1 -1
- package/src/route-content-wrapper.tsx +107 -13
- package/src/router/error-handling.ts +12 -12
- package/src/router/find-match.ts +1 -0
- package/src/router/handler-context.ts +2 -0
- package/src/router/intercept-resolution.ts +15 -8
- package/src/router/loader-resolution.ts +2 -10
- package/src/router/match-middleware/background-revalidation.ts +2 -0
- package/src/router/match-middleware/cache-lookup.ts +230 -38
- package/src/router/pattern-matching.ts +14 -0
- package/src/router/prefetch-default.ts +4 -4
- package/src/router/prerender-match.ts +460 -352
- package/src/router/route-trie-builder.ts +9 -0
- package/src/router/router-context.ts +1 -0
- package/src/router/router-interfaces.ts +33 -0
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +20 -19
- package/src/router/segment-resolution/loader-cache.ts +15 -17
- package/src/router/segment-resolution/revalidation.ts +34 -24
- package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
- package/src/router/trie-matching.ts +3 -0
- package/src/router.ts +107 -1
- package/src/rsc/handler.ts +51 -4
- package/src/rsc/progressive-enhancement.ts +5 -1
- package/src/rsc/response-cache-serve.ts +10 -4
- package/src/rsc/rsc-rendering.ts +53 -11
- package/src/rsc/shell-build-manifest.ts +10 -8
- package/src/rsc/shell-capture-constants.ts +39 -7
- package/src/rsc/shell-capture.ts +108 -19
- package/src/rsc/shell-serve.ts +12 -6
- package/src/rsc/ssr-module-loader.ts +26 -0
- package/src/rsc/types.ts +8 -0
- package/src/segment-boundary-content.ts +25 -0
- package/src/segment-loader-promise.ts +28 -38
- package/src/segment-system.tsx +117 -57
- package/src/server/build-version-table.ts +15 -0
- package/src/server/context.ts +40 -17
- package/src/server/cookie-store.ts +5 -0
- package/src/server/handle-store.ts +13 -0
- package/src/server/loader-registry.ts +6 -1
- package/src/server/request-context.ts +87 -1
- package/src/ssr/ssr-root.tsx +20 -13
- package/src/suspense-audit-react.tsx +98 -0
- package/src/suspense-audit.ts +1030 -0
- package/src/testing/action-form.ts +66 -0
- package/src/testing/flight-runtime.d.ts +27 -0
- package/src/testing/flight.entry.ts +3 -0
- package/src/testing/index.ts +19 -0
- package/src/testing/internal/server-modules.ts +32 -0
- package/src/testing/render-route.tsx +25 -12
- package/src/testing/serve-shell-request.ts +190 -34
- package/src/testing/shell-status.ts +17 -6
- package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
- package/src/thenable-status.ts +10 -0
- package/src/types/handler-context.ts +7 -0
- package/src/types/route-entry.ts +6 -0
- package/src/urls/path-helper.ts +69 -9
- package/src/use-loader.tsx +57 -30
- package/src/vite/discovery/bundle-postprocess.ts +4 -0
- package/src/vite/discovery/discover-routers.ts +15 -0
- package/src/vite/discovery/shell-prerender-phase.ts +5 -5
- package/src/vite/discovery/state.ts +17 -1
- package/src/vite/inject-client-debug.ts +42 -20
- package/src/vite/plugins/virtual-entries.ts +8 -1
- package/src/vite/router-discovery.ts +14 -0
- package/src/vite/utils/bundle-analysis.ts +39 -7
- package/dist/types/browser/react/subscribe-to-nothing.d.ts +0 -7
- package/dist/types/segment-content-promise.d.ts +0 -13
- package/src/browser/react/subscribe-to-nothing.ts +0 -7
- package/src/segment-content-promise.ts +0 -67
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.1",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -125,6 +125,16 @@
|
|
|
125
125
|
"react-server": "./src/cloudflare/index.ts",
|
|
126
126
|
"default": "./src/cloudflare/index.ts"
|
|
127
127
|
},
|
|
128
|
+
"./prerender": {
|
|
129
|
+
"types": "./dist/types/prerender/index.d.ts",
|
|
130
|
+
"react-server": "./src/prerender/index.ts",
|
|
131
|
+
"default": "./src/prerender/index.ts"
|
|
132
|
+
},
|
|
133
|
+
"./prerender/cloudflare": {
|
|
134
|
+
"types": "./dist/types/prerender/cloudflare.d.ts",
|
|
135
|
+
"react-server": "./src/prerender/cloudflare.ts",
|
|
136
|
+
"default": "./src/prerender/cloudflare.ts"
|
|
137
|
+
},
|
|
128
138
|
"./vercel": {
|
|
129
139
|
"types": "./dist/types/vercel/index.d.ts",
|
|
130
140
|
"react-server": "./src/vercel/index.ts",
|
|
@@ -184,7 +194,7 @@
|
|
|
184
194
|
},
|
|
185
195
|
"dependencies": {
|
|
186
196
|
"@types/debug": "^4.1.12",
|
|
187
|
-
"@vitejs/plugin-rsc": "^0.5.
|
|
197
|
+
"@vitejs/plugin-rsc": "^0.5.36",
|
|
188
198
|
"debug": "^4.4.1",
|
|
189
199
|
"magic-string": "^0.30.17",
|
|
190
200
|
"picomatch": "^4.0.4",
|
|
@@ -208,7 +218,7 @@
|
|
|
208
218
|
"jiti": "^2.7.0",
|
|
209
219
|
"react": "^19.3.0",
|
|
210
220
|
"react-dom": "^19.3.0",
|
|
211
|
-
"vitest": "^4.1.
|
|
221
|
+
"vitest": "^4.1.11",
|
|
212
222
|
"@shared/e2e": "0.0.1"
|
|
213
223
|
},
|
|
214
224
|
"peerDependencies": {
|
|
@@ -302,25 +302,26 @@ local/dev behavior, not as proof that segment SWR is active.
|
|
|
302
302
|
|
|
303
303
|
## Key Differences
|
|
304
304
|
|
|
305
|
-
| | `cache()` DSL
|
|
306
|
-
| -------------------- |
|
|
307
|
-
| **Scope** | Route segment tree (handler + children + parallels)
|
|
308
|
-
| **Defined at** | Route definition site (`urls.ts`)
|
|
309
|
-
| **Cache key** | Request type + host + pathname + params + search
|
|
310
|
-
| **Execution on hit** | All-or-nothing: entire handler skipped
|
|
311
|
-
| **Runtime control** | `condition` to disable, custom `key` function
|
|
312
|
-
| **Side effects** | Response side effects throw inside the boundary
|
|
313
|
-
| **Handle data** | Handler pushes replayed; loader pushes re-run live
|
|
314
|
-
| **Loaders** | Always fresh — excluded from cache, opt-in per loader
|
|
315
|
-
| **Nesting** | Inner TTLs override; `key()` partitions compose
|
|
305
|
+
| | `cache()` DSL | `"use cache"` directive |
|
|
306
|
+
| -------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
307
|
+
| **Scope** | Route segment tree (handler + children + parallels) | Single function return value |
|
|
308
|
+
| **Defined at** | Route definition site (`urls.ts`) | Inside function body or at file top |
|
|
309
|
+
| **Cache key** | Request type + router + host + pathname + params + search | Function identity + serialized non-tainted args |
|
|
310
|
+
| **Execution on hit** | All-or-nothing: entire handler skipped | Partial: function body skipped, calling code runs |
|
|
311
|
+
| **Runtime control** | `condition` to disable, custom `key` function | None — if the directive is present, it caches |
|
|
312
|
+
| **Side effects** | Response side effects throw inside the boundary | `ctx.headers.set()`, `ctx.set()`, etc. throw |
|
|
313
|
+
| **Handle data** | Handler pushes replayed; loader pushes re-run live | Captured and replayed when it receives `ctx` |
|
|
314
|
+
| **Loaders** | Always fresh — excluded from cache, opt-in per loader | Can be used inside loaders |
|
|
315
|
+
| **Nesting** | Inner TTLs override; `key()` partitions compose | Compose by calling cached functions from uncached; inner tags reach the outer entry |
|
|
316
316
|
|
|
317
317
|
### cache() Cache Key
|
|
318
318
|
|
|
319
|
-
The default key is `{requestType}:{host}{pathname}[:params][?search]`,
|
|
320
|
-
requestType is `doc`, `partial`, or `intercept`. The same URL is therefore
|
|
321
|
-
separately for full document loads, client navigations, and intercept
|
|
319
|
+
The default key is `{requestType}:{routerId}@{host}{pathname}[:params][?search]`,
|
|
320
|
+
where requestType is `doc`, `partial`, or `intercept`. The same URL is therefore
|
|
321
|
+
cached separately for full document loads, client navigations, and intercept
|
|
322
322
|
navigations. The host keeps tenants apart when one deployment serves several
|
|
323
|
-
domains
|
|
323
|
+
domains, and the router id keeps two routers on one store apart when both
|
|
324
|
+
serve the same host and path (see `/host-router`, "Shared cache store"); the search part is sorted, excludes the router's internal params, and
|
|
324
325
|
honors `createRouter({ cache: { searchParams } })`.
|
|
325
326
|
|
|
326
327
|
A custom `key` function replaces the whole default key (e.g., to key by user role
|
|
@@ -344,8 +345,8 @@ The key is `use-cache:{functionId}:{serializedArgs}` where functionId is a stabl
|
|
|
344
345
|
ID from the Vite transform (module path + export name) and args are serialized
|
|
345
346
|
(stable JSON when every arg is JSON-safe, RSC `encodeReply()` otherwise).
|
|
346
347
|
Request-scoped arguments are excluded: route fields read off `ctx` (handler,
|
|
347
|
-
loader or middleware: host, route name, pathname, params, search) and
|
|
348
|
-
`Request`'s URL are folded in, and `env`
|
|
348
|
+
loader or middleware: router id, host, route name, pathname, params, search) and
|
|
349
|
+
a `Request`'s URL are folded in, and `env`
|
|
349
350
|
is left out. React element slots stay out of the key; any other argument that
|
|
350
351
|
cannot be serialized (a function, a class instance) runs the call uncached. See
|
|
351
352
|
`/use-cache`.
|
package/skills/caching/SKILL.md
CHANGED
|
@@ -164,9 +164,10 @@ cache(
|
|
|
164
164
|
);
|
|
165
165
|
```
|
|
166
166
|
|
|
167
|
-
- **Default key**: `{doc|partial|intercept}:{host}{pathname}[:params][?search]`
|
|
167
|
+
- **Default key**: `{doc|partial|intercept}:{routerId}@{host}{pathname}[:params][?search]`
|
|
168
168
|
— document requests, client navigations, and intercept navigations are cached
|
|
169
|
-
separately
|
|
169
|
+
separately, and so are two routers that share a store (see `/host-router`,
|
|
170
|
+
"Shared cache store"). The search part honors `cache.searchParams` (below).
|
|
170
171
|
- **`key`** is a full override: it bypasses the default key, the store's
|
|
171
172
|
`keyGenerator`, and the search-param filter. The result is stored
|
|
172
173
|
namespaced (`key:` plus its URI encoding), so whatever it returns, request
|
|
@@ -300,8 +301,13 @@ is not stored after it. A `"use cache"` call, a stale entry's background
|
|
|
300
301
|
refresh, a loader's own `cache()`, a route `cache()` render or a
|
|
301
302
|
document-cache render that read its data before one of its tags was
|
|
302
303
|
invalidated still returns what it read, but its store write is skipped:
|
|
303
|
-
|
|
304
|
-
|
|
304
|
+
the old value would be served as newer than the invalidation. The next read
|
|
305
|
+
runs it again. A write that gets through anyway (another colo's marker not
|
|
306
|
+
visible yet) is still rejected on later reads by `CFCacheStore` with KV, which
|
|
307
|
+
stamps an entry with the start of the execution that produced it, not its
|
|
308
|
+
write time; `MemorySegmentCacheStore` refuses such a write. `VercelCacheStore`
|
|
309
|
+
and `CFCacheStore` L1 hits in purge mode or without KV read no marker for
|
|
310
|
+
data, so the write gate is their only guard across requests. A
|
|
305
311
|
`"use cache"` call already running when one of its tags is invalidated is
|
|
306
312
|
not joined by later calls either. Its tags include those of the
|
|
307
313
|
`"use cache"` functions it calls. Another isolate's invalidation is caught
|
|
@@ -523,6 +529,24 @@ already expressible with `cache({ key })`.
|
|
|
523
529
|
| `CFCacheStore` | `@rangojs/router/cache` | Cloudflare Workers (Cache API L1 + optional KV L2) |
|
|
524
530
|
| `VercelCacheStore` | `@rangojs/router/cache` | Vercel Functions (Vercel Runtime Cache) |
|
|
525
531
|
|
|
532
|
+
### Filling a cache before traffic
|
|
533
|
+
|
|
534
|
+
Every cache above fills from the first request that misses. To pay that
|
|
535
|
+
render before a visitor does (after a deploy, after an `updateTag()`), warm
|
|
536
|
+
the URL: `router.prerender({ env, ctx })(url)` sends it through the router's
|
|
537
|
+
handler in a mode where every cache read misses and every write replaces the
|
|
538
|
+
entry, so the route's `ppr` shell, `cache()` records, `"use cache"` results,
|
|
539
|
+
loader caches and document cache entry are rewritten under the keys a
|
|
540
|
+
visitor reads, with the old entries serving until then. See `/prerender` →
|
|
541
|
+
"Warm any route before traffic".
|
|
542
|
+
|
|
543
|
+
A warm only writes to a store that is shared beyond the place the call runs.
|
|
544
|
+
Each store says where its entries can be read (`scope`): `CFCacheStore` with
|
|
545
|
+
`kv` is `"global"`, without `kv` `"local"`; `VercelCacheStore` is `"regional"`
|
|
546
|
+
(a warm fills the region it runs in); `MemorySegmentCacheStore` is `"local"`
|
|
547
|
+
(refused in production, allowed under the dev server; a single-process server
|
|
548
|
+
opts in with `new MemorySegmentCacheStore({ scope: "global" })`).
|
|
549
|
+
|
|
526
550
|
### Memory Store
|
|
527
551
|
|
|
528
552
|
For development, tests, and single-instance deployments. Entries live in
|
|
@@ -535,6 +559,7 @@ const store = new MemorySegmentCacheStore({
|
|
|
535
559
|
defaults: { ttl: 60 }, // segments ignore swr on this store
|
|
536
560
|
maxEntries: 1000, // per-family FIFO cap (default 1000)
|
|
537
561
|
name: "app", // optional: keep entries across Vite HMR module reloads
|
|
562
|
+
scope: "local", // default; "global" only when ONE process serves the app (see /prerender warming)
|
|
538
563
|
});
|
|
539
564
|
```
|
|
540
565
|
|
|
@@ -1094,6 +1119,17 @@ A per-boundary store becomes reachable by `updateTag()`/`revalidateTag()` only
|
|
|
1094
1119
|
once that boundary has been matched in the current process. For data you
|
|
1095
1120
|
invalidate by tag, prefer the app-level store.
|
|
1096
1121
|
|
|
1122
|
+
A store you implement declares where its entries can be read, or
|
|
1123
|
+
`router.prerender()` refuses to warm into it (a store that declares nothing is
|
|
1124
|
+
treated as `"local"`):
|
|
1125
|
+
|
|
1126
|
+
```typescript
|
|
1127
|
+
const store: SegmentCacheStore = {
|
|
1128
|
+
scope: "global", // every location reads the same entries; or "regional"
|
|
1129
|
+
// get, set, delete, ...
|
|
1130
|
+
};
|
|
1131
|
+
```
|
|
1132
|
+
|
|
1097
1133
|
If you implement `SegmentCacheStore` yourself, `invalidateTags(tags)` is called
|
|
1098
1134
|
synchronously inside the invalidating request, and `revalidateTag()` does not
|
|
1099
1135
|
await it. For that request to read its own writes, record the tags and the
|
package/skills/catalog.json
CHANGED
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
},
|
|
186
186
|
{
|
|
187
187
|
"name": "prerender",
|
|
188
|
-
"description": "Pre-render route segments at build time with Prerender (with getParams), Passthrough live fallback, Static segments, and Skip. Use when a page's content is mostly static and shouldn't render on every request, speeding up cold responses, reading build-only data (files, build env), or deciding which routes to prerender vs render live.",
|
|
188
|
+
"description": "Pre-render route segments at build time with Prerender (with getParams), Passthrough live fallback, Static segments, and Skip, refresh them from a running app with on-demand prerender (Prerender onDemand + router.prerender, ISR-style), and warm any other route's runtime caches (ppr shell, cache(), \"use cache\", document cache) before traffic with the same router.prerender. Use when a page's content is mostly static and shouldn't render on every request, speeding up cold responses, reading build-only data (files, build env), refreshing one page from a webhook, cron or queue without a redeploy, removing a deleted item's page (prerender.remove, or notFound() in a refresh), making routes ready after a deploy or an updateTag so no visitor pays the first render, or deciding which routes to prerender vs render live.",
|
|
189
189
|
"argumentHint": "[passthrough]",
|
|
190
190
|
"path": "skills/prerender/SKILL.md"
|
|
191
191
|
},
|
|
@@ -184,6 +184,137 @@ keeps serving a purged shell until `memo.shellMs` passes. Set
|
|
|
184
184
|
user's next request; cross-colo KV propagation (and `tagCacheTtl`) still
|
|
185
185
|
applies. Details: `/caching` → "The fresh-reads cookie".
|
|
186
186
|
|
|
187
|
+
## On-demand prerender on Workers
|
|
188
|
+
|
|
189
|
+
A `Prerender(..., { onDemand })` route (`/prerender` → "On-demand refresh")
|
|
190
|
+
needs a durable store for its refreshed entries. On Workers that is KV:
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
import { createKVPrerenderStore } from "@rangojs/router/prerender/cloudflare";
|
|
194
|
+
|
|
195
|
+
export const router = createRouter<AppBindings>({
|
|
196
|
+
document: Document,
|
|
197
|
+
urls: urlpatterns,
|
|
198
|
+
prerender: (env) => ({
|
|
199
|
+
store: createKVPrerenderStore(env.PRERENDER_KV),
|
|
200
|
+
ttl: 3600, // soft: a stale entry still serves
|
|
201
|
+
// Present = stale-while-revalidate: a stale hit schedules this via waitUntil.
|
|
202
|
+
onRevalidate: (target, liveEnv) => liveEnv.PRERENDER_QUEUE.send({ target }),
|
|
203
|
+
}),
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Add the `PRERENDER_KV` namespace (and the queue, if you use one) to
|
|
208
|
+
`wrangler.jsonc` and to `AppBindings`. Refreshes run from the Worker's other
|
|
209
|
+
handlers, with the live `env`:
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
export default {
|
|
213
|
+
fetch: (request: Request, env: AppBindings, ctx: ExecutionContext) =>
|
|
214
|
+
router.fetch(request, { env, ctx }),
|
|
215
|
+
|
|
216
|
+
// Cron sweep: re-render only what went stale.
|
|
217
|
+
async scheduled(
|
|
218
|
+
_event: ScheduledEvent,
|
|
219
|
+
env: AppBindings,
|
|
220
|
+
ctx: ExecutionContext,
|
|
221
|
+
) {
|
|
222
|
+
const ids = await env.CMS.listProductIds();
|
|
223
|
+
const prerender = router.prerender({ env, ctx });
|
|
224
|
+
ctx.waitUntil(
|
|
225
|
+
prerender.many(
|
|
226
|
+
ids.map((id) => ({ route: "products.detail", params: { id } })),
|
|
227
|
+
{ concurrency: 4, onlyIfStale: true },
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
// Queue consumer: the queue owns dedup across isolates. onlyIfStale: a
|
|
233
|
+
// message can run after another one refreshed the page, or after
|
|
234
|
+
// prerender.remove() removed it; then it renders nothing.
|
|
235
|
+
async queue(
|
|
236
|
+
batch: MessageBatch<{ target: PrerenderTargetObject }>,
|
|
237
|
+
env: AppBindings,
|
|
238
|
+
ctx: ExecutionContext,
|
|
239
|
+
) {
|
|
240
|
+
const targets = batch.messages.map((m) => m.body.target);
|
|
241
|
+
ctx.waitUntil(
|
|
242
|
+
router
|
|
243
|
+
.prerender({ env, ctx })
|
|
244
|
+
.many(targets, { concurrency: 8, onlyIfStale: true }),
|
|
245
|
+
);
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`PrerenderTargetObject` comes from `@rangojs/router/prerender`. It is what
|
|
251
|
+
`onRevalidate` receives, and the `router.prerender({ env, ctx })` runner (and its
|
|
252
|
+
`.many()`) accepts it as is on a router typed with named routes. A queue hands it back as JSON, so type the
|
|
253
|
+
message body with it (as above) or assert it: `JSON.parse(raw) as
|
|
254
|
+
PrerenderTargetObject`.
|
|
255
|
+
|
|
256
|
+
The KV store writes no `expirationTtl` (entries never expire; `ttl` is soft)
|
|
257
|
+
and keys entries by router id and the router's data version, so a deploy that
|
|
258
|
+
changes server code starts from the build entries again.
|
|
259
|
+
`router.prerender({ env, ctx }).markStale(tags)` writes per-tag markers under
|
|
260
|
+
`__rango_pr_tag__/{encoded routerId}/`, a namespace separate from `updateTag()`'s and
|
|
261
|
+
scoped to the router that marked (routers sharing one KV namespace do not mark
|
|
262
|
+
each other's entries); a read checks one marker per tag on the entry,
|
|
263
|
+
uncached, so keep tag counts small. Markers have no expiry (`/prerender` ->
|
|
264
|
+
"KV tag markers"). Neither `updateTag()` nor `revalidateTag()` reaches this
|
|
265
|
+
store.
|
|
266
|
+
|
|
267
|
+
## Warm routes on Workers
|
|
268
|
+
|
|
269
|
+
`router.prerender()` also warms a route that is not on-demand: its `ppr`
|
|
270
|
+
shell, `cache()` records, `"use cache"` results and document cache entry are
|
|
271
|
+
rewritten before a visitor asks (`/prerender` → "Warm any route before
|
|
272
|
+
traffic"). On Workers:
|
|
273
|
+
|
|
274
|
+
- **`CFCacheStore` needs `kv`.** With KV the store is shared by every colo
|
|
275
|
+
(`scope: "global"`) and a warm is allowed; without it entries live in one
|
|
276
|
+
colo's Cache API and the call returns `skipped-store-not-shared`.
|
|
277
|
+
- **Give `scheduled` and `queue` an origin.** Cache keys carry the host, and
|
|
278
|
+
those handlers have no request to take it from:
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
async scheduled(_event: ScheduledEvent, env: AppBindings, ctx: ExecutionContext) {
|
|
282
|
+
const prerender = router.prerender({
|
|
283
|
+
env,
|
|
284
|
+
ctx,
|
|
285
|
+
origin: "https://shop.example",
|
|
286
|
+
});
|
|
287
|
+
ctx.waitUntil(prerender.many(["/", "/products/1"], { onlyIfStale: true }));
|
|
288
|
+
},
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
A webhook route or a server action needs none: the warm uses the origin of
|
|
292
|
+
the request it is called from.
|
|
293
|
+
|
|
294
|
+
- **A warm fills the colo it runs in, and KV.** Another colo that already
|
|
295
|
+
holds its own Cache API copy keeps serving it until it expires; an L1 hit
|
|
296
|
+
never consults KV. After a deploy no colo holds a copy, so the warm's KV
|
|
297
|
+
entry is what each one reads next. For a content change, invalidate and then
|
|
298
|
+
warm, in this order:
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
await updateTag(`product:${id}`); // no colo serves the old copy any more
|
|
302
|
+
await router.prerender({ env, ctx })(`/products/${id}`); // writes the new one
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
A colo with no copy of its own reads the warmed entry from KV. A colo that
|
|
306
|
+
was still holding the old copy treats it as a miss and renders once for
|
|
307
|
+
itself (it does not read the fresher KV entry). With `tagPurge` the purge
|
|
308
|
+
evicts every colo's copy, so they all read the warmed entry.
|
|
309
|
+
|
|
310
|
+
- **Entries under 60 seconds stay in one colo.** KV's minimum `expirationTtl`
|
|
311
|
+
is 60 s, so a segment, item or response entry whose `ttl + swr` is shorter
|
|
312
|
+
is written to the Cache API only, and a warm of it reaches the calling colo
|
|
313
|
+
alone. Shells always reach KV.
|
|
314
|
+
- **KV writes take time to spread.** Cloudflare documents up to 60 seconds or
|
|
315
|
+
more before a write is visible in other locations; a visitor elsewhere in
|
|
316
|
+
that window renders the page as before.
|
|
317
|
+
|
|
187
318
|
## Tracing on Workers
|
|
188
319
|
|
|
189
320
|
`createCloudflareTracing()` from `@rangojs/router/cloudflare` emits the
|
|
@@ -755,7 +755,7 @@ applications do not have to remember to mark every context manually.
|
|
|
755
755
|
|
|
756
756
|
For a `"use cache"` function, a tainted context object is not serialized wholesale
|
|
757
757
|
into the cache key. Rango instead extracts the safe identity dimensions that make
|
|
758
|
-
the call reusable without collisions: host, route name, pathname, params, response
|
|
758
|
+
the call reusable without collisions: router, host, route name, pathname, params, response
|
|
759
759
|
type, and normalized user-facing search params. A shared cached function called
|
|
760
760
|
from two hosts, route scopes, param sets, or query variants therefore does not
|
|
761
761
|
collapse into one entry just because both calls received a `ctx` object.
|
|
@@ -20,11 +20,12 @@ To choose between the in-app layers themselves, start at `/cache-guide`.
|
|
|
20
20
|
| `"use cache"` | one function result | yes | yes | caller, handlers, loaders, rendering |
|
|
21
21
|
| `cache()` | serialized Flight segments | yes | yes | middleware, loaders, HTML rendering |
|
|
22
22
|
| `Prerender()` | build-time Flight segments in the server bundle | yes | yes | middleware, loaders, HTML rendering |
|
|
23
|
+
| `Prerender(..., { onDemand })` | refreshed Flight segments in a writable store | yes | yes | middleware, loaders, HTML rendering |
|
|
23
24
|
| `ppr` | HTML prelude + React postponed state | yes | **yes, before shell commit** | middleware, holes, hydration payload |
|
|
24
25
|
| `createDocumentCacheMiddleware()` | complete response in the app cache store | yes | outer middleware runs; the hit skips its downstream pipeline | nothing downstream |
|
|
25
26
|
| HTTP CDN cache (`s-maxage`) | complete HTTP response outside the app | **no** | **no** | nothing |
|
|
26
27
|
|
|
27
|
-
The first
|
|
28
|
+
The first six rows enter the app. The first five preserve the complete request
|
|
28
29
|
model; a document-cache middleware hit intentionally short-circuits its
|
|
29
30
|
downstream pipeline. The last row is a platform cache: on a hit it serves bytes
|
|
30
31
|
without invoking Rango at all.
|
|
@@ -71,12 +72,31 @@ Worker at the edge, middleware runs there, and PPR shell lookup/resume stays in
|
|
|
71
72
|
that Worker. `CFCacheStore` supplies the app cache families; client assets are
|
|
72
73
|
served separately as assets.
|
|
73
74
|
|
|
75
|
+
An on-demand prerender store (`/prerender` → "On-demand refresh") on Workers
|
|
76
|
+
is `createKVPrerenderStore(env.PRERENDER_KV)` from
|
|
77
|
+
`@rangojs/router/prerender/cloudflare`: refreshes run from `scheduled`,
|
|
78
|
+
`queue` or a webhook route, and every request to an on-demand route pays one
|
|
79
|
+
KV read plus one per tag on the entry.
|
|
80
|
+
|
|
81
|
+
The same `router.prerender()` call warms any other route's caches before
|
|
82
|
+
traffic, when `CFCacheStore` has `kv` (shared by every colo). It fills the
|
|
83
|
+
calling colo and KV; a colo that already holds its own copy keeps it until it
|
|
84
|
+
expires, so a content refresh is `updateTag()` and then the warm: no colo
|
|
85
|
+
serves the old copy, and a colo without one reads the warmed entry
|
|
86
|
+
(`/cloudflare` → "Warm routes on Workers").
|
|
87
|
+
|
|
74
88
|
### Vercel
|
|
75
89
|
|
|
76
90
|
The Vercel preset emits static client assets plus one streaming Node Function.
|
|
77
91
|
HTML, Flight, prerender payloads, and PPR shells are served from that function.
|
|
78
92
|
`VercelCacheStore` uses Runtime Cache inside the function; Runtime Cache is not
|
|
79
|
-
Vercel's CDN/ISR cache.
|
|
93
|
+
Vercel's CDN/ISR cache. Rango's on-demand prerender does not use Vercel ISR
|
|
94
|
+
either: it needs a durable `WritablePrerenderStore` shared by every function
|
|
95
|
+
instance, and v1 ships none for Vercel (the in-memory store is per process),
|
|
96
|
+
so a Vercel app writes a small get/set adapter over its own storage.
|
|
97
|
+
Warming a plain route (`/prerender` → "Warm any route before traffic") needs
|
|
98
|
+
no such store: it writes to `VercelCacheStore`, in the region the call runs
|
|
99
|
+
in.
|
|
80
100
|
|
|
81
101
|
The preset deliberately emits no Vercel `.prerender-config.json`, response
|
|
82
102
|
`chain`, or CDN-stitched resume function. Vercel's open-source Build Output
|
|
@@ -155,15 +175,16 @@ the app store, should own the complete response.
|
|
|
155
175
|
|
|
156
176
|
## Decision guide
|
|
157
177
|
|
|
158
|
-
| Requirement | Choose
|
|
159
|
-
| --------------------------------------------------- |
|
|
160
|
-
| Per-request auth or request shaping | in-function caching; never shared CDN HTML
|
|
161
|
-
| Stable shell with cart/session/live prices | `ppr` with live holes
|
|
162
|
-
| Build-known segments with live loaders | `Prerender()`
|
|
163
|
-
|
|
|
164
|
-
|
|
|
165
|
-
|
|
|
166
|
-
| One
|
|
178
|
+
| Requirement | Choose |
|
|
179
|
+
| --------------------------------------------------- | ----------------------------------------------------- |
|
|
180
|
+
| Per-request auth or request shaping | in-function caching; never shared CDN HTML |
|
|
181
|
+
| Stable shell with cart/session/live prices | `ppr` with live holes |
|
|
182
|
+
| Build-known segments with live loaders | `Prerender()` |
|
|
183
|
+
| Prerendered page refreshed on a CMS edit, no deploy | `Prerender(..., { onDemand })` + `router.prerender()` |
|
|
184
|
+
| Fully public response, whole-page TTL is acceptable | HTTP `s-maxage` + SWR |
|
|
185
|
+
| Whole response reused inside the app store | `createDocumentCacheMiddleware()` |
|
|
186
|
+
| One query or component is expensive | `"use cache"` |
|
|
187
|
+
| One route subtree is expensive | `cache()` |
|
|
167
188
|
|
|
168
189
|
For a large dynamic app, CDN-stitched PPR would mainly improve shell first-byte
|
|
169
190
|
latency and avoid sending the prelude from the function. It would not remove the
|
|
@@ -175,7 +196,7 @@ provably public and shared.
|
|
|
175
196
|
## Related skills
|
|
176
197
|
|
|
177
198
|
- `/ppr` — shell capture, holes, middleware commit point, invalidation
|
|
178
|
-
- `/prerender` — build-time Flight segments
|
|
199
|
+
- `/prerender` — build-time Flight segments, `Prerender + ppr`, and on-demand refresh
|
|
179
200
|
- `/document-cache` — store-backed complete-response middleware
|
|
180
201
|
- `/vercel` — Vercel Build Output preset and Runtime Cache wiring
|
|
181
202
|
- `/cloudflare` — Worker deployment and bindings
|
|
@@ -175,20 +175,22 @@ header or logs to identify an actual CDN hit.
|
|
|
175
175
|
|
|
176
176
|
## Cache Key Generation
|
|
177
177
|
|
|
178
|
-
The default key is the host, pathname, and sorted
|
|
179
|
-
`_rsc*` params removed, `cache.searchParams` filtering
|
|
180
|
-
suffixes that separate response variants:
|
|
178
|
+
The default key is the serving router's id, the host, pathname, and sorted
|
|
179
|
+
search string (internal `_rsc*` params removed, `cache.searchParams` filtering
|
|
180
|
+
applied), followed by suffixes that separate response variants:
|
|
181
181
|
|
|
182
|
-
- HTML document requests: `{host}{pathname}[?search]:html`
|
|
182
|
+
- HTML document requests: `{routerId}@{host}{pathname}[?search]:html`
|
|
183
183
|
- RSC requests (client navigations and full-document Flight fetches):
|
|
184
|
-
`{host}{pathname}[?search]:rsc`
|
|
184
|
+
`{routerId}@{host}{pathname}[?search]:rsc`
|
|
185
185
|
- Client navigations additionally add a hash of the segments the client already
|
|
186
186
|
has (`:{segmentHash}`), so navigations from pages with different layouts get
|
|
187
187
|
different cached payloads, and a `:fragments` marker when the client accepts
|
|
188
188
|
fragment envelopes.
|
|
189
189
|
|
|
190
|
-
A custom `keyGenerator(url)` replaces
|
|
191
|
-
|
|
190
|
+
A custom `keyGenerator(url)` replaces the whole
|
|
191
|
+
`{routerId}@{host}{pathname}[?search]` part, router id included, so a generator
|
|
192
|
+
used by routers that share a store must keep them apart itself; the variant
|
|
193
|
+
suffixes are still appended.
|
|
192
194
|
|
|
193
195
|
## What Gets Cached
|
|
194
196
|
|
|
@@ -320,13 +322,13 @@ everything else.
|
|
|
320
322
|
|
|
321
323
|
## Document Cache vs Segment Cache
|
|
322
324
|
|
|
323
|
-
| Feature | Document Cache
|
|
324
|
-
| ------------ |
|
|
325
|
-
| Granularity | Full response
|
|
326
|
-
| Opt-in | `Cache-Control` `s-maxage`
|
|
327
|
-
| Loaders | Frozen with the response
|
|
328
|
-
| Use case | Static pages
|
|
329
|
-
| Key includes | host + URL + variant (+ segment hash) | host + pathname + params + search |
|
|
325
|
+
| Feature | Document Cache | Segment Cache (`cache()`) |
|
|
326
|
+
| ------------ | ---------------------------------------------- | ------------------------------------------ |
|
|
327
|
+
| Granularity | Full response | Individual segments |
|
|
328
|
+
| Opt-in | `Cache-Control` `s-maxage` | `cache({ ttl, swr })` |
|
|
329
|
+
| Loaders | Frozen with the response | Always run fresh |
|
|
330
|
+
| Use case | Static pages | Dynamic compositions |
|
|
331
|
+
| Key includes | router + host + URL + variant (+ segment hash) | router + host + pathname + params + search |
|
|
330
332
|
|
|
331
333
|
Use document cache for mostly-static pages. Use segment cache when different parts of a page have different cache requirements.
|
|
332
334
|
|
package/skills/hooks/SKILL.md
CHANGED
|
@@ -97,3 +97,25 @@ the one for your case.
|
|
|
97
97
|
| `useAction()` | Server action state | state, actionId, payload, error, result |
|
|
98
98
|
| `useLocationState()` | History state (persists or flash) | T \| undefined |
|
|
99
99
|
| `invalidateClientCache()` | Force client caches to miss (function, not a hook; root entry) | `void` |
|
|
100
|
+
|
|
101
|
+
## Hydration
|
|
102
|
+
|
|
103
|
+
On a document load every router hook renders what the server rendered until
|
|
104
|
+
the document has finished streaming, wherever the component sits. A component
|
|
105
|
+
in a `<Suspense>` or `loading()` boundary that streams in after the rest of
|
|
106
|
+
the page hydrates with the document's pathname, params, search params and
|
|
107
|
+
segments, an idle `useNavigation()` and `useAction()`, a `useLinkStatus()` that
|
|
108
|
+
is not pending, the handle data the HTML was rendered from, and `undefined`
|
|
109
|
+
location state, even when a navigation or an action has started meanwhile. It
|
|
110
|
+
cannot mismatch its server HTML.
|
|
111
|
+
|
|
112
|
+
- Navigation state, params and action state: the component shows the live
|
|
113
|
+
value right after it hydrates.
|
|
114
|
+
- Handle data a loader pushed late, and the entry's location state: every
|
|
115
|
+
reader gets them once the whole document has streamed and hydrated, in one
|
|
116
|
+
transition. A reader re-renders only when its selection changes.
|
|
117
|
+
- A navigation that commits before then mounts its page with the live values
|
|
118
|
+
from its first render. A boundary of the document that persists across that
|
|
119
|
+
navigation and has not hydrated yet then hydrates against the live values;
|
|
120
|
+
where they differ from its HTML, React reports the mismatch and renders that
|
|
121
|
+
boundary on the client.
|
|
@@ -40,10 +40,9 @@ bake into the shell. See `/loader` → "Writing Handles from Loaders". `useHandl
|
|
|
40
40
|
re-renders on every handle update, late loader pushes included.
|
|
41
41
|
|
|
42
42
|
On a document load `useHandle` hydrates with the handle data its HTML was
|
|
43
|
-
rendered from,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
its hydration render, so it cannot mismatch its server HTML.
|
|
43
|
+
rendered from, wherever the reader sits; late loader pushes reach every reader
|
|
44
|
+
once the document has finished streaming and every boundary has hydrated (see
|
|
45
|
+
`SKILL.md`, "Hydration").
|
|
47
46
|
|
|
48
47
|
Handles can be passed as props from server to client components:
|
|
49
48
|
|
|
@@ -166,6 +166,16 @@ function Breadcrumbs() {
|
|
|
166
166
|
const isShopRoute = useSegments((s) => s.path[0] === "shop");
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
+
The selector is applied when the router store changes (a navigation or new
|
|
170
|
+
handle data), as with `useNavigation` and `useAction`. A new selector function
|
|
171
|
+
on a re-render takes effect at the next change. To derive from a prop, select
|
|
172
|
+
the state and derive in render:
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
const segments = useSegments();
|
|
176
|
+
const part = segments.path[depth];
|
|
177
|
+
```
|
|
178
|
+
|
|
169
179
|
### useLinkStatus()
|
|
170
180
|
|
|
171
181
|
Track pending state inside a Link component. `pending` is `true` while a
|
package/skills/hooks/state.md
CHANGED
|
@@ -251,9 +251,9 @@ function FlashBanner() {
|
|
|
251
251
|
```
|
|
252
252
|
|
|
253
253
|
Flash behavior is determined by the definition (`{ flash: true }`), not by which
|
|
254
|
-
hook reads it. `useLocationState` reads the value during render (on
|
|
255
|
-
|
|
256
|
-
SSR and hydration agree), then clears it from `history.state` via
|
|
254
|
+
hook reads it. `useLocationState` reads the value during render (on a
|
|
255
|
+
document load it returns `undefined` until the document has finished streaming
|
|
256
|
+
and hydrating, so SSR and hydration agree), then clears it from `history.state` via
|
|
257
257
|
`replaceState` in a `useEffect`. Every component reading the same flash
|
|
258
258
|
definition sees the value, whenever it mounts, until the next navigation or
|
|
259
259
|
back/forward commits its entry's state: an entry that carries no flash shows
|
|
@@ -342,7 +342,8 @@ development.
|
|
|
342
342
|
|
|
343
343
|
Location state lives in the browser, so the server renders a document without
|
|
344
344
|
it. `useLocationState` therefore hydrates as `undefined` and applies the stored
|
|
345
|
-
value
|
|
345
|
+
value once the document has finished streaming and every boundary has
|
|
346
|
+
hydrated. For most state that is invisible. For state that decides
|
|
346
347
|
how much content is on the page, it is a layout shift on every refresh.
|
|
347
348
|
|
|
348
349
|
The case this option exists for is a "load more" list. `?page=6` loads that
|
|
@@ -383,3 +383,21 @@ Each `createRouter()` app has its own cache version, computed by `vite build` fr
|
|
|
383
383
|
| Server code shared by A and B changes | cleared for A and B | cleared for A and B | A's and B's tabs reload |
|
|
384
384
|
|
|
385
385
|
The host entry (the file that creates the host router and its middleware) is not part of a lazily mounted sub-app's version. If host middleware changes what an app renders from inside a cache boundary, change that app's `version` or invalidate its tags. See `/cache-guide`.
|
|
386
|
+
|
|
387
|
+
## Shared cache store
|
|
388
|
+
|
|
389
|
+
Sub-apps may share one cache store (one KV namespace, one `MemorySegmentCacheStore`). Every entry that is a router's output for a host and path is keyed by that router as well; its key starts with the router's id (`{routerId}@{host}{path}...`):
|
|
390
|
+
|
|
391
|
+
- PPR shells (document, navigation and build-time);
|
|
392
|
+
- route `cache()` records, document-cache responses and response-route entries;
|
|
393
|
+
- a loader's own `cache()` entry, and a `"use cache"` entry of a function called with a handler or loader `ctx`. A `ctx` is one router's: its context variables, its `env`, its `reverse()` map.
|
|
394
|
+
|
|
395
|
+
So two sub-apps never read each other's entry, including where they serve the same host and path:
|
|
396
|
+
|
|
397
|
+
- with the `hostOverride` cookie, which picks the app and forwards the request unchanged, so every app answers under the one preview origin;
|
|
398
|
+
- after a `fallback()` that hands a request to another app's handler;
|
|
399
|
+
- after a `router.prerender()` warm that resolved another app's host as its origin.
|
|
400
|
+
|
|
401
|
+
There is nothing to configure and no namespace to manage. What it relies on is that each router has a stable id: the one the Vite plugin injects for a `createRouter({ ... })` call written with an object literal, or the `id` you pass (`createRouter({ id: "admin", ... })`). A router the plugin cannot reach, for example `createRouter(options)` with an options variable, falls back to a counter (`router_0`, `router_1`, ...) that follows module evaluation order. One such router is fine. With more than one, the build already warns (`N routers use auto-generated IDs`), because routes and cache versions are matched by the same id; the fix is the same here: pass the object literal inline or set an `id`.
|
|
402
|
+
|
|
403
|
+
Not keyed by router, on purpose: a `"use cache"` entry of a function called with plain arguments or the bare `Request` (the function and its arguments name no router, so both apps share the entry), tag markers (an invalidation reaches every app), and any key you build yourself (`cache({ key })` on a route or a loader, the document cache's `keyGenerator`): keep those apart yourself where two apps must not share them.
|
package/skills/loader/SKILL.md
CHANGED
|
@@ -662,9 +662,11 @@ HIT".
|
|
|
662
662
|
|
|
663
663
|
### Cache Key
|
|
664
664
|
|
|
665
|
-
The default cache key is
|
|
666
|
-
|
|
667
|
-
|
|
665
|
+
The default cache key is
|
|
666
|
+
`loader:{loaderId}:{routerId}@{host}{pathname}:{sortedParams}`: the host keeps
|
|
667
|
+
multi-tenant hosts from sharing entries, and the router id keeps two routers
|
|
668
|
+
that mount the loader on one store apart (see `/host-router`, "Shared cache
|
|
669
|
+
store"). This can be customized at two levels:
|
|
668
670
|
|
|
669
671
|
```typescript
|
|
670
672
|
import { cookies } from "@rangojs/router";
|
|
@@ -688,7 +690,7 @@ Resolution priority (same order as route-level `cache()`):
|
|
|
688
690
|
`loader:{loaderId}:key:` plus the result's URI encoding
|
|
689
691
|
2. `store.keyGenerator(ctx, defaultKey)` — store-level modification, stored
|
|
690
692
|
as returned
|
|
691
|
-
3. Default key — `loader:{loaderId}:{host}{pathname}:{sortedParams}`
|
|
693
|
+
3. Default key — `loader:{loaderId}:{routerId}@{host}{pathname}:{sortedParams}`
|
|
692
694
|
|
|
693
695
|
Because the `key()` result is namespaced by the loader id, no value it
|
|
694
696
|
returns, request input included, can name another loader's entry or a
|
|
@@ -426,7 +426,7 @@ export const requireAuthMiddleware: Middleware = async (ctx, next) => {
|
|
|
426
426
|
|
|
427
427
|
Read the flash on the target page with `useLocationState(FlashMessage)`. The `{ flash: true }` option makes it auto-clear after first render. See `/hooks`.
|
|
428
428
|
|
|
429
|
-
A redirect is followed on a link click, `router.push()`, and Back/Forward to an entry the client has to fetch. On Back/Forward the redirect replaces the history entry it was triggered from: Back from the target lands on the entry before the redirecting one, not on the redirecting one again.
|
|
429
|
+
A redirect is followed on a link click, `router.push()`, and Back/Forward to an entry the client has to fetch. On Back/Forward the redirect replaces the history entry it was triggered from: Back from the target lands on the entry before the redirecting one, not on the redirecting one again. The client follows at most 20 redirects in one navigation; a loop (two pages redirecting to each other) stops there with the error boundary and a `[rango] Server redirect loop` console error.
|
|
430
430
|
|
|
431
431
|
## Authentication Middleware
|
|
432
432
|
|