@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26
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/README.md +294 -28
- package/dist/bin/rango.js +355 -47
- package/dist/vite/index.js +1658 -1239
- package/package.json +3 -3
- package/skills/cache-guide/SKILL.md +9 -5
- package/skills/caching/SKILL.md +4 -4
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +40 -29
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +229 -15
- package/skills/middleware/SKILL.md +109 -30
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +189 -19
- package/skills/rango/SKILL.md +1 -2
- package/skills/response-routes/SKILL.md +3 -3
- package/skills/route/SKILL.md +44 -3
- package/skills/router-setup/SKILL.md +80 -3
- package/skills/theme/SKILL.md +5 -4
- package/skills/typesafety/SKILL.md +59 -16
- package/skills/use-cache/SKILL.md +16 -2
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +56 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +29 -48
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +19 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +66 -443
- package/src/browser/navigation-client.ts +34 -62
- package/src/browser/navigation-store.ts +4 -33
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/partial-update.ts +103 -151
- package/src/browser/prefetch/cache.ts +67 -0
- package/src/browser/prefetch/fetch.ts +137 -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 +154 -44
- package/src/browser/react/NavigationProvider.tsx +32 -0
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +29 -11
- package/src/browser/react/location-state.ts +6 -4
- 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 +23 -45
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +21 -64
- package/src/browser/react/use-navigation.ts +7 -32
- package/src/browser/react/use-params.ts +5 -34
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +3 -6
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +75 -114
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +46 -22
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +458 -405
- package/src/browser/types.ts +21 -35
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +38 -13
- package/src/build/generate-route-types.ts +4 -0
- package/src/build/index.ts +1 -0
- package/src/build/route-trie.ts +19 -3
- package/src/build/route-types/codegen.ts +13 -4
- package/src/build/route-types/include-resolution.ts +13 -0
- package/src/build/route-types/per-module-writer.ts +15 -3
- package/src/build/route-types/router-processing.ts +170 -18
- package/src/build/runtime-discovery.ts +13 -1
- 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 +136 -123
- package/src/cache/cache-scope.ts +76 -83
- package/src/cache/cf/cf-cache-store.ts +12 -7
- package/src/cache/document-cache.ts +93 -69
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/index.ts +0 -15
- package/src/cache/memory-segment-store.ts +43 -69
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +140 -117
- package/src/cache/taint.ts +30 -3
- package/src/cache/types.ts +1 -115
- package/src/client.rsc.tsx +0 -1
- package/src/client.tsx +53 -76
- package/src/errors.ts +6 -1
- package/src/handle.ts +1 -1
- package/src/handles/MetaTags.tsx +5 -2
- package/src/host/cookie-handler.ts +8 -3
- package/src/host/index.ts +0 -3
- package/src/host/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +53 -10
- package/src/index.ts +73 -43
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +60 -18
- package/src/prerender.ts +76 -18
- package/src/reverse.ts +11 -7
- package/src/root-error-boundary.tsx +30 -26
- package/src/route-definition/dsl-helpers.ts +9 -6
- package/src/route-definition/index.ts +0 -3
- package/src/route-definition/redirect.ts +15 -3
- package/src/route-map-builder.ts +38 -2
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +7 -0
- package/src/router/content-negotiation.ts +1 -1
- package/src/router/debug-manifest.ts +16 -3
- package/src/router/handler-context.ts +96 -17
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +6 -11
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +62 -54
- package/src/router/match-context.ts +3 -0
- package/src/router/match-handlers.ts +185 -11
- package/src/router/match-middleware/background-revalidation.ts +65 -85
- package/src/router/match-middleware/cache-lookup.ts +78 -10
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/match-result.ts +0 -9
- package/src/router/metrics.ts +233 -13
- package/src/router/middleware-types.ts +34 -39
- package/src/router/middleware.ts +290 -130
- package/src/router/pattern-matching.ts +61 -10
- package/src/router/prerender-match.ts +36 -6
- package/src/router/preview-match.ts +7 -1
- package/src/router/revalidation.ts +61 -2
- package/src/router/router-context.ts +15 -0
- package/src/router/router-interfaces.ts +158 -40
- package/src/router/router-options.ts +223 -1
- package/src/router/router-registry.ts +5 -2
- package/src/router/segment-resolution/fresh.ts +165 -242
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +102 -98
- package/src/router/segment-resolution/revalidation.ts +394 -272
- package/src/router/segment-resolution/static-store.ts +2 -2
- package/src/router/segment-resolution.ts +1 -3
- package/src/router/segment-wrappers.ts +3 -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 +20 -2
- package/src/router/types.ts +7 -1
- package/src/router.ts +203 -18
- package/src/rsc/handler-context.ts +13 -2
- package/src/rsc/handler.ts +489 -438
- package/src/rsc/helpers.ts +125 -5
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/manifest-init.ts +3 -2
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +245 -19
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +47 -43
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +166 -66
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +20 -2
- package/src/search-params.ts +38 -23
- package/src/server/context.ts +61 -7
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +84 -12
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +275 -49
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +67 -28
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +4 -18
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +6 -1
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +22 -0
- package/src/types/handler-context.ts +103 -16
- package/src/types/index.ts +1 -1
- package/src/types/loader-types.ts +9 -6
- package/src/types/route-config.ts +17 -26
- package/src/types/route-entry.ts +28 -0
- package/src/types/segments.ts +0 -5
- package/src/urls/include-helper.ts +49 -8
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +30 -12
- package/src/urls/path-helper.ts +17 -2
- package/src/urls/pattern-types.ts +21 -1
- package/src/urls/response-types.ts +29 -7
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +27 -9
- package/src/vite/discovery/bundle-postprocess.ts +32 -52
- package/src/vite/discovery/discover-routers.ts +52 -26
- package/src/vite/discovery/prerender-collection.ts +58 -41
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/state.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/index.ts +10 -51
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/expose-internal-ids.ts +4 -3
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/plugins/version-plugin.ts +188 -18
- package/src/vite/rango.ts +61 -36
- package/src/vite/router-discovery.ts +173 -100
- package/src/vite/utils/prerender-utils.ts +81 -0
- package/src/vite/utils/shared-utils.ts +19 -9
- package/skills/testing/SKILL.md +0 -226
- package/src/browser/lru-cache.ts +0 -61
- package/src/browser/react/prefetch.ts +0 -27
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/route-definition/route-function.ts +0 -119
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/{CLAUDE.md → AGENTS.md} +0 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Helpers for Segment Resolution
|
|
3
|
+
*
|
|
4
|
+
* Common utilities used by both fresh and revalidation resolution paths:
|
|
5
|
+
* - Handler result processing (Response vs ReactNode)
|
|
6
|
+
* - Static handler interception (build-time pre-rendered components)
|
|
7
|
+
* - Layout handler resolution with static fallback
|
|
8
|
+
* - Error boundary segment creation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ReactNode } from "react";
|
|
12
|
+
import { DataNotFoundError } from "../../errors";
|
|
13
|
+
import {
|
|
14
|
+
createErrorInfo,
|
|
15
|
+
createErrorSegment,
|
|
16
|
+
createNotFoundInfo,
|
|
17
|
+
createNotFoundSegment,
|
|
18
|
+
} from "../error-handling.js";
|
|
19
|
+
import { getRequestContext } from "../../server/request-context.js";
|
|
20
|
+
import { DefaultErrorFallback } from "../../default-error-boundary.js";
|
|
21
|
+
import type { EntryData } from "../../server/context";
|
|
22
|
+
import type { ResolvedSegment, ErrorInfo, HandlerContext } from "../../types";
|
|
23
|
+
import type { SegmentResolutionDeps } from "../types.js";
|
|
24
|
+
import { debugLog } from "../logging.js";
|
|
25
|
+
import { tryStaticLookup } from "./static-store.js";
|
|
26
|
+
import type { TelemetrySink } from "../telemetry.js";
|
|
27
|
+
import { resolveSink, safeEmit, getRequestId } from "../telemetry.js";
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Handler result processing
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Handle Response returns from handlers.
|
|
35
|
+
* When a handler returns a Response (e.g., redirect), throw it to trigger
|
|
36
|
+
* the short-circuit mechanism. Otherwise return the ReactNode.
|
|
37
|
+
*/
|
|
38
|
+
export function handleHandlerResult(
|
|
39
|
+
result: ReactNode | Response | Promise<ReactNode> | Promise<Response>,
|
|
40
|
+
): ReactNode {
|
|
41
|
+
if (result instanceof Response) {
|
|
42
|
+
throw result;
|
|
43
|
+
}
|
|
44
|
+
if (result instanceof Promise) {
|
|
45
|
+
return result.then((resolved) => {
|
|
46
|
+
if (resolved instanceof Response) {
|
|
47
|
+
throw resolved;
|
|
48
|
+
}
|
|
49
|
+
return resolved;
|
|
50
|
+
}) as ReactNode;
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Static handler interception
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Try to resolve a component from the build-time static store.
|
|
61
|
+
* Returns undefined synchronously when the entry is not a static prerender,
|
|
62
|
+
* avoiding unnecessary promise wrapping on the hot path.
|
|
63
|
+
*/
|
|
64
|
+
export function tryStaticHandler(
|
|
65
|
+
entry: EntryData,
|
|
66
|
+
segmentId: string,
|
|
67
|
+
): Promise<ReactNode | undefined> | undefined {
|
|
68
|
+
const entryAny = entry as any;
|
|
69
|
+
if (entryAny.isStaticPrerender && entryAny.staticHandlerId) {
|
|
70
|
+
return tryStaticLookup(entryAny.staticHandlerId, segmentId);
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Try to resolve a parallel slot component from the build-time static store.
|
|
77
|
+
* Returns undefined synchronously when no static handler ID exists for the slot.
|
|
78
|
+
*/
|
|
79
|
+
export function tryStaticSlot(
|
|
80
|
+
parallelEntry: EntryData,
|
|
81
|
+
slot: string,
|
|
82
|
+
segmentId: string,
|
|
83
|
+
): Promise<ReactNode | undefined> | undefined {
|
|
84
|
+
const slotStaticId = (parallelEntry as any).staticHandlerIds?.[slot];
|
|
85
|
+
if (slotStaticId) {
|
|
86
|
+
return tryStaticLookup(slotStaticId, segmentId);
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolve a layout or cache entry's handler component.
|
|
93
|
+
* Checks build-time static store first, then invokes the handler.
|
|
94
|
+
*/
|
|
95
|
+
export async function resolveLayoutComponent<TEnv>(
|
|
96
|
+
entry: EntryData,
|
|
97
|
+
context: HandlerContext<any, TEnv>,
|
|
98
|
+
): Promise<ReactNode> {
|
|
99
|
+
const component = await tryStaticHandler(entry, entry.shortCode);
|
|
100
|
+
if (component !== undefined) return component;
|
|
101
|
+
return typeof entry.handler === "function"
|
|
102
|
+
? handleHandlerResult(await entry.handler(context))
|
|
103
|
+
: (entry.handler as ReactNode);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Error boundary segment creation
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Context for error reporting in segment resolution.
|
|
112
|
+
* When provided, callOnError is invoked with this context.
|
|
113
|
+
*/
|
|
114
|
+
export interface ErrorReportContext {
|
|
115
|
+
request?: Request;
|
|
116
|
+
url?: URL;
|
|
117
|
+
routeKey?: string;
|
|
118
|
+
env?: any;
|
|
119
|
+
isPartial?: boolean;
|
|
120
|
+
requestStartTime?: number;
|
|
121
|
+
telemetry?: TelemetrySink;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Handle a caught error during segment resolution by creating an
|
|
126
|
+
* error or not-found segment with the nearest boundary.
|
|
127
|
+
*
|
|
128
|
+
* Called by resolveWithErrorBoundary to produce error/notFound segments.
|
|
129
|
+
*/
|
|
130
|
+
export function catchSegmentError<TEnv>(
|
|
131
|
+
error: unknown,
|
|
132
|
+
entry: EntryData,
|
|
133
|
+
params: Record<string, string>,
|
|
134
|
+
deps: SegmentResolutionDeps<TEnv>,
|
|
135
|
+
report?: ErrorReportContext,
|
|
136
|
+
pathname?: string,
|
|
137
|
+
): ResolvedSegment {
|
|
138
|
+
const reportError = (
|
|
139
|
+
handledByBoundary: boolean,
|
|
140
|
+
metadata?: Record<string, unknown>,
|
|
141
|
+
) => {
|
|
142
|
+
if (!report) return;
|
|
143
|
+
deps.callOnError(error, "handler", {
|
|
144
|
+
request: report.request as Request,
|
|
145
|
+
url: report.url,
|
|
146
|
+
routeKey: report.routeKey,
|
|
147
|
+
params,
|
|
148
|
+
segmentId: entry.shortCode,
|
|
149
|
+
segmentType: entry.type as any,
|
|
150
|
+
env: report.env,
|
|
151
|
+
isPartial: report.isPartial,
|
|
152
|
+
handledByBoundary,
|
|
153
|
+
metadata,
|
|
154
|
+
requestStartTime: report.requestStartTime,
|
|
155
|
+
});
|
|
156
|
+
if (report.telemetry) {
|
|
157
|
+
const errorObj =
|
|
158
|
+
error instanceof Error ? error : new Error(String(error));
|
|
159
|
+
safeEmit(resolveSink(report.telemetry), {
|
|
160
|
+
type: "handler.error",
|
|
161
|
+
timestamp: performance.now(),
|
|
162
|
+
requestId: report.request ? getRequestId(report.request) : undefined,
|
|
163
|
+
segmentId: entry.shortCode,
|
|
164
|
+
segmentType: entry.type,
|
|
165
|
+
error: errorObj,
|
|
166
|
+
handledByBoundary,
|
|
167
|
+
pathname,
|
|
168
|
+
routeKey: report.routeKey,
|
|
169
|
+
params,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const setResponseStatus = (status: number) => {
|
|
175
|
+
const reqCtx = getRequestContext();
|
|
176
|
+
if (reqCtx) {
|
|
177
|
+
reqCtx.setStatus(status);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
if (error instanceof DataNotFoundError) {
|
|
182
|
+
const notFoundFallback = deps.findNearestNotFoundBoundary(entry);
|
|
183
|
+
|
|
184
|
+
if (notFoundFallback) {
|
|
185
|
+
const notFoundInfo = createNotFoundInfo(
|
|
186
|
+
error,
|
|
187
|
+
entry.shortCode,
|
|
188
|
+
entry.type,
|
|
189
|
+
pathname,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
reportError(true, {
|
|
193
|
+
notFound: true,
|
|
194
|
+
message: notFoundInfo.message,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
debugLog("segment", "notFound boundary handled error", {
|
|
198
|
+
segmentId: entry.shortCode,
|
|
199
|
+
message: notFoundInfo.message,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
setResponseStatus(404);
|
|
203
|
+
|
|
204
|
+
return createNotFoundSegment(
|
|
205
|
+
notFoundInfo,
|
|
206
|
+
notFoundFallback,
|
|
207
|
+
entry,
|
|
208
|
+
params,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const fallback = deps.findNearestErrorBoundary(entry);
|
|
214
|
+
const segmentType: ErrorInfo["segmentType"] = entry.type;
|
|
215
|
+
const errorInfo = createErrorInfo(error, entry.shortCode, segmentType);
|
|
216
|
+
const effectiveFallback = fallback ?? DefaultErrorFallback;
|
|
217
|
+
|
|
218
|
+
reportError(!!effectiveFallback);
|
|
219
|
+
|
|
220
|
+
debugLog("segment", "error boundary handled error", {
|
|
221
|
+
segmentId: entry.shortCode,
|
|
222
|
+
boundary: fallback ? "custom" : "default",
|
|
223
|
+
message: errorInfo.message,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
setResponseStatus(500);
|
|
227
|
+
|
|
228
|
+
return createErrorSegment(errorInfo, effectiveFallback, entry, params);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Generic error boundary wrapper for segment resolution.
|
|
233
|
+
* Catches non-Response errors and produces an error/notFound segment
|
|
234
|
+
* via catchSegmentError. Response throws (e.g. redirects) are re-thrown.
|
|
235
|
+
*
|
|
236
|
+
* The caller provides a `wrapError` callback to shape the error segment
|
|
237
|
+
* into the expected return type (e.g. ResolvedSegment[] for the fresh
|
|
238
|
+
* path, or SegmentRevalidationResult for the revalidation path).
|
|
239
|
+
*/
|
|
240
|
+
export async function resolveWithErrorBoundary<TEnv, TResult>(
|
|
241
|
+
entry: EntryData,
|
|
242
|
+
params: Record<string, string>,
|
|
243
|
+
resolveFn: () => Promise<TResult>,
|
|
244
|
+
wrapError: (segment: ResolvedSegment) => TResult,
|
|
245
|
+
deps: SegmentResolutionDeps<TEnv>,
|
|
246
|
+
report?: ErrorReportContext,
|
|
247
|
+
pathname?: string,
|
|
248
|
+
): Promise<TResult> {
|
|
249
|
+
try {
|
|
250
|
+
return await resolveFn();
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (error instanceof Response) throw error;
|
|
253
|
+
const segment = catchSegmentError(
|
|
254
|
+
error,
|
|
255
|
+
entry,
|
|
256
|
+
params,
|
|
257
|
+
deps,
|
|
258
|
+
report,
|
|
259
|
+
pathname,
|
|
260
|
+
);
|
|
261
|
+
return wrapError(segment);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
* this module wraps the loader execution with cache lookup/store using the
|
|
6
6
|
* getItem()/setItem() methods on SegmentCacheStore.
|
|
7
7
|
*
|
|
8
|
-
* Cache key
|
|
8
|
+
* Cache key resolution (3-tier, matching CacheScope.resolveKey):
|
|
9
|
+
* 1. options.key(requestCtx) — full override
|
|
10
|
+
* 2. store.keyGenerator(requestCtx, defaultKey) — store-level modification
|
|
11
|
+
* 3. loader:{loaderId}:{pathname}:{sortedParams} — default
|
|
12
|
+
*
|
|
13
|
+
* Values are serialized via RSC Flight (serializeResult/deserializeResult),
|
|
14
|
+
* supporting ReactNode, Promises, null, and all RSC-serializable types.
|
|
9
15
|
*
|
|
10
16
|
* On hit: returns cached data directly, skips loader execution.
|
|
11
17
|
* On stale hit (SWR): returns stale data, schedules background revalidation.
|
|
@@ -14,11 +20,32 @@
|
|
|
14
20
|
|
|
15
21
|
import type { LoaderEntry } from "../../server/context.js";
|
|
16
22
|
import type { HandlerContext } from "../../types.js";
|
|
17
|
-
import type { SegmentCacheStore } from "../../cache/types.js";
|
|
18
23
|
import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
|
|
19
24
|
import { getRequestContext } from "../../server/request-context.js";
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
import { sortedRouteParams } from "../../cache/cache-key-utils.js";
|
|
26
|
+
import {
|
|
27
|
+
resolveTtl,
|
|
28
|
+
resolveSwrWindow,
|
|
29
|
+
resolveCacheKey,
|
|
30
|
+
resolveCacheStore,
|
|
31
|
+
DEFAULT_ROUTE_TTL,
|
|
32
|
+
} from "../../cache/cache-policy.js";
|
|
33
|
+
import { readThroughItem } from "../../cache/read-through-swr.js";
|
|
34
|
+
// Lazy-loaded to avoid pulling @vitejs/plugin-rsc/rsc into modules that
|
|
35
|
+
// import segment-resolution but never use loader caching.
|
|
36
|
+
let _serializeResult: typeof import("../../cache/segment-codec.js").serializeResult;
|
|
37
|
+
let _deserializeResult: typeof import("../../cache/segment-codec.js").deserializeResult;
|
|
38
|
+
async function getCodec() {
|
|
39
|
+
if (!_serializeResult) {
|
|
40
|
+
const mod = await import("../../cache/segment-codec.js");
|
|
41
|
+
_serializeResult = mod.serializeResult;
|
|
42
|
+
_deserializeResult = mod.deserializeResult;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
serializeResult: _serializeResult,
|
|
46
|
+
deserializeResult: _deserializeResult,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
22
49
|
|
|
23
50
|
function debugLoaderCacheLog(message: string): void {
|
|
24
51
|
if (INTERNAL_RANGO_DEBUG) {
|
|
@@ -26,55 +53,65 @@ function debugLoaderCacheLog(message: string): void {
|
|
|
26
53
|
}
|
|
27
54
|
}
|
|
28
55
|
|
|
29
|
-
function
|
|
56
|
+
function getDefaultLoaderCacheKey(
|
|
30
57
|
loaderId: string,
|
|
31
58
|
pathname: string,
|
|
32
59
|
params: Record<string, string>,
|
|
33
60
|
): string {
|
|
34
|
-
const paramStr =
|
|
35
|
-
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
36
|
-
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
37
|
-
.join("&");
|
|
38
|
-
|
|
61
|
+
const paramStr = sortedRouteParams(params);
|
|
39
62
|
const base = paramStr ? `${pathname}:${paramStr}` : pathname;
|
|
40
63
|
return `loader:${loaderId}:${base}`;
|
|
41
64
|
}
|
|
42
65
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Resolve cache key using the shared 3-tier priority.
|
|
68
|
+
*/
|
|
69
|
+
async function resolveLoaderKey(
|
|
70
|
+
loaderEntry: LoaderEntry,
|
|
71
|
+
store: import("../../cache/types.js").SegmentCacheStore,
|
|
72
|
+
loaderId: string,
|
|
73
|
+
pathname: string,
|
|
74
|
+
params: Record<string, string>,
|
|
75
|
+
): Promise<string> {
|
|
76
|
+
const options = loaderEntry.cache!.options;
|
|
77
|
+
const defaultKey = getDefaultLoaderCacheKey(loaderId, pathname, params);
|
|
78
|
+
if (options === false) return defaultKey;
|
|
79
|
+
return resolveCacheKey(options.key, store, defaultKey, "LoaderCache");
|
|
53
80
|
}
|
|
54
81
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Resolve tags from cache options (static array or function).
|
|
84
|
+
* Fails open: a thrown tag callback falls back to no tags rather than
|
|
85
|
+
* aborting the request. Tags are additive metadata (not identity), so
|
|
86
|
+
* a missing tag does not cause cache collisions.
|
|
87
|
+
*/
|
|
88
|
+
function resolveTags(loaderEntry: LoaderEntry): string[] | undefined {
|
|
89
|
+
const options = loaderEntry.cache?.options;
|
|
90
|
+
if (!options || !options.tags) return undefined;
|
|
62
91
|
|
|
63
|
-
if (options.
|
|
64
|
-
|
|
65
|
-
|
|
92
|
+
if (typeof options.tags === "function") {
|
|
93
|
+
const requestCtx = getRequestContext();
|
|
94
|
+
if (!requestCtx) return undefined;
|
|
95
|
+
try {
|
|
96
|
+
return options.tags(requestCtx);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error(
|
|
99
|
+
`[LoaderCache] Tags function failed, caching without tags:`,
|
|
100
|
+
error,
|
|
101
|
+
);
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return options.tags;
|
|
66
107
|
}
|
|
67
108
|
|
|
68
|
-
function
|
|
109
|
+
function getLoaderStore(
|
|
69
110
|
loaderEntry: LoaderEntry,
|
|
70
|
-
|
|
71
|
-
): number | undefined {
|
|
111
|
+
): import("../../cache/types.js").SegmentCacheStore | null {
|
|
72
112
|
const cacheConfig = loaderEntry.cache;
|
|
73
|
-
if (!cacheConfig || cacheConfig.options === false) return
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (options.swr !== undefined) return options.swr;
|
|
77
|
-
return store.defaults?.swr;
|
|
113
|
+
if (!cacheConfig || cacheConfig.options === false) return null;
|
|
114
|
+
return resolveCacheStore(cacheConfig.options.store);
|
|
78
115
|
}
|
|
79
116
|
|
|
80
117
|
/**
|
|
@@ -110,72 +147,39 @@ export function resolveLoaderData<TEnv>(
|
|
|
110
147
|
}
|
|
111
148
|
|
|
112
149
|
const loaderId = loaderEntry.loader.$$id;
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
const swr =
|
|
150
|
+
const ttl = resolveTtl(options.ttl, store.defaults, DEFAULT_ROUTE_TTL);
|
|
151
|
+
const swrWindow = resolveSwrWindow(options.swr, store.defaults);
|
|
152
|
+
const swr = swrWindow || undefined;
|
|
153
|
+
const tags = resolveTags(loaderEntry);
|
|
116
154
|
|
|
117
155
|
// Wrap ctx.use() so cache HIT primes the handler's memoization map.
|
|
118
156
|
// ctx.use() closes over the match context's loaderPromises (not request context's).
|
|
119
157
|
// By intercepting ctx.use(), we inject cached data into the correct map.
|
|
120
158
|
const originalUse = ctx.use;
|
|
121
159
|
const dataPromise = (async () => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
};
|
|
146
|
-
if (requestCtx?.waitUntil) {
|
|
147
|
-
requestCtx.waitUntil(revalidate);
|
|
148
|
-
} else {
|
|
149
|
-
revalidate();
|
|
150
|
-
}
|
|
151
|
-
return data;
|
|
152
|
-
}
|
|
153
|
-
} catch {
|
|
154
|
-
// Cache lookup failed, fall through to fresh execution
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Cache miss — execute loader via ctx.use() (which memoizes it)
|
|
158
|
-
debugLoaderCacheLog(`[LoaderCache] MISS: ${key}`);
|
|
159
|
-
const data = await originalUse(loaderEntry.loader);
|
|
160
|
-
|
|
161
|
-
// Non-blocking cache write
|
|
162
|
-
const requestCtx = getRequestContext();
|
|
163
|
-
const cacheWrite = async () => {
|
|
164
|
-
try {
|
|
165
|
-
const serialized = JSON.stringify(data);
|
|
166
|
-
await store.setItem!(key, serialized, { ttl, swr });
|
|
167
|
-
debugLoaderCacheLog(`[LoaderCache] Cached: ${key}`);
|
|
168
|
-
} catch {
|
|
169
|
-
// Cache write failed silently
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
if (requestCtx?.waitUntil) {
|
|
173
|
-
requestCtx.waitUntil(cacheWrite);
|
|
174
|
-
} else {
|
|
175
|
-
await cacheWrite();
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return data;
|
|
160
|
+
const codec = await getCodec();
|
|
161
|
+
const key = await resolveLoaderKey(
|
|
162
|
+
loaderEntry,
|
|
163
|
+
store,
|
|
164
|
+
loaderId,
|
|
165
|
+
pathname,
|
|
166
|
+
ctx.params,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
return readThroughItem({
|
|
170
|
+
getItem: (k) => store.getItem!(k),
|
|
171
|
+
setItem: (k, v, o) => store.setItem!(k, v, o),
|
|
172
|
+
key,
|
|
173
|
+
execute: () => originalUse(loaderEntry.loader),
|
|
174
|
+
serialize: (d) => codec.serializeResult(d),
|
|
175
|
+
deserialize: (v) => codec.deserializeResult(v),
|
|
176
|
+
storeOptions: { ttl, swr, tags },
|
|
177
|
+
onHit: () => debugLoaderCacheLog(`[LoaderCache] HIT: ${key}`),
|
|
178
|
+
onStale: () => debugLoaderCacheLog(`[LoaderCache] STALE: ${key}`),
|
|
179
|
+
onMiss: () => debugLoaderCacheLog(`[LoaderCache] MISS: ${key}`),
|
|
180
|
+
onCached: () => debugLoaderCacheLog(`[LoaderCache] Cached: ${key}`),
|
|
181
|
+
host: getRequestContext(),
|
|
182
|
+
});
|
|
179
183
|
})();
|
|
180
184
|
|
|
181
185
|
// Temporarily replace ctx.use() so the handler's call returns cached data.
|