@rangojs/router 0.0.0-experimental.121 → 0.0.0-experimental.124
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 +7 -2
- package/dist/vite/index.js +47 -6
- package/package.json +61 -21
- package/skills/cache-guide/SKILL.md +8 -6
- package/skills/caching/SKILL.md +148 -1
- package/skills/hooks/SKILL.md +38 -27
- package/skills/host-router/SKILL.md +16 -2
- package/skills/intercept/SKILL.md +4 -2
- package/skills/layout/SKILL.md +11 -6
- package/skills/loader/SKILL.md +6 -2
- package/skills/middleware/SKILL.md +4 -2
- package/skills/migrate-nextjs/SKILL.md +38 -16
- package/skills/parallel/SKILL.md +9 -4
- package/skills/rango/SKILL.md +27 -15
- package/skills/route/SKILL.md +4 -2
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +98 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +89 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +118 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/skills/use-cache/SKILL.md +9 -7
- package/src/browser/action-fence.ts +37 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +14 -1
- package/src/browser/navigation-client.ts +14 -1
- package/src/browser/navigation-store-handle.ts +39 -0
- package/src/browser/navigation-store.ts +26 -12
- package/src/browser/prefetch/fetch.ts +7 -0
- package/src/browser/rango-state.ts +176 -97
- package/src/browser/react/index.ts +0 -6
- package/src/browser/rsc-router.tsx +12 -4
- package/src/browser/server-action-bridge.ts +77 -15
- package/src/browser/types.ts +7 -1
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +95 -1
- package/src/cache/cache-runtime.ts +79 -13
- package/src/cache/cache-scope.ts +55 -4
- package/src/cache/cache-tag.ts +135 -0
- package/src/cache/cf/cf-cache-store.ts +2080 -224
- package/src/cache/cf/index.ts +15 -1
- package/src/cache/document-cache.ts +74 -7
- package/src/cache/index.ts +17 -0
- package/src/cache/memory-segment-store.ts +164 -14
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +27 -0
- package/src/client.rsc.tsx +1 -1
- package/src/client.tsx +0 -6
- package/src/component-utils.ts +19 -0
- package/src/handle.ts +29 -9
- package/src/host/testing.ts +43 -14
- package/src/index.rsc.ts +29 -1
- package/src/index.ts +43 -1
- package/src/loader.rsc.ts +24 -3
- package/src/loader.ts +16 -2
- package/src/prerender.ts +24 -3
- package/src/router/basename.ts +14 -0
- package/src/router/match-handlers.ts +62 -20
- package/src/router/prerender-match.ts +6 -0
- package/src/router/router-interfaces.ts +7 -0
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/loader-cache.ts +8 -17
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router.ts +36 -7
- package/src/rsc/handler.ts +13 -1
- package/src/rsc/helpers.ts +19 -0
- package/src/rsc/progressive-enhancement.ts +2 -0
- package/src/rsc/response-route-handler.ts +8 -1
- package/src/rsc/rsc-rendering.ts +2 -0
- package/src/rsc/types.ts +2 -0
- package/src/runtime-env.ts +18 -0
- package/src/server/cookie-store.ts +52 -1
- package/src/server/request-context.ts +105 -2
- package/src/static-handler.ts +25 -3
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +581 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +110 -0
- package/src/testing/flight-normalize.ts +38 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +234 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +119 -0
- package/src/testing/internal/context.ts +390 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +80 -0
- package/src/testing/render-handler.ts +360 -0
- package/src/testing/render-route.tsx +594 -0
- package/src/testing/run-loader.ts +474 -0
- package/src/testing/run-middleware.ts +231 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +5 -1
- package/src/types/global-namespace.ts +11 -1
- package/src/types/handler-context.ts +16 -5
- package/src/browser/react/use-client-cache.ts +0 -58
package/dist/bin/rango.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
-
var __esm = (fn, res) => function __init() {
|
|
5
|
-
|
|
4
|
+
var __esm = (fn, res, err) => function __init() {
|
|
5
|
+
if (err) throw err[0];
|
|
6
|
+
try {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
} catch (e) {
|
|
9
|
+
throw err = [e], e;
|
|
10
|
+
}
|
|
6
11
|
};
|
|
7
12
|
var __export = (target, all) => {
|
|
8
13
|
for (var name in all)
|
package/dist/vite/index.js
CHANGED
|
@@ -2130,7 +2130,7 @@ import { resolve } from "node:path";
|
|
|
2130
2130
|
// package.json
|
|
2131
2131
|
var package_default = {
|
|
2132
2132
|
name: "@rangojs/router",
|
|
2133
|
-
version: "0.0.0-experimental.
|
|
2133
|
+
version: "0.0.0-experimental.124",
|
|
2134
2134
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
2135
2135
|
keywords: [
|
|
2136
2136
|
"react",
|
|
@@ -2256,6 +2256,31 @@ var package_default = {
|
|
|
2256
2256
|
"./host/testing": {
|
|
2257
2257
|
types: "./src/host/testing.ts",
|
|
2258
2258
|
default: "./src/host/testing.ts"
|
|
2259
|
+
},
|
|
2260
|
+
"./testing": {
|
|
2261
|
+
types: "./src/testing/index.ts",
|
|
2262
|
+
default: "./src/testing/index.ts"
|
|
2263
|
+
},
|
|
2264
|
+
"./testing/vitest": {
|
|
2265
|
+
types: "./src/testing/vitest.ts",
|
|
2266
|
+
default: "./dist/testing/vitest.js"
|
|
2267
|
+
},
|
|
2268
|
+
"./testing/dom": {
|
|
2269
|
+
types: "./src/testing/dom.entry.ts",
|
|
2270
|
+
default: "./src/testing/dom.entry.ts"
|
|
2271
|
+
},
|
|
2272
|
+
"./testing/e2e": {
|
|
2273
|
+
types: "./src/testing/e2e/index.ts",
|
|
2274
|
+
default: "./src/testing/e2e/index.ts"
|
|
2275
|
+
},
|
|
2276
|
+
"./testing/flight": {
|
|
2277
|
+
types: "./src/testing/flight.entry.ts",
|
|
2278
|
+
"react-server": "./src/testing/flight.entry.ts",
|
|
2279
|
+
default: "./src/testing/flight.entry.ts"
|
|
2280
|
+
},
|
|
2281
|
+
"./testing/flight-matchers": {
|
|
2282
|
+
types: "./src/testing/flight-matchers.ts",
|
|
2283
|
+
default: "./src/testing/flight-matchers.ts"
|
|
2259
2284
|
}
|
|
2260
2285
|
},
|
|
2261
2286
|
publishConfig: {
|
|
@@ -2263,14 +2288,15 @@ var package_default = {
|
|
|
2263
2288
|
tag: "experimental"
|
|
2264
2289
|
},
|
|
2265
2290
|
scripts: {
|
|
2266
|
-
build: "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
|
|
2291
|
+
build: "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm dlx esbuild src/testing/vitest.ts --bundle --format=esm --outfile=dist/testing/vitest.js --platform=node --packages=external && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
|
|
2267
2292
|
prepublishOnly: "pnpm build",
|
|
2268
2293
|
typecheck: "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
|
|
2269
2294
|
test: "playwright test",
|
|
2270
2295
|
"test:ui": "playwright test --ui",
|
|
2271
2296
|
"test:hmr-local": "playwright test --project=dev-warmup --project=hmr-routes --project=hmr-basename --project=hmr-prerender --no-deps --workers=1",
|
|
2272
2297
|
"test:unit": "vitest run",
|
|
2273
|
-
"test:unit:watch": "vitest"
|
|
2298
|
+
"test:unit:watch": "vitest",
|
|
2299
|
+
"test:unit:rsc": "vitest run --config vitest.rsc.config.ts"
|
|
2274
2300
|
},
|
|
2275
2301
|
dependencies: {
|
|
2276
2302
|
"@types/debug": "^4.1.12",
|
|
@@ -2278,35 +2304,50 @@ var package_default = {
|
|
|
2278
2304
|
debug: "^4.4.1",
|
|
2279
2305
|
"magic-string": "^0.30.17",
|
|
2280
2306
|
picomatch: "^4.0.3",
|
|
2281
|
-
"rsc-html-stream": "^0.0.7"
|
|
2307
|
+
"rsc-html-stream": "^0.0.7",
|
|
2308
|
+
tinyexec: "^0.3.2"
|
|
2282
2309
|
},
|
|
2283
2310
|
devDependencies: {
|
|
2284
2311
|
"@playwright/test": "^1.49.1",
|
|
2285
2312
|
"@shared/e2e": "workspace:*",
|
|
2313
|
+
"@testing-library/dom": "^10.4.1",
|
|
2314
|
+
"@testing-library/react": "^16.3.2",
|
|
2286
2315
|
"@types/node": "^24.10.1",
|
|
2287
2316
|
"@types/react": "catalog:",
|
|
2288
2317
|
"@types/react-dom": "catalog:",
|
|
2289
2318
|
esbuild: "^0.27.0",
|
|
2319
|
+
"happy-dom": "^20.10.1",
|
|
2290
2320
|
jiti: "^2.6.1",
|
|
2291
2321
|
react: "catalog:",
|
|
2292
2322
|
"react-dom": "catalog:",
|
|
2293
|
-
tinyexec: "^0.3.2",
|
|
2294
2323
|
typescript: "^5.3.0",
|
|
2295
2324
|
vitest: "^4.0.0"
|
|
2296
2325
|
},
|
|
2297
2326
|
peerDependencies: {
|
|
2298
2327
|
"@cloudflare/vite-plugin": "^1.38.0",
|
|
2328
|
+
"@playwright/test": "^1.49.1",
|
|
2329
|
+
"@testing-library/react": ">=16",
|
|
2299
2330
|
"@vitejs/plugin-rsc": "^0.5.26",
|
|
2300
2331
|
react: ">=19.2.6 <20",
|
|
2301
2332
|
"react-dom": ">=19.2.6 <20",
|
|
2302
|
-
vite: "^8.0.0"
|
|
2333
|
+
vite: "^8.0.0",
|
|
2334
|
+
vitest: ">=3"
|
|
2303
2335
|
},
|
|
2304
2336
|
peerDependenciesMeta: {
|
|
2305
2337
|
"@cloudflare/vite-plugin": {
|
|
2306
2338
|
optional: true
|
|
2307
2339
|
},
|
|
2340
|
+
"@playwright/test": {
|
|
2341
|
+
optional: true
|
|
2342
|
+
},
|
|
2343
|
+
"@testing-library/react": {
|
|
2344
|
+
optional: true
|
|
2345
|
+
},
|
|
2308
2346
|
vite: {
|
|
2309
2347
|
optional: true
|
|
2348
|
+
},
|
|
2349
|
+
vitest: {
|
|
2350
|
+
optional: true
|
|
2310
2351
|
}
|
|
2311
2352
|
}
|
|
2312
2353
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.0.0-experimental.
|
|
3
|
+
"version": "0.0.0-experimental.124",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -126,57 +126,97 @@
|
|
|
126
126
|
"./host/testing": {
|
|
127
127
|
"types": "./src/host/testing.ts",
|
|
128
128
|
"default": "./src/host/testing.ts"
|
|
129
|
+
},
|
|
130
|
+
"./testing": {
|
|
131
|
+
"types": "./src/testing/index.ts",
|
|
132
|
+
"default": "./src/testing/index.ts"
|
|
133
|
+
},
|
|
134
|
+
"./testing/vitest": {
|
|
135
|
+
"types": "./src/testing/vitest.ts",
|
|
136
|
+
"default": "./dist/testing/vitest.js"
|
|
137
|
+
},
|
|
138
|
+
"./testing/dom": {
|
|
139
|
+
"types": "./src/testing/dom.entry.ts",
|
|
140
|
+
"default": "./src/testing/dom.entry.ts"
|
|
141
|
+
},
|
|
142
|
+
"./testing/e2e": {
|
|
143
|
+
"types": "./src/testing/e2e/index.ts",
|
|
144
|
+
"default": "./src/testing/e2e/index.ts"
|
|
145
|
+
},
|
|
146
|
+
"./testing/flight": {
|
|
147
|
+
"types": "./src/testing/flight.entry.ts",
|
|
148
|
+
"react-server": "./src/testing/flight.entry.ts",
|
|
149
|
+
"default": "./src/testing/flight.entry.ts"
|
|
150
|
+
},
|
|
151
|
+
"./testing/flight-matchers": {
|
|
152
|
+
"types": "./src/testing/flight-matchers.ts",
|
|
153
|
+
"default": "./src/testing/flight-matchers.ts"
|
|
129
154
|
}
|
|
130
155
|
},
|
|
131
156
|
"publishConfig": {
|
|
132
157
|
"access": "public",
|
|
133
158
|
"tag": "experimental"
|
|
134
159
|
},
|
|
135
|
-
"scripts": {
|
|
136
|
-
"build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
|
|
137
|
-
"prepublishOnly": "pnpm build",
|
|
138
|
-
"typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
|
|
139
|
-
"test": "playwright test",
|
|
140
|
-
"test:ui": "playwright test --ui",
|
|
141
|
-
"test:hmr-local": "playwright test --project=dev-warmup --project=hmr-routes --project=hmr-basename --project=hmr-prerender --no-deps --workers=1",
|
|
142
|
-
"test:unit": "vitest run",
|
|
143
|
-
"test:unit:watch": "vitest"
|
|
144
|
-
},
|
|
145
160
|
"dependencies": {
|
|
146
161
|
"@types/debug": "^4.1.12",
|
|
147
162
|
"@vitejs/plugin-rsc": "^0.5.26",
|
|
148
163
|
"debug": "^4.4.1",
|
|
149
164
|
"magic-string": "^0.30.17",
|
|
150
165
|
"picomatch": "^4.0.3",
|
|
151
|
-
"rsc-html-stream": "^0.0.7"
|
|
166
|
+
"rsc-html-stream": "^0.0.7",
|
|
167
|
+
"tinyexec": "^0.3.2"
|
|
152
168
|
},
|
|
153
169
|
"devDependencies": {
|
|
154
170
|
"@playwright/test": "^1.49.1",
|
|
155
|
-
"@
|
|
171
|
+
"@testing-library/dom": "^10.4.1",
|
|
172
|
+
"@testing-library/react": "^16.3.2",
|
|
156
173
|
"@types/node": "^24.10.1",
|
|
157
|
-
"@types/react": "
|
|
158
|
-
"@types/react-dom": "
|
|
174
|
+
"@types/react": "^19.2.7",
|
|
175
|
+
"@types/react-dom": "^19.2.3",
|
|
159
176
|
"esbuild": "^0.27.0",
|
|
177
|
+
"happy-dom": "^20.10.1",
|
|
160
178
|
"jiti": "^2.6.1",
|
|
161
|
-
"react": "
|
|
162
|
-
"react-dom": "
|
|
163
|
-
"tinyexec": "^0.3.2",
|
|
179
|
+
"react": "^19.2.6",
|
|
180
|
+
"react-dom": "^19.2.6",
|
|
164
181
|
"typescript": "^5.3.0",
|
|
165
|
-
"vitest": "^4.0.0"
|
|
182
|
+
"vitest": "^4.0.0",
|
|
183
|
+
"@shared/e2e": "0.0.1"
|
|
166
184
|
},
|
|
167
185
|
"peerDependencies": {
|
|
168
186
|
"@cloudflare/vite-plugin": "^1.38.0",
|
|
187
|
+
"@playwright/test": "^1.49.1",
|
|
188
|
+
"@testing-library/react": ">=16",
|
|
169
189
|
"@vitejs/plugin-rsc": "^0.5.26",
|
|
170
190
|
"react": ">=19.2.6 <20",
|
|
171
191
|
"react-dom": ">=19.2.6 <20",
|
|
172
|
-
"vite": "^8.0.0"
|
|
192
|
+
"vite": "^8.0.0",
|
|
193
|
+
"vitest": ">=3"
|
|
173
194
|
},
|
|
174
195
|
"peerDependenciesMeta": {
|
|
175
196
|
"@cloudflare/vite-plugin": {
|
|
176
197
|
"optional": true
|
|
177
198
|
},
|
|
199
|
+
"@playwright/test": {
|
|
200
|
+
"optional": true
|
|
201
|
+
},
|
|
202
|
+
"@testing-library/react": {
|
|
203
|
+
"optional": true
|
|
204
|
+
},
|
|
178
205
|
"vite": {
|
|
179
206
|
"optional": true
|
|
207
|
+
},
|
|
208
|
+
"vitest": {
|
|
209
|
+
"optional": true
|
|
180
210
|
}
|
|
211
|
+
},
|
|
212
|
+
"scripts": {
|
|
213
|
+
"build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm dlx esbuild src/testing/vitest.ts --bundle --format=esm --outfile=dist/testing/vitest.js --platform=node --packages=external && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
|
|
214
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
|
|
215
|
+
"test": "playwright test",
|
|
216
|
+
"test:ui": "playwright test --ui",
|
|
217
|
+
"test:hmr-local": "playwright test --project=dev-warmup --project=hmr-routes --project=hmr-basename --project=hmr-prerender --no-deps --workers=1",
|
|
218
|
+
"test:unit": "vitest run",
|
|
219
|
+
"test:unit:watch": "vitest",
|
|
220
|
+
"test:unit:rsc": "vitest run --config vitest.rsc.config.ts"
|
|
181
221
|
}
|
|
182
|
-
}
|
|
222
|
+
}
|
|
@@ -7,8 +7,9 @@ argument-hint:
|
|
|
7
7
|
# cache() vs "use cache" — When to Use Which
|
|
8
8
|
|
|
9
9
|
Both mechanisms share the same backing store and cache profiles, and both accept
|
|
10
|
-
an optional `tags` field (
|
|
11
|
-
below). They differ in scope, cache
|
|
10
|
+
an optional `tags` field (honored by the built-in stores — invalidate with
|
|
11
|
+
`updateTag`/`revalidateTag`; see "Two axes" below). They differ in scope, cache
|
|
12
|
+
key, execution model, and runtime control.
|
|
12
13
|
|
|
13
14
|
## Two axes — do not conflate
|
|
14
15
|
|
|
@@ -18,10 +19,11 @@ caching:
|
|
|
18
19
|
|
|
19
20
|
1. **Stored-value freshness** — _is a cached value still good?_
|
|
20
21
|
→ `"use cache"` (fn/component), `cache()` (segment), loader `cache()` (loader data).
|
|
21
|
-
Entries expire by **TTL/SWR
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
Entries expire by **TTL/SWR** and can be tagged (`cache({ tags })` or runtime
|
|
23
|
+
`cacheTag(...tags)`). Built-in stores (`MemorySegmentCacheStore`, `CFCacheStore`)
|
|
24
|
+
index by tag; invalidate on demand with `updateTag(...tags)` (awaitable,
|
|
25
|
+
read-your-own-writes) or `revalidateTag(...tags)` (background, non-blocking).
|
|
26
|
+
Both hard-purge; the difference is awaitability, not stale-serving.
|
|
25
27
|
2. **Client-update selection** — _should this segment re-run and stream to the
|
|
26
28
|
client on this navigation/action?_
|
|
27
29
|
→ `revalidate()`. Covered in `/loader` and `/route`, **not here**.
|
package/skills/caching/SKILL.md
CHANGED
|
@@ -81,6 +81,78 @@ cache(
|
|
|
81
81
|
);
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
## Tag-Based Invalidation
|
|
85
|
+
|
|
86
|
+
Tag cached entries, then invalidate them on demand. Tags can be attached three ways:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// 1. Static tags in the cache() DSL
|
|
90
|
+
cache({ ttl: 300, tags: ["products"] }, () => [path("/products", List)]);
|
|
91
|
+
|
|
92
|
+
// 2. Dynamic tags (function of ctx)
|
|
93
|
+
cache(
|
|
94
|
+
{ ttl: 300, tags: (ctx) => [`product:${ctx.params.id}`, "products"] },
|
|
95
|
+
() => [path("/products/:id", Detail)],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// 3. Runtime tags inside a "use cache" function
|
|
99
|
+
async function getProduct(id: string) {
|
|
100
|
+
"use cache";
|
|
101
|
+
cacheTag(`product:${id}`, "products"); // variadic, additive
|
|
102
|
+
return db.getProduct(id);
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Invalidate with one of two server-only verbs (both variadic, imported from
|
|
107
|
+
`@rangojs/router`):
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// Server Action — read-your-own-writes. Await it so the action's own re-render
|
|
111
|
+
// (and the next navigation) sees fresh data.
|
|
112
|
+
async function updateProduct(formData: FormData) {
|
|
113
|
+
"use server";
|
|
114
|
+
await db.updateProduct(formData);
|
|
115
|
+
await updateTag("products");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Route handler / webhook — background, non-blocking (waitUntil). Hard-purge:
|
|
119
|
+
// the next read re-renders fresh (NOT stale-while-revalidate).
|
|
120
|
+
export async function POST() {
|
|
121
|
+
"use server";
|
|
122
|
+
revalidateTag("products");
|
|
123
|
+
return new Response("ok");
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
| API | Timing | Use in | Semantics |
|
|
128
|
+
| ------------------------ | --------------------------- | ------------------------- | ----------------------------------------------------- |
|
|
129
|
+
| `updateTag(...tags)` | awaitable (`Promise<void>`) | server actions | immediate; next read is fresh |
|
|
130
|
+
| `revalidateTag(...tags)` | background (`void`) | route handlers / webhooks | background (non-blocking); next read re-renders fresh |
|
|
131
|
+
|
|
132
|
+
Both built-in stores support tags. For `CFCacheStore`, distributed (cross-colo)
|
|
133
|
+
invalidation requires a `kv` namespace — the tag-invalidation markers live in
|
|
134
|
+
that same namespace; there is **no** separate tag-invalidation store to wire.
|
|
135
|
+
If no tag-capable store is configured, `updateTag`/`revalidateTag` warn and no-op.
|
|
136
|
+
|
|
137
|
+
By default `CFCacheStore` reads the KV marker on every tagged cache read
|
|
138
|
+
(strongest invalidation latency). To cut KV reads on hot tagged routes, set
|
|
139
|
+
`tagCacheTtl` (seconds) to cache each marker in the per-colo edge cache for that
|
|
140
|
+
window — the colo running `updateTag`/`revalidateTag` writes the fresh marker
|
|
141
|
+
into its own edge cache immediately (read-your-own-writes), while other colos
|
|
142
|
+
converge within `tagCacheTtl` (the **maximum extra cross-colo invalidation
|
|
143
|
+
latency** when no purge is wired). Keep it small (e.g. 30–60), or wire a purge
|
|
144
|
+
(below) and set it large. (Contrast `tagInvalidationTtl`, which must be _large_
|
|
145
|
+
— it bounds how long the KV marker itself lives and must exceed your max entry
|
|
146
|
+
TTL+SWR.)
|
|
147
|
+
|
|
148
|
+
To make other colos prompt without a short `tagCacheTtl`, pass `onRevalidateTag`:
|
|
149
|
+
each cached marker carries a namespaced Cloudflare `Cache-Tag`, and the hook is
|
|
150
|
+
handed exactly those tags (batched, once per `updateTag`/`revalidateTag` call) to
|
|
151
|
+
feed Cloudflare's purge-by-tag API — evicting the cached lookups everywhere.
|
|
152
|
+
Purge-by-tag is available on all plans (since April 2025), subject to per-plan
|
|
153
|
+
rate limits, so the batched single call matters. With a purge wired, `tagCacheTtl`
|
|
154
|
+
becomes a pure read-cost reducer + fallback window.
|
|
155
|
+
|
|
84
156
|
## Named Profile Shorthand
|
|
85
157
|
|
|
86
158
|
Use a named cache profile string instead of an options object. The profile must be
|
|
@@ -212,6 +284,80 @@ const router = createRouter<AppBindings>({
|
|
|
212
284
|
KV entries require `expirationTtl >= 60s`. Short-lived entries (< 60s total TTL)
|
|
213
285
|
are only cached in L1.
|
|
214
286
|
|
|
287
|
+
### Resilience & latency budgets
|
|
288
|
+
|
|
289
|
+
Every cache read is **fail-safe**: a degraded tier never stalls or fails the
|
|
290
|
+
request — it degrades to the next tier (L1 → L2 → render). Three optional latency
|
|
291
|
+
budgets (milliseconds) bound each tier so a slow colo or KV namespace cannot pin
|
|
292
|
+
a request behind it:
|
|
293
|
+
|
|
294
|
+
| Option | Default | Bounds |
|
|
295
|
+
| --------------------- | ------- | ----------------------------------- |
|
|
296
|
+
| `edgeLookupTimeoutMs` | `10` | L1 `cache.match` (the lookup) |
|
|
297
|
+
| `edgeReadTimeoutMs` | `20` | L1 body read (CF streams it lazily) |
|
|
298
|
+
| `kvReadTimeoutMs` | `170` | L2 / KV read |
|
|
299
|
+
|
|
300
|
+
Set any to `0` (or a negative value) to disable that budget and always await the
|
|
301
|
+
read. A non-finite value (e.g. `Number(env.UNSET)`) falls back to the default.
|
|
302
|
+
The tag-invalidation marker reads inherit these same budgets and **fail open** on
|
|
303
|
+
a KV timeout — the entry is served rather than wrongly treated as invalidated.
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
new CFCacheStore({
|
|
307
|
+
ctx,
|
|
308
|
+
kv: env.CACHE_KV,
|
|
309
|
+
defaults: { ttl: 60, swr: 300 },
|
|
310
|
+
// Raise a budget only if your HEALTHY reads legitimately run slower (large
|
|
311
|
+
// Flight payloads, far-from-colo regions); measure the p99 first. These are
|
|
312
|
+
// degradation guard-rails, not tuning levers for "slow is normal here".
|
|
313
|
+
kvReadTimeoutMs: 250,
|
|
314
|
+
});
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Failure handling, by kind — none of these fail the request:
|
|
318
|
+
|
|
319
|
+
| Failure | Behavior |
|
|
320
|
+
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
321
|
+
| Transient read error (5xx/blip) | Degrade to the next tier; entry left intact |
|
|
322
|
+
| Read budget exceeded (timeout) | Abandon the read, degrade to the next tier |
|
|
323
|
+
| Corrupt / unparseable L1 entry | Reported corrupt; degrade to L2 (served if present). The L1 entry is evicted ONLY when L2 has no copy — so the evict can't race the L2→L1 promote |
|
|
324
|
+
| Corrupt / unparseable KV entry | Reported corrupt; evicted (self-heal) + render (no tier below it) |
|
|
325
|
+
| Write failure | No-op (entry simply not cached); never throws |
|
|
326
|
+
|
|
327
|
+
Each is surfaced to the router's `onError` callback (phase `"cache"`, with
|
|
328
|
+
`metadata.category` one of `cache-read`, `cache-corrupt`, `cache-write`,
|
|
329
|
+
`cache-delete`, `cache-invalidate`, `stale-revalidation`) so you can observe
|
|
330
|
+
cache health without affecting users.
|
|
331
|
+
|
|
332
|
+
### Validating cache behavior with `debug`
|
|
333
|
+
|
|
334
|
+
Pass `debug` to emit one structured event per L1 read — use it to confirm on a
|
|
335
|
+
real deployment (via `wrangler tail`) that the store behaves as expected before
|
|
336
|
+
relying on it. It is intended for validation, not steady-state production.
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
new CFCacheStore({
|
|
340
|
+
ctx,
|
|
341
|
+
kv: env.CACHE_KV,
|
|
342
|
+
debug: true, // logs each CFCacheReadDebugEvent to the console
|
|
343
|
+
// ...or capture programmatically:
|
|
344
|
+
// debug: (event) => myTelemetry.record(event),
|
|
345
|
+
});
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Each event reports which tier answered and why (`outcome`: `l1-fresh`,
|
|
349
|
+
`l1-stale-revalidate`, `l1-revalidating-guarded`, `match-timeout`, `match-error`,
|
|
350
|
+
`body-timeout`, `body-error`, `non-200`, `tag-invalidated`, `l1-miss`, `kv-fresh`,
|
|
351
|
+
`kv-stale`, `kv-stale-suppressed`, `kv-miss`, `kv-timeout`, `error`), the
|
|
352
|
+
staleness / revalidating timestamps, and the measured per-tier durations:
|
|
353
|
+
`matchMs` (the L1 `match`), `markerMs` (the tag-marker resolution tail for a
|
|
354
|
+
tagged entry, between `matchMs` and `bodyReadMs`; absent or 0 for an untagged
|
|
355
|
+
entry or a per-request memo hit), and `bodyReadMs` (the L1 body read). A
|
|
356
|
+
persistently large `markerMs` signals a degraded KV namespace; on a healthy
|
|
357
|
+
deployment KV keeps markers hot in its per-colo edge cache, so it stays a few
|
|
358
|
+
milliseconds. `match-error` (a transient `cache.match` rejection that falls
|
|
359
|
+
through to L2) is kept distinct from a plain `l1-miss`.
|
|
360
|
+
|
|
215
361
|
## Cache purity & tainted objects
|
|
216
362
|
|
|
217
363
|
A `cache()` boundary caches everything except loaders, so anything read inside a
|
|
@@ -325,6 +471,7 @@ cache({ store: checkoutCache }, () => [
|
|
|
325
471
|
```typescript
|
|
326
472
|
import { urls } from "@rangojs/router";
|
|
327
473
|
import { MemorySegmentCacheStore } from "@rangojs/router/cache";
|
|
474
|
+
import * as CartActions from "./actions/cart";
|
|
328
475
|
|
|
329
476
|
// Custom store for checkout (short TTL)
|
|
330
477
|
const checkoutCache = new MemorySegmentCacheStore({
|
|
@@ -353,7 +500,7 @@ export const urlpatterns = urls(({ path, layout, cache, loader, revalidate }) =>
|
|
|
353
500
|
path("/shop/product/:slug", ProductPage, { name: "product" }, () => [
|
|
354
501
|
loader(ProductLoader, () => [cache({ ttl: 120 })]),
|
|
355
502
|
loader(CartLoader, () => [
|
|
356
|
-
revalidate((
|
|
503
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
357
504
|
]),
|
|
358
505
|
]),
|
|
359
506
|
]),
|
package/skills/hooks/SKILL.md
CHANGED
|
@@ -683,33 +683,44 @@ ProductState.delete();
|
|
|
683
683
|
| `.write()` | yes (replace this slot) | no | throws |
|
|
684
684
|
| `.delete()` | yes (remove this slot) | no | throws |
|
|
685
685
|
|
|
686
|
-
## Cache
|
|
686
|
+
## Cache Control
|
|
687
687
|
|
|
688
|
-
###
|
|
688
|
+
### invalidateClientCache()
|
|
689
689
|
|
|
690
|
-
|
|
690
|
+
Force the client's caches to miss after a mutation the router can't see (a REST
|
|
691
|
+
call, a WebSocket push, a login). It is a plain function, not a hook, so it works
|
|
692
|
+
from module-level callbacks too. Imported from the root entry `@rangojs/router`,
|
|
693
|
+
it is selected by export conditions: in a client component it marks the caches
|
|
694
|
+
stale immediately; from a handler/server component it writes a rotated
|
|
695
|
+
`Set-Cookie` for the responding client.
|
|
691
696
|
|
|
692
697
|
```tsx
|
|
693
698
|
"use client";
|
|
694
|
-
import {
|
|
699
|
+
import { invalidateClientCache } from "@rangojs/router";
|
|
695
700
|
|
|
696
701
|
function SaveButton() {
|
|
697
|
-
const { clear } = useClientCache();
|
|
698
|
-
|
|
699
702
|
const handleSave = async () => {
|
|
700
703
|
await fetch("/api/data", {
|
|
701
704
|
method: "POST",
|
|
702
705
|
body: JSON.stringify(data),
|
|
703
706
|
});
|
|
704
707
|
|
|
705
|
-
// Invalidate
|
|
706
|
-
|
|
708
|
+
// Invalidate the client's caches after the mutation
|
|
709
|
+
invalidateClientCache();
|
|
707
710
|
};
|
|
708
711
|
|
|
709
712
|
return <button onClick={handleSave}>Save</button>;
|
|
710
713
|
}
|
|
711
714
|
```
|
|
712
715
|
|
|
716
|
+
A module-level subscription works the same way (no component needed):
|
|
717
|
+
|
|
718
|
+
```ts
|
|
719
|
+
import { invalidateClientCache } from "@rangojs/router";
|
|
720
|
+
|
|
721
|
+
socket.on("catalog-updated", () => invalidateClientCache());
|
|
722
|
+
```
|
|
723
|
+
|
|
713
724
|
**Use cases**: REST API mutations, WebSocket updates, non-RSC data changes.
|
|
714
725
|
|
|
715
726
|
## Outlet Components
|
|
@@ -892,22 +903,22 @@ See `/links` for the full URL generation guide. `ctx.reverse()` is server-only;
|
|
|
892
903
|
|
|
893
904
|
## Hook Summary
|
|
894
905
|
|
|
895
|
-
| Hook
|
|
896
|
-
|
|
|
897
|
-
| `useParams()`
|
|
898
|
-
| `usePathname()`
|
|
899
|
-
| `useSearchParams()`
|
|
900
|
-
| `useHref()`
|
|
901
|
-
| `useMount()`
|
|
902
|
-
| `useReverse()`
|
|
903
|
-
| `useNavigation()`
|
|
904
|
-
| `useRouter()`
|
|
905
|
-
| `useSegments()`
|
|
906
|
-
| `useLinkStatus()`
|
|
907
|
-
| `useLoader()`
|
|
908
|
-
| `useFetchLoader()`
|
|
909
|
-
| `useRefreshLoaders()`
|
|
910
|
-
| `useHandle()`
|
|
911
|
-
| `useAction()`
|
|
912
|
-
| `useLocationState()`
|
|
913
|
-
| `
|
|
906
|
+
| Hook | Purpose | Returns |
|
|
907
|
+
| ------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------ |
|
|
908
|
+
| `useParams()` | Route params | `Readonly<T>` (default `Record<string, string>`) or selected value |
|
|
909
|
+
| `usePathname()` | Current pathname | `string` |
|
|
910
|
+
| `useSearchParams()` | URL search params | `ReadonlyURLSearchParams` |
|
|
911
|
+
| `useHref()` | Mount-aware href | `(path) => string` |
|
|
912
|
+
| `useMount()` | Current include() mount path | `string` |
|
|
913
|
+
| `useReverse()` | Local reverse for imported routes | `(name, params?, search?) => string` |
|
|
914
|
+
| `useNavigation()` | Reactive navigation state | state, location, isStreaming |
|
|
915
|
+
| `useRouter()` | Stable router actions | push, replace, refresh, prefetch, back, forward |
|
|
916
|
+
| `useSegments()` | URL path & segment IDs | path, segmentIds, location |
|
|
917
|
+
| `useLinkStatus()` | Link pending state | { pending } |
|
|
918
|
+
| `useLoader()` | Loader data (strict) | data, isLoading, error |
|
|
919
|
+
| `useFetchLoader()` | Loader with on-demand fetch | data, load, isLoading |
|
|
920
|
+
| `useRefreshLoaders()` | Refresh cross-loader group(s) | `() => (groups: string \| string[]) => Promise<void>` |
|
|
921
|
+
| `useHandle()` | Accumulated handle data | T (handle type) |
|
|
922
|
+
| `useAction()` | Server action state | state, error, result |
|
|
923
|
+
| `useLocationState()` | History state (persists or flash) | T \| undefined |
|
|
924
|
+
| `invalidateClientCache()` | Force client caches to miss (function, not a hook; root entry) | `void` |
|
|
@@ -189,12 +189,26 @@ Logs pattern matching, route registration, and cookie override decisions to cons
|
|
|
189
189
|
## Testing
|
|
190
190
|
|
|
191
191
|
```typescript
|
|
192
|
-
import {
|
|
192
|
+
import {
|
|
193
|
+
createTestRequest,
|
|
194
|
+
testPattern,
|
|
195
|
+
matchesHost,
|
|
196
|
+
} from "@rangojs/router/host/testing";
|
|
193
197
|
|
|
194
|
-
// Test pattern matching
|
|
198
|
+
// Test pattern matching (host-only)
|
|
195
199
|
testPattern("admin.*", "admin.example.com"); // true
|
|
196
200
|
testPattern([".", "www.*"], "example.com"); // true
|
|
197
201
|
|
|
202
|
+
// Path-based patterns need the third pathname arg (defaults to "/", so a
|
|
203
|
+
// host-only pattern still works with two args):
|
|
204
|
+
testPattern("**.workers.dev/admin", "foo.workers.dev", "/admin"); // true
|
|
205
|
+
|
|
206
|
+
// Or match a pattern against a real Request (hostname + pathname from the URL):
|
|
207
|
+
matchesHost(
|
|
208
|
+
"**.workers.dev/admin",
|
|
209
|
+
new Request("https://foo.workers.dev/admin"),
|
|
210
|
+
); // true
|
|
211
|
+
|
|
198
212
|
// Create requests for integration tests
|
|
199
213
|
const request = createTestRequest({
|
|
200
214
|
host: "admin.example.com",
|
|
@@ -107,8 +107,10 @@ Use named revalidation contracts on both the outer producer and the intercept
|
|
|
107
107
|
consumer when they share `ctx.set()` data:
|
|
108
108
|
|
|
109
109
|
```typescript
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
import * as ProductActions from "./actions/product";
|
|
111
|
+
|
|
112
|
+
export const revalidateProductShell = (ctx) =>
|
|
113
|
+
ctx.isAction(ProductActions) || undefined;
|
|
112
114
|
|
|
113
115
|
layout(ProductLayout, () => [
|
|
114
116
|
revalidate(revalidateProductShell), // producer reruns
|