@rangojs/router 0.20.0 → 0.22.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/dist/bin/rango.js +39 -15
- package/dist/types/browser/event-controller.d.ts +49 -0
- package/dist/types/browser/history-state.d.ts +16 -4
- package/dist/types/browser/navigation-store.d.ts +9 -0
- package/dist/types/browser/navigation-transaction.d.ts +7 -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/rango-state.d.ts +3 -1
- package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
- package/dist/types/browser/react/context.d.ts +25 -0
- package/dist/types/browser/react/location-state-shared.d.ts +84 -10
- package/dist/types/browser/react/location-state.d.ts +7 -2
- 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 +5 -6
- package/dist/types/cache/cf/cf-cache-store.d.ts +34 -7
- package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
- package/dist/types/cache/document-cache.d.ts +8 -5
- package/dist/types/cache/handle-snapshot.d.ts +59 -50
- package/dist/types/cache/index.d.ts +3 -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 +29 -4
- 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 +84 -24
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +35 -17
- package/dist/types/client-urls/navigation.d.ts +7 -1
- package/dist/types/client-urls/optimistic-location.d.ts +10 -4
- package/dist/types/errors.d.ts +6 -0
- package/dist/types/handles/deferred-resolution.d.ts +18 -1
- 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/build-shell-capture.d.ts +8 -6
- 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 +15 -1
- package/dist/types/router/navigation-snapshot.d.ts +8 -1
- 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 +32 -1
- package/dist/types/router/router-options.d.ts +39 -5
- package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +23 -10
- 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/router-versions.d.ts +39 -0
- package/dist/types/rsc/handler-context.d.ts +5 -0
- package/dist/types/rsc/shell-build-manifest.d.ts +9 -6
- 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 +12 -6
- 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 +53 -0
- package/dist/types/server/context.d.ts +12 -4
- package/dist/types/server/handle-store.d.ts +89 -46
- package/dist/types/server/request-context.d.ts +84 -9
- 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/build-versions.d.ts +32 -0
- package/dist/types/testing/dom.entry.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +3 -1
- package/dist/types/testing/index.d.ts +6 -0
- package/dist/types/testing/internal/seed-vars.d.ts +7 -2
- package/dist/types/testing/internal/server-modules.d.ts +8 -0
- package/dist/types/testing/location-state-key.d.ts +5 -1
- package/dist/types/testing/render-route.d.ts +77 -6
- package/dist/types/testing/serve-shell-request.d.ts +55 -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/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/thenable-status.d.ts +6 -0
- package/dist/types/types/handler-context.d.ts +15 -5
- 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/build-versions.d.ts +393 -0
- package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
- package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
- package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
- package/dist/types/vite/discovery/state.d.ts +25 -0
- package/dist/types/vite/encryption-key.d.ts +18 -0
- package/dist/types/vite/inject-client-debug.d.ts +6 -25
- package/dist/types/vite/plugin-types.d.ts +27 -0
- package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
- package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
- package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
- package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
- package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
- package/dist/types/vite/plugins/virtual-entries.d.ts +18 -1
- package/dist/types/vite/router-discovery.d.ts +6 -0
- package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
- package/dist/types/vite/utils/client-chunks.d.ts +2 -0
- package/dist/types/vite/utils/compare-strings.d.ts +7 -0
- package/dist/types/vite/utils/package-resolution.d.ts +8 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
- package/dist/vite/index.js +1707 -931
- package/package.json +12 -3
- package/skills/cache-guide/SKILL.md +102 -37
- package/skills/caching/SKILL.md +56 -9
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +16 -0
- package/skills/cloudflare/SKILL.md +133 -1
- package/skills/comparison/references/framework-comparison.md +17 -13
- package/skills/deployment-caching/SKILL.md +33 -12
- package/skills/document-cache/SKILL.md +16 -14
- package/skills/hooks/SKILL.md +25 -0
- package/skills/hooks/handle-and-actions.md +5 -0
- package/skills/hooks/navigation.md +38 -0
- package/skills/hooks/state.md +213 -11
- package/skills/hooks/urls.md +40 -1
- package/skills/host-router/SKILL.md +31 -0
- package/skills/intercept/SKILL.md +6 -0
- package/skills/loader/SKILL.md +25 -5
- package/skills/middleware/SKILL.md +2 -0
- package/skills/migrate-nextjs/SKILL.md +82 -9
- package/skills/ppr/SKILL.md +150 -48
- package/skills/prerender/SKILL.md +651 -10
- package/skills/rango/SKILL.md +9 -7
- package/skills/response-routes/SKILL.md +5 -5
- package/skills/route/SKILL.md +29 -0
- package/skills/router-setup/SKILL.md +15 -1
- package/skills/testing/SKILL.md +32 -27
- package/skills/testing/cache-prerender.md +18 -17
- package/skills/testing/client-components.md +193 -8
- package/skills/testing/handles.md +2 -0
- package/skills/testing/loader.md +1 -1
- package/skills/testing/middleware.md +1 -1
- package/skills/testing/render-handler.md +18 -18
- package/skills/testing/serve-shell-request.md +167 -22
- package/skills/testing/server-actions.md +17 -17
- package/skills/testing/server-tree.md +1 -1
- package/skills/typesafety/generated-files-and-cli.md +1 -1
- package/skills/use-cache/SKILL.md +25 -10
- package/skills/vercel/SKILL.md +5 -3
- package/skills/view-transitions/SKILL.md +9 -4
- package/src/bin/rango.ts +1 -1
- package/src/browser/event-controller.ts +119 -2
- package/src/browser/history-state.ts +39 -17
- package/src/browser/link-interceptor.ts +2 -2
- package/src/browser/navigation-bridge.ts +104 -41
- package/src/browser/navigation-store.ts +26 -0
- package/src/browser/navigation-transaction.ts +24 -12
- package/src/browser/network-error-handler.ts +2 -0
- package/src/browser/partial-update.ts +58 -14
- package/src/browser/prefetch/default-strategy.ts +22 -42
- package/src/browser/rango-state.ts +3 -1
- package/src/browser/react/Link.tsx +37 -43
- package/src/browser/react/NavigationProvider.tsx +85 -1
- package/src/browser/react/context.ts +30 -0
- package/src/browser/react/location-state-shared.ts +200 -34
- package/src/browser/react/location-state.ts +28 -77
- package/src/browser/react/use-action.ts +4 -1
- package/src/browser/react/use-handle.ts +3 -0
- 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 +93 -31
- package/src/browser/segment-reconciler.ts +3 -3
- package/src/browser/server-action-bridge.ts +43 -7
- 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 +48 -36
- package/src/cache/cf/cf-cache-store.ts +91 -66
- package/src/cache/cf/cf-cache-types.ts +12 -4
- package/src/cache/document-cache.ts +35 -14
- package/src/cache/handle-snapshot.ts +118 -86
- package/src/cache/index.ts +5 -0
- package/src/cache/invalidation-order.ts +53 -0
- package/src/cache/memory-segment-store.ts +52 -1
- package/src/cache/shell-snapshot.ts +52 -5
- package/src/cache/store-scope.ts +36 -0
- package/src/cache/tag-invalidation.ts +7 -5
- package/src/cache/types.ts +90 -24
- package/src/cache/vercel/vercel-cache-store.ts +68 -26
- package/src/client-urls/client-root.tsx +1 -0
- package/src/client-urls/navigation.ts +10 -0
- package/src/client-urls/optimistic-location.ts +10 -4
- package/src/client.tsx +40 -3
- package/src/errors.ts +13 -6
- package/src/handles/deferred-resolution.ts +36 -0
- 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/build-shell-capture.ts +11 -9
- 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/manifest.ts +2 -0
- package/src/router/match-middleware/background-revalidation.ts +2 -0
- package/src/router/match-middleware/cache-lookup.ts +239 -54
- package/src/router/navigation-snapshot.ts +25 -6
- 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/request-classification.ts +15 -12
- package/src/router/route-trie-builder.ts +9 -0
- package/src/router/router-context.ts +1 -0
- package/src/router/router-interfaces.ts +35 -1
- package/src/router/router-options.ts +45 -5
- package/src/router/segment-resolution/fresh.ts +20 -19
- package/src/router/segment-resolution/loader-cache.ts +110 -77
- 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-versions.ts +60 -0
- package/src/router.ts +107 -1
- package/src/rsc/full-payload.ts +6 -2
- package/src/rsc/handler-context.ts +5 -0
- package/src/rsc/handler.ts +64 -7
- package/src/rsc/progressive-enhancement.ts +5 -1
- package/src/rsc/response-cache-serve.ts +10 -4
- package/src/rsc/rsc-rendering.ts +65 -12
- package/src/rsc/shell-build-manifest.ts +17 -12
- package/src/rsc/shell-capture-constants.ts +39 -7
- package/src/rsc/shell-capture.ts +144 -45
- package/src/rsc/shell-serve.ts +12 -6
- package/src/rsc/ssr-module-loader.ts +26 -0
- package/src/rsc/types.ts +12 -6
- 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 +130 -0
- package/src/server/context.ts +40 -17
- package/src/server/cookie-store.ts +5 -0
- package/src/server/handle-store.ts +357 -127
- package/src/server/request-context.ts +149 -12
- package/src/ssr/ssr-root.tsx +32 -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/build-versions.ts +64 -0
- package/src/testing/dispatch.ts +6 -0
- package/src/testing/dom.entry.ts +1 -0
- package/src/testing/flight-runtime.d.ts +27 -0
- package/src/testing/flight.entry.ts +4 -0
- package/src/testing/index.ts +22 -0
- package/src/testing/internal/context.ts +2 -1
- package/src/testing/internal/seed-vars.ts +14 -2
- package/src/testing/internal/server-modules.ts +32 -0
- package/src/testing/location-state-key.ts +18 -8
- package/src/testing/render-handler.ts +2 -1
- package/src/testing/render-route.tsx +293 -46
- package/src/testing/serve-shell-request.ts +262 -37
- package/src/testing/shell-status.ts +17 -6
- package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
- package/src/testing/vitest-stubs/version.ts +4 -1
- package/src/thenable-status.ts +10 -0
- package/src/types/handler-context.ts +15 -5
- 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/build-versions.ts +1281 -0
- package/src/vite/discovery/bundle-postprocess.ts +4 -0
- package/src/vite/discovery/discover-routers.ts +15 -0
- package/src/vite/discovery/installed-packages.ts +159 -0
- package/src/vite/discovery/prerender-collection.ts +38 -1
- package/src/vite/discovery/router-versions-phase.ts +336 -0
- package/src/vite/discovery/shell-prerender-phase.ts +33 -43
- package/src/vite/discovery/state.ts +36 -1
- package/src/vite/discovery/virtual-module-codegen.ts +7 -2
- package/src/vite/encryption-key.ts +101 -9
- package/src/vite/inject-client-debug.ts +42 -20
- package/src/vite/plugin-types.ts +28 -0
- package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
- package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
- package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
- package/src/vite/plugins/expose-internal-ids.ts +12 -3
- package/src/vite/plugins/stable-rsc-output.ts +147 -0
- package/src/vite/plugins/version-plugin.ts +19 -24
- package/src/vite/plugins/version.d.ts +17 -2
- package/src/vite/plugins/virtual-entries.ts +39 -6
- package/src/vite/rango.ts +28 -3
- package/src/vite/router-discovery.ts +96 -13
- package/src/vite/utils/bundle-analysis.ts +39 -7
- package/src/vite/utils/client-chunks.ts +5 -1
- package/src/vite/utils/compare-strings.ts +9 -0
- package/src/vite/utils/package-resolution.ts +9 -0
- package/src/vite/utils/prerender-utils.ts +4 -1
- package/src/vite/utils/shared-utils.ts +1 -1
- package/dist/types/segment-content-promise.d.ts +0 -13
- 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.0",
|
|
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",
|
|
@@ -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": {
|
|
@@ -217,7 +227,6 @@
|
|
|
217
227
|
"@playwright/test": "^1.49.1",
|
|
218
228
|
"@testing-library/react": ">=16",
|
|
219
229
|
"@vercel/functions": "^3.0.0",
|
|
220
|
-
"@vitejs/plugin-rsc": "^0.5.35",
|
|
221
230
|
"react": ">=19.2.8 <20",
|
|
222
231
|
"react-dom": ">=19.2.8 <20",
|
|
223
232
|
"vite": "^8.0.16",
|
|
@@ -166,24 +166,87 @@ exemption there). The route then serves uncached; see `/ppr`.
|
|
|
166
166
|
|
|
167
167
|
### Cross-deploy safety: version-segmented store keys
|
|
168
168
|
|
|
169
|
-
`CFCacheStore`
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
The
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
the
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
169
|
+
`CFCacheStore` and `VercelCacheStore` prefix every **physical** store key with a
|
|
170
|
+
version, so a build only reads entries that code like its own wrote. The version
|
|
171
|
+
is a hash of the code, computed per `createRouter()` by `vite build`, not the
|
|
172
|
+
build time. Each router has two:
|
|
173
|
+
|
|
174
|
+
- The **data version** (a hash of the router's server code) keys cached RSC
|
|
175
|
+
data: segment entries from `cache()`, `"use cache"` values, loader data.
|
|
176
|
+
- The **document version** (the data version plus the SSR output, the client
|
|
177
|
+
asset file names, `base`, and the router's `Prerender` payloads) keys stored
|
|
178
|
+
HTML (PPR shells, document-cache responses). It is also sent to the browser,
|
|
179
|
+
and a mismatch with the tab's `_rsc_v` reloads the tab.
|
|
180
|
+
|
|
181
|
+
Same code builds to the same versions, so a rebuild or redeploy of unchanged
|
|
182
|
+
code keeps the cache, and a deploy that changes app A keeps app B's cache.
|
|
183
|
+
|
|
184
|
+
| Deploy | Cached data | Stored HTML | Open tabs |
|
|
185
|
+
| ------------------------------------- | ------------------- | --------------------- | ----------------------- |
|
|
186
|
+
| Rebuild, no code change | kept | kept | untouched |
|
|
187
|
+
| Server code of app A changes | cleared for A only | cleared for A only | A's tabs reload |
|
|
188
|
+
| Client code of any app changes | kept for every app | cleared for every app | every app's tabs reload |
|
|
189
|
+
| 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 |
|
|
190
|
+
|
|
191
|
+
"Server code" is everything the app's server can run: its own modules, the
|
|
192
|
+
dependencies it imports (bundled, or on the node preset left external and
|
|
193
|
+
resolved from `node_modules`), and a stylesheet its server code links
|
|
194
|
+
(`import "./x.css"` in a server component, or `import href from "./x.css?url"`
|
|
195
|
+
for a document `<link>`): the stylesheet's hashed URL is in what the server
|
|
196
|
+
renders, so a change to the compiled CSS clears that app's cached data. A
|
|
197
|
+
`clientUrls()` module is client code that also tells the server what to run:
|
|
198
|
+
changing which loaders a route declares, its `loading` or its transition
|
|
199
|
+
counts as a server change; changing the components it renders does not. Under
|
|
200
|
+
a host router, apps are independent when the host mounts them lazily
|
|
201
|
+
(`.lazy(() => import(...))`); routers one module imports statically share
|
|
202
|
+
that module's code.
|
|
203
|
+
|
|
204
|
+
The key families: `CFCacheStore` keys segment entries and `fn:` items with the
|
|
205
|
+
data version and `doc:` responses and `shell2:` shells with the document
|
|
206
|
+
version. `VercelCacheStore` does the same by default (families `s`, `i` data;
|
|
207
|
+
`r`, `h` document). `MemorySegmentCacheStore` has no version; the process is its
|
|
208
|
+
scope.
|
|
209
|
+
|
|
210
|
+
Tag invalidation markers are stored without a version, so `updateTag()` and
|
|
211
|
+
`revalidateTag()` apply to entries of every version, including a version that
|
|
212
|
+
comes back in a rollback.
|
|
213
|
+
|
|
214
|
+
- **Force a clear:** change `version` (`createRouter({ version })`,
|
|
215
|
+
`createRSCHandler({ version })`, `new CFCacheStore({ version })` or
|
|
216
|
+
`new VercelCacheStore({ version })` all use that exact value for both
|
|
217
|
+
versions), or invalidate tags.
|
|
218
|
+
- **Keep the old "every deploy clears the cache" behavior:** set `version` to a
|
|
219
|
+
per-deploy value (a build id), or on Vercel keep a deployment-scoped
|
|
220
|
+
`getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID })`.
|
|
221
|
+
- **Separate environments:** two deployments built from the same code have the
|
|
222
|
+
same versions. If they share a store (the same KV namespace), they share
|
|
223
|
+
entries. Give each environment its own KV namespace or cache namespace, or its
|
|
224
|
+
own `version`.
|
|
225
|
+
|
|
226
|
+
Two things give a router a new version on every build, and with it a cold
|
|
227
|
+
cache on every deploy:
|
|
228
|
+
|
|
229
|
+
- **No stable encryption key.** The key that encrypts inline server-action
|
|
230
|
+
bound arguments is part of the version of every router whose code encrypts
|
|
231
|
+
with it. Pass `rango({ encryptionKey: process.env.RANGO_ENCRYPTION_KEY })`
|
|
232
|
+
(see `/use-cache`); the build prints a note when a router needs it.
|
|
233
|
+
- **A `Prerender` or `Static()` handler whose output differs per build**: a
|
|
234
|
+
timestamp, a random id, or an inline action with bound arguments rendered at
|
|
235
|
+
build time (its arguments are encrypted with a random IV, stable key or not).
|
|
236
|
+
- **An import the server build leaves external and cannot read**: a package
|
|
237
|
+
that is not in `node_modules` at build time, a path that does not resolve, a
|
|
238
|
+
URL. The build names it.
|
|
239
|
+
|
|
240
|
+
Every build prints each router's versions and writes what they were computed
|
|
241
|
+
from to `node_modules/.rangojs-router-build/cache-versions.json`. Diff that
|
|
242
|
+
file between two builds to see which chunk or payload moved a version; its
|
|
243
|
+
`unownedFiles` lists the server files in no router's version (the host entry
|
|
244
|
+
of lazily mounted apps), where a change keeps every app's cache.
|
|
245
|
+
|
|
246
|
+
A custom persistent store keys the same way with `getCacheVersions()` from
|
|
247
|
+
`@rangojs/router/cache`, called per operation inside the request: `data` for
|
|
248
|
+
segment entries and items, `document` for responses and shells, no version on
|
|
249
|
+
its tag-invalidation records. See `/caching` for store setup.
|
|
187
250
|
|
|
188
251
|
### Client cache: forward/back is mutation-aware
|
|
189
252
|
|
|
@@ -226,8 +289,9 @@ recompute for a merely-aging entry.
|
|
|
226
289
|
`/document-cache`. A platform CDN may independently consume the same header
|
|
227
290
|
and bypass the function on hits; see `/deployment-caching`.
|
|
228
291
|
|
|
229
|
-
SWR softens normal TTL expiry, **not** a
|
|
230
|
-
has no stale entry to serve (see
|
|
292
|
+
SWR softens normal TTL expiry, **not** a version change — a deploy that changes
|
|
293
|
+
a router's code reads under a new version and has no stale entry to serve (see
|
|
294
|
+
version-segmented store keys above).
|
|
231
295
|
|
|
232
296
|
Store support is layer-specific. `CFCacheStore` and `VercelCacheStore` support
|
|
233
297
|
SWR for segment, document/response, item (`"use cache"` and cached loaders), and
|
|
@@ -238,25 +302,26 @@ local/dev behavior, not as proof that segment SWR is active.
|
|
|
238
302
|
|
|
239
303
|
## Key Differences
|
|
240
304
|
|
|
241
|
-
| | `cache()` DSL
|
|
242
|
-
| -------------------- |
|
|
243
|
-
| **Scope** | Route segment tree (handler + children + parallels)
|
|
244
|
-
| **Defined at** | Route definition site (`urls.ts`)
|
|
245
|
-
| **Cache key** | Request type + host + pathname + params + search
|
|
246
|
-
| **Execution on hit** | All-or-nothing: entire handler skipped
|
|
247
|
-
| **Runtime control** | `condition` to disable, custom `key` function
|
|
248
|
-
| **Side effects** | Response side effects throw inside the boundary
|
|
249
|
-
| **Handle data** | Handler pushes replayed; loader pushes re-run live
|
|
250
|
-
| **Loaders** | Always fresh — excluded from cache, opt-in per loader
|
|
251
|
-
| **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 |
|
|
252
316
|
|
|
253
317
|
### cache() Cache Key
|
|
254
318
|
|
|
255
|
-
The default key is `{requestType}:{host}{pathname}[:params][?search]`,
|
|
256
|
-
requestType is `doc`, `partial`, or `intercept`. The same URL is therefore
|
|
257
|
-
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
|
|
258
322
|
navigations. The host keeps tenants apart when one deployment serves several
|
|
259
|
-
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
|
|
260
325
|
honors `createRouter({ cache: { searchParams } })`.
|
|
261
326
|
|
|
262
327
|
A custom `key` function replaces the whole default key (e.g., to key by user role
|
|
@@ -280,8 +345,8 @@ The key is `use-cache:{functionId}:{serializedArgs}` where functionId is a stabl
|
|
|
280
345
|
ID from the Vite transform (module path + export name) and args are serialized
|
|
281
346
|
(stable JSON when every arg is JSON-safe, RSC `encodeReply()` otherwise).
|
|
282
347
|
Request-scoped arguments are excluded: route fields read off `ctx` (handler,
|
|
283
|
-
loader or middleware: host, route name, pathname, params, search) and
|
|
284
|
-
`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`
|
|
285
350
|
is left out. React element slots stay out of the key; any other argument that
|
|
286
351
|
cannot be serialized (a function, a class instance) runs the call uncached. See
|
|
287
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
|
|
|
@@ -564,9 +589,13 @@ const router = createRouter<AppBindings>({
|
|
|
564
589
|
});
|
|
565
590
|
```
|
|
566
591
|
|
|
567
|
-
`CFCacheStore` prefixes every key with
|
|
568
|
-
|
|
569
|
-
|
|
592
|
+
`CFCacheStore` prefixes every key with a version computed per `createRouter()`
|
|
593
|
+
from that router's built code: segment entries and `fn:` items with the data
|
|
594
|
+
version, `doc:` responses and `shell2:` shells with the document version. A
|
|
595
|
+
deploy that changes a router's code reads under a new version; a deploy that does
|
|
596
|
+
not keeps the entries. Tag invalidation markers carry no version, so `updateTag()`
|
|
597
|
+
reaches entries of every version. `version` replaces both versions with one
|
|
598
|
+
value. See `/cache-guide` → "Cross-deploy safety" and `/cloudflare` for bindings.
|
|
570
599
|
|
|
571
600
|
### With KV L2 Persistence
|
|
572
601
|
|
|
@@ -755,10 +784,11 @@ the store adds SWR, tag invalidation, and the family split on top of it.
|
|
|
755
784
|
import { getCache, waitUntil } from "@vercel/functions";
|
|
756
785
|
import { VercelCacheStore } from "@rangojs/router/cache";
|
|
757
786
|
|
|
758
|
-
//
|
|
787
|
+
// No deployment id in the namespace: the store versions its keys itself, so a
|
|
788
|
+
// deploy that does not change a router keeps that router's entries.
|
|
759
789
|
// One handle per process: the store keeps its PPR shell and tag-marker
|
|
760
790
|
// memos per handle.
|
|
761
|
-
const runtimeCache = getCache(
|
|
791
|
+
const runtimeCache = getCache();
|
|
762
792
|
|
|
763
793
|
const router = createRouter({
|
|
764
794
|
document: Document,
|
|
@@ -794,6 +824,12 @@ beyond the per-item cap are dropped with a warning. The official client swallows
|
|
|
794
824
|
`expireTag` failures, so treat `updateTag()` on Vercel as best-effort. See
|
|
795
825
|
`/vercel` for the preset and the remaining options (`version`, `name`, `debug`).
|
|
796
826
|
|
|
827
|
+
`VercelCacheStore` versions its keys by default: families `s` and `i` take the
|
|
828
|
+
router's data version, `r` and `h` its document version, and tag markers
|
|
829
|
+
(`rg:tm:{tag}`) take none. `version` replaces both. To clear the cache on every
|
|
830
|
+
deploy, set `version` to a per-deploy value or keep a deployment-scoped
|
|
831
|
+
`getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID })`.
|
|
832
|
+
|
|
797
833
|
## Cache purity & tainted objects
|
|
798
834
|
|
|
799
835
|
A `cache()` boundary caches everything except loaders, so anything read inside a
|
|
@@ -1083,6 +1119,17 @@ A per-boundary store becomes reachable by `updateTag()`/`revalidateTag()` only
|
|
|
1083
1119
|
once that boundary has been matched in the current process. For data you
|
|
1084
1120
|
invalidate by tag, prefer the app-level store.
|
|
1085
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
|
+
|
|
1086
1133
|
If you implement `SegmentCacheStore` yourself, `invalidateTags(tags)` is called
|
|
1087
1134
|
synchronously inside the invalidating request, and `revalidateTag()` does not
|
|
1088
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
|
},
|
|
@@ -376,6 +376,22 @@ a hard load of the target URL renders the full route.
|
|
|
376
376
|
throwers in the client `ErrorBoundary`; `useFetchLoader` works unchanged
|
|
377
377
|
(route-independent lane; `revalidate()` deliberately not consulted).
|
|
378
378
|
Pinned in the router repo's client-urls hook-probe e2e.
|
|
379
|
+
- **Location state follows the same split as the route hooks.** Inside the
|
|
380
|
+
optimistic branch (the destination and the group's client layouts)
|
|
381
|
+
`useLocationState` reads the state the navigation carries (`<Link state>`,
|
|
382
|
+
`router.push(url, { state })`) from the first optimistic render, before the
|
|
383
|
+
server answers: a product page can show the name its link carried while
|
|
384
|
+
its loader streams. Chrome outside the group, and content a navigation
|
|
385
|
+
keeps on screen (a same-route navigation, a destination that suspends with
|
|
386
|
+
no boundary), keep the committed entry's state until the canonical commit.
|
|
387
|
+
State a redirect carries arrives with that commit (an action's
|
|
388
|
+
`setLocationState` without a redirect is applied when its response
|
|
389
|
+
arrives), and a navigation that is superseded or cancelled leaves none
|
|
390
|
+
behind. A `flash` slot read in the branch is cleared from history once the
|
|
391
|
+
navigation commits, not before. In the branch the value is the object the
|
|
392
|
+
navigation passed; from the commit on it is the entry's copy (a structured
|
|
393
|
+
clone), so an object-valued slot is a new, equal object at that point.
|
|
394
|
+
Do not key an effect on its identity across the two.
|
|
379
395
|
- **Editing the module in dev:** projections refresh via HMR discovery; if a
|
|
380
396
|
route-shape edit ever serves stale routes, restart dev — and if you are
|
|
381
397
|
developing the router itself, rebuild the router dist before `pnpm dev`
|
|
@@ -48,7 +48,8 @@ export default defineConfig({
|
|
|
48
48
|
Worker runs the RSC entry and loads SSR inside the same isolate.
|
|
49
49
|
|
|
50
50
|
`buildEnv: "auto"` gives `Prerender` and `Static` handlers a real `ctx.env` at
|
|
51
|
-
build time through Wrangler's `getPlatformProxy()` (local bindings)
|
|
51
|
+
build time through Wrangler's `getPlatformProxy()` (local bindings), reading
|
|
52
|
+
the wrangler config from the Vite root whatever the working directory. Omit it
|
|
52
53
|
(default `false`) when build-time rendering must not touch bindings; `ctx.env`
|
|
53
54
|
then throws during build rendering. See `/prerender`.
|
|
54
55
|
|
|
@@ -183,6 +184,137 @@ keeps serving a purged shell until `memo.shellMs` passes. Set
|
|
|
183
184
|
user's next request; cross-colo KV propagation (and `tagCacheTtl`) still
|
|
184
185
|
applies. Details: `/caching` → "The fresh-reads cookie".
|
|
185
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
|
+
|
|
186
318
|
## Tracing on Workers
|
|
187
319
|
|
|
188
320
|
`createCloudflareTracing()` from `@rangojs/router/cloudflare` emits the
|
|
@@ -57,7 +57,7 @@ and multi-region composition without replacing its routing or data model.
|
|
|
57
57
|
| Middleware | Global + segment-scoped subtree | Single root Proxy (matcher filters paths); no subtree scope | Request/server-function middleware | [Hono middleware + handler interceptors](https://waku.gg/#interceptors) |
|
|
58
58
|
| Observability | Built-in CF + Vercel OTel phase spans, `Server-Timing`, perf waterfall | Built-in OTel spans; no equivalent router-phase waterfall | Client/data devtools | No equivalent built-in phase model |
|
|
59
59
|
| Deploy targets | Node, Cloudflare, Vercel — all with presets | Node/Vercel first; other platforms through adapters | Broad Vite/Rsbuild runtime support | Node plus adapters |
|
|
60
|
-
| Deployment skew | Automatic
|
|
60
|
+
| Deployment skew | Automatic version handshake for navigation, prefetch, and actions; safe reload on mismatch | `deploymentId` mismatch reload; Vercel can pin clients to an immutable deploy | Application/platform policy | Build-id mismatch reload (v1.0-beta) |
|
|
61
61
|
| Multi-tenant | `createHostRouter()` built-in | Roll your own | Roll your own | Roll your own |
|
|
62
62
|
| Testing primitives | Ships `@rangojs/router/testing` (handlers/loaders/mw/Flight/e2e) | No comparable handler/Flight primitives | Documented patterns; no shipped utils | No comparable handler/Flight primitives |
|
|
63
63
|
| Client runtime | ~50 KB Rango + ~115 KB React/RSC, per-route chunks | Configuration-dependent | Configuration-dependent | Deliberately light |
|
|
@@ -336,8 +336,9 @@ first-class primitive.
|
|
|
336
336
|
|
|
337
337
|
### Deployment skew is detected before stale code executes
|
|
338
338
|
|
|
339
|
-
|
|
340
|
-
|
|
339
|
+
`vite build` computes a document version per `createRouter()` from that router's
|
|
340
|
+
built code (the server code, the SSR output, the client asset file names, `base`,
|
|
341
|
+
and its Prerender payloads) and ships it in the initial payload metadata. The browser returns that version on every partial navigation,
|
|
341
342
|
prefetch, and Server Action. If an old tab reaches a newer server, request
|
|
342
343
|
classification detects the mismatch before resolving the route or executing the
|
|
343
344
|
action and responds with `X-RSC-Reload`. The browser then performs a clean document
|
|
@@ -348,13 +349,16 @@ falling through to a misleading 404.
|
|
|
348
349
|
The protection covers fresh, completed-prefetch, and in-flight-prefetch responses.
|
|
349
350
|
For action requests, the reload returns to the same-origin referrer rather than the
|
|
350
351
|
internal action URL. The cache side follows the same correctness rule:
|
|
351
|
-
`CFCacheStore` automatically
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
352
|
+
`CFCacheStore` and `VercelCacheStore` automatically version their physical keys
|
|
353
|
+
(Cache API and KV keys on Cloudflare): cached RSC data (segment entries, `"use
|
|
354
|
+
cache"` values, loader data) by a data version, a hash of the router's server
|
|
355
|
+
code, and stored HTML (PPR shells, document-cache responses) by the document
|
|
356
|
+
version. A build whose router code changed cannot replay Flight containing an old
|
|
357
|
+
component shape or dead client-chunk reference, and a rebuild of unchanged code
|
|
358
|
+
keeps its cache. Tag invalidation markers carry no version, so `updateTag()`
|
|
359
|
+
reaches entries of every version. The `version` option replaces both versions.
|
|
360
|
+
The browser's Rango state also includes the document version, rotating HTTP and
|
|
361
|
+
in-memory prefetch cache identity on boot.
|
|
358
362
|
|
|
359
363
|
Next.js deserves explicit credit here: its
|
|
360
364
|
[`deploymentId`](https://nextjs.org/docs/app/api-reference/config/next-config-js/deploymentId)
|
|
@@ -587,10 +591,10 @@ dropped rather than warmed. `useLinkStatus()` exposes `{ pending }` for the owni
|
|
|
587
591
|
link.
|
|
588
592
|
|
|
589
593
|
**Rango State ties prefetch and client-cache invalidation together.** It is a
|
|
590
|
-
session-cookie value shaped as `{
|
|
594
|
+
session-cookie value shaped as `{documentVersion}:{invalidationTimestamp}`. Navigation
|
|
591
595
|
and prefetch requests send it as `X-Rango-State`, and RSC responses vary on that
|
|
592
596
|
header, so the browser HTTP cache and Rango's decoded prefetch map share one cache
|
|
593
|
-
identity. A deployment changes the version; a mutation rotates the timestamp. Old
|
|
597
|
+
identity. A deployment that changes the router's code or client assets changes the version; a mutation rotates the timestamp. Old
|
|
594
598
|
responses become unreachable under the retired identity instead of requiring every
|
|
595
599
|
cache layer to delete the same entries successfully.
|
|
596
600
|
|
|
@@ -751,7 +755,7 @@ applications do not have to remember to mark every context manually.
|
|
|
751
755
|
|
|
752
756
|
For a `"use cache"` function, a tainted context object is not serialized wholesale
|
|
753
757
|
into the cache key. Rango instead extracts the safe identity dimensions that make
|
|
754
|
-
the call reusable without collisions: host, route name, pathname, params, response
|
|
758
|
+
the call reusable without collisions: router, host, route name, pathname, params, response
|
|
755
759
|
type, and normalized user-facing search params. A shared cached function called
|
|
756
760
|
from two hosts, route scopes, param sets, or query variants therefore does not
|
|
757
761
|
collapse into one entry just because both calls received a `ctx` object.
|