@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
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document-Level Cache Middleware
|
|
3
|
+
*
|
|
4
|
+
* Caches full HTTP responses at the edge based on Cache-Control headers.
|
|
5
|
+
* Routes opt-in to caching by setting s-maxage or stale-while-revalidate headers.
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. Check cache for existing response
|
|
9
|
+
* 2. If fresh hit → return cached response
|
|
10
|
+
* 3. If stale hit (within SWR window) → return cached, revalidate in background
|
|
11
|
+
* 4. If miss → run handler, cache if response has cache headers
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { MiddlewareFn, MiddlewareContext } from "../router/middleware.js";
|
|
15
|
+
import { getRequestContext } from "../server/request-context.js";
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Constants
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
/** Header indicating cache status for debugging */
|
|
22
|
+
const CACHE_STATUS_HEADER = "x-document-cache-status";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Simple hash function for segment IDs.
|
|
26
|
+
* Creates a short, deterministic hash to differentiate cache keys
|
|
27
|
+
* based on which segments the client already has.
|
|
28
|
+
*/
|
|
29
|
+
function hashSegmentIds(segmentIds: string): string {
|
|
30
|
+
if (!segmentIds) return "";
|
|
31
|
+
|
|
32
|
+
let hash = 0;
|
|
33
|
+
for (let i = 0; i < segmentIds.length; i++) {
|
|
34
|
+
const char = segmentIds.charCodeAt(i);
|
|
35
|
+
hash = ((hash << 5) - hash + char) | 0;
|
|
36
|
+
}
|
|
37
|
+
// Convert to base36 for shorter string, take absolute value
|
|
38
|
+
return Math.abs(hash).toString(36);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Cache Control Parsing
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
interface CacheDirectives {
|
|
46
|
+
sMaxAge?: number;
|
|
47
|
+
staleWhileRevalidate?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parse Cache-Control header for s-maxage and stale-while-revalidate directives
|
|
52
|
+
*/
|
|
53
|
+
function parseCacheControl(header: string | null): CacheDirectives | null {
|
|
54
|
+
if (!header) return null;
|
|
55
|
+
|
|
56
|
+
const directives: CacheDirectives = {};
|
|
57
|
+
|
|
58
|
+
// Parse s-maxage
|
|
59
|
+
const sMaxAgeMatch = header.match(/s-maxage\s*=\s*(\d+)/i);
|
|
60
|
+
if (sMaxAgeMatch) {
|
|
61
|
+
directives.sMaxAge = parseInt(sMaxAgeMatch[1], 10);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Parse stale-while-revalidate
|
|
65
|
+
const swrMatch = header.match(/stale-while-revalidate\s*=\s*(\d+)/i);
|
|
66
|
+
if (swrMatch) {
|
|
67
|
+
directives.staleWhileRevalidate = parseInt(swrMatch[1], 10);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Only return if we have at least s-maxage (required for document caching)
|
|
71
|
+
if (directives.sMaxAge !== undefined) {
|
|
72
|
+
return directives;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if response should be cached based on Cache-Control headers
|
|
80
|
+
*/
|
|
81
|
+
function shouldCacheResponse(response: Response): CacheDirectives | null {
|
|
82
|
+
// Only cache successful responses
|
|
83
|
+
if (response.status !== 200) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const cacheControl = response.headers.get("Cache-Control");
|
|
88
|
+
return parseCacheControl(cacheControl);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ============================================================================
|
|
92
|
+
// Response Helpers
|
|
93
|
+
// ============================================================================
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Add cache status header to response for debugging
|
|
97
|
+
*/
|
|
98
|
+
function addCacheStatusHeader(
|
|
99
|
+
response: Response,
|
|
100
|
+
status: "HIT" | "STALE" | "MISS",
|
|
101
|
+
): Response {
|
|
102
|
+
const headers = new Headers(response.headers);
|
|
103
|
+
headers.set(CACHE_STATUS_HEADER, status);
|
|
104
|
+
|
|
105
|
+
return new Response(response.body, {
|
|
106
|
+
status: response.status,
|
|
107
|
+
statusText: response.statusText,
|
|
108
|
+
headers,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Run onResponse callbacks registered on the request context
|
|
114
|
+
*/
|
|
115
|
+
function runOnResponseCallbacks(
|
|
116
|
+
response: Response,
|
|
117
|
+
callbacks: Array<(response: Response) => Response>,
|
|
118
|
+
): Response {
|
|
119
|
+
let result = response;
|
|
120
|
+
for (const callback of callbacks) {
|
|
121
|
+
result = callback(result);
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ============================================================================
|
|
127
|
+
// Document Cache Middleware
|
|
128
|
+
// ============================================================================
|
|
129
|
+
|
|
130
|
+
export interface DocumentCacheOptions<TEnv = any> {
|
|
131
|
+
/**
|
|
132
|
+
* Skip caching for specific paths (e.g., API routes)
|
|
133
|
+
*/
|
|
134
|
+
skipPaths?: string[];
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Custom cache key generator
|
|
138
|
+
*/
|
|
139
|
+
keyGenerator?: (url: URL) => string;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Callback to determine if caching should be enabled for this request.
|
|
143
|
+
* Receives the middleware context and returns true to enable caching.
|
|
144
|
+
* If not provided, caching is enabled by default.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```typescript
|
|
148
|
+
* createDocumentCacheMiddleware({
|
|
149
|
+
* isEnabled: (ctx) => !ctx.request.headers.has('x-skip-cache'),
|
|
150
|
+
* })
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
isEnabled?: (ctx: MiddlewareContext<TEnv>) => boolean | Promise<boolean>;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Enable debug logging for cache operations.
|
|
157
|
+
* Logs HIT, MISS, STALE, and REVALIDATED events.
|
|
158
|
+
* Defaults to false.
|
|
159
|
+
*/
|
|
160
|
+
debug?: boolean;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Create document cache middleware
|
|
165
|
+
*
|
|
166
|
+
* Add this middleware to your router to enable document-level caching.
|
|
167
|
+
* It uses the cache store's getResponse/putResponse methods for caching.
|
|
168
|
+
* Routes opt-in by setting Cache-Control headers with s-maxage.
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```typescript
|
|
172
|
+
* // Add middleware to router
|
|
173
|
+
* const router = createRouter<AppEnv>()
|
|
174
|
+
* .use(createDocumentCacheMiddleware({
|
|
175
|
+
* isEnabled: (ctx) => ctx.url.pathname !== '/admin',
|
|
176
|
+
* }))
|
|
177
|
+
* .route("home", (ctx) => {
|
|
178
|
+
* ctx.headers.set("Cache-Control", "s-maxage=60, stale-while-revalidate=300");
|
|
179
|
+
* return <HomePage />;
|
|
180
|
+
* });
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
export function createDocumentCacheMiddleware<TEnv = any>(
|
|
184
|
+
options: DocumentCacheOptions<TEnv> = {},
|
|
185
|
+
): MiddlewareFn<TEnv> {
|
|
186
|
+
const { skipPaths = [], keyGenerator, isEnabled, debug = false } = options;
|
|
187
|
+
|
|
188
|
+
const log = debug
|
|
189
|
+
? (message: string) => console.log(message)
|
|
190
|
+
: () => {};
|
|
191
|
+
|
|
192
|
+
return async function documentCacheMiddleware(
|
|
193
|
+
ctx: MiddlewareContext<TEnv>,
|
|
194
|
+
next: () => Promise<Response>,
|
|
195
|
+
): Promise<Response> {
|
|
196
|
+
const url = ctx.url;
|
|
197
|
+
|
|
198
|
+
// Skip RSC action requests (mutations shouldn't be cached)
|
|
199
|
+
if (url.searchParams.has("_rsc_action")) {
|
|
200
|
+
return next();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Skip loader requests (have their own caching)
|
|
204
|
+
if (url.searchParams.has("_rsc_loader")) {
|
|
205
|
+
return next();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Skip configured paths
|
|
209
|
+
if (skipPaths.some((path) => url.pathname.startsWith(path))) {
|
|
210
|
+
return next();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check if caching is enabled for this request
|
|
214
|
+
if (isEnabled) {
|
|
215
|
+
const enabled = await isEnabled(ctx);
|
|
216
|
+
if (!enabled) {
|
|
217
|
+
return next();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Get request context and cache store
|
|
222
|
+
const requestCtx = getRequestContext();
|
|
223
|
+
const store = requestCtx?._cacheStore;
|
|
224
|
+
|
|
225
|
+
// Skip if no cache store or store doesn't support response caching
|
|
226
|
+
if (!store?.getResponse || !store?.putResponse) {
|
|
227
|
+
return next();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Determine request type for cache key differentiation
|
|
231
|
+
const isPartial = url.searchParams.has("_rsc_partial");
|
|
232
|
+
const typeLabel = isPartial ? "RSC" : "HTML";
|
|
233
|
+
|
|
234
|
+
// Generate cache key
|
|
235
|
+
// For partial requests, include hash of client segments to prevent serving
|
|
236
|
+
// wrong cached response when navigating from different pages with different layouts
|
|
237
|
+
const clientSegments = url.searchParams.get("_rsc_segments") || "";
|
|
238
|
+
const segmentHash = isPartial && clientSegments ? `:${hashSegmentIds(clientSegments)}` : "";
|
|
239
|
+
const typeSuffix = isPartial ? ":rsc" : ":html";
|
|
240
|
+
const cacheKey = keyGenerator
|
|
241
|
+
? keyGenerator(url) + segmentHash + typeSuffix
|
|
242
|
+
: `${url.pathname}${segmentHash}${typeSuffix}`;
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
// 1. Check cache
|
|
246
|
+
const cached = await store.getResponse(cacheKey);
|
|
247
|
+
|
|
248
|
+
if (cached && cached.response.status === 200) {
|
|
249
|
+
if (!cached.shouldRevalidate) {
|
|
250
|
+
// Fresh hit - return immediately
|
|
251
|
+
log(`[DocumentCache] HIT ${typeLabel}: ${url.pathname}`);
|
|
252
|
+
let response = addCacheStatusHeader(cached.response, "HIT");
|
|
253
|
+
// Run onResponse callbacks even for cache hits
|
|
254
|
+
if (requestCtx && requestCtx._onResponseCallbacks.length > 0) {
|
|
255
|
+
response = runOnResponseCallbacks(
|
|
256
|
+
response,
|
|
257
|
+
requestCtx._onResponseCallbacks,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
return response;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Stale hit - return cached response, revalidate in background
|
|
264
|
+
log(`[DocumentCache] STALE ${typeLabel}: ${url.pathname} (revalidating)`);
|
|
265
|
+
|
|
266
|
+
if (requestCtx) {
|
|
267
|
+
requestCtx.waitUntil(async () => {
|
|
268
|
+
try {
|
|
269
|
+
const fresh = await next();
|
|
270
|
+
const directives = shouldCacheResponse(fresh);
|
|
271
|
+
|
|
272
|
+
if (directives) {
|
|
273
|
+
await store.putResponse!(
|
|
274
|
+
cacheKey,
|
|
275
|
+
fresh,
|
|
276
|
+
directives.sMaxAge!,
|
|
277
|
+
directives.staleWhileRevalidate,
|
|
278
|
+
);
|
|
279
|
+
log(`[DocumentCache] REVALIDATED ${typeLabel}: ${url.pathname}`);
|
|
280
|
+
}
|
|
281
|
+
} catch (error) {
|
|
282
|
+
console.error(`[DocumentCache] Revalidation failed:`, error);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let response = addCacheStatusHeader(cached.response, "STALE");
|
|
288
|
+
// Run onResponse callbacks even for stale cache hits
|
|
289
|
+
if (requestCtx && requestCtx._onResponseCallbacks.length > 0) {
|
|
290
|
+
response = runOnResponseCallbacks(
|
|
291
|
+
response,
|
|
292
|
+
requestCtx._onResponseCallbacks,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
return response;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 2. Cache miss - run handler
|
|
299
|
+
const originalResponse = await next();
|
|
300
|
+
|
|
301
|
+
// 3. Cache if response has appropriate headers
|
|
302
|
+
const directives = shouldCacheResponse(originalResponse);
|
|
303
|
+
|
|
304
|
+
if (directives) {
|
|
305
|
+
log(`[DocumentCache] MISS ${typeLabel}: ${url.pathname} (caching with s-maxage=${directives.sMaxAge})`);
|
|
306
|
+
|
|
307
|
+
// Tee the body so we can return one stream and cache the other
|
|
308
|
+
const [returnStream, cacheStream] = originalResponse.body!.tee();
|
|
309
|
+
|
|
310
|
+
// Clone response for caching (non-blocking)
|
|
311
|
+
if (requestCtx) {
|
|
312
|
+
requestCtx.waitUntil(async () => {
|
|
313
|
+
try {
|
|
314
|
+
await store.putResponse!(
|
|
315
|
+
cacheKey,
|
|
316
|
+
new Response(cacheStream, originalResponse),
|
|
317
|
+
directives.sMaxAge!,
|
|
318
|
+
directives.staleWhileRevalidate,
|
|
319
|
+
);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
console.error(`[DocumentCache] Cache write failed:`, error);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return addCacheStatusHeader(
|
|
327
|
+
new Response(returnStream, originalResponse),
|
|
328
|
+
"MISS",
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// No cache headers - pass through
|
|
333
|
+
return originalResponse;
|
|
334
|
+
} catch (error) {
|
|
335
|
+
console.error(`[DocumentCache] Error:`, error);
|
|
336
|
+
// On any cache error, fall through to handler
|
|
337
|
+
return next();
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Store
|
|
3
|
+
*
|
|
4
|
+
* Server-side caching for RSC segments and loader data.
|
|
5
|
+
*
|
|
6
|
+
* Main exports for users:
|
|
7
|
+
* - SegmentCacheStore - Interface for implementing custom cache stores
|
|
8
|
+
* - MemorySegmentCacheStore - In-memory cache for development/testing
|
|
9
|
+
* - CFCacheStore - Cloudflare edge cache store for production
|
|
10
|
+
* - CacheScope / createCacheScope - Request-scoped cache provider
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Generic cache store types (reserved for future extensibility)
|
|
14
|
+
// These types support caching arbitrary values like Response, Stream, etc.
|
|
15
|
+
// Currently unused - segment caching uses SegmentCacheStore directly.
|
|
16
|
+
export type {
|
|
17
|
+
CacheStore,
|
|
18
|
+
CacheEntry,
|
|
19
|
+
CacheValue,
|
|
20
|
+
CacheValueType,
|
|
21
|
+
CachePutOptions,
|
|
22
|
+
CacheMetadata,
|
|
23
|
+
} from "./types.js";
|
|
24
|
+
|
|
25
|
+
// Generic memory cache (reserved for future extensibility)
|
|
26
|
+
export { MemoryCacheStore } from "./memory-store.js";
|
|
27
|
+
|
|
28
|
+
// Segment cache store types and implementations
|
|
29
|
+
export type {
|
|
30
|
+
SegmentCacheStore,
|
|
31
|
+
SegmentCacheProvider,
|
|
32
|
+
CachedEntryData,
|
|
33
|
+
CachedEntryResult,
|
|
34
|
+
CacheGetResult,
|
|
35
|
+
SerializedSegmentData,
|
|
36
|
+
SegmentHandleData,
|
|
37
|
+
CacheConfig,
|
|
38
|
+
CacheConfigOrFactory,
|
|
39
|
+
} from "./types.js";
|
|
40
|
+
|
|
41
|
+
export { MemorySegmentCacheStore } from "./memory-segment-store.js";
|
|
42
|
+
|
|
43
|
+
// Cloudflare cache store
|
|
44
|
+
export {
|
|
45
|
+
CFCacheStore,
|
|
46
|
+
type CFCacheStoreOptions,
|
|
47
|
+
CACHE_STALE_AT_HEADER,
|
|
48
|
+
CACHE_STATUS_HEADER,
|
|
49
|
+
} from "./cf/index.js";
|
|
50
|
+
|
|
51
|
+
// Cache scope
|
|
52
|
+
export { CacheScope, createCacheScope } from "./cache-scope.js";
|
|
53
|
+
|
|
54
|
+
// Document-level cache middleware
|
|
55
|
+
export {
|
|
56
|
+
createDocumentCacheMiddleware,
|
|
57
|
+
type DocumentCacheOptions,
|
|
58
|
+
} from "./document-cache.js";
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-Memory Segment Cache Store
|
|
3
|
+
*
|
|
4
|
+
* Simple in-memory implementation of SegmentCacheStore.
|
|
5
|
+
* Uses globalThis to survive HMR in development.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { SegmentCacheStore, CachedEntryData, CacheDefaults, CacheGetResult } from "./types.js";
|
|
9
|
+
import type { RequestContext } from "../server/request-context.js";
|
|
10
|
+
|
|
11
|
+
const CACHE_GLOBAL_KEY = "__rsc_router_segment_cache_store__";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Options for MemorySegmentCacheStore
|
|
15
|
+
*/
|
|
16
|
+
export interface MemorySegmentCacheStoreOptions<TEnv = unknown> {
|
|
17
|
+
/**
|
|
18
|
+
* Default cache options for cache() boundaries.
|
|
19
|
+
* When cache() is called without explicit ttl/swr,
|
|
20
|
+
* these defaults are used.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* const store = new MemorySegmentCacheStore({
|
|
25
|
+
* defaults: { ttl: 60, swr: 300 }
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
defaults?: CacheDefaults;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Custom key generator applied to all cache operations.
|
|
33
|
+
* Receives the full RequestContext and the default-generated key.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* keyGenerator: (ctx, defaultKey) => {
|
|
38
|
+
* const locale = ctx.cookie('locale') || 'en';
|
|
39
|
+
* return `${locale}:${defaultKey}`;
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
keyGenerator?: (
|
|
44
|
+
ctx: RequestContext<TEnv>,
|
|
45
|
+
defaultKey: string
|
|
46
|
+
) => string | Promise<string>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* In-memory segment cache store.
|
|
51
|
+
*
|
|
52
|
+
* Suitable for development and single-instance deployments.
|
|
53
|
+
* For production with multiple instances, use a distributed store
|
|
54
|
+
* like Cloudflare KV or Redis.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* // Basic usage
|
|
59
|
+
* const store = new MemorySegmentCacheStore();
|
|
60
|
+
*
|
|
61
|
+
* // With defaults for cache() boundaries
|
|
62
|
+
* const store = new MemorySegmentCacheStore({
|
|
63
|
+
* defaults: { ttl: 60 }
|
|
64
|
+
* });
|
|
65
|
+
*
|
|
66
|
+
* createRSCHandler({
|
|
67
|
+
* router,
|
|
68
|
+
* cache: { store }
|
|
69
|
+
* })
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export class MemorySegmentCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
73
|
+
private cache: Map<string, CachedEntryData>;
|
|
74
|
+
readonly defaults?: CacheDefaults;
|
|
75
|
+
readonly keyGenerator?: (
|
|
76
|
+
ctx: RequestContext<TEnv>,
|
|
77
|
+
defaultKey: string
|
|
78
|
+
) => string | Promise<string>;
|
|
79
|
+
|
|
80
|
+
constructor(options?: MemorySegmentCacheStoreOptions<TEnv>) {
|
|
81
|
+
// Use globalThis to survive HMR in development
|
|
82
|
+
this.cache =
|
|
83
|
+
(globalThis as any)[CACHE_GLOBAL_KEY] ??
|
|
84
|
+
((globalThis as any)[CACHE_GLOBAL_KEY] = new Map<string, CachedEntryData>());
|
|
85
|
+
this.defaults = options?.defaults;
|
|
86
|
+
this.keyGenerator = options?.keyGenerator;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async get(key: string): Promise<CacheGetResult | null> {
|
|
90
|
+
const cached = this.cache.get(key);
|
|
91
|
+
|
|
92
|
+
if (!cached) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check expiration
|
|
97
|
+
if (Date.now() > cached.expiresAt) {
|
|
98
|
+
this.cache.delete(key);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Memory store doesn't support SWR - never triggers revalidation
|
|
103
|
+
return { data: cached, shouldRevalidate: false };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async set(key: string, data: CachedEntryData, ttl: number, _swr?: number): Promise<void> {
|
|
107
|
+
// Note: Memory store doesn't implement SWR - entries just expire at TTL
|
|
108
|
+
// For SWR support, use CFCacheStore or similar distributed cache
|
|
109
|
+
const entry: CachedEntryData = {
|
|
110
|
+
...data,
|
|
111
|
+
expiresAt: Date.now() + ttl * 1000,
|
|
112
|
+
};
|
|
113
|
+
this.cache.set(key, entry);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async delete(key: string): Promise<boolean> {
|
|
117
|
+
return this.cache.delete(key);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async clear(): Promise<void> {
|
|
121
|
+
this.cache.clear();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get cache statistics for debugging purposes.
|
|
126
|
+
* @internal
|
|
127
|
+
*/
|
|
128
|
+
getStats(): { size: number; keys: string[] } {
|
|
129
|
+
return {
|
|
130
|
+
size: this.cache.size,
|
|
131
|
+
keys: Array.from(this.cache.keys()),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Reset the global cache state.
|
|
137
|
+
* Useful for test isolation - call this in beforeEach to ensure
|
|
138
|
+
* tests don't share cache state via globalThis.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```typescript
|
|
142
|
+
* beforeEach(() => {
|
|
143
|
+
* MemorySegmentCacheStore.resetGlobalCache();
|
|
144
|
+
* });
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
static resetGlobalCache(): void {
|
|
148
|
+
delete (globalThis as any)[CACHE_GLOBAL_KEY];
|
|
149
|
+
}
|
|
150
|
+
}
|