@rangojs/router 0.0.0-experimental.124 → 0.0.0-experimental.126
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 +6 -4
- package/dist/bin/rango.js +3 -4
- package/dist/vite/index.js +315 -68
- package/package.json +19 -18
- package/skills/breadcrumbs/SKILL.md +60 -0
- package/skills/hooks/SKILL.md +2 -2
- package/skills/route/SKILL.md +6 -0
- package/skills/server-actions/SKILL.md +25 -1
- package/skills/testing/SKILL.md +17 -17
- package/skills/testing/cache-prerender.md +29 -3
- package/skills/testing/flight.md +13 -10
- package/skills/testing/render-handler.md +3 -0
- package/skills/testing/server-tree.md +1 -1
- package/skills/testing/setup.md +1 -1
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +1 -1
- package/src/browser/action-fence.ts +10 -0
- package/src/browser/event-controller.ts +1 -83
- package/src/browser/navigation-store-handle.ts +3 -4
- package/src/browser/navigation-store.ts +0 -39
- package/src/browser/navigation-transaction.ts +0 -32
- package/src/browser/partial-update.ts +23 -84
- package/src/browser/prefetch/cache.ts +6 -45
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +2 -23
- package/src/browser/react/Link.tsx +0 -2
- package/src/browser/react/NavigationProvider.tsx +2 -1
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +0 -2
- package/src/browser/react/index.ts +0 -45
- package/src/browser/react/location-state-shared.ts +0 -13
- package/src/browser/react/location-state.ts +0 -1
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +0 -5
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +0 -3
- package/src/browser/react/use-params.ts +0 -2
- package/src/browser/react/use-router.ts +2 -1
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/rsc-router.tsx +10 -3
- package/src/browser/server-action-bridge.ts +51 -3
- package/src/browser/types.ts +23 -5
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/index.ts +8 -9
- package/src/build/route-trie.ts +46 -11
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/router-processing.ts +0 -8
- package/src/cache/cache-policy.ts +0 -54
- package/src/cache/cache-runtime.ts +48 -24
- package/src/cache/cache-scope.ts +0 -27
- package/src/cache/cache-tag.ts +0 -37
- package/src/cache/cf/cf-cache-store.ts +72 -45
- package/src/cache/cf/index.ts +0 -24
- package/src/cache/document-cache.ts +10 -36
- package/src/cache/handle-snapshot.ts +0 -40
- package/src/cache/index.ts +0 -27
- package/src/cache/memory-segment-store.ts +0 -52
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/types.ts +0 -98
- package/src/client.rsc.tsx +4 -22
- package/src/client.tsx +19 -32
- package/src/context-var.ts +12 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/handle.ts +2 -12
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +6 -0
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +1 -65
- package/src/host/testing.ts +0 -16
- package/src/host/types.ts +6 -2
- package/src/href-client.ts +0 -4
- package/src/index.rsc.ts +27 -2
- package/src/index.ts +7 -0
- package/src/internal-debug.ts +2 -4
- package/src/loader.rsc.ts +4 -15
- package/src/loader.ts +3 -9
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +23 -30
- package/src/prerender.ts +34 -0
- package/src/redirect-origin.ts +100 -0
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +1 -44
- package/src/route-definition/dsl-helpers.ts +7 -19
- package/src/route-definition/helpers-types.ts +3 -3
- package/src/route-definition/redirect.ts +43 -9
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-map-builder.ts +0 -16
- package/src/router/content-negotiation.ts +0 -13
- package/src/router/error-handling.ts +12 -16
- package/src/router/find-match.ts +4 -31
- package/src/router/intercept-resolution.ts +10 -1
- package/src/router/lazy-includes.ts +1 -57
- package/src/router/loader-resolution.ts +25 -23
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +1 -25
- package/src/router/match-api.ts +0 -20
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +0 -43
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +96 -179
- package/src/router/match-middleware/cache-store.ts +0 -31
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +1 -52
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +0 -116
- package/src/router/middleware.ts +77 -60
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +5 -56
- package/src/router/prerender-match.ts +56 -51
- package/src/router/request-classification.ts +1 -38
- package/src/router/revalidation.ts +14 -62
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +0 -27
- package/src/router/router-interfaces.ts +10 -0
- package/src/router/segment-resolution/fresh.ts +25 -57
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +35 -23
- package/src/router/segment-resolution/revalidation.ts +188 -283
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +0 -3
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +0 -22
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +66 -45
- package/src/router/types.ts +1 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +8 -11
- package/src/rsc/handler-context.ts +1 -0
- package/src/rsc/handler.ts +20 -4
- package/src/rsc/helpers.ts +71 -3
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/origin-guard.ts +9 -15
- package/src/rsc/progressive-enhancement.ts +10 -1
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-route-handler.ts +23 -18
- package/src/rsc/rsc-rendering.ts +2 -7
- package/src/rsc/runtime-warnings.ts +14 -0
- package/src/rsc/server-action.ts +34 -29
- package/src/rsc/types.ts +6 -3
- package/src/search-params.ts +0 -16
- package/src/segment-loader-promise.ts +14 -2
- package/src/segment-system.tsx +79 -88
- package/src/server/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +29 -92
- package/src/ssr/index.tsx +14 -14
- package/src/static-handler.ts +2 -27
- package/src/testing/cache-status.ts +44 -48
- package/src/testing/collect-handle.ts +1 -24
- package/src/testing/dispatch.ts +43 -6
- package/src/testing/e2e/index.ts +1 -22
- package/src/testing/e2e/matchers.ts +0 -16
- package/src/testing/flight-matchers.ts +0 -13
- package/src/testing/flight-normalize.ts +3 -30
- package/src/testing/flight.ts +46 -48
- package/src/testing/generated-routes.ts +1 -41
- package/src/testing/index.ts +1 -21
- package/src/testing/internal/context.ts +3 -45
- package/src/testing/internal/seed-vars.ts +0 -26
- package/src/testing/render-handler.ts +31 -61
- package/src/testing/render-route.tsx +75 -103
- package/src/testing/run-loader.ts +0 -96
- package/src/testing/run-middleware.ts +0 -26
- package/src/theme/ThemeProvider.tsx +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/error-types.ts +25 -89
- package/src/types/global-namespace.ts +4 -14
- package/src/types/handler-context.ts +28 -9
- package/src/types/index.ts +0 -10
- package/src/types/request-scope.ts +0 -19
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +0 -13
- package/src/urls/include-helper.ts +0 -4
- package/src/urls/index.ts +0 -6
- package/src/urls/path-helper-types.ts +2 -2
- package/src/urls/path-helper.ts +0 -54
- package/src/urls/urls-function.ts +0 -13
- package/src/use-loader.tsx +0 -186
- package/src/vite/discovery/bundle-postprocess.ts +2 -1
- package/src/vite/discovery/discover-routers.ts +28 -18
- package/src/vite/discovery/prerender-collection.ts +2 -4
- package/src/vite/discovery/state.ts +5 -0
- package/src/vite/discovery/virtual-module-codegen.ts +1 -11
- package/src/vite/plugin-types.ts +35 -9
- package/src/vite/plugins/cjs-to-esm.ts +0 -11
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +0 -10
- package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
- package/src/vite/plugins/expose-action-id.ts +2 -73
- package/src/vite/plugins/expose-id-utils.ts +0 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
- package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
- package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
- package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
- package/src/vite/plugins/expose-internal-ids.ts +10 -0
- package/src/vite/plugins/performance-tracks.ts +0 -3
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +21 -46
- package/src/vite/plugins/version-injector.ts +0 -20
- package/src/vite/plugins/version-plugin.ts +1 -49
- package/src/vite/plugins/virtual-entries.ts +0 -15
- package/src/vite/rango.ts +2 -108
- package/src/vite/router-discovery.ts +9 -1
- package/src/vite/utils/ast-handler-extract.ts +0 -16
- package/src/vite/utils/bundle-analysis.ts +6 -13
- package/src/vite/utils/client-chunks.ts +0 -6
- package/src/vite/utils/forward-user-plugins.ts +0 -22
- package/src/vite/utils/manifest-utils.ts +0 -4
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +0 -35
- package/src/vite/utils/shared-utils.ts +3 -35
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -9,6 +9,7 @@ import type { NonceProvider } from "../rsc/types.js";
|
|
|
9
9
|
import type { ExecutionContext } from "../server/request-context.js";
|
|
10
10
|
import type { SerializedSegmentData } from "../cache/types.js";
|
|
11
11
|
import type { MiddlewareEntry, MiddlewareFn } from "./middleware.js";
|
|
12
|
+
import type { ExtractParams } from "../types/route-config.js";
|
|
12
13
|
import { RSC_ROUTER_BRAND } from "./router-registry.js";
|
|
13
14
|
import type { RangoOptions, RootLayoutProps } from "./router-options.js";
|
|
14
15
|
import type { DefaultVars } from "../types/global-namespace.js";
|
|
@@ -105,9 +106,14 @@ export interface Rango<
|
|
|
105
106
|
* createRouter({ document: RootLayout })
|
|
106
107
|
* .use(loggerMiddleware) // All routes
|
|
107
108
|
* .use("/api/*", rateLimiter) // Pattern match
|
|
109
|
+
* .use("/users/:id", (ctx) => {}) // ctx.params.id is typed
|
|
108
110
|
* .routes(urlpatterns)
|
|
109
111
|
* ```
|
|
110
112
|
*/
|
|
113
|
+
use<Pattern extends string>(
|
|
114
|
+
pattern: Pattern,
|
|
115
|
+
middleware: MiddlewareFn<TEnv, ExtractParams<Pattern>>,
|
|
116
|
+
): Rango<TEnv, TRoutes>;
|
|
111
117
|
use(
|
|
112
118
|
patternOrMiddleware: string | MiddlewareFn<TEnv>,
|
|
113
119
|
middleware?: MiddlewareFn<TEnv>,
|
|
@@ -225,6 +231,10 @@ export interface RangoInternal<
|
|
|
225
231
|
/**
|
|
226
232
|
* Add global middleware that runs on all routes
|
|
227
233
|
*/
|
|
234
|
+
use<Pattern extends string>(
|
|
235
|
+
pattern: Pattern,
|
|
236
|
+
middleware: MiddlewareFn<TEnv, ExtractParams<Pattern>>,
|
|
237
|
+
): Rango<TEnv, TRoutes>;
|
|
228
238
|
use(
|
|
229
239
|
patternOrMiddleware: string | MiddlewareFn<TEnv>,
|
|
230
240
|
middleware?: MiddlewareFn<TEnv>,
|
|
@@ -27,59 +27,17 @@ import {
|
|
|
27
27
|
tryStaticSlot,
|
|
28
28
|
resolveLayoutComponent,
|
|
29
29
|
resolveWithErrorBoundary,
|
|
30
|
+
warnOnStreamedResponse,
|
|
30
31
|
} from "./helpers.js";
|
|
31
32
|
import { applyViewTransitionDefault } from "./view-transition-default.js";
|
|
32
33
|
import { getRouterContext } from "../router-context.js";
|
|
33
|
-
import {
|
|
34
|
+
import { observeStreamedHandler } from "./streamed-handler-telemetry.js";
|
|
34
35
|
import {
|
|
35
36
|
track,
|
|
36
37
|
RangoContext,
|
|
37
38
|
runInsideLoaderScope,
|
|
38
39
|
} from "../../server/context.js";
|
|
39
40
|
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
// Streamed handler telemetry
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Attach a fire-and-forget rejection observer to a streamed handler promise.
|
|
46
|
-
* React catches the actual error via its error boundary; this only emits
|
|
47
|
-
* the handler.error telemetry event.
|
|
48
|
-
*/
|
|
49
|
-
function observeStreamedHandler(
|
|
50
|
-
promise: Promise<ReactNode>,
|
|
51
|
-
segmentId: string,
|
|
52
|
-
segmentType: string,
|
|
53
|
-
pathname?: string,
|
|
54
|
-
routeKey?: string,
|
|
55
|
-
params?: Record<string, string>,
|
|
56
|
-
): void {
|
|
57
|
-
let routerCtx;
|
|
58
|
-
try {
|
|
59
|
-
routerCtx = getRouterContext();
|
|
60
|
-
} catch {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
if (!routerCtx?.telemetry) return;
|
|
64
|
-
const sink = resolveSink(routerCtx.telemetry);
|
|
65
|
-
const reqId = routerCtx.requestId;
|
|
66
|
-
promise.catch((err: unknown) => {
|
|
67
|
-
const errorObj = err instanceof Error ? err : new Error(String(err));
|
|
68
|
-
safeEmit(sink, {
|
|
69
|
-
type: "handler.error",
|
|
70
|
-
timestamp: performance.now(),
|
|
71
|
-
requestId: reqId,
|
|
72
|
-
segmentId,
|
|
73
|
-
segmentType,
|
|
74
|
-
error: errorObj,
|
|
75
|
-
handledByBoundary: true,
|
|
76
|
-
pathname,
|
|
77
|
-
routeKey,
|
|
78
|
-
params,
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
41
|
// ---------------------------------------------------------------------------
|
|
84
42
|
// Fresh path (full match, no revalidation)
|
|
85
43
|
// ---------------------------------------------------------------------------
|
|
@@ -133,18 +91,32 @@ export async function resolveLoaders<TEnv>(
|
|
|
133
91
|
|
|
134
92
|
// Loading disabled: still start all loaders in parallel, but only emit
|
|
135
93
|
// settled promises so handlers don't stream loading placeholders.
|
|
136
|
-
|
|
94
|
+
//
|
|
95
|
+
// Wrap each loader promise with wrapLoaderPromise BEFORE awaiting. The wrapped
|
|
96
|
+
// promise resolves to a LoaderDataResult and never rejects, routing a failed
|
|
97
|
+
// loader to its own per-loader error boundary. Awaiting the RAW promises here
|
|
98
|
+
// instead would (1) propagate a rejection to the segment-level boundary,
|
|
99
|
+
// collapsing the whole entry and discarding successful sibling data, and
|
|
100
|
+
// (2) leave the other in-flight raw promises without a .catch, producing
|
|
101
|
+
// unhandled rejections. Mirrors the loading path and intercept-resolution.
|
|
102
|
+
const pendingLoaderData = loaderEntries.map((loaderEntry, i) => {
|
|
103
|
+
const { loader } = loaderEntry;
|
|
104
|
+
const segmentId = `${shortCode}D${i}.${loader.$$id}`;
|
|
137
105
|
const start = performance.now();
|
|
138
|
-
const
|
|
139
|
-
|
|
106
|
+
const wrapped = deps.wrapLoaderPromise(
|
|
107
|
+
runInsideLoaderScope(() =>
|
|
108
|
+
resolveLoaderData(loaderEntry, ctx, ctx.pathname),
|
|
109
|
+
),
|
|
110
|
+
entry,
|
|
111
|
+
segmentId,
|
|
112
|
+
ctx.pathname,
|
|
140
113
|
);
|
|
141
|
-
return {
|
|
114
|
+
return { wrapped, start, segmentId, loaderId: loader.$$id };
|
|
142
115
|
});
|
|
143
|
-
await Promise.all(pendingLoaderData.map((p) => p.
|
|
116
|
+
await Promise.all(pendingLoaderData.map((p) => p.wrapped));
|
|
144
117
|
|
|
145
118
|
return loaderEntries.map((loaderEntry, i) => {
|
|
146
119
|
const { loader } = loaderEntry;
|
|
147
|
-
const segmentId = `${shortCode}D${i}.${loader.$$id}`;
|
|
148
120
|
const pending = pendingLoaderData[i]!;
|
|
149
121
|
if (ms && !ms.metrics.some((m) => m.label === `loader:${loader.$$id}`)) {
|
|
150
122
|
// All loaders ran in parallel via Promise.all — each span covers
|
|
@@ -160,19 +132,14 @@ export async function resolveLoaders<TEnv>(
|
|
|
160
132
|
);
|
|
161
133
|
}
|
|
162
134
|
return {
|
|
163
|
-
id: segmentId,
|
|
135
|
+
id: pending.segmentId,
|
|
164
136
|
namespace: entry.id,
|
|
165
137
|
type: "loader" as const,
|
|
166
138
|
index: i,
|
|
167
139
|
component: null,
|
|
168
140
|
params: ctx.params,
|
|
169
141
|
loaderId: loader.$$id,
|
|
170
|
-
loaderData:
|
|
171
|
-
pending.promise,
|
|
172
|
-
entry,
|
|
173
|
-
segmentId,
|
|
174
|
-
ctx.pathname,
|
|
175
|
-
),
|
|
142
|
+
loaderData: pending.wrapped,
|
|
176
143
|
belongsToRoute,
|
|
177
144
|
};
|
|
178
145
|
});
|
|
@@ -297,6 +264,7 @@ export async function resolveSegment<TEnv>(
|
|
|
297
264
|
if (entry.loading) {
|
|
298
265
|
const result = handleHandlerResult(handler(context));
|
|
299
266
|
if (result instanceof Promise) {
|
|
267
|
+
warnOnStreamedResponse(result, entry.id);
|
|
300
268
|
result.finally(doneRouteHandler).catch(() => {});
|
|
301
269
|
const tracked = deps.trackHandler(result, {
|
|
302
270
|
segmentId: entry.shortCode,
|
|
@@ -52,6 +52,40 @@ export function handleHandlerResult(
|
|
|
52
52
|
return result;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Dev-only: warn when a handler on a route that declares loading() resolves or
|
|
57
|
+
* rejects with a Response (e.g. redirect()).
|
|
58
|
+
*
|
|
59
|
+
* On a non-loading route a returned/thrown Response short-circuits to an HTTP
|
|
60
|
+
* redirect. But when the route declares loading(), the handler result is
|
|
61
|
+
* streamed (not awaited at the resolution boundary), so the Response surfaces
|
|
62
|
+
* only during RSC serialization and is rendered into the stream instead of
|
|
63
|
+
* becoming a 302/308 — a silent failure mode. Issue redirects from middleware,
|
|
64
|
+
* a loader, or a synchronous handler return instead. Compiled out in production.
|
|
65
|
+
*/
|
|
66
|
+
export function warnOnStreamedResponse(
|
|
67
|
+
result: Promise<unknown>,
|
|
68
|
+
entryId: string,
|
|
69
|
+
): void {
|
|
70
|
+
if (process.env.NODE_ENV === "production") return;
|
|
71
|
+
// A Response can surface either as a rejection (handleHandlerResult rethrows a
|
|
72
|
+
// resolved Response) or as a resolved value (the raw parallel-slot handler is
|
|
73
|
+
// not run through handleHandlerResult). Check both so every streamed path is
|
|
74
|
+
// covered. Each handler is an independent observer; it does not consume the
|
|
75
|
+
// rejection for the trackHandler/observeStreamedHandler chains.
|
|
76
|
+
const check = (value: unknown) => {
|
|
77
|
+
if (value instanceof Response) {
|
|
78
|
+
console.warn(
|
|
79
|
+
`[rango] Handler for "${entryId}" returned a Response (e.g. ` +
|
|
80
|
+
`redirect()), but it declares loading(): the Response is rendered ` +
|
|
81
|
+
`into the RSC stream, NOT sent as an HTTP redirect. Issue redirects ` +
|
|
82
|
+
`from middleware, a loader, or a synchronous handler return.`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
result.then(check, check);
|
|
87
|
+
}
|
|
88
|
+
|
|
55
89
|
// ---------------------------------------------------------------------------
|
|
56
90
|
// Static handler interception
|
|
57
91
|
// ---------------------------------------------------------------------------
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Cache key resolution (3-tier, matching CacheScope.resolveKey):
|
|
9
9
|
* 1. options.key(requestCtx) — full override
|
|
10
10
|
* 2. store.keyGenerator(requestCtx, defaultKey) — store-level modification
|
|
11
|
-
* 3. loader:{loaderId}:{pathname}:{sortedParams} — default
|
|
11
|
+
* 3. loader:{loaderId}:{host}{pathname}:{sortedParams} — default
|
|
12
12
|
*
|
|
13
13
|
* Values are serialized via RSC Flight (serializeResult/deserializeResult),
|
|
14
14
|
* supporting ReactNode, Promises, null, and all RSC-serializable types.
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import type { LoaderEntry } from "../../server/context.js";
|
|
22
|
-
import type { HandlerContext } from "../../types.js";
|
|
22
|
+
import type { HandlerContext, InternalHandlerContext } from "../../types.js";
|
|
23
23
|
import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
|
|
24
24
|
import { getRequestContext } from "../../server/request-context.js";
|
|
25
25
|
import { sortedRouteParams } from "../../cache/cache-key-utils.js";
|
|
@@ -57,12 +57,13 @@ function debugLoaderCacheLog(message: string): void {
|
|
|
57
57
|
|
|
58
58
|
function getDefaultLoaderCacheKey(
|
|
59
59
|
loaderId: string,
|
|
60
|
+
host: string,
|
|
60
61
|
pathname: string,
|
|
61
62
|
params: Record<string, string>,
|
|
62
63
|
): string {
|
|
63
64
|
const paramStr = sortedRouteParams(params);
|
|
64
65
|
const base = paramStr ? `${pathname}:${paramStr}` : pathname;
|
|
65
|
-
return `loader:${loaderId}:${base}`;
|
|
66
|
+
return `loader:${loaderId}:${host}${base}`;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
/**
|
|
@@ -76,7 +77,13 @@ async function resolveLoaderKey(
|
|
|
76
77
|
params: Record<string, string>,
|
|
77
78
|
): Promise<string> {
|
|
78
79
|
const options = loaderEntry.cache!.options;
|
|
79
|
-
|
|
80
|
+
// The host is part of the loader cache identity, matching the route-level
|
|
81
|
+
// cache (cache-scope getCacheKeyBase: `${host}${pathname}`) and "use cache"
|
|
82
|
+
// (cache-runtime pushes ctx.url.host). Without it, a multi-tenant host router
|
|
83
|
+
// serving the same pathname for different hosts would leak one host's cached
|
|
84
|
+
// loader data to another.
|
|
85
|
+
const host = getRequestContext()?.url?.host ?? "localhost";
|
|
86
|
+
const defaultKey = getDefaultLoaderCacheKey(loaderId, host, pathname, params);
|
|
80
87
|
if (options === false) return defaultKey;
|
|
81
88
|
return resolveCacheKey(options.key, store, defaultKey, "LoaderCache");
|
|
82
89
|
}
|
|
@@ -139,15 +146,30 @@ export function resolveLoaderData<TEnv>(
|
|
|
139
146
|
const swrWindow = resolveSwrWindow(options.swr, store.defaults);
|
|
140
147
|
const swr = swrWindow || undefined;
|
|
141
148
|
const tags = resolveTags(loaderEntry);
|
|
142
|
-
// Loader tags are config-derived, so they are the complete set whether this is
|
|
143
|
-
// a cache hit or miss; record them every time so a document built from this
|
|
144
|
-
// loader is tagged for invalidation.
|
|
145
149
|
recordRequestTags(tags);
|
|
146
150
|
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
+
// A handler that later awaits this same loader via ctx.use(loader) must get
|
|
152
|
+
// THIS memoized promise, not a fresh execution. Rather than rebind ctx.use
|
|
153
|
+
// once per cached loader (O(N) chained wrappers + a synchronous
|
|
154
|
+
// capture-before-overwrite invariant), install a single stable interceptor on
|
|
155
|
+
// the first cached loader that consults a per-ctx override table, then just
|
|
156
|
+
// prime the table for each subsequent cached loader. The captured pre-
|
|
157
|
+
// interceptor `originalUse` (whatever setup mode installed it) runs the
|
|
158
|
+
// cache-miss execute, so a loader never awaits its own in-flight promise.
|
|
159
|
+
const internal = ctx as InternalHandlerContext<any, TEnv>;
|
|
160
|
+
let overrides = internal._loaderCacheOverrides;
|
|
161
|
+
if (!overrides) {
|
|
162
|
+
overrides = internal._loaderCacheOverrides = new Map();
|
|
163
|
+
const originalUse = ctx.use;
|
|
164
|
+
internal._loaderCacheOriginalUse = originalUse;
|
|
165
|
+
ctx.use = ((item: any) => {
|
|
166
|
+
const cached = overrides!.get(item?.$$id);
|
|
167
|
+
if (cached) return cached;
|
|
168
|
+
return originalUse(item);
|
|
169
|
+
}) as typeof ctx.use;
|
|
170
|
+
}
|
|
171
|
+
const runMiss = internal._loaderCacheOriginalUse!;
|
|
172
|
+
|
|
151
173
|
const dataPromise = (async () => {
|
|
152
174
|
const codec = await getCodec();
|
|
153
175
|
const key = await resolveLoaderKey(
|
|
@@ -162,7 +184,7 @@ export function resolveLoaderData<TEnv>(
|
|
|
162
184
|
getItem: (k) => store.getItem!(k),
|
|
163
185
|
setItem: (k, v, o) => store.setItem!(k, v, o),
|
|
164
186
|
key,
|
|
165
|
-
execute: () =>
|
|
187
|
+
execute: () => runMiss(loaderEntry.loader),
|
|
166
188
|
serialize: (d) => codec.serializeResult(d),
|
|
167
189
|
deserialize: (v) => codec.deserializeResult(v),
|
|
168
190
|
storeOptions: { ttl, swr, tags },
|
|
@@ -174,17 +196,7 @@ export function resolveLoaderData<TEnv>(
|
|
|
174
196
|
});
|
|
175
197
|
})();
|
|
176
198
|
|
|
177
|
-
|
|
178
|
-
// This is needed because ctx.use() closes over the match context's loaderPromises
|
|
179
|
-
// map which is separate from the request context. By wrapping use(), we intercept
|
|
180
|
-
// the handler's call and return the shared dataPromise.
|
|
181
|
-
const wrappedUse = ((item: any) => {
|
|
182
|
-
if (item === loaderEntry.loader || item?.$$id === loaderId) {
|
|
183
|
-
return dataPromise;
|
|
184
|
-
}
|
|
185
|
-
return originalUse(item);
|
|
186
|
-
}) as typeof ctx.use;
|
|
187
|
-
ctx.use = wrappedUse;
|
|
199
|
+
overrides.set(loaderId, dataPromise);
|
|
188
200
|
|
|
189
201
|
return dataPromise;
|
|
190
202
|
}
|