@rangojs/router 0.0.0-experimental.130 → 0.0.0-experimental.132
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/dist/bin/rango.js +69 -24
- package/dist/vite/index.js +182 -41
- package/package.json +6 -3
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/event-controller.ts +5 -4
- package/src/browser/partial-update.ts +32 -16
- package/src/browser/react/NavigationProvider.tsx +6 -83
- package/src/browser/react/filter-segment-order.ts +17 -0
- package/src/browser/react/use-link-status.ts +10 -2
- package/src/browser/react/use-navigation.ts +10 -2
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/include-resolution.ts +109 -21
- package/src/build/route-types/per-module-writer.ts +15 -2
- package/src/cache/cache-key-utils.ts +29 -13
- package/src/cache/cf/cf-cache-store.ts +129 -5
- package/src/decode-loader-results.ts +11 -1
- package/src/encode-kv.ts +49 -0
- package/src/handles/meta.ts +5 -1
- package/src/host/cookie-handler.ts +2 -21
- package/src/prerender/param-hash.ts +6 -5
- package/src/regex-escape.ts +8 -0
- package/src/route-definition/dsl-helpers.ts +6 -2
- package/src/router/error-handling.ts +32 -1
- package/src/router/handler-context.ts +6 -1
- package/src/router/instrument.ts +56 -14
- package/src/router/intercept-resolution.ts +16 -1
- package/src/router/loader-resolution.ts +49 -19
- package/src/router/match-middleware/background-revalidation.ts +6 -0
- package/src/router/match-middleware/cache-store.ts +6 -0
- package/src/router/middleware.ts +67 -27
- package/src/router/pattern-matching.ts +3 -9
- package/src/router/revalidation.ts +65 -23
- package/src/router/router-context.ts +1 -0
- package/src/router/router-options.ts +3 -3
- package/src/router/segment-resolution/fresh.ts +8 -9
- package/src/router/segment-resolution/helpers.ts +11 -10
- package/src/router/segment-resolution/loader-cache.ts +13 -0
- package/src/router/segment-resolution/revalidation.ts +4 -4
- package/src/router/segment-wrappers.ts +3 -0
- package/src/router/trie-matching.ts +74 -20
- package/src/router.ts +2 -2
- package/src/rsc/progressive-enhancement.ts +20 -0
- package/src/rsc/server-action.ts +124 -47
- package/src/search-params.ts +8 -6
- package/src/segment-system.tsx +7 -1
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/handle-store.ts +14 -14
- package/src/server/request-context.ts +5 -26
- package/src/ssr/index.tsx +5 -4
- package/src/testing/render-handler.ts +11 -0
- package/src/vite/plugins/expose-id-utils.ts +77 -2
- package/src/vite/plugins/expose-ids/export-analysis.ts +30 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +82 -12
- package/src/vite/utils/prerender-utils.ts +1 -3
|
@@ -112,10 +112,25 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
112
112
|
};
|
|
113
113
|
stale?: boolean;
|
|
114
114
|
},
|
|
115
|
+
options?: {
|
|
116
|
+
/**
|
|
117
|
+
* Skip the intercept's middleware execution. Set ONLY by the post-response
|
|
118
|
+
* background re-render paths (proactive caching, stale background
|
|
119
|
+
* revalidation), whose sole purpose is to re-render the segment tree to
|
|
120
|
+
* populate the cache. The foreground request already ran the intercept
|
|
121
|
+
* middleware before the response was sent — it validated auth, set cookies,
|
|
122
|
+
* and wrote context vars into the request context's shared `_variables`,
|
|
123
|
+
* which the background render reuses. Re-running middleware here would fire
|
|
124
|
+
* its side effects a SECOND time, and a middleware that short-circuits with
|
|
125
|
+
* a Response would `throw` and silently abort the cache write. Never set on
|
|
126
|
+
* the foreground path.
|
|
127
|
+
*/
|
|
128
|
+
skipMiddleware?: boolean;
|
|
129
|
+
},
|
|
115
130
|
): Promise<ResolvedSegment[]> {
|
|
116
131
|
const segments: ResolvedSegment[] = [];
|
|
117
132
|
|
|
118
|
-
if (interceptEntry.middleware.length > 0) {
|
|
133
|
+
if (!options?.skipMiddleware && interceptEntry.middleware.length > 0) {
|
|
119
134
|
const requestCtx = getRequestContext();
|
|
120
135
|
if (!requestCtx?.res) {
|
|
121
136
|
throw new Error(
|
|
@@ -109,16 +109,40 @@ export function wrapLoaderWithErrorHandling<T>(
|
|
|
109
109
|
};
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
// Render fallback on server
|
|
112
|
+
// Render fallback on server. The user ErrorBoundaryHandler may throw
|
|
113
|
+
// synchronously; if it does we must NOT let that rejection escape — the
|
|
114
|
+
// wrapped LoaderDataResult promise is contracted to never reject (see
|
|
115
|
+
// segment-resolution/fresh.ts `await Promise.all(...wrapped)`), and a
|
|
116
|
+
// rejection here would collapse the whole entry and discard healthy
|
|
117
|
+
// sibling loader data. On a fallback-render throw, fall back to the
|
|
118
|
+
// no-boundary result (fallback: null) so the client throws the ORIGINAL
|
|
119
|
+
// error, and the wrapped promise still resolves to a LoaderDataResult.
|
|
113
120
|
let renderedFallback: ReactNode;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
121
|
+
try {
|
|
122
|
+
if (typeof fallback === "function") {
|
|
123
|
+
// ErrorBoundaryHandler - call with error info
|
|
124
|
+
const props: ErrorBoundaryFallbackProps = {
|
|
125
|
+
error: errorInfo,
|
|
126
|
+
};
|
|
127
|
+
renderedFallback = fallback(props);
|
|
128
|
+
} else {
|
|
129
|
+
renderedFallback = fallback;
|
|
130
|
+
}
|
|
131
|
+
} catch (fallbackError) {
|
|
132
|
+
debugLog("loader", "error boundary fallback render threw", {
|
|
133
|
+
segmentId,
|
|
134
|
+
message: errorInfo.message,
|
|
135
|
+
fallbackError:
|
|
136
|
+
fallbackError instanceof Error
|
|
137
|
+
? fallbackError.message
|
|
138
|
+
: String(fallbackError),
|
|
139
|
+
});
|
|
140
|
+
return {
|
|
141
|
+
__loaderResult: true,
|
|
142
|
+
ok: false,
|
|
117
143
|
error: errorInfo,
|
|
144
|
+
fallback: null,
|
|
118
145
|
};
|
|
119
|
-
renderedFallback = fallback(props);
|
|
120
|
-
} else {
|
|
121
|
-
renderedFallback = fallback;
|
|
122
146
|
}
|
|
123
147
|
|
|
124
148
|
debugLog("loader", "loader error wrapped with boundary fallback", {
|
|
@@ -542,18 +566,21 @@ export function setupBuildUse<TEnv>(ctx: HandlerContext<any, TEnv>): void {
|
|
|
542
566
|
);
|
|
543
567
|
}
|
|
544
568
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
)
|
|
548
|
-
|
|
569
|
+
// Wrap with withDefer so ctx.use(Handle).defer(...) works on the build /
|
|
570
|
+
// prerender path, matching production setupLoaderAccess. Without it a
|
|
571
|
+
// prerender handler calling .defer() throws "defer is not a function".
|
|
572
|
+
return withDefer(
|
|
573
|
+
(dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
|
|
574
|
+
if (!store) return;
|
|
549
575
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
576
|
+
const valueOrPromise =
|
|
577
|
+
typeof dataOrFn === "function"
|
|
578
|
+
? (dataOrFn as () => Promise<unknown>)()
|
|
579
|
+
: dataOrFn;
|
|
554
580
|
|
|
555
|
-
|
|
556
|
-
|
|
581
|
+
store.push(handle.$$id, segmentId, valueOrPromise);
|
|
582
|
+
},
|
|
583
|
+
);
|
|
557
584
|
}
|
|
558
585
|
|
|
559
586
|
// Loader case: not available during pre-rendering
|
|
@@ -581,8 +608,11 @@ export function setupLoaderAccessSilent<TEnv>(
|
|
|
581
608
|
|
|
582
609
|
ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
|
|
583
610
|
if (isHandle(item)) {
|
|
584
|
-
// Silent mode - return a no-op so handle data is not pushed during caching
|
|
585
|
-
|
|
611
|
+
// Silent mode - return a no-op so handle data is not pushed during caching.
|
|
612
|
+
// Wrap with withDefer so ctx.use(Handle).defer(...) still resolves to a
|
|
613
|
+
// callable resolver (also a no-op here), matching production's push shape
|
|
614
|
+
// instead of throwing "defer is not a function".
|
|
615
|
+
return withDefer((_dataOrFn: unknown) => {});
|
|
586
616
|
}
|
|
587
617
|
|
|
588
618
|
return useLoader(item as LoaderDefinition<any, any>, null);
|
|
@@ -203,6 +203,12 @@ export function withBackgroundRevalidation<TEnv>(
|
|
|
203
203
|
ctx.matched.params,
|
|
204
204
|
freshHandlerContext,
|
|
205
205
|
true,
|
|
206
|
+
undefined,
|
|
207
|
+
// Skip intercept middleware: this is a post-response background
|
|
208
|
+
// re-render to refresh a stale cached route. The foreground
|
|
209
|
+
// already ran the middleware; re-running it would double its side
|
|
210
|
+
// effects and a short-circuit Response would abort the write.
|
|
211
|
+
{ skipMiddleware: true },
|
|
206
212
|
),
|
|
207
213
|
);
|
|
208
214
|
}
|
|
@@ -243,6 +243,12 @@ export function withCacheStore<TEnv>(
|
|
|
243
243
|
proactiveHandlerContext,
|
|
244
244
|
true, // belongsToRoute
|
|
245
245
|
// No revalidationContext = render fresh
|
|
246
|
+
undefined,
|
|
247
|
+
// Skip intercept middleware: the foreground already ran it
|
|
248
|
+
// before the response was sent. Re-running here (post-response,
|
|
249
|
+
// background) would fire side effects twice and a short-circuit
|
|
250
|
+
// Response would silently abort this cache write.
|
|
251
|
+
{ skipMiddleware: true },
|
|
246
252
|
),
|
|
247
253
|
);
|
|
248
254
|
}
|
package/src/router/middleware.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/// <reference types="vite/types/importMeta.d.ts" />
|
|
2
2
|
|
|
3
3
|
import { contextGet, contextSet } from "../context-var.js";
|
|
4
|
+
import { escapeRegExp } from "../regex-escape.js";
|
|
5
|
+
import { parsePattern as parseRoutePattern } from "./pattern-matching.js";
|
|
4
6
|
import { safeDecodeURIComponent } from "./url-params.js";
|
|
5
7
|
import { fireAndForgetWaitUntil } from "../types/request-scope.js";
|
|
6
8
|
import type {
|
|
@@ -48,7 +50,43 @@ function getMiddlewareMetricLabel<TEnv>(
|
|
|
48
50
|
return `middleware:${scope}#${ordinal + 1}`;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Compile a middleware scope pattern to a regex + param names.
|
|
55
|
+
*
|
|
56
|
+
* Middleware scopes reuse the route pattern parser (`parsePattern` from
|
|
57
|
+
* pattern-matching.ts), so they support the same param forms as routes —
|
|
58
|
+
* optional (`:x?`), constrained (`:x(en|gb)`), and suffix (`:x.html`) — in
|
|
59
|
+
* addition to the trailing-`*` wildcard middleware relies on. Before this
|
|
60
|
+
* unification the middleware-side parser handled only static, bare `:param`,
|
|
61
|
+
* and trailing `*`, so e.g. `router.use("/:locale(en|gb)/*", mw)` silently
|
|
62
|
+
* named the param "locale(en|gb)" and never enforced the constraint.
|
|
63
|
+
*
|
|
64
|
+
* Middleware matching semantics deliberately differ from route matching, so we
|
|
65
|
+
* emit the regex here rather than route through `compilePattern`:
|
|
66
|
+
* - `*` alone matches every path (`/^.*$/`).
|
|
67
|
+
* - A trailing `*` segment is an OPTIONAL subtree match (`(?:/.*)?`): `/admin/*`
|
|
68
|
+
* matches `/admin`, `/admin/`, and `/admin/users`. It contributes no param
|
|
69
|
+
* name (unlike route wildcards, which capture `*`).
|
|
70
|
+
* - A NON-trailing `*` is also OPTIONAL (`(?:/.*)?`), matching zero-or-more
|
|
71
|
+
* intermediate segments: `/a/<star>/b` matches both `/a/b` and `/a/x/b`. This
|
|
72
|
+
* mirrors the pre-unification parser, which compiled every `*` part as
|
|
73
|
+
* optional regardless of position.
|
|
74
|
+
* - A pattern without a trailing `*` tolerates a trailing slash (`/?$`).
|
|
75
|
+
* - Constraints are baked into the regex as an alternation so `matchMiddleware`
|
|
76
|
+
* (a bare `regex.test`) enforces them without extra validation. Constraint
|
|
77
|
+
* values are matched against the raw (still URL-encoded) path segment, which
|
|
78
|
+
* matches the pre-unification middleware behavior (it never decoded for
|
|
79
|
+
* matching); the constraint string is regex-escaped so values like `en.gb`
|
|
80
|
+
* are treated literally.
|
|
81
|
+
*
|
|
82
|
+
* The route segment parser only recognizes `/`-prefixed segments, but the
|
|
83
|
+
* pre-unification middleware parser split on `/` and dropped empty parts, so a
|
|
84
|
+
* leading slash was irrelevant: `use("admin/*")` and `use("/admin/*")` scoped
|
|
85
|
+
* identically. Normalize a non-`*` pattern to have a leading slash before
|
|
86
|
+
* parsing so that behavior is preserved (without it, `parseRoutePattern("admin/*")`
|
|
87
|
+
* drops the static `admin` and the scope explodes to every path).
|
|
88
|
+
*/
|
|
89
|
+
export function compileMiddlewarePattern(pattern: string): {
|
|
52
90
|
regex: RegExp;
|
|
53
91
|
paramNames: string[];
|
|
54
92
|
} {
|
|
@@ -56,45 +94,47 @@ export function parsePattern(pattern: string): {
|
|
|
56
94
|
return { regex: /^.*$/, paramNames: [] };
|
|
57
95
|
}
|
|
58
96
|
|
|
97
|
+
const normalizedPattern = pattern.startsWith("/") ? pattern : `/${pattern}`;
|
|
98
|
+
const segments = parseRoutePattern(normalizedPattern);
|
|
59
99
|
const paramNames: string[] = [];
|
|
60
100
|
let regexStr = "^";
|
|
101
|
+
let hasTrailingWildcard = false;
|
|
61
102
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
for (let i = 0; i < parts.length; i++) {
|
|
65
|
-
const part = parts[i];
|
|
103
|
+
for (let i = 0; i < segments.length; i++) {
|
|
104
|
+
const segment = segments[i];
|
|
66
105
|
|
|
67
|
-
if (
|
|
68
|
-
//
|
|
106
|
+
if (segment.type === "wildcard") {
|
|
107
|
+
// Optional subtree match (parity with the original middleware parser,
|
|
108
|
+
// which compiled every `*` as `(?:/.*)?`). A trailing `*` matches the
|
|
109
|
+
// subtree; a non-trailing `*` matches zero-or-more intermediate segments,
|
|
110
|
+
// so `/a/<star>/b` still matches `/a/b`.
|
|
69
111
|
regexStr += "(?:/.*)?";
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
112
|
+
if (i === segments.length - 1) {
|
|
113
|
+
hasTrailingWildcard = true;
|
|
114
|
+
}
|
|
115
|
+
} else if (segment.type === "param") {
|
|
116
|
+
paramNames.push(segment.value);
|
|
117
|
+
const suffixPattern = segment.suffix ? escapeRegExp(segment.suffix) : "";
|
|
118
|
+
const valuePattern = segment.constraint
|
|
119
|
+
? `(${segment.constraint.map(escapeRegExp).join("|")})`
|
|
120
|
+
: "([^/]+)";
|
|
121
|
+
if (segment.optional) {
|
|
122
|
+
regexStr += `(?:/${valuePattern}${suffixPattern})?`;
|
|
123
|
+
} else {
|
|
124
|
+
regexStr += `/${valuePattern}${suffixPattern}`;
|
|
125
|
+
}
|
|
75
126
|
} else {
|
|
76
|
-
//
|
|
77
|
-
regexStr += "/" +
|
|
127
|
+
// Static literal
|
|
128
|
+
regexStr += "/" + escapeRegExp(segment.value);
|
|
78
129
|
}
|
|
79
130
|
}
|
|
80
131
|
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
regexStr += "/?$";
|
|
84
|
-
} else {
|
|
85
|
-
regexStr += "$";
|
|
86
|
-
}
|
|
132
|
+
// Without a trailing `*`, match exactly with an optional trailing slash.
|
|
133
|
+
regexStr += hasTrailingWildcard ? "$" : "/?$";
|
|
87
134
|
|
|
88
135
|
return { regex: new RegExp(regexStr), paramNames };
|
|
89
136
|
}
|
|
90
137
|
|
|
91
|
-
/**
|
|
92
|
-
* Escape special regex characters
|
|
93
|
-
*/
|
|
94
|
-
function escapeRegex(str: string): string {
|
|
95
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
96
|
-
}
|
|
97
|
-
|
|
98
138
|
/**
|
|
99
139
|
* Extract params from a pathname using a pattern's regex and param names.
|
|
100
140
|
*
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { RouteEntry, TrailingSlashMode } from "../types";
|
|
8
8
|
import type { EntryData } from "../server/context";
|
|
9
9
|
import { debugLog, isRouterDebugEnabled } from "./logging.js";
|
|
10
|
+
import { escapeRegExp } from "../regex-escape.js";
|
|
10
11
|
import { safeDecodeURIComponent } from "./url-params.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -151,7 +152,7 @@ export function compilePattern(pattern: string): CompiledPattern {
|
|
|
151
152
|
regexPattern += "/(.*)";
|
|
152
153
|
} else if (segment.type === "param") {
|
|
153
154
|
paramNames.push(segment.value);
|
|
154
|
-
const suffixPattern = segment.suffix ?
|
|
155
|
+
const suffixPattern = segment.suffix ? escapeRegExp(segment.suffix) : "";
|
|
155
156
|
// Constrained params capture anything here; the allowed values are
|
|
156
157
|
// checked post-decode in findMatch so URL-encoded constraint values
|
|
157
158
|
// (e.g. `:lang(en GB)` via `/en%20GB`) still match.
|
|
@@ -169,7 +170,7 @@ export function compilePattern(pattern: string): CompiledPattern {
|
|
|
169
170
|
}
|
|
170
171
|
} else {
|
|
171
172
|
// Static segment
|
|
172
|
-
regexPattern += `/${
|
|
173
|
+
regexPattern += `/${escapeRegExp(segment.value)}`;
|
|
173
174
|
}
|
|
174
175
|
}
|
|
175
176
|
|
|
@@ -229,13 +230,6 @@ function satisfiesConstraints(
|
|
|
229
230
|
return true;
|
|
230
231
|
}
|
|
231
232
|
|
|
232
|
-
/**
|
|
233
|
-
* Escape special regex characters in a string
|
|
234
|
-
*/
|
|
235
|
-
function escapeRegex(str: string): string {
|
|
236
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
237
|
-
}
|
|
238
|
-
|
|
239
233
|
/**
|
|
240
234
|
* Build the named-params record from a regex match. Optional segments that
|
|
241
235
|
* didn't capture leave the corresponding group `undefined`; we skip those
|
|
@@ -231,29 +231,71 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
231
231
|
: undefined;
|
|
232
232
|
|
|
233
233
|
for (const { name, fn } of revalidations) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
234
|
+
let result: any;
|
|
235
|
+
try {
|
|
236
|
+
result = fn({
|
|
237
|
+
currentParams: prevSegment?.params || prevParams, // Use segment params if available, else route params
|
|
238
|
+
currentUrl: prevUrl,
|
|
239
|
+
nextParams,
|
|
240
|
+
nextUrl,
|
|
241
|
+
defaultShouldRevalidate: currentSuggestion,
|
|
242
|
+
context,
|
|
243
|
+
// Segment metadata (which segment is being evaluated)
|
|
244
|
+
segmentType: segment.type,
|
|
245
|
+
layoutName: segment.layoutName,
|
|
246
|
+
slotName: segment.slot,
|
|
247
|
+
// Action context (only populated when triggered by server action)
|
|
248
|
+
actionId: actionContext?.actionId,
|
|
249
|
+
isAction: makeIsAction(actionContext?.actionId),
|
|
250
|
+
actionUrl: actionContext?.actionUrl,
|
|
251
|
+
actionResult: actionContext?.actionResult,
|
|
252
|
+
formData: actionContext?.formData,
|
|
253
|
+
method: request.method,
|
|
254
|
+
routeName: toRouteName,
|
|
255
|
+
fromRouteName,
|
|
256
|
+
toRouteName,
|
|
257
|
+
stale,
|
|
258
|
+
});
|
|
259
|
+
} catch (error) {
|
|
260
|
+
// A thrown Response is control flow (e.g. `throw redirect(...)`), not a
|
|
261
|
+
// failure: re-throw it so the handler chokepoint (match-handlers.ts)
|
|
262
|
+
// turns it into the intended redirect/response. This mirrors how that
|
|
263
|
+
// catch special-cases `error instanceof Response`.
|
|
264
|
+
if (error instanceof Response) throw error;
|
|
265
|
+
// Fail open for genuine errors: a buggy user revalidate fn must not
|
|
266
|
+
// collapse the whole entry's loader batch into a failed partial render.
|
|
267
|
+
// Mirror the dynamic-tags fail-open in cache/cache-policy.ts: log and
|
|
268
|
+
// defer to the current default decision, leaving currentSuggestion
|
|
269
|
+
// unchanged. TODO: route through callOnError(phase "revalidation") once
|
|
270
|
+
// evaluateRevalidation is given the onError seam (today the error only
|
|
271
|
+
// reaches onError via the entry-collapse path in match-handlers.ts).
|
|
272
|
+
console.error(
|
|
273
|
+
`[revalidate] "${name}" threw for segment "${segment.id}"; using default decision:`,
|
|
274
|
+
error,
|
|
275
|
+
);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// The revalidate fn contract (handler-context.ts) is SYNCHRONOUS: it must
|
|
280
|
+
// return a boolean, a { defaultShouldRevalidate } object, or null/undefined.
|
|
281
|
+
// A Promise-returning (async) fn matches none of the decision branches below
|
|
282
|
+
// and silently falls through keeping the current default — a hard-to-find
|
|
283
|
+
// misuse. We do NOT await it (that would change the sync contract); instead
|
|
284
|
+
// we surface it as a dev-mode warning so the silent drop is diagnosable.
|
|
285
|
+
// Mirrors defer.ts: gated to dev, stripped from production builds.
|
|
286
|
+
if (
|
|
287
|
+
process.env.NODE_ENV !== "production" &&
|
|
288
|
+
result != null &&
|
|
289
|
+
typeof (result as { then?: unknown }).then === "function"
|
|
290
|
+
) {
|
|
291
|
+
console.warn(
|
|
292
|
+
`[rango] revalidate fn "${name}" returned a Promise; revalidate ` +
|
|
293
|
+
`functions must be synchronous (return a boolean, ` +
|
|
294
|
+
`{ defaultShouldRevalidate }, or null/undefined). The async result ` +
|
|
295
|
+
`was IGNORED and the default (${currentSuggestion}) was kept. ` +
|
|
296
|
+
`Move async work into a loader instead.`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
257
299
|
|
|
258
300
|
if (typeof result === "boolean") {
|
|
259
301
|
debugLog("revalidation", "hard decision", {
|
|
@@ -154,6 +154,7 @@ export interface RouterContext<TEnv = any> {
|
|
|
154
154
|
handlerContext: HandlerContext<any, TEnv>,
|
|
155
155
|
belongsToRoute: boolean,
|
|
156
156
|
revalidationContext?: RevalidationContext,
|
|
157
|
+
options?: { skipMiddleware?: boolean },
|
|
157
158
|
) => Promise<ResolvedSegment[]>;
|
|
158
159
|
|
|
159
160
|
collectWithMarkers?: <T>(
|
|
@@ -579,9 +579,9 @@ export interface RangoOptions<TEnv = any> {
|
|
|
579
579
|
* start/end/error, loader start/end/error, handler errors, cache decisions,
|
|
580
580
|
* revalidation decisions, timeouts, origin rejections.
|
|
581
581
|
*
|
|
582
|
-
* This is the EVENT surface. Phase-duration SPANS (request/
|
|
583
|
-
* timing wired into a tracing backend) come from the
|
|
584
|
-
* option below — a sink does not emit them, because async-context nesting
|
|
582
|
+
* This is the EVENT surface. Phase-duration SPANS (request/middleware/action/
|
|
583
|
+
* handler/loader/render/ssr timing wired into a tracing backend) come from the
|
|
584
|
+
* separate `tracing` option below — a sink does not emit them, because async-context nesting
|
|
585
585
|
* cannot be faithfully reconstructed from after-the-fact start/end events.
|
|
586
586
|
*
|
|
587
587
|
* No-op when not configured (zero overhead).
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
import { applyViewTransitionDefault } from "./view-transition-default.js";
|
|
31
31
|
import { getRouterContext } from "../router-context.js";
|
|
32
32
|
import { observeStreamedHandler } from "./streamed-handler-telemetry.js";
|
|
33
|
-
import {
|
|
33
|
+
import { observeHandler } from "../instrument.js";
|
|
34
34
|
import {
|
|
35
35
|
track,
|
|
36
36
|
RangoContext,
|
|
@@ -260,7 +260,7 @@ export async function resolveSegment<TEnv>(
|
|
|
260
260
|
const doneRouteHandler = track(`handler:${entry.id}`, 2);
|
|
261
261
|
if (entry.loading) {
|
|
262
262
|
const result = handleHandlerResult(
|
|
263
|
-
|
|
263
|
+
observeHandler(entry.id, handler, context),
|
|
264
264
|
);
|
|
265
265
|
if (result instanceof Promise) {
|
|
266
266
|
warnOnStreamedResponse(result, entry.id);
|
|
@@ -284,7 +284,7 @@ export async function resolveSegment<TEnv>(
|
|
|
284
284
|
}
|
|
285
285
|
} else {
|
|
286
286
|
component = handleHandlerResult(
|
|
287
|
-
await
|
|
287
|
+
await observeHandler(entry.id, handler, context),
|
|
288
288
|
);
|
|
289
289
|
doneRouteHandler();
|
|
290
290
|
}
|
|
@@ -511,9 +511,7 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
511
511
|
if (hasLoadingFallback) {
|
|
512
512
|
const result =
|
|
513
513
|
typeof handler === "function"
|
|
514
|
-
?
|
|
515
|
-
handler(context),
|
|
516
|
-
)
|
|
514
|
+
? observeHandler(`${parallelEntry.id}.${slot}`, handler, context)
|
|
517
515
|
: handler;
|
|
518
516
|
if (result instanceof Promise) {
|
|
519
517
|
result.finally(doneParallelHandler).catch(() => {});
|
|
@@ -537,9 +535,10 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
537
535
|
} else {
|
|
538
536
|
component =
|
|
539
537
|
typeof handler === "function"
|
|
540
|
-
? await
|
|
541
|
-
|
|
542
|
-
|
|
538
|
+
? await observeHandler(
|
|
539
|
+
`${parallelEntry.id}.${slot}`,
|
|
540
|
+
handler,
|
|
541
|
+
context,
|
|
543
542
|
)
|
|
544
543
|
: handler;
|
|
545
544
|
doneParallelHandler();
|
|
@@ -23,7 +23,7 @@ import type { ResolvedSegment, ErrorInfo, HandlerContext } from "../../types";
|
|
|
23
23
|
import type { SegmentResolutionDeps } from "../types.js";
|
|
24
24
|
import { debugLog } from "../logging.js";
|
|
25
25
|
import { tryStaticLookup } from "./static-store.js";
|
|
26
|
-
import {
|
|
26
|
+
import { observeHandler } from "../instrument.js";
|
|
27
27
|
import type { TelemetrySink } from "../telemetry.js";
|
|
28
28
|
import { resolveSink, safeEmit, getRequestId } from "../telemetry.js";
|
|
29
29
|
|
|
@@ -131,15 +131,16 @@ export async function resolveLayoutComponent<TEnv>(
|
|
|
131
131
|
entry: EntryData,
|
|
132
132
|
context: HandlerContext<any, TEnv>,
|
|
133
133
|
): Promise<ReactNode> {
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
134
|
+
// Static/prerender hit: no handler runs, so emit no rango.handler span.
|
|
135
|
+
const staticComponent = await tryStaticHandler(entry, entry.shortCode);
|
|
136
|
+
if (staticComponent !== undefined) return staticComponent;
|
|
137
|
+
const handler = entry.handler;
|
|
138
|
+
if (typeof handler !== "function") return handler as ReactNode;
|
|
139
|
+
// Wrap ONLY the handler call in the rango.handler span (the perf metric is owned
|
|
140
|
+
// by track("handler:<id>") at the call site). handleHandlerResult stays OUTSIDE
|
|
141
|
+
// the span so a handler that returns a Response (redirect control flow, which it
|
|
142
|
+
// rethrows) is not recorded as a span error — mirrors the route-handler sites.
|
|
143
|
+
return handleHandlerResult(await observeHandler(entry.id, handler, context));
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
// ---------------------------------------------------------------------------
|
|
@@ -175,6 +175,19 @@ export function resolveLoaderData<TEnv>(
|
|
|
175
175
|
}
|
|
176
176
|
const runMiss = internal._loaderCacheOriginalUse!;
|
|
177
177
|
|
|
178
|
+
// Dedup the cache read-through across repeated resolutions of the SAME
|
|
179
|
+
// loaderId in one request. An orphan layout with parallel slots inherits its
|
|
180
|
+
// parent route's loaders, so resolveOrphanLayout (fresh.ts) re-resolves the
|
|
181
|
+
// parent's loaders under a different shortCode — calling resolveLoaderData
|
|
182
|
+
// again for the same loaderId. The cache key (loader:{loaderId}:{host}
|
|
183
|
+
// {pathname}:{sortedParams}) does not include the shortCode and ctx/params
|
|
184
|
+
// are identical, so both resolutions produce the same data. Reuse the already
|
|
185
|
+
// in-flight dataPromise instead of issuing a second getItem/setItem (e.g. a
|
|
186
|
+
// second KV round-trip) for one logical cached loader. The shortCode only
|
|
187
|
+
// affects the emitted segmentId in resolveLoaders, not the cached value.
|
|
188
|
+
const existing = overrides.get(loaderId);
|
|
189
|
+
if (existing) return existing;
|
|
190
|
+
|
|
178
191
|
const dataPromise = (async () => {
|
|
179
192
|
const codec = await getCodec();
|
|
180
193
|
const key = await resolveLoaderKey(
|
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
} from "./helpers.js";
|
|
44
44
|
import { applyViewTransitionDefault } from "./view-transition-default.js";
|
|
45
45
|
import { getRouterContext } from "../router-context.js";
|
|
46
|
-
import { observeEvent,
|
|
46
|
+
import { observeEvent, observeHandler } from "../instrument.js";
|
|
47
47
|
import { observeStreamedHandler } from "./streamed-handler-telemetry.js";
|
|
48
48
|
import {
|
|
49
49
|
track,
|
|
@@ -794,14 +794,14 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
794
794
|
: routeEntry.handler;
|
|
795
795
|
if (!routeEntry.loading) {
|
|
796
796
|
const result = handleHandlerResult(
|
|
797
|
-
await
|
|
797
|
+
await observeHandler(entry.id, handler, context),
|
|
798
798
|
);
|
|
799
799
|
doneHandler();
|
|
800
800
|
return result;
|
|
801
801
|
}
|
|
802
802
|
if (!actionContext) {
|
|
803
803
|
const result = handleHandlerResult(
|
|
804
|
-
|
|
804
|
+
observeHandler(entry.id, handler, context),
|
|
805
805
|
);
|
|
806
806
|
if (result instanceof Promise) {
|
|
807
807
|
warnOnStreamedResponse(result, routeEntry.id);
|
|
@@ -827,7 +827,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
827
827
|
entryId: entry.id,
|
|
828
828
|
});
|
|
829
829
|
const actionResult = handleHandlerResult(
|
|
830
|
-
await
|
|
830
|
+
await observeHandler(entry.id, handler, context),
|
|
831
831
|
);
|
|
832
832
|
doneHandler();
|
|
833
833
|
return {
|
|
@@ -94,6 +94,7 @@ export interface SegmentWrappers<TEnv = any> {
|
|
|
94
94
|
context: HandlerContext<any, TEnv>,
|
|
95
95
|
belongsToRoute?: boolean,
|
|
96
96
|
revalidationContext?: any,
|
|
97
|
+
options?: { skipMiddleware?: boolean },
|
|
97
98
|
) => Promise<ResolvedSegment[]>;
|
|
98
99
|
resolveInterceptLoadersOnly: (
|
|
99
100
|
interceptEntry: InterceptEntry,
|
|
@@ -245,6 +246,7 @@ export function createSegmentWrappers<TEnv = any>(
|
|
|
245
246
|
context: HandlerContext<any, TEnv>,
|
|
246
247
|
belongsToRoute: boolean = true,
|
|
247
248
|
revalidationContext?: any,
|
|
249
|
+
options?: { skipMiddleware?: boolean },
|
|
248
250
|
): ReturnType<typeof _resolveInterceptEntry> {
|
|
249
251
|
return _resolveInterceptEntry(
|
|
250
252
|
interceptEntry,
|
|
@@ -254,6 +256,7 @@ export function createSegmentWrappers<TEnv = any>(
|
|
|
254
256
|
belongsToRoute,
|
|
255
257
|
segmentDeps,
|
|
256
258
|
revalidationContext,
|
|
259
|
+
options,
|
|
257
260
|
);
|
|
258
261
|
}
|
|
259
262
|
|