@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,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router Timeout
|
|
3
|
+
*
|
|
4
|
+
* Types, resolution logic, and helpers for request-level timeouts.
|
|
5
|
+
* Timeouts wrap action execution and render-start phases with
|
|
6
|
+
* a Promise.race mechanism, returning 504 on expiry.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Public types
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
export interface RouterTimeouts {
|
|
14
|
+
/** Timeout for server action execution (ms). */
|
|
15
|
+
actionMs?: number;
|
|
16
|
+
/** Timeout for initial render/response production (ms). */
|
|
17
|
+
renderStartMs?: number;
|
|
18
|
+
/** Timeout for idle streaming after render starts (ms). Reserved for PR 2. */
|
|
19
|
+
streamIdleMs?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type TimeoutPhase = "action" | "render-start" | "stream-idle";
|
|
23
|
+
|
|
24
|
+
export interface TimeoutContext<TEnv = any> {
|
|
25
|
+
phase: TimeoutPhase;
|
|
26
|
+
request: Request;
|
|
27
|
+
url: URL;
|
|
28
|
+
env: TEnv;
|
|
29
|
+
routeKey?: string;
|
|
30
|
+
actionId?: string;
|
|
31
|
+
durationMs: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type OnTimeoutCallback<TEnv = any> = (
|
|
35
|
+
ctx: TimeoutContext<TEnv>,
|
|
36
|
+
) => Response | Promise<Response>;
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Internal resolved form
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
export interface ResolvedTimeouts {
|
|
43
|
+
actionMs: number | undefined;
|
|
44
|
+
renderStartMs: number | undefined;
|
|
45
|
+
streamIdleMs: number | undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Merge the `timeout` shorthand with the structured `timeouts` object.
|
|
50
|
+
*
|
|
51
|
+
* - `timeout` applies to `actionMs` and `renderStartMs` (NOT `streamIdleMs`).
|
|
52
|
+
* - Explicit `timeouts.*` values override the shorthand.
|
|
53
|
+
* - Returns `undefined` for any phase that has no configured value.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveTimeouts(
|
|
56
|
+
timeout?: number,
|
|
57
|
+
timeouts?: RouterTimeouts,
|
|
58
|
+
): ResolvedTimeouts {
|
|
59
|
+
return {
|
|
60
|
+
actionMs: timeouts?.actionMs ?? timeout ?? undefined,
|
|
61
|
+
renderStartMs: timeouts?.renderStartMs ?? timeout ?? undefined,
|
|
62
|
+
streamIdleMs: timeouts?.streamIdleMs ?? undefined,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Error class
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
export class RouterTimeoutError extends Error {
|
|
71
|
+
override name = "RouterTimeoutError" as const;
|
|
72
|
+
phase: TimeoutPhase;
|
|
73
|
+
durationMs: number;
|
|
74
|
+
|
|
75
|
+
constructor(phase: TimeoutPhase, durationMs: number) {
|
|
76
|
+
super(
|
|
77
|
+
`Request timed out during ${phase} after ${Math.round(durationMs)}ms`,
|
|
78
|
+
);
|
|
79
|
+
this.phase = phase;
|
|
80
|
+
this.durationMs = durationMs;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Race helper
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
type TimeoutResult<T> =
|
|
89
|
+
| { result: T; timedOut: false }
|
|
90
|
+
| { timedOut: true; durationMs: number };
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Race an operation against a deadline.
|
|
94
|
+
*
|
|
95
|
+
* Returns a discriminated union so callers handle the timeout case
|
|
96
|
+
* without try/catch. Non-timeout errors from the operation re-throw.
|
|
97
|
+
*
|
|
98
|
+
* When `timeoutMs` is `undefined` or `<= 0`, the operation runs
|
|
99
|
+
* without any deadline (pass-through).
|
|
100
|
+
*/
|
|
101
|
+
export async function withTimeout<T>(
|
|
102
|
+
operation: Promise<T>,
|
|
103
|
+
timeoutMs: number | undefined,
|
|
104
|
+
phase: TimeoutPhase,
|
|
105
|
+
): Promise<TimeoutResult<T>> {
|
|
106
|
+
if (timeoutMs == null || timeoutMs <= 0) {
|
|
107
|
+
return { result: await operation, timedOut: false };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const start = performance.now();
|
|
111
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
112
|
+
|
|
113
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
114
|
+
timer = setTimeout(() => {
|
|
115
|
+
reject(new RouterTimeoutError(phase, performance.now() - start));
|
|
116
|
+
}, timeoutMs);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const result = await Promise.race([operation, timeoutPromise]);
|
|
121
|
+
clearTimeout(timer!);
|
|
122
|
+
return { result, timedOut: false };
|
|
123
|
+
} catch (error) {
|
|
124
|
+
clearTimeout(timer!);
|
|
125
|
+
if (error instanceof RouterTimeoutError) {
|
|
126
|
+
return { timedOut: true, durationMs: error.durationMs };
|
|
127
|
+
}
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Default response
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Create the default 504 response for a timed-out request.
|
|
138
|
+
* Includes `X-Rango-Timeout-Phase` header for observability.
|
|
139
|
+
*/
|
|
140
|
+
export function createDefaultTimeoutResponse(phase: TimeoutPhase): Response {
|
|
141
|
+
return new Response("Request timed out", {
|
|
142
|
+
status: 504,
|
|
143
|
+
headers: {
|
|
144
|
+
"Content-Type": "text/plain;charset=utf-8",
|
|
145
|
+
"X-Rango-Timeout-Phase": phase,
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
@@ -114,7 +114,25 @@ function walkTrie(
|
|
|
114
114
|
if (result) return result;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
// Priority 2:
|
|
117
|
+
// Priority 2: Suffix-param match (e.g., :productId.html)
|
|
118
|
+
if (node.xp) {
|
|
119
|
+
for (const suffix in node.xp) {
|
|
120
|
+
if (segment.endsWith(suffix) && segment.length > suffix.length) {
|
|
121
|
+
const paramValue = segment.slice(0, -suffix.length);
|
|
122
|
+
paramValues.push(paramValue);
|
|
123
|
+
const result = walkTrie(
|
|
124
|
+
node.xp[suffix].c,
|
|
125
|
+
segments,
|
|
126
|
+
index + 1,
|
|
127
|
+
paramValues,
|
|
128
|
+
);
|
|
129
|
+
paramValues.pop();
|
|
130
|
+
if (result) return result;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Priority 3: Param match
|
|
118
136
|
if (node.p) {
|
|
119
137
|
paramValues.push(segment);
|
|
120
138
|
const result = walkTrie(node.p.c, segments, index + 1, paramValues);
|
|
@@ -122,7 +140,7 @@ function walkTrie(
|
|
|
122
140
|
if (result) return result;
|
|
123
141
|
}
|
|
124
142
|
|
|
125
|
-
// Priority
|
|
143
|
+
// Priority 4: Wildcard match (consumes rest)
|
|
126
144
|
if (node.w) {
|
|
127
145
|
const rest = joinRemainingSegments(segments, index);
|
|
128
146
|
return {
|
package/src/router/types.ts
CHANGED
|
@@ -83,7 +83,13 @@ export interface SegmentResolutionDeps<TEnv = any> {
|
|
|
83
83
|
requestStartTime?: number;
|
|
84
84
|
},
|
|
85
85
|
) => Promise<LoaderDataResult<T>>;
|
|
86
|
-
trackHandler: <T>(
|
|
86
|
+
trackHandler: <T>(
|
|
87
|
+
promise: Promise<T>,
|
|
88
|
+
errorContext?: {
|
|
89
|
+
segmentId?: string;
|
|
90
|
+
segmentType?: string;
|
|
91
|
+
},
|
|
92
|
+
) => Promise<T>;
|
|
87
93
|
findNearestErrorBoundary: (
|
|
88
94
|
entry: EntryData | null,
|
|
89
95
|
) => ReactNode | ErrorBoundaryHandler | null;
|
package/src/router.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import { createCacheScope } from "./cache/cache-scope.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
setCacheProfiles,
|
|
5
|
+
resolveCacheProfiles,
|
|
6
|
+
} from "./cache/profile-registry.js";
|
|
4
7
|
import { isCachedFunction } from "./cache/taint.js";
|
|
5
8
|
import { assertClientComponent } from "./component-utils.js";
|
|
6
9
|
import { DefaultDocument } from "./components/DefaultDocument.js";
|
|
@@ -24,7 +27,10 @@ import {
|
|
|
24
27
|
type MetricsStore,
|
|
25
28
|
} from "./server/context";
|
|
26
29
|
import { createHandleStore, type HandleStore } from "./server/handle-store.js";
|
|
27
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
getRequestContext,
|
|
32
|
+
_getRequestContext,
|
|
33
|
+
} from "./server/request-context.js";
|
|
28
34
|
import type {
|
|
29
35
|
ErrorPhase,
|
|
30
36
|
HandlerContext,
|
|
@@ -65,12 +71,14 @@ import {
|
|
|
65
71
|
extractStaticPrefix,
|
|
66
72
|
traverseBack,
|
|
67
73
|
} from "./router/pattern-matching.js";
|
|
74
|
+
import { resolveSink, safeEmit, getRequestId } from "./router/telemetry.js";
|
|
68
75
|
import { evaluateRevalidation } from "./router/revalidation.js";
|
|
69
76
|
import {
|
|
70
77
|
type RouterContext,
|
|
71
78
|
runWithRouterContext,
|
|
72
79
|
} from "./router/router-context.js";
|
|
73
80
|
import { resolveThemeConfig } from "./theme/constants.js";
|
|
81
|
+
import { resolveTimeouts } from "./router/timeout.js";
|
|
74
82
|
|
|
75
83
|
// Extracted content negotiation utilities
|
|
76
84
|
import { flattenNamedRoutes } from "./router/content-negotiation.js";
|
|
@@ -87,6 +95,7 @@ import type {
|
|
|
87
95
|
} from "./router/router-options.js";
|
|
88
96
|
import type {
|
|
89
97
|
RSCRouter,
|
|
98
|
+
RSCRouterInternal,
|
|
90
99
|
RouterRequestInput,
|
|
91
100
|
} from "./router/router-interfaces.js";
|
|
92
101
|
|
|
@@ -107,11 +116,16 @@ export { RSC_ROUTER_BRAND, RouterRegistry } from "./router/router-registry.js";
|
|
|
107
116
|
export type {
|
|
108
117
|
RSCRouterOptions,
|
|
109
118
|
RootLayoutProps,
|
|
119
|
+
SSRStreamMode,
|
|
120
|
+
SSROptions,
|
|
121
|
+
ResolveStreamingContext,
|
|
110
122
|
} from "./router/router-options.js";
|
|
111
123
|
export type {
|
|
112
124
|
RSCRouter,
|
|
125
|
+
RSCRouterInternal,
|
|
113
126
|
RouterRequestInput,
|
|
114
127
|
} from "./router/router-interfaces.js";
|
|
128
|
+
export { toInternal } from "./router/router-interfaces.js";
|
|
115
129
|
|
|
116
130
|
export function createRouter<TEnv = any>(
|
|
117
131
|
options: RSCRouterOptions<TEnv> = {},
|
|
@@ -133,14 +147,25 @@ export function createRouter<TEnv = any>(
|
|
|
133
147
|
$$sourceFile: injectedSourceFile,
|
|
134
148
|
nonce,
|
|
135
149
|
version,
|
|
150
|
+
prefetchCacheControl: prefetchCacheControlOption,
|
|
136
151
|
warmup: warmupOption,
|
|
137
152
|
allowDebugManifest: allowDebugManifestOption = false,
|
|
153
|
+
telemetry: telemetrySink,
|
|
154
|
+
ssr: ssrOption,
|
|
155
|
+
timeout: timeoutShorthand,
|
|
156
|
+
timeouts: timeoutsOption,
|
|
157
|
+
onTimeout,
|
|
158
|
+
originCheck: originCheckOption,
|
|
138
159
|
} = options;
|
|
139
160
|
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
161
|
+
// Resolve telemetry sink (no-op when not configured)
|
|
162
|
+
const telemetry = resolveSink(telemetrySink);
|
|
163
|
+
|
|
164
|
+
// Resolve cache profiles: merge user config with guaranteed default profile.
|
|
165
|
+
// This resolved map is both stored on the router (for per-request context)
|
|
166
|
+
// and written to the global registry (for DSL-time cache("profileName")).
|
|
167
|
+
const resolvedCacheProfiles = resolveCacheProfiles(cacheProfilesOption);
|
|
168
|
+
setCacheProfiles(resolvedCacheProfiles);
|
|
144
169
|
|
|
145
170
|
// Source file: prefer Vite-injected path (zero cost), fall back to
|
|
146
171
|
// stack trace parsing for non-Vite environments (e.g. tests).
|
|
@@ -175,6 +200,12 @@ export function createRouter<TEnv = any>(
|
|
|
175
200
|
const routerId =
|
|
176
201
|
userProvidedId ?? injectedId ?? `router_${nextRouterAutoId()}`;
|
|
177
202
|
|
|
203
|
+
// Resolve prefetch cache control (default: 'private, max-age=300')
|
|
204
|
+
const prefetchCacheControl =
|
|
205
|
+
prefetchCacheControlOption !== undefined
|
|
206
|
+
? prefetchCacheControlOption
|
|
207
|
+
: "private, max-age=300";
|
|
208
|
+
|
|
178
209
|
// Resolve warmup enabled flag (default: true)
|
|
179
210
|
const warmupEnabled = warmupOption !== false;
|
|
180
211
|
|
|
@@ -183,15 +214,29 @@ export function createRouter<TEnv = any>(
|
|
|
183
214
|
? resolveThemeConfig(themeOption)
|
|
184
215
|
: null;
|
|
185
216
|
|
|
217
|
+
// Resolve timeout config (merge shorthand + structured)
|
|
218
|
+
const resolvedTimeouts = resolveTimeouts(timeoutShorthand, timeoutsOption);
|
|
219
|
+
|
|
186
220
|
/**
|
|
187
221
|
* Wrapper for invokeOnError that binds the router's onError callback.
|
|
188
222
|
* Uses the shared utility from router/error-handling.ts for consistent behavior.
|
|
223
|
+
*
|
|
224
|
+
* Deduplicates via per-request WeakSet stored on the ALS request context.
|
|
225
|
+
* A closure-level WeakSet would silently swallow errors if the same object
|
|
226
|
+
* instance is thrown across separate requests (e.g. a singleton error).
|
|
189
227
|
*/
|
|
190
228
|
function callOnError(
|
|
191
229
|
error: unknown,
|
|
192
230
|
phase: ErrorPhase,
|
|
193
231
|
context: Parameters<typeof invokeOnError<TEnv>>[3],
|
|
194
232
|
): void {
|
|
233
|
+
if (error != null && typeof error === "object") {
|
|
234
|
+
const reportedErrors = _getRequestContext()?._reportedErrors;
|
|
235
|
+
if (reportedErrors) {
|
|
236
|
+
if (reportedErrors.has(error)) return;
|
|
237
|
+
reportedErrors.add(error);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
195
240
|
invokeOnError(onError, error, phase, context, "Router");
|
|
196
241
|
}
|
|
197
242
|
|
|
@@ -281,6 +326,11 @@ export function createRouter<TEnv = any>(
|
|
|
281
326
|
const mergedRouteMap: Record<string, string> =
|
|
282
327
|
flattenNamedRoutes(staticRouteNames);
|
|
283
328
|
|
|
329
|
+
// Track names that came from the static seed so we can silently overwrite
|
|
330
|
+
// them during routes() registration. The gen file may be stale during HMR,
|
|
331
|
+
// so conflicts between seeded and runtime-registered values are expected.
|
|
332
|
+
const seededNames = new Set(Object.keys(mergedRouteMap));
|
|
333
|
+
|
|
284
334
|
// Lazy precomputed entries lookup: rebuilt when per-router data arrives.
|
|
285
335
|
// In production multi-router setups, per-router data is loaded lazily via
|
|
286
336
|
// ensureRouterManifest(). At createRouter() time the data isn't available yet,
|
|
@@ -307,8 +357,18 @@ export function createRouter<TEnv = any>(
|
|
|
307
357
|
return precomputedByPrefix;
|
|
308
358
|
}
|
|
309
359
|
|
|
310
|
-
// Wrapper to pass debugPerformance to external createMetricsStore
|
|
311
|
-
|
|
360
|
+
// Wrapper to pass debugPerformance to external createMetricsStore.
|
|
361
|
+
// Also checks per-request flag set by ctx.debugPerformance() in middleware.
|
|
362
|
+
const getMetricsStore = () => {
|
|
363
|
+
const reqCtx = _getRequestContext();
|
|
364
|
+
const enabled = debugPerformance || !!reqCtx?._debugPerformance;
|
|
365
|
+
if (!enabled) return undefined;
|
|
366
|
+
if (!reqCtx) {
|
|
367
|
+
return createMetricsStore(true);
|
|
368
|
+
}
|
|
369
|
+
reqCtx._metricsStore ??= createMetricsStore(true);
|
|
370
|
+
return reqCtx._metricsStore;
|
|
371
|
+
};
|
|
312
372
|
|
|
313
373
|
// Wrapper to pass defaults to error/notFound boundary finders
|
|
314
374
|
const findNearestErrorBoundary = (entry: EntryData | null) =>
|
|
@@ -319,17 +379,46 @@ export function createRouter<TEnv = any>(
|
|
|
319
379
|
|
|
320
380
|
// Helper to get handleStore from request context
|
|
321
381
|
const getHandleStore = (): HandleStore | undefined => {
|
|
322
|
-
return
|
|
382
|
+
return _getRequestContext()?._handleStore;
|
|
323
383
|
};
|
|
324
384
|
|
|
325
|
-
// Track a pending handler promise (non-blocking)
|
|
326
|
-
|
|
385
|
+
// Track a pending handler promise (non-blocking).
|
|
386
|
+
// Attaches a side-effect .catch() to report streaming handler errors to onError
|
|
387
|
+
// without altering the rejection chain (React's streaming error boundary still handles it).
|
|
388
|
+
const trackHandler = <T>(
|
|
389
|
+
promise: Promise<T>,
|
|
390
|
+
errorContext?: {
|
|
391
|
+
segmentId?: string;
|
|
392
|
+
segmentType?: string;
|
|
393
|
+
},
|
|
394
|
+
): Promise<T> => {
|
|
327
395
|
const store = getHandleStore();
|
|
328
|
-
|
|
396
|
+
const tracked = store ? store.track(promise) : promise;
|
|
397
|
+
|
|
398
|
+
// Report streaming handler errors to onError as a side-effect.
|
|
399
|
+
// The rejection still propagates to the RSC stream for client error boundaries.
|
|
400
|
+
// Captures request context eagerly (closure) so the catch handler has full context.
|
|
401
|
+
const reqCtx = _getRequestContext();
|
|
402
|
+
if (reqCtx && onError) {
|
|
403
|
+
tracked.catch((error) => {
|
|
404
|
+
callOnError(error, "handler", {
|
|
405
|
+
request: reqCtx.request,
|
|
406
|
+
url: reqCtx.url,
|
|
407
|
+
routeKey: reqCtx._routeName,
|
|
408
|
+
params: reqCtx.params as Record<string, string>,
|
|
409
|
+
env: reqCtx.env as TEnv,
|
|
410
|
+
segmentId: errorContext?.segmentId,
|
|
411
|
+
segmentType: errorContext?.segmentType as any,
|
|
412
|
+
handledByBoundary: true,
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return tracked;
|
|
329
418
|
};
|
|
330
419
|
|
|
331
420
|
// Wrapper for wrapLoaderWithErrorHandling that uses router's error boundary finder
|
|
332
|
-
// Includes onError callback for loader error notification
|
|
421
|
+
// Includes onError callback for loader error notification and telemetry emission.
|
|
333
422
|
function wrapLoaderPromise<T>(
|
|
334
423
|
promise: Promise<T>,
|
|
335
424
|
entry: EntryData,
|
|
@@ -345,7 +434,25 @@ export function createRouter<TEnv = any>(
|
|
|
345
434
|
requestStartTime?: number;
|
|
346
435
|
},
|
|
347
436
|
): Promise<LoaderDataResult<T>> {
|
|
348
|
-
|
|
437
|
+
const loaderStart = telemetrySink ? performance.now() : 0;
|
|
438
|
+
const loaderRequestId = telemetrySink
|
|
439
|
+
? errorContext?.request
|
|
440
|
+
? getRequestId(errorContext.request)
|
|
441
|
+
: undefined
|
|
442
|
+
: undefined;
|
|
443
|
+
if (telemetrySink) {
|
|
444
|
+
const loaderName = segmentId.split(".").pop() || "unknown";
|
|
445
|
+
safeEmit(telemetry, {
|
|
446
|
+
type: "loader.start",
|
|
447
|
+
timestamp: loaderStart,
|
|
448
|
+
requestId: loaderRequestId,
|
|
449
|
+
segmentId,
|
|
450
|
+
loaderName,
|
|
451
|
+
pathname,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const result = wrapLoaderWithErrorHandling(
|
|
349
456
|
promise,
|
|
350
457
|
entry,
|
|
351
458
|
segmentId,
|
|
@@ -368,9 +475,42 @@ export function createRouter<TEnv = any>(
|
|
|
368
475
|
handledByBoundary: ctx.handledByBoundary,
|
|
369
476
|
requestStartTime: errorContext.requestStartTime,
|
|
370
477
|
});
|
|
478
|
+
if (telemetrySink) {
|
|
479
|
+
const errorObj =
|
|
480
|
+
error instanceof Error ? error : new Error(String(error));
|
|
481
|
+
safeEmit(telemetry, {
|
|
482
|
+
type: "loader.error",
|
|
483
|
+
timestamp: performance.now(),
|
|
484
|
+
requestId: loaderRequestId,
|
|
485
|
+
segmentId: ctx.segmentId,
|
|
486
|
+
loaderName: ctx.loaderName,
|
|
487
|
+
pathname,
|
|
488
|
+
error: errorObj,
|
|
489
|
+
handledByBoundary: ctx.handledByBoundary,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
371
492
|
}
|
|
372
493
|
: undefined,
|
|
373
494
|
);
|
|
495
|
+
|
|
496
|
+
// Emit loader.end after the promise settles (fire-and-forget)
|
|
497
|
+
if (telemetrySink) {
|
|
498
|
+
const loaderName = segmentId.split(".").pop() || "unknown";
|
|
499
|
+
result.then((r) => {
|
|
500
|
+
safeEmit(telemetry, {
|
|
501
|
+
type: "loader.end",
|
|
502
|
+
timestamp: performance.now(),
|
|
503
|
+
requestId: loaderRequestId,
|
|
504
|
+
segmentId,
|
|
505
|
+
loaderName,
|
|
506
|
+
pathname,
|
|
507
|
+
durationMs: performance.now() - loaderStart,
|
|
508
|
+
ok: r.ok,
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return result;
|
|
374
514
|
}
|
|
375
515
|
|
|
376
516
|
// Dependencies object for extracted segment resolution functions.
|
|
@@ -450,6 +590,7 @@ export function createRouter<TEnv = any>(
|
|
|
450
590
|
resolveLoadersOnlyWithRevalidation,
|
|
451
591
|
resolveInterceptLoadersOnly,
|
|
452
592
|
resolveLoadersOnly,
|
|
593
|
+
telemetry: telemetrySink,
|
|
453
594
|
};
|
|
454
595
|
}
|
|
455
596
|
|
|
@@ -465,8 +606,15 @@ export function createRouter<TEnv = any>(
|
|
|
465
606
|
pathname: string,
|
|
466
607
|
params: Record<string, string>,
|
|
467
608
|
buildVars?: Record<string, any>,
|
|
609
|
+
isPassthroughRoute?: boolean,
|
|
468
610
|
) {
|
|
469
|
-
return _matchForPrerender(
|
|
611
|
+
return _matchForPrerender(
|
|
612
|
+
pathname,
|
|
613
|
+
params,
|
|
614
|
+
prerenderDeps,
|
|
615
|
+
buildVars,
|
|
616
|
+
isPassthroughRoute,
|
|
617
|
+
);
|
|
470
618
|
}
|
|
471
619
|
|
|
472
620
|
async function renderStaticSegment(
|
|
@@ -490,6 +638,7 @@ export function createRouter<TEnv = any>(
|
|
|
490
638
|
defaultErrorBoundary,
|
|
491
639
|
findMatch,
|
|
492
640
|
findInterceptForRoute,
|
|
641
|
+
telemetry: telemetrySink,
|
|
493
642
|
});
|
|
494
643
|
|
|
495
644
|
const { match, matchPartial, matchError, previewMatch } = matchHandlers;
|
|
@@ -499,7 +648,7 @@ export function createRouter<TEnv = any>(
|
|
|
499
648
|
* The type system tracks accumulated routes through the builder chain
|
|
500
649
|
* Initial TRoutes is {} (empty) to avoid poisoning accumulated types with Record<string, string>
|
|
501
650
|
*/
|
|
502
|
-
const router:
|
|
651
|
+
const router: RSCRouterInternal<TEnv, {}> = {
|
|
503
652
|
__brand: RSC_ROUTER_BRAND,
|
|
504
653
|
id: routerId,
|
|
505
654
|
|
|
@@ -550,6 +699,7 @@ export function createRouter<TEnv = any>(
|
|
|
550
699
|
parent: syntheticMapRoot,
|
|
551
700
|
counters: {},
|
|
552
701
|
mountIndex: currentMountIndex,
|
|
702
|
+
cacheProfiles: resolvedCacheProfiles,
|
|
553
703
|
},
|
|
554
704
|
() => {
|
|
555
705
|
handlerResult = urlPatterns.handler() as AllUseItems[];
|
|
@@ -564,10 +714,15 @@ export function createRouter<TEnv = any>(
|
|
|
564
714
|
|
|
565
715
|
// Collect route keys that have prerender handlers (for non-trie match path)
|
|
566
716
|
let prerenderRouteKeys: Set<string> | undefined;
|
|
717
|
+
let passthroughRouteKeys: Set<string> | undefined;
|
|
567
718
|
for (const [name, entry] of manifest.entries()) {
|
|
568
719
|
if (entry.type === "route" && entry.isPrerender) {
|
|
569
720
|
if (!prerenderRouteKeys) prerenderRouteKeys = new Set();
|
|
570
721
|
prerenderRouteKeys.add(name);
|
|
722
|
+
if (entry.prerenderDef?.options?.passthrough === true) {
|
|
723
|
+
if (!passthroughRouteKeys) passthroughRouteKeys = new Set();
|
|
724
|
+
passthroughRouteKeys.add(name);
|
|
725
|
+
}
|
|
571
726
|
}
|
|
572
727
|
}
|
|
573
728
|
|
|
@@ -590,7 +745,9 @@ export function createRouter<TEnv = any>(
|
|
|
590
745
|
trailingSlash: trailingSlashConfig,
|
|
591
746
|
handler: urlPatterns.handler,
|
|
592
747
|
mountIndex: currentMountIndex,
|
|
748
|
+
cacheProfiles: resolvedCacheProfiles,
|
|
593
749
|
...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
|
|
750
|
+
...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
|
|
594
751
|
});
|
|
595
752
|
}
|
|
596
753
|
} else {
|
|
@@ -607,21 +764,30 @@ export function createRouter<TEnv = any>(
|
|
|
607
764
|
trailingSlash: trailingSlashConfig,
|
|
608
765
|
handler: urlPatterns.handler,
|
|
609
766
|
mountIndex: currentMountIndex,
|
|
767
|
+
cacheProfiles: resolvedCacheProfiles,
|
|
610
768
|
...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
|
|
769
|
+
...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
|
|
611
770
|
});
|
|
612
771
|
}
|
|
613
772
|
|
|
614
773
|
// Build route map from registered patterns
|
|
615
774
|
for (const [name, pattern] of routePatterns.entries()) {
|
|
616
|
-
// Runtime validation: warn if key already exists with different pattern
|
|
775
|
+
// Runtime validation: warn if key already exists with different pattern.
|
|
776
|
+
// Skip warning for entries that came from the static seed — the gen file
|
|
777
|
+
// can be stale during HMR, so runtime registration is authoritative.
|
|
617
778
|
const existingPattern = mergedRouteMap[name];
|
|
618
|
-
if (
|
|
779
|
+
if (
|
|
780
|
+
existingPattern !== undefined &&
|
|
781
|
+
existingPattern !== pattern &&
|
|
782
|
+
!seededNames.has(name)
|
|
783
|
+
) {
|
|
619
784
|
console.warn(
|
|
620
785
|
`[@rangojs/router] Route name conflict: "${name}" already maps to "${existingPattern}", ` +
|
|
621
786
|
`overwriting with "${pattern}". Use unique route names to avoid this.`,
|
|
622
787
|
);
|
|
623
788
|
}
|
|
624
789
|
mergedRouteMap[name] = pattern;
|
|
790
|
+
seededNames.delete(name);
|
|
625
791
|
}
|
|
626
792
|
|
|
627
793
|
// Detect lazy includes in handler result and create placeholder entries
|
|
@@ -710,12 +876,31 @@ export function createRouter<TEnv = any>(
|
|
|
710
876
|
// Expose resolved theme configuration for NavigationProvider and MetaTags
|
|
711
877
|
themeConfig: resolvedThemeConfig,
|
|
712
878
|
|
|
879
|
+
// Expose resolved cache profiles for per-request resolution
|
|
880
|
+
cacheProfiles: resolvedCacheProfiles,
|
|
881
|
+
|
|
882
|
+
// Expose prefetch cache control for RSC handler
|
|
883
|
+
prefetchCacheControl,
|
|
884
|
+
|
|
713
885
|
// Expose warmup enabled flag for handler and client
|
|
714
886
|
warmupEnabled,
|
|
715
887
|
|
|
888
|
+
// Expose router-wide performance debugging for request-level metrics setup
|
|
889
|
+
debugPerformance,
|
|
890
|
+
|
|
716
891
|
// Expose debug manifest flag for handler
|
|
717
892
|
allowDebugManifest: allowDebugManifestOption,
|
|
718
893
|
|
|
894
|
+
// Expose origin check configuration for handler (default: enabled)
|
|
895
|
+
originCheck: originCheckOption ?? true,
|
|
896
|
+
|
|
897
|
+
// Expose SSR configuration for handler
|
|
898
|
+
ssr: ssrOption,
|
|
899
|
+
|
|
900
|
+
// Expose resolved timeouts for RSC handler
|
|
901
|
+
timeouts: resolvedTimeouts,
|
|
902
|
+
onTimeout,
|
|
903
|
+
|
|
719
904
|
// Expose global middleware for RSC handler
|
|
720
905
|
middleware: globalMiddleware,
|
|
721
906
|
|
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
* RSC rendering) so they can be standalone modules without closure coupling.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type {
|
|
9
|
+
import type { RSCRouterInternal } from "../router/router-interfaces.js";
|
|
10
10
|
import type { ErrorPhase } from "../types.js";
|
|
11
11
|
import type { InvokeOnErrorContext } from "../router/error-handling.js";
|
|
12
12
|
import type { RSCDependencies, LoadSSRModule } from "./types.js";
|
|
13
|
+
import type { SSRStreamMode } from "../router/router-options.js";
|
|
13
14
|
|
|
14
15
|
export interface HandlerContext<TEnv = unknown> {
|
|
15
|
-
router:
|
|
16
|
+
router: RSCRouterInternal<TEnv, any>;
|
|
16
17
|
version: string;
|
|
17
18
|
renderToReadableStream: RSCDependencies["renderToReadableStream"];
|
|
18
19
|
decodeReply: RSCDependencies["decodeReply"];
|
|
@@ -31,4 +32,14 @@ export interface HandlerContext<TEnv = unknown> {
|
|
|
31
32
|
redirectUrl: string,
|
|
32
33
|
locationState?: Record<string, unknown>,
|
|
33
34
|
) => Response;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the SSR stream mode for a given request.
|
|
38
|
+
* Returns "stream" when no resolveStreaming callback is configured.
|
|
39
|
+
*/
|
|
40
|
+
resolveStreamMode: (
|
|
41
|
+
request: Request,
|
|
42
|
+
env: TEnv,
|
|
43
|
+
url: URL,
|
|
44
|
+
) => Promise<SSRStreamMode>;
|
|
34
45
|
}
|