@rangojs/router 0.0.0-experimental.2
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/CLAUDE.md +7 -0
- package/README.md +19 -0
- package/dist/vite/index.js +1298 -0
- package/package.json +140 -0
- package/skills/caching/SKILL.md +319 -0
- package/skills/document-cache/SKILL.md +152 -0
- package/skills/hooks/SKILL.md +359 -0
- package/skills/intercept/SKILL.md +292 -0
- package/skills/layout/SKILL.md +216 -0
- package/skills/loader/SKILL.md +365 -0
- package/skills/middleware/SKILL.md +442 -0
- package/skills/parallel/SKILL.md +255 -0
- package/skills/route/SKILL.md +141 -0
- package/skills/router-setup/SKILL.md +403 -0
- package/skills/theme/SKILL.md +54 -0
- package/skills/typesafety/SKILL.md +352 -0
- package/src/__mocks__/version.ts +6 -0
- package/src/__tests__/component-utils.test.ts +76 -0
- package/src/__tests__/route-definition.test.ts +63 -0
- package/src/__tests__/urls.test.tsx +436 -0
- package/src/browser/event-controller.ts +876 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/link-interceptor.ts +121 -0
- package/src/browser/lru-cache.ts +69 -0
- package/src/browser/merge-segment-loaders.ts +126 -0
- package/src/browser/navigation-bridge.ts +893 -0
- package/src/browser/navigation-client.ts +162 -0
- package/src/browser/navigation-store.ts +823 -0
- package/src/browser/partial-update.ts +559 -0
- package/src/browser/react/Link.tsx +248 -0
- package/src/browser/react/NavigationProvider.tsx +275 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +53 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +120 -0
- package/src/browser/react/location-state.ts +62 -0
- package/src/browser/react/use-action.ts +240 -0
- package/src/browser/react/use-client-cache.ts +56 -0
- package/src/browser/react/use-handle.ts +178 -0
- package/src/browser/react/use-href.tsx +208 -0
- package/src/browser/react/use-link-status.ts +134 -0
- package/src/browser/react/use-navigation.ts +150 -0
- package/src/browser/react/use-segments.ts +188 -0
- package/src/browser/request-controller.ts +164 -0
- package/src/browser/rsc-router.tsx +353 -0
- package/src/browser/scroll-restoration.ts +324 -0
- package/src/browser/server-action-bridge.ts +747 -0
- package/src/browser/shallow.ts +35 -0
- package/src/browser/types.ts +464 -0
- package/src/cache/__tests__/document-cache.test.ts +522 -0
- package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
- package/src/cache/__tests__/memory-store.test.ts +484 -0
- package/src/cache/cache-scope.ts +565 -0
- package/src/cache/cf/__tests__/cf-cache-store.test.ts +428 -0
- package/src/cache/cf/cf-cache-store.ts +428 -0
- package/src/cache/cf/index.ts +19 -0
- package/src/cache/document-cache.ts +340 -0
- package/src/cache/index.ts +58 -0
- package/src/cache/memory-segment-store.ts +150 -0
- package/src/cache/memory-store.ts +253 -0
- package/src/cache/types.ts +387 -0
- package/src/client.rsc.tsx +88 -0
- package/src/client.tsx +621 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +23 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +259 -0
- package/src/handle.ts +120 -0
- package/src/handles/MetaTags.tsx +193 -0
- package/src/handles/index.ts +6 -0
- package/src/handles/meta.ts +247 -0
- package/src/href-client.ts +128 -0
- package/src/href-context.ts +33 -0
- package/src/href.ts +177 -0
- package/src/index.rsc.ts +79 -0
- package/src/index.ts +87 -0
- package/src/loader.rsc.ts +204 -0
- package/src/loader.ts +47 -0
- package/src/network-error-thrower.tsx +21 -0
- package/src/outlet-context.ts +15 -0
- package/src/root-error-boundary.tsx +277 -0
- package/src/route-content-wrapper.tsx +198 -0
- package/src/route-definition.ts +1371 -0
- package/src/route-map-builder.ts +146 -0
- package/src/route-types.ts +198 -0
- package/src/route-utils.ts +89 -0
- package/src/router/__tests__/match-context.test.ts +104 -0
- package/src/router/__tests__/match-pipelines.test.ts +537 -0
- package/src/router/__tests__/match-result.test.ts +566 -0
- package/src/router/__tests__/on-error.test.ts +935 -0
- package/src/router/__tests__/pattern-matching.test.ts +577 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/handler-context.ts +158 -0
- package/src/router/loader-resolution.ts +326 -0
- package/src/router/manifest.ts +138 -0
- package/src/router/match-context.ts +264 -0
- package/src/router/match-middleware/background-revalidation.ts +236 -0
- package/src/router/match-middleware/cache-lookup.ts +261 -0
- package/src/router/match-middleware/cache-store.ts +266 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +268 -0
- package/src/router/match-middleware/segment-resolution.ts +174 -0
- package/src/router/match-pipelines.ts +214 -0
- package/src/router/match-result.ts +214 -0
- package/src/router/metrics.ts +62 -0
- package/src/router/middleware.test.ts +1355 -0
- package/src/router/middleware.ts +748 -0
- package/src/router/pattern-matching.ts +272 -0
- package/src/router/revalidation.ts +190 -0
- package/src/router/router-context.ts +299 -0
- package/src/router/types.ts +96 -0
- package/src/router.ts +3876 -0
- package/src/rsc/__tests__/helpers.test.ts +175 -0
- package/src/rsc/handler.ts +1060 -0
- package/src/rsc/helpers.ts +64 -0
- package/src/rsc/index.ts +56 -0
- package/src/rsc/nonce.ts +18 -0
- package/src/rsc/types.ts +237 -0
- package/src/segment-system.tsx +456 -0
- package/src/server/__tests__/request-context.test.ts +171 -0
- package/src/server/context.ts +417 -0
- package/src/server/handle-store.ts +230 -0
- package/src/server/loader-registry.ts +174 -0
- package/src/server/request-context.ts +554 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +146 -0
- package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
- package/src/ssr/index.tsx +234 -0
- package/src/theme/ThemeProvider.tsx +291 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/__tests__/theme.test.ts +120 -0
- package/src/theme/constants.ts +55 -0
- package/src/theme/index.ts +58 -0
- package/src/theme/theme-context.ts +70 -0
- package/src/theme/theme-script.ts +152 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types.ts +1561 -0
- package/src/urls.ts +726 -0
- package/src/use-loader.tsx +346 -0
- package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
- package/src/vite/expose-action-id.ts +344 -0
- package/src/vite/expose-handle-id.ts +209 -0
- package/src/vite/expose-loader-id.ts +357 -0
- package/src/vite/expose-location-state-id.ts +177 -0
- package/src/vite/index.ts +787 -0
- package/src/vite/package-resolution.ts +125 -0
- package/src/vite/version.d.ts +12 -0
- package/src/vite/virtual-entries.ts +109 -0
package/package.json
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rangojs/router",
|
|
3
|
+
"version": "0.0.0-experimental.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Django-inspired RSC router with composable URL patterns",
|
|
6
|
+
"author": "Ivo Todorov",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/ivogt/vite-rsc.git",
|
|
11
|
+
"directory": "packages/rangojs-router"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/ivogt/vite-rsc#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/ivogt/vite-rsc/issues"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public",
|
|
19
|
+
"tag": "experimental"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"react",
|
|
23
|
+
"rsc",
|
|
24
|
+
"react-server-components",
|
|
25
|
+
"router",
|
|
26
|
+
"vite"
|
|
27
|
+
],
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"react-server": "./src/index.rsc.ts",
|
|
31
|
+
"types": "./src/index.ts",
|
|
32
|
+
"default": "./src/index.ts"
|
|
33
|
+
},
|
|
34
|
+
"./server": {
|
|
35
|
+
"types": "./src/server.ts",
|
|
36
|
+
"import": "./src/server.ts"
|
|
37
|
+
},
|
|
38
|
+
"./client": {
|
|
39
|
+
"react-server": "./src/client.rsc.tsx",
|
|
40
|
+
"types": "./src/client.tsx",
|
|
41
|
+
"default": "./src/client.tsx"
|
|
42
|
+
},
|
|
43
|
+
"./browser": {
|
|
44
|
+
"types": "./src/browser/index.ts",
|
|
45
|
+
"default": "./src/browser/index.ts"
|
|
46
|
+
},
|
|
47
|
+
"./ssr": {
|
|
48
|
+
"types": "./src/ssr/index.tsx",
|
|
49
|
+
"default": "./src/ssr/index.tsx"
|
|
50
|
+
},
|
|
51
|
+
"./rsc": {
|
|
52
|
+
"react-server": "./src/rsc/index.ts",
|
|
53
|
+
"types": "./src/rsc/index.ts",
|
|
54
|
+
"default": "./src/rsc/index.ts"
|
|
55
|
+
},
|
|
56
|
+
"./vite": {
|
|
57
|
+
"types": "./src/vite/index.ts",
|
|
58
|
+
"import": "./dist/vite/index.js"
|
|
59
|
+
},
|
|
60
|
+
"./types": {
|
|
61
|
+
"types": "./src/vite/version.d.ts"
|
|
62
|
+
},
|
|
63
|
+
"./internal/deps/browser": {
|
|
64
|
+
"types": "./src/deps/browser.ts",
|
|
65
|
+
"default": "./src/deps/browser.ts"
|
|
66
|
+
},
|
|
67
|
+
"./internal/deps/ssr": {
|
|
68
|
+
"types": "./src/deps/ssr.ts",
|
|
69
|
+
"default": "./src/deps/ssr.ts"
|
|
70
|
+
},
|
|
71
|
+
"./internal/deps/rsc": {
|
|
72
|
+
"react-server": "./src/deps/rsc.ts",
|
|
73
|
+
"types": "./src/deps/rsc.ts",
|
|
74
|
+
"default": "./src/deps/rsc.ts"
|
|
75
|
+
},
|
|
76
|
+
"./internal/deps/html-stream-client": {
|
|
77
|
+
"types": "./src/deps/html-stream-client.ts",
|
|
78
|
+
"default": "./src/deps/html-stream-client.ts"
|
|
79
|
+
},
|
|
80
|
+
"./internal/deps/html-stream-server": {
|
|
81
|
+
"types": "./src/deps/html-stream-server.ts",
|
|
82
|
+
"default": "./src/deps/html-stream-server.ts"
|
|
83
|
+
},
|
|
84
|
+
"./cache": {
|
|
85
|
+
"react-server": "./src/cache/index.ts",
|
|
86
|
+
"types": "./src/cache/index.ts",
|
|
87
|
+
"default": "./src/cache/index.ts"
|
|
88
|
+
},
|
|
89
|
+
"./theme": {
|
|
90
|
+
"types": "./src/theme/index.ts",
|
|
91
|
+
"default": "./src/theme/index.ts"
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"files": [
|
|
95
|
+
"src",
|
|
96
|
+
"dist",
|
|
97
|
+
"skills",
|
|
98
|
+
"CLAUDE.md",
|
|
99
|
+
"README.md"
|
|
100
|
+
],
|
|
101
|
+
"peerDependencies": {
|
|
102
|
+
"@cloudflare/vite-plugin": "^1.21.0",
|
|
103
|
+
"@vitejs/plugin-rsc": "^0.5.14",
|
|
104
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
105
|
+
"vite": "^7.3.0"
|
|
106
|
+
},
|
|
107
|
+
"peerDependenciesMeta": {
|
|
108
|
+
"@cloudflare/vite-plugin": {
|
|
109
|
+
"optional": true
|
|
110
|
+
},
|
|
111
|
+
"vite": {
|
|
112
|
+
"optional": true
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
"dependencies": {
|
|
116
|
+
"@vitejs/plugin-rsc": "^0.5.14",
|
|
117
|
+
"magic-string": "^0.30.17",
|
|
118
|
+
"rsc-html-stream": "^0.0.7"
|
|
119
|
+
},
|
|
120
|
+
"devDependencies": {
|
|
121
|
+
"@playwright/test": "^1.49.1",
|
|
122
|
+
"@types/node": "^24.10.1",
|
|
123
|
+
"@types/react": "^19.2.7",
|
|
124
|
+
"@types/react-dom": "^19.2.3",
|
|
125
|
+
"react": "^19.2.1",
|
|
126
|
+
"react-dom": "^19.2.1",
|
|
127
|
+
"esbuild": "^0.27.0",
|
|
128
|
+
"tinyexec": "^0.3.2",
|
|
129
|
+
"typescript": "^5.3.0",
|
|
130
|
+
"vitest": "^2.1.8"
|
|
131
|
+
},
|
|
132
|
+
"scripts": {
|
|
133
|
+
"build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external",
|
|
134
|
+
"typecheck": "tsc --noEmit",
|
|
135
|
+
"test": "playwright test",
|
|
136
|
+
"test:ui": "playwright test --ui",
|
|
137
|
+
"test:unit": "vitest run",
|
|
138
|
+
"test:unit:watch": "vitest"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: caching
|
|
3
|
+
description: Configure segment caching with memory or Cloudflare KV stores in rsc-router
|
|
4
|
+
argument-hint: [setup]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Caching
|
|
8
|
+
|
|
9
|
+
rsc-router supports segment-level caching with stale-while-revalidate (SWR) for optimal performance.
|
|
10
|
+
|
|
11
|
+
## Router Cache Configuration
|
|
12
|
+
|
|
13
|
+
Configure caching in `createRSCRouter`:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { createRSCRouter } from "rsc-router/server";
|
|
17
|
+
import { MemorySegmentCacheStore } from "rsc-router/cache";
|
|
18
|
+
|
|
19
|
+
const store = new MemorySegmentCacheStore({
|
|
20
|
+
defaults: { ttl: 60, swr: 300 }
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const router = createRSCRouter<AppEnv>({
|
|
24
|
+
document: Document,
|
|
25
|
+
cache: {
|
|
26
|
+
store,
|
|
27
|
+
enabled: true,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Dynamic Cache Configuration
|
|
33
|
+
|
|
34
|
+
Use a factory function for environment-based config:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
const router = createRSCRouter<AppEnv>({
|
|
38
|
+
document: Document,
|
|
39
|
+
cache: (env) => ({
|
|
40
|
+
store: new CFCacheStore({
|
|
41
|
+
baseUrl: env.Bindings.CACHE_URL,
|
|
42
|
+
waitUntil: (fn) => env.ctx.waitUntil(fn),
|
|
43
|
+
}),
|
|
44
|
+
enabled: env.Bindings.CACHE_ENABLED === "true",
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Cache Stores
|
|
50
|
+
|
|
51
|
+
### Memory Store (Development)
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { MemorySegmentCacheStore } from "rsc-router/cache";
|
|
55
|
+
|
|
56
|
+
const store = new MemorySegmentCacheStore({
|
|
57
|
+
defaults: { ttl: 60, swr: 300 }
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Debug cache stats
|
|
61
|
+
console.log(store.getStats());
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Features:
|
|
65
|
+
- Survives HMR in development
|
|
66
|
+
- No true SWR (entries expire at TTL)
|
|
67
|
+
- Good for development and single-instance deployments
|
|
68
|
+
|
|
69
|
+
### Cloudflare Cache Store (Production)
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { CFCacheStore } from "rsc-router/cache/cf";
|
|
73
|
+
|
|
74
|
+
const store = new CFCacheStore({
|
|
75
|
+
namespace: "rsc-cache", // Optional, uses caches.default
|
|
76
|
+
baseUrl: "https://cache.example.com/",
|
|
77
|
+
defaults: { ttl: 3600, swr: 7200 },
|
|
78
|
+
waitUntil: (fn) => ctx.waitUntil(fn),
|
|
79
|
+
version: "1.0.0", // For cache invalidation on deploy
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Features:
|
|
84
|
+
- Cloudflare Cache API integration
|
|
85
|
+
- True SWR with atomic revalidation (prevents thundering herd)
|
|
86
|
+
- Non-blocking writes via `waitUntil`
|
|
87
|
+
- Version-based cache invalidation
|
|
88
|
+
|
|
89
|
+
## Cache Options
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
interface CacheOptions {
|
|
93
|
+
// Time-to-live in seconds
|
|
94
|
+
ttl: number;
|
|
95
|
+
|
|
96
|
+
// Stale-while-revalidate window (seconds after TTL)
|
|
97
|
+
swr?: number;
|
|
98
|
+
|
|
99
|
+
// Override cache store for this boundary
|
|
100
|
+
store?: SegmentCacheStore;
|
|
101
|
+
|
|
102
|
+
// Conditional caching
|
|
103
|
+
condition?: (ctx) => boolean;
|
|
104
|
+
|
|
105
|
+
// Custom cache key
|
|
106
|
+
key?: (ctx) => string | Promise<string>;
|
|
107
|
+
|
|
108
|
+
// Tags for invalidation
|
|
109
|
+
tags?: string[] | ((ctx) => string[]);
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Route-Level Caching
|
|
114
|
+
|
|
115
|
+
Use `cache()` in handlers to set cache boundaries:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import { map } from "rsc-router/server";
|
|
119
|
+
|
|
120
|
+
export default map<typeof routes>(({ route, layout, cache }) => [
|
|
121
|
+
// Cache entire layout and children
|
|
122
|
+
cache({ ttl: 3600, swr: 7200 }, () => [
|
|
123
|
+
layout(<StaticLayout />, () => [
|
|
124
|
+
route("about", AboutPage),
|
|
125
|
+
route("contact", ContactPage),
|
|
126
|
+
]),
|
|
127
|
+
]),
|
|
128
|
+
|
|
129
|
+
// Different cache settings per route
|
|
130
|
+
cache({ ttl: 60 }, () => [
|
|
131
|
+
route("dashboard", DashboardPage),
|
|
132
|
+
]),
|
|
133
|
+
|
|
134
|
+
// No caching (default)
|
|
135
|
+
route("checkout", CheckoutPage),
|
|
136
|
+
]);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Conditional Caching
|
|
140
|
+
|
|
141
|
+
Skip cache based on request context:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
cache({
|
|
145
|
+
ttl: 3600,
|
|
146
|
+
condition: (ctx) => {
|
|
147
|
+
// Don't cache for authenticated users
|
|
148
|
+
const hasAuth = ctx.request.headers.has("Authorization");
|
|
149
|
+
return !hasAuth;
|
|
150
|
+
},
|
|
151
|
+
}, () => [
|
|
152
|
+
route("products", ProductList),
|
|
153
|
+
])
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Custom Cache Keys
|
|
157
|
+
|
|
158
|
+
Override the default cache key:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
cache({
|
|
162
|
+
ttl: 3600,
|
|
163
|
+
key: (ctx) => {
|
|
164
|
+
// Include user segment in cache key
|
|
165
|
+
const segment = ctx.request.headers.get("x-user-segment") || "default";
|
|
166
|
+
return `${segment}:products:${ctx.params.category}`;
|
|
167
|
+
},
|
|
168
|
+
}, () => [
|
|
169
|
+
route("products.category", CategoryPage),
|
|
170
|
+
])
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Store-level key modification:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
const store = new CFCacheStore({
|
|
177
|
+
keyGenerator: (ctx, defaultKey) => {
|
|
178
|
+
const region = ctx.request.cf?.colo || "unknown";
|
|
179
|
+
return `${region}:${defaultKey}`;
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Stale-While-Revalidate (SWR)
|
|
185
|
+
|
|
186
|
+
SWR serves stale content while fetching fresh data in the background:
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
Timeline:
|
|
190
|
+
├─ 0 to TTL ─────────┤ Fresh content served
|
|
191
|
+
├─ TTL to TTL+SWR ───┤ Stale content served, revalidation in background
|
|
192
|
+
├─ After TTL+SWR ────┤ Cache expired, new fetch required
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
cache({
|
|
197
|
+
ttl: 60, // Fresh for 60 seconds
|
|
198
|
+
swr: 300, // Stale-but-usable for 5 more minutes
|
|
199
|
+
}, () => [
|
|
200
|
+
route("feed", FeedPage),
|
|
201
|
+
])
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Multi-Store Setup
|
|
205
|
+
|
|
206
|
+
Use different stores for different data patterns:
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
const hotStore = new MemorySegmentCacheStore({
|
|
210
|
+
defaults: { ttl: 10 }
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const coldStore = new CFCacheStore({
|
|
214
|
+
defaults: { ttl: 3600, swr: 7200 }
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
export default map<typeof routes>(({ route, cache }) => [
|
|
218
|
+
// Frequently changing data - short TTL, memory
|
|
219
|
+
cache({ store: hotStore, ttl: 10 }, () => [
|
|
220
|
+
route("dashboard", DashboardPage),
|
|
221
|
+
route("notifications", NotificationsPage),
|
|
222
|
+
]),
|
|
223
|
+
|
|
224
|
+
// Rarely changing data - long TTL, edge cache
|
|
225
|
+
cache({ store: coldStore, ttl: 3600 }, () => [
|
|
226
|
+
route("archive", ArchivePage),
|
|
227
|
+
route("docs", DocsPage),
|
|
228
|
+
]),
|
|
229
|
+
]);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Cache Invalidation
|
|
233
|
+
|
|
234
|
+
### Version-Based (Cloudflare)
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
import packageJson from "./package.json";
|
|
238
|
+
|
|
239
|
+
const store = new CFCacheStore({
|
|
240
|
+
version: packageJson.version, // All cache invalidated on deploy
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Tag-Based (Future)
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
cache({
|
|
248
|
+
tags: (ctx) => [`product:${ctx.params.id}`, "products"],
|
|
249
|
+
}, () => [
|
|
250
|
+
route("products.detail", ProductDetail),
|
|
251
|
+
])
|
|
252
|
+
|
|
253
|
+
// Invalidate by tag
|
|
254
|
+
await store.invalidateTag("products");
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Complete Production Example
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
// router.tsx
|
|
261
|
+
import { createRSCRouter } from "rsc-router/server";
|
|
262
|
+
import { CFCacheStore } from "rsc-router/cache/cf";
|
|
263
|
+
import packageJson from "./package.json";
|
|
264
|
+
|
|
265
|
+
const router = createRSCRouter<AppEnv>({
|
|
266
|
+
document: Document,
|
|
267
|
+
cache: (env) => ({
|
|
268
|
+
store: new CFCacheStore({
|
|
269
|
+
baseUrl: "https://rsc-cache.internal/",
|
|
270
|
+
defaults: { ttl: 300, swr: 3600 },
|
|
271
|
+
waitUntil: (fn) => env.ctx.waitUntil(fn),
|
|
272
|
+
version: packageJson.version,
|
|
273
|
+
}),
|
|
274
|
+
enabled: env.Bindings.NODE_ENV === "production",
|
|
275
|
+
}),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// handlers/shop.tsx
|
|
279
|
+
export default map<typeof shopRoutes>(({ route, layout, cache }) => [
|
|
280
|
+
// Static pages - aggressive caching
|
|
281
|
+
cache({ ttl: 86400, swr: 604800 }, () => [
|
|
282
|
+
route("shop.about", AboutPage),
|
|
283
|
+
route("shop.terms", TermsPage),
|
|
284
|
+
]),
|
|
285
|
+
|
|
286
|
+
// Product listing - moderate caching
|
|
287
|
+
cache({ ttl: 300, swr: 3600 }, () => [
|
|
288
|
+
route("shop.products", ProductList),
|
|
289
|
+
]),
|
|
290
|
+
|
|
291
|
+
// Product detail - cache with product-specific key
|
|
292
|
+
cache({
|
|
293
|
+
ttl: 600,
|
|
294
|
+
swr: 3600,
|
|
295
|
+
key: (ctx) => `product:${ctx.params.slug}`,
|
|
296
|
+
}, () => [
|
|
297
|
+
route("shop.product", ProductDetail),
|
|
298
|
+
]),
|
|
299
|
+
|
|
300
|
+
// User-specific pages - no caching
|
|
301
|
+
route("shop.cart", CartPage),
|
|
302
|
+
route("shop.checkout", CheckoutPage),
|
|
303
|
+
]);
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Cache Headers (Cloudflare)
|
|
307
|
+
|
|
308
|
+
CFCacheStore sets these headers for debugging:
|
|
309
|
+
|
|
310
|
+
- `x-edge-cache-status`: `HIT` or `REVALIDATING`
|
|
311
|
+
- `x-edge-cache-stale-at`: Timestamp when entry becomes stale
|
|
312
|
+
|
|
313
|
+
## What Gets Cached
|
|
314
|
+
|
|
315
|
+
- Route segments (components, layouts, loading states)
|
|
316
|
+
- Handle data (breadcrumbs, metadata)
|
|
317
|
+
- **Not cached by default**: Loader data (fetched fresh each request)
|
|
318
|
+
|
|
319
|
+
To cache loader results, use loader-level caching or external caching strategies.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: document-cache
|
|
3
|
+
description: Cache full HTTP responses at the edge with Cache-Control headers
|
|
4
|
+
argument-hint: [setup]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Document Cache
|
|
8
|
+
|
|
9
|
+
Caches complete HTTP responses (HTML/RSC) at the edge based on Cache-Control headers. Routes opt-in by setting `s-maxage`.
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
Add middleware to router:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { createRSCRouter, createDocumentCacheMiddleware } from "@rangojs/router/server";
|
|
17
|
+
import { CFCacheStore } from "@rangojs/router/cache/cf";
|
|
18
|
+
|
|
19
|
+
const router = createRSCRouter<AppEnv>({
|
|
20
|
+
document: Document,
|
|
21
|
+
cache: (env) => ({
|
|
22
|
+
store: new CFCacheStore({ ctx: env.ctx }),
|
|
23
|
+
}),
|
|
24
|
+
})
|
|
25
|
+
.use(createDocumentCacheMiddleware())
|
|
26
|
+
.routes(routes);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Route Opt-In
|
|
30
|
+
|
|
31
|
+
Routes opt-in by setting `Cache-Control` with `s-maxage`:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
route("home", (ctx) => {
|
|
35
|
+
ctx.headers.set("Cache-Control", "s-maxage=60, stale-while-revalidate=300");
|
|
36
|
+
return <HomePage />;
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Middleware Options
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
createDocumentCacheMiddleware({
|
|
44
|
+
// Skip specific paths
|
|
45
|
+
skipPaths: ["/api", "/admin"],
|
|
46
|
+
|
|
47
|
+
// Custom cache key
|
|
48
|
+
keyGenerator: (url) => url.pathname,
|
|
49
|
+
|
|
50
|
+
// Conditional caching
|
|
51
|
+
isEnabled: (ctx) => !ctx.request.headers.has("x-preview"),
|
|
52
|
+
|
|
53
|
+
// Debug logging
|
|
54
|
+
debug: true,
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## How It Works
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
Request → Check Cache
|
|
62
|
+
↓
|
|
63
|
+
┌──────┴──────┐
|
|
64
|
+
│ │
|
|
65
|
+
HIT MISS
|
|
66
|
+
│ │
|
|
67
|
+
↓ ↓
|
|
68
|
+
Fresh? Run handler
|
|
69
|
+
│ │
|
|
70
|
+
Yes → Return Has s-maxage?
|
|
71
|
+
│ │
|
|
72
|
+
No (stale) Yes → Cache + Return
|
|
73
|
+
│ │
|
|
74
|
+
↓ No → Return (no cache)
|
|
75
|
+
Return stale,
|
|
76
|
+
revalidate in
|
|
77
|
+
background (SWR)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Cache Status Header
|
|
81
|
+
|
|
82
|
+
Response includes `x-document-cache-status`:
|
|
83
|
+
- `HIT` - Fresh cache hit
|
|
84
|
+
- `STALE` - Served stale, revalidating in background
|
|
85
|
+
- `MISS` - Cache miss, response was generated fresh
|
|
86
|
+
|
|
87
|
+
## Cache Key Generation
|
|
88
|
+
|
|
89
|
+
Default keys differentiate:
|
|
90
|
+
- HTML requests: `{pathname}:html`
|
|
91
|
+
- RSC partials: `{pathname}:{segmentHash}:rsc`
|
|
92
|
+
|
|
93
|
+
Segment hash ensures different cached responses for navigations from different source pages (with different layouts).
|
|
94
|
+
|
|
95
|
+
## What Gets Cached
|
|
96
|
+
|
|
97
|
+
- Full HTML responses (document requests)
|
|
98
|
+
- RSC payloads (client navigation)
|
|
99
|
+
- Only 200 OK responses with `s-maxage`
|
|
100
|
+
|
|
101
|
+
## What's NOT Cached
|
|
102
|
+
|
|
103
|
+
- Server actions (`_rsc_action`)
|
|
104
|
+
- Loader requests (`_rsc_loader`)
|
|
105
|
+
- Responses without `s-maxage`
|
|
106
|
+
- Non-200 responses
|
|
107
|
+
|
|
108
|
+
## Complete Example
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
// router.tsx
|
|
112
|
+
const router = createRSCRouter<AppEnv>({
|
|
113
|
+
document: Document,
|
|
114
|
+
cache: (env) => ({
|
|
115
|
+
store: new CFCacheStore({ ctx: env.ctx }),
|
|
116
|
+
}),
|
|
117
|
+
})
|
|
118
|
+
.use(createDocumentCacheMiddleware({
|
|
119
|
+
skipPaths: ["/api"],
|
|
120
|
+
debug: process.env.NODE_ENV === "development",
|
|
121
|
+
}))
|
|
122
|
+
.routes(routes);
|
|
123
|
+
|
|
124
|
+
// handlers.tsx
|
|
125
|
+
route("blog", (ctx) => {
|
|
126
|
+
// Cache for 5 min, serve stale for 1 hour while revalidating
|
|
127
|
+
ctx.headers.set("Cache-Control", "s-maxage=300, stale-while-revalidate=3600");
|
|
128
|
+
return <BlogIndex />;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
route("blog.post", (ctx) => {
|
|
132
|
+
// Long cache for individual posts
|
|
133
|
+
ctx.headers.set("Cache-Control", "s-maxage=3600, stale-while-revalidate=86400");
|
|
134
|
+
return <BlogPost slug={ctx.params.slug} />;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
route("dashboard", (ctx) => {
|
|
138
|
+
// No cache header = not cached
|
|
139
|
+
return <Dashboard />;
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Document Cache vs Segment Cache
|
|
144
|
+
|
|
145
|
+
| Feature | Document Cache | Segment Cache |
|
|
146
|
+
|---------|---------------|---------------|
|
|
147
|
+
| Granularity | Full response | Individual segments |
|
|
148
|
+
| Opt-in | `s-maxage` header | `cache()` DSL |
|
|
149
|
+
| Use case | Static pages | Dynamic compositions |
|
|
150
|
+
| Key includes | URL + segment hash | Route params |
|
|
151
|
+
|
|
152
|
+
Use document cache for mostly-static pages. Use segment cache when different parts of a page have different cache requirements.
|