@rangojs/router 0.0.0-experimental.0f44aca1
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/AGENTS.md +5 -0
- package/README.md +899 -0
- package/dist/bin/rango.js +1601 -0
- package/dist/vite/index.js +5214 -0
- package/package.json +176 -0
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +220 -0
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +112 -0
- package/skills/document-cache/SKILL.md +182 -0
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +704 -0
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +313 -0
- package/skills/layout/SKILL.md +310 -0
- package/skills/links/SKILL.md +239 -0
- package/skills/loader/SKILL.md +596 -0
- package/skills/middleware/SKILL.md +339 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +305 -0
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +118 -0
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +385 -0
- package/skills/router-setup/SKILL.md +439 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +79 -0
- package/skills/typesafety/SKILL.md +623 -0
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +273 -0
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/event-controller.ts +899 -0
- package/src/browser/history-state.ts +80 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +141 -0
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +134 -0
- package/src/browser/navigation-bridge.ts +645 -0
- package/src/browser/navigation-client.ts +215 -0
- package/src/browser/navigation-store.ts +806 -0
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +550 -0
- package/src/browser/prefetch/cache.ts +146 -0
- package/src/browser/prefetch/fetch.ts +135 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +360 -0
- package/src/browser/react/NavigationProvider.tsx +386 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +59 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +162 -0
- package/src/browser/react/location-state.ts +107 -0
- package/src/browser/react/mount-context.ts +37 -0
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +218 -0
- package/src/browser/react/use-client-cache.ts +58 -0
- package/src/browser/react/use-handle.ts +162 -0
- package/src/browser/react/use-href.tsx +40 -0
- package/src/browser/react/use-link-status.ts +135 -0
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +99 -0
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +63 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +171 -0
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +431 -0
- package/src/browser/scroll-restoration.ts +400 -0
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +83 -0
- package/src/browser/server-action-bridge.ts +667 -0
- package/src/browser/shallow.ts +40 -0
- package/src/browser/types.ts +538 -0
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +438 -0
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +35 -0
- package/src/build/route-trie.ts +265 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +411 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +469 -0
- package/src/build/route-types/scan-filter.ts +78 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +338 -0
- package/src/cache/cache-scope.ts +382 -0
- package/src/cache/cf/cf-cache-store.ts +540 -0
- package/src/cache/cf/index.ts +25 -0
- package/src/cache/document-cache.ts +369 -0
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +43 -0
- package/src/cache/memory-segment-store.ts +328 -0
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +98 -0
- package/src/cache/types.ts +342 -0
- package/src/client.rsc.tsx +85 -0
- package/src/client.tsx +601 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +27 -0
- package/src/context-var.ts +86 -0
- package/src/debug.ts +243 -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 +365 -0
- package/src/handle.ts +135 -0
- package/src/handles/MetaTags.tsx +246 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +7 -0
- package/src/handles/meta.ts +264 -0
- package/src/host/cookie-handler.ts +165 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +53 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +352 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +146 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +222 -0
- package/src/index.rsc.ts +233 -0
- package/src/index.ts +277 -0
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +89 -0
- package/src/loader.ts +64 -0
- package/src/network-error-thrower.tsx +23 -0
- package/src/outlet-context.ts +15 -0
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +185 -0
- package/src/prerender.ts +463 -0
- package/src/reverse.ts +330 -0
- package/src/root-error-boundary.tsx +289 -0
- package/src/route-content-wrapper.tsx +196 -0
- package/src/route-definition/dsl-helpers.ts +934 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -0
- package/src/route-map-builder.ts +275 -0
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +259 -0
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/find-match.ts +158 -0
- package/src/router/handler-context.ts +451 -0
- package/src/router/intercept-resolution.ts +395 -0
- package/src/router/lazy-includes.ts +234 -0
- package/src/router/loader-resolution.ts +420 -0
- package/src/router/logging.ts +248 -0
- package/src/router/manifest.ts +267 -0
- package/src/router/match-api.ts +620 -0
- package/src/router/match-context.ts +266 -0
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +223 -0
- package/src/router/match-middleware/cache-lookup.ts +634 -0
- package/src/router/match-middleware/cache-store.ts +295 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +306 -0
- package/src/router/match-middleware/segment-resolution.ts +192 -0
- package/src/router/match-pipelines.ts +179 -0
- package/src/router/match-result.ts +219 -0
- package/src/router/metrics.ts +282 -0
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +748 -0
- package/src/router/pattern-matching.ts +563 -0
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +289 -0
- package/src/router/router-context.ts +316 -0
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +570 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +198 -0
- package/src/router/segment-resolution/revalidation.ts +1239 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +289 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +239 -0
- package/src/router/types.ts +170 -0
- package/src/router.ts +1002 -0
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +1089 -0
- package/src/rsc/helpers.ts +198 -0
- package/src/rsc/index.ts +36 -0
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +32 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +235 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +348 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +263 -0
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +454 -0
- package/src/server/context.ts +591 -0
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +308 -0
- package/src/server/loader-registry.ts +133 -0
- package/src/server/request-context.ts +914 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +51 -0
- package/src/ssr/index.tsx +365 -0
- package/src/static-handler.ts +114 -0
- package/src/theme/ThemeProvider.tsx +297 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/constants.ts +62 -0
- package/src/theme/index.ts +48 -0
- package/src/theme/theme-context.ts +44 -0
- package/src/theme/theme-script.ts +155 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +687 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +102 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -0
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -0
- package/src/use-loader.tsx +354 -0
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +110 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +16 -0
- package/src/vite/plugin-types.ts +131 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/plugins/expose-action-id.ts +365 -0
- package/src/vite/plugins/expose-id-utils.ts +287 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +254 -0
- package/src/vite/plugins/version.d.ts +12 -0
- package/src/vite/plugins/virtual-entries.ts +123 -0
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +510 -0
- package/src/vite/router-discovery.ts +785 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/utils/package-resolution.ts +121 -0
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
|
@@ -0,0 +1,369 @@
|
|
|
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
|
+
import { sortedSearchString } from "./cache-key-utils.js";
|
|
17
|
+
import { runBackground } from "./background-task.js";
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Constants
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
/** Header indicating cache status for debugging */
|
|
24
|
+
const CACHE_STATUS_HEADER = "x-document-cache-status";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Simple hash function for segment IDs.
|
|
28
|
+
* Creates a short, deterministic hash to differentiate cache keys
|
|
29
|
+
* based on which segments the client already has.
|
|
30
|
+
*/
|
|
31
|
+
function hashSegmentIds(segmentIds: string): string {
|
|
32
|
+
if (!segmentIds) return "";
|
|
33
|
+
|
|
34
|
+
let hash = 0;
|
|
35
|
+
for (let i = 0; i < segmentIds.length; i++) {
|
|
36
|
+
const char = segmentIds.charCodeAt(i);
|
|
37
|
+
hash = ((hash << 5) - hash + char) | 0;
|
|
38
|
+
}
|
|
39
|
+
// Convert to base36 for shorter string, take absolute value
|
|
40
|
+
return Math.abs(hash).toString(36);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// Cache Control Parsing
|
|
45
|
+
// ============================================================================
|
|
46
|
+
|
|
47
|
+
interface CacheDirectives {
|
|
48
|
+
sMaxAge?: number;
|
|
49
|
+
staleWhileRevalidate?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Parse Cache-Control header for s-maxage and stale-while-revalidate directives
|
|
54
|
+
*/
|
|
55
|
+
function parseCacheControl(header: string | null): CacheDirectives | null {
|
|
56
|
+
if (!header) return null;
|
|
57
|
+
|
|
58
|
+
const directives: CacheDirectives = {};
|
|
59
|
+
|
|
60
|
+
// Parse s-maxage
|
|
61
|
+
const sMaxAgeMatch = header.match(/s-maxage\s*=\s*(\d+)/i);
|
|
62
|
+
if (sMaxAgeMatch) {
|
|
63
|
+
directives.sMaxAge = parseInt(sMaxAgeMatch[1], 10);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Parse stale-while-revalidate
|
|
67
|
+
const swrMatch = header.match(/stale-while-revalidate\s*=\s*(\d+)/i);
|
|
68
|
+
if (swrMatch) {
|
|
69
|
+
directives.staleWhileRevalidate = parseInt(swrMatch[1], 10);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Only return if we have at least s-maxage (required for document caching)
|
|
73
|
+
if (directives.sMaxAge !== undefined) {
|
|
74
|
+
return directives;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if response should be cached based on Cache-Control headers
|
|
82
|
+
*/
|
|
83
|
+
function shouldCacheResponse(response: Response): CacheDirectives | null {
|
|
84
|
+
// Only cache successful responses
|
|
85
|
+
if (response.status !== 200) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const cacheControl = response.headers.get("Cache-Control");
|
|
90
|
+
return parseCacheControl(cacheControl);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// Response Helpers
|
|
95
|
+
// ============================================================================
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Add cache status header to response for debugging
|
|
99
|
+
*/
|
|
100
|
+
function addCacheStatusHeader(
|
|
101
|
+
response: Response,
|
|
102
|
+
status: "HIT" | "STALE" | "MISS",
|
|
103
|
+
): Response {
|
|
104
|
+
const headers = new Headers(response.headers);
|
|
105
|
+
headers.set(CACHE_STATUS_HEADER, status);
|
|
106
|
+
|
|
107
|
+
return new Response(response.body, {
|
|
108
|
+
status: response.status,
|
|
109
|
+
statusText: response.statusText,
|
|
110
|
+
headers,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Drain and run onResponse callbacks registered on the request context.
|
|
116
|
+
* Mirrors the drain semantics of finalizeResponse() in rsc/helpers.ts:
|
|
117
|
+
* callbacks are spliced out so they fire at most once per request.
|
|
118
|
+
*/
|
|
119
|
+
function drainOnResponseCallbacks(
|
|
120
|
+
response: Response,
|
|
121
|
+
requestCtx:
|
|
122
|
+
| { _onResponseCallbacks: Array<(r: Response) => Response> }
|
|
123
|
+
| undefined,
|
|
124
|
+
): Response {
|
|
125
|
+
if (!requestCtx || requestCtx._onResponseCallbacks.length === 0) {
|
|
126
|
+
return response;
|
|
127
|
+
}
|
|
128
|
+
const callbacks = requestCtx._onResponseCallbacks;
|
|
129
|
+
requestCtx._onResponseCallbacks = [];
|
|
130
|
+
let result = response;
|
|
131
|
+
for (const callback of callbacks) {
|
|
132
|
+
result = callback(result) ?? result;
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ============================================================================
|
|
138
|
+
// Document Cache Middleware
|
|
139
|
+
// ============================================================================
|
|
140
|
+
|
|
141
|
+
export interface DocumentCacheOptions<TEnv = any> {
|
|
142
|
+
/**
|
|
143
|
+
* Skip caching for specific paths (e.g., API routes)
|
|
144
|
+
*/
|
|
145
|
+
skipPaths?: string[];
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Custom cache key generator
|
|
149
|
+
*/
|
|
150
|
+
keyGenerator?: (url: URL) => string;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Callback to determine if caching should be enabled for this request.
|
|
154
|
+
* Receives the middleware context and returns true to enable caching.
|
|
155
|
+
* If not provided, caching is enabled by default.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```typescript
|
|
159
|
+
* createDocumentCacheMiddleware({
|
|
160
|
+
* isEnabled: (ctx) => !ctx.request.headers.has('x-skip-cache'),
|
|
161
|
+
* })
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
isEnabled?: (ctx: MiddlewareContext<TEnv>) => boolean | Promise<boolean>;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Enable debug logging for cache operations.
|
|
168
|
+
* Logs HIT, MISS, STALE, and REVALIDATED events.
|
|
169
|
+
* Defaults to false.
|
|
170
|
+
*/
|
|
171
|
+
debug?: boolean;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Create document cache middleware
|
|
176
|
+
*
|
|
177
|
+
* Add this middleware to your router to enable document-level caching.
|
|
178
|
+
* It uses the cache store's getResponse/putResponse methods for caching.
|
|
179
|
+
* Routes opt-in by setting Cache-Control headers with s-maxage.
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* ```typescript
|
|
183
|
+
* // Add middleware to router
|
|
184
|
+
* const router = createRouter<AppEnv>()
|
|
185
|
+
* .use(createDocumentCacheMiddleware({
|
|
186
|
+
* isEnabled: (ctx) => ctx.url.pathname !== '/admin',
|
|
187
|
+
* }))
|
|
188
|
+
* .route("home", (ctx) => {
|
|
189
|
+
* ctx.headers.set("Cache-Control", "s-maxage=60, stale-while-revalidate=300");
|
|
190
|
+
* return <HomePage />;
|
|
191
|
+
* });
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
export function createDocumentCacheMiddleware<TEnv = any>(
|
|
195
|
+
options: DocumentCacheOptions<TEnv> = {},
|
|
196
|
+
): MiddlewareFn<TEnv> {
|
|
197
|
+
const { skipPaths = [], keyGenerator, isEnabled, debug = false } = options;
|
|
198
|
+
|
|
199
|
+
const log = debug ? (message: string) => console.log(message) : () => {};
|
|
200
|
+
|
|
201
|
+
return async function documentCacheMiddleware(
|
|
202
|
+
ctx: MiddlewareContext<TEnv>,
|
|
203
|
+
next: () => Promise<Response>,
|
|
204
|
+
): Promise<Response> {
|
|
205
|
+
const url = ctx.url;
|
|
206
|
+
|
|
207
|
+
// Only cache GET requests — mutations and other methods must not be cached
|
|
208
|
+
if (ctx.request.method !== "GET") {
|
|
209
|
+
return next();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Skip RSC action requests (mutations shouldn't be cached)
|
|
213
|
+
if (url.searchParams.has("_rsc_action")) {
|
|
214
|
+
return next();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Skip loader requests (have their own caching)
|
|
218
|
+
if (url.searchParams.has("_rsc_loader")) {
|
|
219
|
+
return next();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Skip configured paths
|
|
223
|
+
if (skipPaths.some((path) => url.pathname.startsWith(path))) {
|
|
224
|
+
return next();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Check if caching is enabled for this request
|
|
228
|
+
if (isEnabled) {
|
|
229
|
+
const enabled = await isEnabled(ctx);
|
|
230
|
+
if (!enabled) {
|
|
231
|
+
return next();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Get request context and cache store
|
|
236
|
+
const requestCtx = getRequestContext();
|
|
237
|
+
const store = requestCtx?._cacheStore;
|
|
238
|
+
|
|
239
|
+
// Skip if no cache store or store doesn't support response caching
|
|
240
|
+
if (!store?.getResponse || !store?.putResponse) {
|
|
241
|
+
return next();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Determine request type for cache key differentiation
|
|
245
|
+
const isPartial = url.searchParams.has("_rsc_partial");
|
|
246
|
+
const typeLabel = isPartial ? "RSC" : "HTML";
|
|
247
|
+
|
|
248
|
+
// Track whether next() has been called so the catch block knows
|
|
249
|
+
// whether it is safe to fall through to the handler.
|
|
250
|
+
let handlerCalled = false;
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
// Generate cache key inside try so a throwing keyGenerator degrades
|
|
254
|
+
// gracefully to the origin handler instead of rejecting the request.
|
|
255
|
+
// This is a deliberate fail-open-to-origin policy: the fallback is
|
|
256
|
+
// "serve uncached from origin", not "use a different cache key".
|
|
257
|
+
const clientSegments = url.searchParams.get("_rsc_segments") || "";
|
|
258
|
+
const segmentHash =
|
|
259
|
+
isPartial && clientSegments ? `:${hashSegmentIds(clientSegments)}` : "";
|
|
260
|
+
const typeSuffix = isPartial ? ":rsc" : ":html";
|
|
261
|
+
|
|
262
|
+
let searchSuffix = "";
|
|
263
|
+
if (!keyGenerator) {
|
|
264
|
+
const sorted = sortedSearchString(url.searchParams);
|
|
265
|
+
if (sorted) {
|
|
266
|
+
searchSuffix = `?${sorted}`;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const cacheKey = keyGenerator
|
|
271
|
+
? keyGenerator(url) + segmentHash + typeSuffix
|
|
272
|
+
: `${url.pathname}${searchSuffix}${segmentHash}${typeSuffix}`;
|
|
273
|
+
// 1. Check cache
|
|
274
|
+
const cached = await store.getResponse(cacheKey);
|
|
275
|
+
|
|
276
|
+
if (cached && cached.response.status === 200) {
|
|
277
|
+
if (!cached.shouldRevalidate) {
|
|
278
|
+
// Fresh hit - return immediately
|
|
279
|
+
log(`[DocumentCache] HIT ${typeLabel}: ${url.pathname}`);
|
|
280
|
+
return drainOnResponseCallbacks(
|
|
281
|
+
addCacheStatusHeader(cached.response, "HIT"),
|
|
282
|
+
requestCtx,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Stale hit - return cached response, revalidate in background
|
|
287
|
+
log(
|
|
288
|
+
`[DocumentCache] STALE ${typeLabel}: ${url.pathname} (revalidating)`,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
runBackground(requestCtx, async () => {
|
|
292
|
+
try {
|
|
293
|
+
const fresh = await next();
|
|
294
|
+
const directives = shouldCacheResponse(fresh);
|
|
295
|
+
|
|
296
|
+
if (directives) {
|
|
297
|
+
await store.putResponse!(
|
|
298
|
+
cacheKey,
|
|
299
|
+
fresh,
|
|
300
|
+
directives.sMaxAge!,
|
|
301
|
+
directives.staleWhileRevalidate,
|
|
302
|
+
);
|
|
303
|
+
log(`[DocumentCache] REVALIDATED ${typeLabel}: ${url.pathname}`);
|
|
304
|
+
}
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.error(`[DocumentCache] Revalidation failed:`, error);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
return drainOnResponseCallbacks(
|
|
311
|
+
addCacheStatusHeader(cached.response, "STALE"),
|
|
312
|
+
requestCtx,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// 2. Cache miss - run handler
|
|
317
|
+
handlerCalled = true;
|
|
318
|
+
const originalResponse = await next();
|
|
319
|
+
|
|
320
|
+
// 3. Cache if response has appropriate headers
|
|
321
|
+
const directives = shouldCacheResponse(originalResponse);
|
|
322
|
+
|
|
323
|
+
if (directives) {
|
|
324
|
+
log(
|
|
325
|
+
`[DocumentCache] MISS ${typeLabel}: ${url.pathname} (caching with s-maxage=${directives.sMaxAge})`,
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// If the response has no body (e.g., 200 with empty body), skip caching
|
|
329
|
+
if (!originalResponse.body) {
|
|
330
|
+
return originalResponse;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Tee the body so we can return one stream and cache the other
|
|
334
|
+
const [returnStream, cacheStream] = originalResponse.body.tee();
|
|
335
|
+
|
|
336
|
+
// Clone response for caching (non-blocking)
|
|
337
|
+
runBackground(requestCtx, async () => {
|
|
338
|
+
try {
|
|
339
|
+
await store.putResponse!(
|
|
340
|
+
cacheKey,
|
|
341
|
+
new Response(cacheStream, originalResponse),
|
|
342
|
+
directives.sMaxAge!,
|
|
343
|
+
directives.staleWhileRevalidate,
|
|
344
|
+
);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
console.error(`[DocumentCache] Cache write failed:`, error);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
return addCacheStatusHeader(
|
|
351
|
+
new Response(returnStream, originalResponse),
|
|
352
|
+
"MISS",
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// No cache headers - pass through
|
|
357
|
+
return originalResponse;
|
|
358
|
+
} catch (error) {
|
|
359
|
+
console.error(`[DocumentCache] Error:`, error);
|
|
360
|
+
if (handlerCalled) {
|
|
361
|
+
// Post-handler failure (e.g. body.tee()): do not call next() again
|
|
362
|
+
// as that would re-run handler side effects.
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
// Pre-handler failure (cache lookup): degrade gracefully to origin
|
|
366
|
+
return next();
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle Capture
|
|
3
|
+
*
|
|
4
|
+
* Captures handle pushes during cached function execution.
|
|
5
|
+
* Extracted from cache-runtime.ts so tests can import without
|
|
6
|
+
* pulling in @vitejs/plugin-rsc/rsc dependencies.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { HandleStore } from "../server/handle-store.js";
|
|
10
|
+
import type { SegmentHandleData } from "./types.js";
|
|
11
|
+
|
|
12
|
+
export interface HandleCapture {
|
|
13
|
+
data: Record<string, SegmentHandleData>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Active capture tokens per HandleStore.
|
|
18
|
+
*
|
|
19
|
+
* Instead of mutating handleStore.push (which breaks when overlapping
|
|
20
|
+
* captures finish out of order), we install a single interceptor on
|
|
21
|
+
* first use and manage a set of active capture tokens. Each push fans
|
|
22
|
+
* out to every active token. Stopping a capture simply removes the
|
|
23
|
+
* token — order does not matter.
|
|
24
|
+
*/
|
|
25
|
+
const activeCapturesMap = new WeakMap<HandleStore, Set<HandleCapture>>();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* One-time interceptor installation. Wraps the original push so every
|
|
29
|
+
* call fans out to all active capture tokens. Installed once per
|
|
30
|
+
* HandleStore instance; subsequent startHandleCapture calls on the
|
|
31
|
+
* same store just add tokens to the Set.
|
|
32
|
+
*/
|
|
33
|
+
function ensureInterceptorInstalled(handleStore: HandleStore): void {
|
|
34
|
+
if (activeCapturesMap.has(handleStore)) return;
|
|
35
|
+
|
|
36
|
+
const captures = new Set<HandleCapture>();
|
|
37
|
+
activeCapturesMap.set(handleStore, captures);
|
|
38
|
+
|
|
39
|
+
const originalPush = handleStore.push.bind(handleStore);
|
|
40
|
+
handleStore.push = (
|
|
41
|
+
handleName: string,
|
|
42
|
+
segmentId: string,
|
|
43
|
+
value: unknown,
|
|
44
|
+
) => {
|
|
45
|
+
for (const capture of captures) {
|
|
46
|
+
if (!capture.data[segmentId]) {
|
|
47
|
+
capture.data[segmentId] = {};
|
|
48
|
+
}
|
|
49
|
+
if (!capture.data[segmentId][handleName]) {
|
|
50
|
+
capture.data[segmentId][handleName] = [];
|
|
51
|
+
}
|
|
52
|
+
capture.data[segmentId][handleName].push(value);
|
|
53
|
+
}
|
|
54
|
+
originalPush(handleName, segmentId, value);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Start capturing handle pushes for a cached function execution.
|
|
60
|
+
*
|
|
61
|
+
* Concurrency-safe: multiple overlapping captures on the same
|
|
62
|
+
* HandleStore are independent. Each capture registers a token in a
|
|
63
|
+
* Set; stopping removes it. No ordering requirement (LIFO not needed).
|
|
64
|
+
*/
|
|
65
|
+
export function startHandleCapture(handleStore: HandleStore): {
|
|
66
|
+
capture: HandleCapture;
|
|
67
|
+
stop: () => void;
|
|
68
|
+
} {
|
|
69
|
+
ensureInterceptorInstalled(handleStore);
|
|
70
|
+
|
|
71
|
+
const capture: HandleCapture = { data: {} };
|
|
72
|
+
const captures = activeCapturesMap.get(handleStore)!;
|
|
73
|
+
captures.add(capture);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
capture,
|
|
77
|
+
stop() {
|
|
78
|
+
captures.delete(capture);
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle Snapshot
|
|
3
|
+
*
|
|
4
|
+
* Capture and restore handle data for cached segments.
|
|
5
|
+
* Handle data (breadcrumbs, metadata from ctx.use(Handle)) is collected
|
|
6
|
+
* during segment resolution and stored alongside cached segments.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ResolvedSegment } from "../types.js";
|
|
10
|
+
import type { HandleStore } from "../server/handle-store.js";
|
|
11
|
+
import type { SegmentHandleData } from "./types.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Capture handle data for a set of segments from the handle store.
|
|
15
|
+
* Used when caching segments to preserve their handle data.
|
|
16
|
+
*/
|
|
17
|
+
export function captureHandles(
|
|
18
|
+
segments: ResolvedSegment[],
|
|
19
|
+
handleStore: HandleStore,
|
|
20
|
+
): Record<string, SegmentHandleData> {
|
|
21
|
+
const handles: Record<string, SegmentHandleData> = {};
|
|
22
|
+
for (const seg of segments) {
|
|
23
|
+
handles[seg.id] = handleStore.getDataForSegment(seg.id);
|
|
24
|
+
}
|
|
25
|
+
return handles;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Restore handle data from a cached snapshot into the handle store.
|
|
30
|
+
* Used when serving cached segments to replay their handle data.
|
|
31
|
+
*/
|
|
32
|
+
export function restoreHandles(
|
|
33
|
+
handles: Record<string, SegmentHandleData>,
|
|
34
|
+
handleStore: HandleStore,
|
|
35
|
+
): void {
|
|
36
|
+
for (const [segId, segHandles] of Object.entries(handles)) {
|
|
37
|
+
if (Object.keys(segHandles).length > 0) {
|
|
38
|
+
handleStore.replaySegmentData(segId, segHandles);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
// Segment cache store types and implementations
|
|
14
|
+
export type {
|
|
15
|
+
SegmentCacheStore,
|
|
16
|
+
SegmentCacheProvider,
|
|
17
|
+
CachedEntryData,
|
|
18
|
+
CachedEntryResult,
|
|
19
|
+
CacheGetResult,
|
|
20
|
+
SerializedSegmentData,
|
|
21
|
+
SegmentHandleData,
|
|
22
|
+
CacheConfig,
|
|
23
|
+
CacheConfigOrFactory,
|
|
24
|
+
} from "./types.js";
|
|
25
|
+
|
|
26
|
+
export { MemorySegmentCacheStore } from "./memory-segment-store.js";
|
|
27
|
+
|
|
28
|
+
// Cloudflare cache store
|
|
29
|
+
export {
|
|
30
|
+
CFCacheStore,
|
|
31
|
+
type CFCacheStoreOptions,
|
|
32
|
+
CACHE_STALE_AT_HEADER,
|
|
33
|
+
CACHE_STATUS_HEADER,
|
|
34
|
+
} from "./cf/index.js";
|
|
35
|
+
|
|
36
|
+
// Cache scope
|
|
37
|
+
export { CacheScope, createCacheScope } from "./cache-scope.js";
|
|
38
|
+
|
|
39
|
+
// Document-level cache middleware
|
|
40
|
+
export {
|
|
41
|
+
createDocumentCacheMiddleware,
|
|
42
|
+
type DocumentCacheOptions,
|
|
43
|
+
} from "./document-cache.js";
|