@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +6 -10
- package/README.md +289 -938
- package/dist/bin/rango.js +271 -46
- package/dist/vite/index.js +673 -193
- package/package.json +10 -8
- package/skills/api-client/SKILL.md +1 -1
- package/skills/breadcrumbs/SKILL.md +31 -14
- package/skills/cache-guide/SKILL.md +5 -2
- package/skills/caching/SKILL.md +59 -4
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +83 -2
- package/skills/debug-manifest/SKILL.md +1 -1
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +9 -1
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +8 -8
- package/skills/hooks/SKILL.md +54 -892
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +4 -4
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +46 -14
- package/skills/layout/SKILL.md +27 -10
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +23 -1
- package/skills/middleware/SKILL.md +7 -3
- package/skills/migrate-nextjs/SKILL.md +167 -6
- package/skills/migrate-react-router/SKILL.md +59 -677
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/observability/SKILL.md +9 -1
- package/skills/parallel/SKILL.md +23 -4
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +28 -18
- package/skills/rango/SKILL.md +84 -25
- package/skills/response-routes/SKILL.md +15 -1
- package/skills/route/SKILL.md +71 -4
- package/skills/router-setup/SKILL.md +14 -3
- package/skills/scripts/SKILL.md +1 -1
- package/skills/server-actions/SKILL.md +3 -2
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +1 -1
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/handles.md +4 -2
- package/skills/testing/render-handler.md +15 -14
- package/skills/testing/reverse-and-types.md +8 -7
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -919
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +30 -3
- package/skills/vercel/SKILL.md +1 -1
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/browser/event-controller.ts +62 -10
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +65 -16
- package/src/browser/navigation-client.ts +32 -2
- package/src/browser/navigation-store.ts +128 -14
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +76 -17
- package/src/browser/prefetch/cache.ts +51 -11
- package/src/browser/prefetch/fetch.ts +59 -21
- package/src/browser/prefetch/queue.ts +19 -4
- package/src/browser/react/Link.tsx +13 -3
- package/src/browser/react/NavigationProvider.tsx +108 -4
- package/src/browser/response-adapter.ts +38 -9
- package/src/browser/rsc-router.tsx +54 -4
- package/src/browser/scroll-restoration.ts +7 -5
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +22 -10
- package/src/browser/types.ts +54 -1
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/index.ts +3 -1
- package/src/build/route-trie.ts +35 -7
- package/src/build/route-types/include-resolution.ts +347 -47
- package/src/build/runtime-discovery.ts +4 -1
- package/src/cache/cache-key-utils.ts +29 -0
- package/src/cache/cache-runtime.ts +262 -71
- package/src/cache/cache-scope.ts +2 -17
- package/src/cache/cache-tag.ts +60 -14
- package/src/cache/cf/cf-cache-store.ts +243 -20
- package/src/cache/document-cache.ts +54 -21
- package/src/cache/index.ts +1 -0
- package/src/cache/memory-segment-store.ts +110 -3
- package/src/cache/profile-registry.ts +15 -0
- package/src/cache/read-through-swr.ts +15 -1
- package/src/cache/segment-codec.ts +4 -4
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/types.ts +158 -0
- package/src/cache/vercel/vercel-cache-store.ts +401 -124
- package/src/client.rsc.tsx +0 -3
- package/src/client.tsx +0 -3
- package/src/cloudflare/tracing.ts +7 -8
- package/src/defer.ts +11 -22
- package/src/handle.ts +37 -15
- package/src/handles/MetaTags.tsx +16 -82
- package/src/handles/breadcrumbs.ts +12 -14
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +7 -8
- package/src/handles/meta.ts +7 -44
- package/src/host/errors.ts +15 -0
- package/src/host/index.ts +1 -0
- package/src/index.rsc.ts +8 -2
- package/src/index.ts +19 -13
- package/src/internal-debug.ts +11 -8
- package/src/prerender.ts +17 -4
- package/src/redirect-origin.ts +14 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/route-content-wrapper.tsx +12 -5
- package/src/route-definition/dsl-helpers.ts +21 -32
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +43 -43
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/resolve-handler-use.ts +0 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -4
- package/src/route-types.ts +0 -5
- package/src/router/find-match.ts +86 -8
- package/src/router/instrument.ts +9 -4
- package/src/router/lazy-includes.ts +72 -12
- package/src/router/loader-resolution.ts +14 -2
- package/src/router/manifest.ts +56 -11
- package/src/router/match-api.ts +76 -32
- package/src/router/match-handlers.ts +181 -135
- package/src/router/match-middleware/background-revalidation.ts +40 -23
- package/src/router/match-middleware/cache-store.ts +39 -24
- package/src/router/match-result.ts +35 -15
- package/src/router/middleware.ts +64 -38
- package/src/router/navigation-snapshot.ts +7 -5
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +53 -64
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +11 -5
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +23 -8
- package/src/router/route-snapshot.ts +14 -2
- package/src/router/router-context.ts +3 -1
- package/src/router/router-interfaces.ts +32 -1
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +39 -3
- package/src/router/segment-resolution/loader-cache.ts +93 -2
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +3 -0
- package/src/router/segment-resolution/view-transition-default.ts +35 -15
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/telemetry-otel.ts +6 -8
- package/src/router/telemetry.ts +9 -1
- package/src/router/tracing.ts +14 -5
- package/src/router/trie-matching.ts +19 -11
- package/src/router/url-params.ts +13 -0
- package/src/router.ts +47 -16
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler.ts +60 -33
- package/src/rsc/manifest-init.ts +1 -1
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/progressive-enhancement.ts +61 -4
- package/src/rsc/redirect-guard.ts +2 -1
- package/src/rsc/rsc-rendering.ts +429 -37
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +30 -0
- package/src/segment-loader-promise.ts +18 -0
- package/src/segment-system.tsx +149 -14
- package/src/server/context.ts +67 -9
- package/src/server/cookie-store.ts +73 -1
- package/src/server/loader-registry.ts +13 -1
- package/src/server/request-context.ts +169 -10
- package/src/ssr/index.tsx +462 -178
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/testing/collect-handle.ts +14 -8
- package/src/testing/dispatch.ts +152 -40
- package/src/testing/generated-routes.ts +27 -11
- package/src/testing/index.ts +6 -0
- package/src/testing/render-handler.ts +14 -0
- package/src/testing/render-route.tsx +13 -10
- package/src/testing/run-transition-when.ts +164 -0
- package/src/theme/ThemeProvider.tsx +36 -26
- package/src/types/handler-context.ts +1 -1
- package/src/types/index.ts +2 -0
- package/src/types/route-config.ts +19 -7
- package/src/types/segments.ts +100 -0
- package/src/urls/include-helper.ts +10 -8
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +44 -12
- package/src/urls/path-helper.ts +5 -0
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +43 -18
- package/src/urls/urls-function.ts +0 -1
- package/src/vercel/tracing.ts +7 -7
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/index.ts +7 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugins/vercel-output.ts +114 -25
- package/src/vite/plugins/version-injector.ts +22 -7
- package/src/vite/plugins/virtual-entries.ts +80 -22
- package/src/vite/rango.ts +29 -19
- package/src/vite/router-discovery.ts +171 -43
- package/src/vite/utils/prerender-utils.ts +17 -4
- package/src/vite/utils/shared-utils.ts +47 -0
- package/src/network-error-thrower.tsx +0 -18
package/src/router/match-api.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type { MatchContext } from "./match-context.js";
|
|
|
26
26
|
import type { MatchApiDeps, ActionContext } from "./types.js";
|
|
27
27
|
import {
|
|
28
28
|
getRequestContext,
|
|
29
|
+
_getRequestContext,
|
|
29
30
|
setRequestContextPrevRouteKey,
|
|
30
31
|
} from "../server/request-context.js";
|
|
31
32
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
@@ -47,31 +48,67 @@ export async function createMatchContextForFull<TEnv>(
|
|
|
47
48
|
deps: MatchApiDeps<TEnv>,
|
|
48
49
|
findInterceptForRoute: MatchApiDeps<TEnv>["findInterceptForRoute"],
|
|
49
50
|
): Promise<MatchContext<TEnv> | { type: "redirect"; redirectUrl: string }> {
|
|
50
|
-
const
|
|
51
|
-
const
|
|
51
|
+
const reqCtx = _getRequestContext();
|
|
52
|
+
const cleanUrl = reqCtx?.url ?? stripInternalParams(new URL(request.url));
|
|
53
|
+
const pathname = cleanUrl.pathname;
|
|
52
54
|
|
|
53
55
|
const metricsStore = deps.getMetricsStore();
|
|
54
56
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
57
|
+
const isHmr = !!request.headers.get("X-RSC-HMR");
|
|
58
|
+
// HMR: clear the manifest cache so stale handler references are discarded.
|
|
59
|
+
if (isHmr) {
|
|
60
|
+
clearManifestCache();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Reuse the snapshot classifyRequest already resolved when it is a full-render
|
|
64
|
+
// snapshot (isSSR:true) and HMR has not invalidated it — mirrors the partial
|
|
65
|
+
// path. A partial-mode snapshot (isSSR:false) carries the wrong loading()
|
|
66
|
+
// entries and lives in a different manifest partition, so it is NOT reusable.
|
|
67
|
+
const classified = reqCtx?._classifiedRoute;
|
|
68
|
+
const classifiedRoute =
|
|
69
|
+
!isHmr && classified?.isSSR === true ? classified : undefined;
|
|
70
|
+
|
|
71
|
+
// On the reuse path, findMatch + manifest-loading already ran during
|
|
72
|
+
// classifyRequest (which carries no metricsStore), so only the cheap
|
|
73
|
+
// entries/cacheScope materialization is timed here under a distinct label. On
|
|
74
|
+
// the fresh path, resolveRoute emits route-matching + manifest-loading as usual.
|
|
75
|
+
const routeMatchStart = metricsStore ? performance.now() : 0;
|
|
60
76
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
77
|
+
let snapshot: RouteSnapshot<TEnv>;
|
|
78
|
+
if (classifiedRoute && classifiedRoute.manifestEntry) {
|
|
79
|
+
snapshot = ensureFullRouteSnapshot(classifiedRoute);
|
|
80
|
+
if (metricsStore) {
|
|
81
|
+
metricsStore.metrics.push({
|
|
82
|
+
label: "route-matching:reuse",
|
|
83
|
+
duration: performance.now() - routeMatchStart,
|
|
84
|
+
startTime: routeMatchStart - metricsStore.requestStart,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
const result = await resolveRoute<TEnv>(pathname, {
|
|
89
|
+
findMatch: (p) => deps.findMatch(p, metricsStore),
|
|
90
|
+
metricsStore,
|
|
91
|
+
isSSR: true,
|
|
64
92
|
});
|
|
65
|
-
}
|
|
66
93
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
94
|
+
if (!result) {
|
|
95
|
+
throw new RouteNotFoundError(`No route matched for ${pathname}`, {
|
|
96
|
+
cause: { pathname, method: request.method },
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (result.type === "redirect") {
|
|
101
|
+
// Preserve the raw query string (including internal _rsc* params) on the
|
|
102
|
+
// redirect target, matching the pre-reuse behavior of `url.search`.
|
|
103
|
+
const rawSearch = (reqCtx?.originalUrl ?? new URL(request.url)).search;
|
|
104
|
+
return {
|
|
105
|
+
type: "redirect",
|
|
106
|
+
redirectUrl: result.redirectTo + rawSearch,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
73
109
|
|
|
74
|
-
|
|
110
|
+
snapshot = result.snapshot;
|
|
111
|
+
}
|
|
75
112
|
|
|
76
113
|
const { matched } = snapshot;
|
|
77
114
|
|
|
@@ -79,8 +116,6 @@ export async function createMatchContextForFull<TEnv>(
|
|
|
79
116
|
matched.pt = true;
|
|
80
117
|
}
|
|
81
118
|
|
|
82
|
-
const cleanUrl = stripInternalParams(url);
|
|
83
|
-
|
|
84
119
|
const handlerContext = createHandlerContext(
|
|
85
120
|
matched.params,
|
|
86
121
|
request,
|
|
@@ -164,8 +199,13 @@ export async function createMatchContextForPartial<TEnv>(
|
|
|
164
199
|
findInterceptForRoute: MatchApiDeps<TEnv>["findInterceptForRoute"],
|
|
165
200
|
actionContext?: ActionContext,
|
|
166
201
|
): Promise<MatchContext<TEnv> | null> {
|
|
167
|
-
const
|
|
168
|
-
|
|
202
|
+
const reqCtx = _getRequestContext();
|
|
203
|
+
// resolveNavigation needs the raw URL (reads internal _rsc_segments /
|
|
204
|
+
// _rsc_stale params that stripInternalParams removes); the clean URL feeds the
|
|
205
|
+
// handler context. Both come off the request context when present.
|
|
206
|
+
const rawUrl = reqCtx?.originalUrl ?? new URL(request.url);
|
|
207
|
+
const cleanUrl = reqCtx?.url ?? stripInternalParams(rawUrl);
|
|
208
|
+
const pathname = cleanUrl.pathname;
|
|
169
209
|
|
|
170
210
|
const metricsStore = deps.getMetricsStore();
|
|
171
211
|
|
|
@@ -177,11 +217,13 @@ export async function createMatchContextForPartial<TEnv>(
|
|
|
177
217
|
}
|
|
178
218
|
|
|
179
219
|
// Reuse the classified snapshot when available and not invalidated by HMR.
|
|
180
|
-
// classifyRequest
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
220
|
+
// classifyRequest resolves a partial request with isSSR=false, matching this
|
|
221
|
+
// path. Never reuse an isSSR:true (full-render) snapshot — its loading()
|
|
222
|
+
// entries and manifest partition are wrong here. On HMR, discard to pick up
|
|
223
|
+
// manifest changes.
|
|
224
|
+
const classified = reqCtx?._classifiedRoute;
|
|
225
|
+
const classifiedRoute =
|
|
226
|
+
!isHmr && classified?.isSSR !== true ? classified : undefined;
|
|
185
227
|
|
|
186
228
|
// Time route matching. On the reuse path, only nav findMatch calls are new
|
|
187
229
|
// (current-route findMatch and manifest-loading were already timed during
|
|
@@ -220,7 +262,7 @@ export async function createMatchContextForPartial<TEnv>(
|
|
|
220
262
|
matched.pt = true;
|
|
221
263
|
}
|
|
222
264
|
|
|
223
|
-
const nav = resolveNavigation(request,
|
|
265
|
+
const nav = await resolveNavigation(request, rawUrl, matched.routeKey, {
|
|
224
266
|
findMatch: deps.findMatch,
|
|
225
267
|
});
|
|
226
268
|
if (!nav) {
|
|
@@ -243,8 +285,6 @@ export async function createMatchContextForPartial<TEnv>(
|
|
|
243
285
|
});
|
|
244
286
|
}
|
|
245
287
|
|
|
246
|
-
const cleanUrl = stripInternalParams(url);
|
|
247
|
-
|
|
248
288
|
const handlerContext = createHandlerContext(
|
|
249
289
|
matched.params,
|
|
250
290
|
request,
|
|
@@ -287,7 +327,11 @@ export async function createMatchContextForPartial<TEnv>(
|
|
|
287
327
|
});
|
|
288
328
|
}
|
|
289
329
|
|
|
290
|
-
setRequestContextPrevRouteKey(
|
|
330
|
+
setRequestContextPrevRouteKey(
|
|
331
|
+
nav.effectiveFromMatch?.routeKey,
|
|
332
|
+
nav.effectiveFromUrl,
|
|
333
|
+
nav.effectiveFromMatch?.params ?? nav.prevParams,
|
|
334
|
+
);
|
|
291
335
|
|
|
292
336
|
const interceptSelectorContext: InterceptSelectorContext = {
|
|
293
337
|
from: nav.effectiveFromUrl,
|
|
@@ -400,7 +444,7 @@ export async function matchError<TEnv>(
|
|
|
400
444
|
|
|
401
445
|
debugLog("matchError", "matching error", { pathname });
|
|
402
446
|
|
|
403
|
-
const matched = deps.findMatch(pathname);
|
|
447
|
+
const matched = await deps.findMatch(pathname);
|
|
404
448
|
if (!matched) {
|
|
405
449
|
debugWarn("matchError", "no route matched", { pathname });
|
|
406
450
|
return null;
|
|
@@ -41,6 +41,109 @@ import {
|
|
|
41
41
|
} from "./telemetry.js";
|
|
42
42
|
import { _getRequestContext } from "../server/request-context.js";
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Per-call telemetry lifecycle emitter for match()/matchPartial(). Each method
|
|
46
|
+
* reproduces the exact event object the two functions used to emit inline and is
|
|
47
|
+
* gated on the same per-call flag (`enabled` = the request's `emitTelemetry`), so
|
|
48
|
+
* a PPR shell-capture run (enabled=false) emits nothing while a foreground run
|
|
49
|
+
* emits byte-identical events. Extracted so the two transactions can't drift;
|
|
50
|
+
* pinned by thrown-response-telemetry.test.ts and
|
|
51
|
+
* shell-capture-telemetry-suppression.test.ts.
|
|
52
|
+
*/
|
|
53
|
+
interface LifecycleEmitter {
|
|
54
|
+
start(): void;
|
|
55
|
+
end(segmentCount: number, cacheHit: boolean, status?: number): void;
|
|
56
|
+
cacheDecision(
|
|
57
|
+
routeKey: string,
|
|
58
|
+
state: {
|
|
59
|
+
cacheHit: boolean;
|
|
60
|
+
cacheSource?: "runtime" | "prerender";
|
|
61
|
+
shouldRevalidate?: boolean;
|
|
62
|
+
},
|
|
63
|
+
segments: CacheSegmentSignal[],
|
|
64
|
+
): void;
|
|
65
|
+
error(error: Error, phase: string): void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createLifecycleEmitter(args: {
|
|
69
|
+
enabled: boolean;
|
|
70
|
+
sink: TelemetrySink;
|
|
71
|
+
requestId: string | undefined;
|
|
72
|
+
method: string;
|
|
73
|
+
pathname: string;
|
|
74
|
+
transaction: "match" | "matchPartial";
|
|
75
|
+
isPartial: boolean;
|
|
76
|
+
matchStart: number;
|
|
77
|
+
}): LifecycleEmitter {
|
|
78
|
+
return {
|
|
79
|
+
start(): void {
|
|
80
|
+
if (!args.enabled) return;
|
|
81
|
+
safeEmit(args.sink, {
|
|
82
|
+
type: "request.start",
|
|
83
|
+
timestamp: args.matchStart,
|
|
84
|
+
requestId: args.requestId,
|
|
85
|
+
method: args.method,
|
|
86
|
+
pathname: args.pathname,
|
|
87
|
+
transaction: args.transaction,
|
|
88
|
+
isPartial: args.isPartial,
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
end(segmentCount: number, cacheHit: boolean, status?: number): void {
|
|
92
|
+
if (!args.enabled) return;
|
|
93
|
+
safeEmit(args.sink, {
|
|
94
|
+
type: "request.end",
|
|
95
|
+
timestamp: performance.now(),
|
|
96
|
+
requestId: args.requestId,
|
|
97
|
+
method: args.method,
|
|
98
|
+
pathname: args.pathname,
|
|
99
|
+
transaction: args.transaction,
|
|
100
|
+
durationMs: performance.now() - args.matchStart,
|
|
101
|
+
segmentCount,
|
|
102
|
+
cacheHit,
|
|
103
|
+
// Only a thrown-Response short-circuit passes a status; a normal render
|
|
104
|
+
// completion omits it (the Response is built after match()).
|
|
105
|
+
...(status !== undefined && { status }),
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
cacheDecision(
|
|
109
|
+
routeKey: string,
|
|
110
|
+
state: {
|
|
111
|
+
cacheHit: boolean;
|
|
112
|
+
cacheSource?: "runtime" | "prerender";
|
|
113
|
+
shouldRevalidate?: boolean;
|
|
114
|
+
},
|
|
115
|
+
segments: CacheSegmentSignal[],
|
|
116
|
+
): void {
|
|
117
|
+
if (!args.enabled) return;
|
|
118
|
+
safeEmit(args.sink, {
|
|
119
|
+
type: "cache.decision",
|
|
120
|
+
timestamp: performance.now(),
|
|
121
|
+
requestId: args.requestId,
|
|
122
|
+
pathname: args.pathname,
|
|
123
|
+
routeKey,
|
|
124
|
+
hit: state.cacheHit,
|
|
125
|
+
shouldRevalidate: !!state.shouldRevalidate,
|
|
126
|
+
source: state.cacheSource,
|
|
127
|
+
segments,
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
error(error: Error, phase: string): void {
|
|
131
|
+
if (!args.enabled) return;
|
|
132
|
+
safeEmit(args.sink, {
|
|
133
|
+
type: "request.error",
|
|
134
|
+
timestamp: performance.now(),
|
|
135
|
+
requestId: args.requestId,
|
|
136
|
+
method: args.method,
|
|
137
|
+
pathname: args.pathname,
|
|
138
|
+
transaction: args.transaction,
|
|
139
|
+
error,
|
|
140
|
+
phase,
|
|
141
|
+
durationMs: performance.now() - args.matchStart,
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
44
147
|
export interface MatchHandlerDeps<TEnv = any> {
|
|
45
148
|
buildRouterContext: () => RouterContext<TEnv>;
|
|
46
149
|
callOnError: (error: unknown, phase: ErrorPhase, context: any) => void;
|
|
@@ -155,42 +258,43 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
155
258
|
}
|
|
156
259
|
|
|
157
260
|
async function match(request: Request, env: TEnv): Promise<MatchResult> {
|
|
158
|
-
|
|
261
|
+
// Silence telemetry for the PPR background shell capture: it re-runs match()
|
|
262
|
+
// under a derived request context flagged _shellCaptureRun (shell-capture.ts
|
|
263
|
+
// attemptCapture), re-using the foreground Request — a second request.start/
|
|
264
|
+
// cache.decision/request.end stamped with the same WeakMap-keyed requestId
|
|
265
|
+
// would double-count dashboards. Derived here (inside the capture's active
|
|
266
|
+
// request-context ALS) so the read sees the derived context, not module state.
|
|
267
|
+
const emitTelemetry =
|
|
268
|
+
hasTelemetry && !_getRequestContext()?._shellCaptureRun;
|
|
269
|
+
const requestId = emitTelemetry ? getRequestId(request) : undefined;
|
|
159
270
|
return runWithRouterLogContext({ request, transaction: "match" }, () => {
|
|
160
271
|
const routerCtx = buildRouterContext();
|
|
272
|
+
// Also mute in-pipeline observeEvent emitters (revalidation.decision,
|
|
273
|
+
// cache-lookup's cache.decision) which read routerCtx.telemetry.
|
|
274
|
+
if (!emitTelemetry) routerCtx.telemetry = undefined;
|
|
161
275
|
routerCtx.requestId = requestId;
|
|
162
276
|
return runWithRouterContext(routerCtx, async () =>
|
|
163
277
|
withRouterLogScope("match", async () => {
|
|
164
278
|
const matchStart = performance.now();
|
|
165
|
-
const pathname =
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
279
|
+
const pathname =
|
|
280
|
+
_getRequestContext()?.url?.pathname ??
|
|
281
|
+
new URL(request.url).pathname;
|
|
282
|
+
const emitter = createLifecycleEmitter({
|
|
283
|
+
enabled: emitTelemetry,
|
|
284
|
+
sink: telemetry,
|
|
285
|
+
requestId,
|
|
286
|
+
method: request.method,
|
|
287
|
+
pathname,
|
|
288
|
+
transaction: "match",
|
|
289
|
+
isPartial: false,
|
|
290
|
+
matchStart,
|
|
291
|
+
});
|
|
292
|
+
emitter.start();
|
|
177
293
|
|
|
178
294
|
const result = await createMatchContextForFull(request, env);
|
|
179
295
|
|
|
180
296
|
if ("type" in result && result.type === "redirect") {
|
|
181
|
-
|
|
182
|
-
safeEmit(telemetry, {
|
|
183
|
-
type: "request.end",
|
|
184
|
-
timestamp: performance.now(),
|
|
185
|
-
requestId,
|
|
186
|
-
method: request.method,
|
|
187
|
-
pathname,
|
|
188
|
-
transaction: "match",
|
|
189
|
-
durationMs: performance.now() - matchStart,
|
|
190
|
-
segmentCount: 0,
|
|
191
|
-
cacheHit: false,
|
|
192
|
-
});
|
|
193
|
-
}
|
|
297
|
+
emitter.end(0, false);
|
|
194
298
|
return {
|
|
195
299
|
segments: [],
|
|
196
300
|
matched: [],
|
|
@@ -210,51 +314,26 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
210
314
|
if (hasTelemetry || cacheSignalEnabled) {
|
|
211
315
|
const signalSegments = buildSignal(ctx.routeKey, state);
|
|
212
316
|
recordSignalIfEnabled(signalSegments);
|
|
213
|
-
|
|
214
|
-
safeEmit(telemetry, {
|
|
215
|
-
type: "cache.decision",
|
|
216
|
-
timestamp: performance.now(),
|
|
217
|
-
requestId,
|
|
218
|
-
pathname,
|
|
219
|
-
routeKey: ctx.routeKey,
|
|
220
|
-
hit: state.cacheHit,
|
|
221
|
-
shouldRevalidate: !!state.shouldRevalidate,
|
|
222
|
-
source: state.cacheSource,
|
|
223
|
-
segments: signalSegments,
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
if (hasTelemetry) {
|
|
228
|
-
safeEmit(telemetry, {
|
|
229
|
-
type: "request.end",
|
|
230
|
-
timestamp: performance.now(),
|
|
231
|
-
requestId,
|
|
232
|
-
method: request.method,
|
|
233
|
-
pathname,
|
|
234
|
-
transaction: "match",
|
|
235
|
-
durationMs: performance.now() - matchStart,
|
|
236
|
-
segmentCount: matchResult.segments.length,
|
|
237
|
-
cacheHit: state.cacheHit,
|
|
238
|
-
});
|
|
317
|
+
emitter.cacheDecision(ctx.routeKey, state, signalSegments);
|
|
239
318
|
}
|
|
319
|
+
emitter.end(matchResult.segments.length, state.cacheHit);
|
|
240
320
|
return matchResult;
|
|
241
321
|
} catch (error) {
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
error: errorObj,
|
|
253
|
-
phase: error instanceof Response ? "redirect" : "routing",
|
|
254
|
-
durationMs: performance.now() - matchStart,
|
|
255
|
-
});
|
|
322
|
+
if (error instanceof Response) {
|
|
323
|
+
// A thrown Response (middleware short-circuit — redirect / auth
|
|
324
|
+
// gate) is a COMPLETED request from the consumer's seat, not an
|
|
325
|
+
// error: emit request.end (the same shape the non-thrown redirect
|
|
326
|
+
// result above already emits), never request.error with a
|
|
327
|
+
// synthetic "[object Response]" error. Rethrow so the caller
|
|
328
|
+
// drives the redirect. Carry the Response's status so a sink can
|
|
329
|
+
// tell a 3xx short-circuit from a 2xx completion.
|
|
330
|
+
emitter.end(0, false, error.status);
|
|
331
|
+
throw error;
|
|
256
332
|
}
|
|
257
|
-
|
|
333
|
+
emitter.error(
|
|
334
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
335
|
+
"routing",
|
|
336
|
+
);
|
|
258
337
|
callOnError(error, "routing", {
|
|
259
338
|
request,
|
|
260
339
|
url: ctx.url,
|
|
@@ -294,27 +373,34 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
294
373
|
context: TEnv,
|
|
295
374
|
actionContext?: ActionContext,
|
|
296
375
|
): Promise<MatchResult | null> {
|
|
297
|
-
|
|
376
|
+
// See match() above: the PPR shell capture re-runs matchPartial() under a
|
|
377
|
+
// _shellCaptureRun context and must stay invisible to the sink.
|
|
378
|
+
const emitTelemetry =
|
|
379
|
+
hasTelemetry && !_getRequestContext()?._shellCaptureRun;
|
|
380
|
+
const partialRequestId = emitTelemetry ? getRequestId(request) : undefined;
|
|
298
381
|
return runWithRouterLogContext(
|
|
299
382
|
{ request, transaction: "matchPartial" },
|
|
300
383
|
() => {
|
|
301
384
|
const routerCtx = buildRouterContext();
|
|
385
|
+
if (!emitTelemetry) routerCtx.telemetry = undefined;
|
|
302
386
|
routerCtx.requestId = partialRequestId;
|
|
303
387
|
return runWithRouterContext(routerCtx, async () =>
|
|
304
388
|
withRouterLogScope("matchPartial", async () => {
|
|
305
389
|
const matchStart = performance.now();
|
|
306
|
-
const pathname =
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
390
|
+
const pathname =
|
|
391
|
+
_getRequestContext()?.url?.pathname ??
|
|
392
|
+
new URL(request.url).pathname;
|
|
393
|
+
const emitter = createLifecycleEmitter({
|
|
394
|
+
enabled: emitTelemetry,
|
|
395
|
+
sink: telemetry,
|
|
396
|
+
requestId: partialRequestId,
|
|
397
|
+
method: request.method,
|
|
398
|
+
pathname,
|
|
399
|
+
transaction: "matchPartial",
|
|
400
|
+
isPartial: true,
|
|
401
|
+
matchStart,
|
|
402
|
+
});
|
|
403
|
+
emitter.start();
|
|
318
404
|
|
|
319
405
|
const ctx = await createMatchContextForPartial(
|
|
320
406
|
request,
|
|
@@ -322,19 +408,7 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
322
408
|
actionContext,
|
|
323
409
|
);
|
|
324
410
|
if (!ctx) {
|
|
325
|
-
|
|
326
|
-
safeEmit(telemetry, {
|
|
327
|
-
type: "request.end",
|
|
328
|
-
timestamp: performance.now(),
|
|
329
|
-
requestId: partialRequestId,
|
|
330
|
-
method: request.method,
|
|
331
|
-
pathname,
|
|
332
|
-
transaction: "matchPartial",
|
|
333
|
-
durationMs: performance.now() - matchStart,
|
|
334
|
-
segmentCount: 0,
|
|
335
|
-
cacheHit: false,
|
|
336
|
-
});
|
|
337
|
-
}
|
|
411
|
+
emitter.end(0, false);
|
|
338
412
|
return null;
|
|
339
413
|
}
|
|
340
414
|
|
|
@@ -361,53 +435,25 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
361
435
|
if (hasTelemetry || cacheSignalEnabled) {
|
|
362
436
|
const signalSegments = buildSignal(ctx.routeKey, state);
|
|
363
437
|
recordSignalIfEnabled(signalSegments);
|
|
364
|
-
|
|
365
|
-
safeEmit(telemetry, {
|
|
366
|
-
type: "cache.decision",
|
|
367
|
-
timestamp: performance.now(),
|
|
368
|
-
requestId: partialRequestId,
|
|
369
|
-
pathname,
|
|
370
|
-
routeKey: ctx.routeKey,
|
|
371
|
-
hit: state.cacheHit,
|
|
372
|
-
shouldRevalidate: !!state.shouldRevalidate,
|
|
373
|
-
source: state.cacheSource,
|
|
374
|
-
segments: signalSegments,
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
if (hasTelemetry) {
|
|
379
|
-
safeEmit(telemetry, {
|
|
380
|
-
type: "request.end",
|
|
381
|
-
timestamp: performance.now(),
|
|
382
|
-
requestId: partialRequestId,
|
|
383
|
-
method: request.method,
|
|
384
|
-
pathname,
|
|
385
|
-
transaction: "matchPartial",
|
|
386
|
-
durationMs: performance.now() - matchStart,
|
|
387
|
-
segmentCount: matchResult.segments.length,
|
|
388
|
-
cacheHit: state.cacheHit,
|
|
389
|
-
});
|
|
438
|
+
emitter.cacheDecision(ctx.routeKey, state, signalSegments);
|
|
390
439
|
}
|
|
440
|
+
emitter.end(matchResult.segments.length, state.cacheHit);
|
|
391
441
|
return matchResult;
|
|
392
442
|
} catch (error) {
|
|
393
443
|
flushRevalidationTrace();
|
|
394
|
-
if (
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
method: request.method,
|
|
403
|
-
pathname,
|
|
404
|
-
transaction: "matchPartial",
|
|
405
|
-
error: errorObj,
|
|
406
|
-
phase: error instanceof Response ? "redirect" : phase,
|
|
407
|
-
durationMs: performance.now() - matchStart,
|
|
408
|
-
});
|
|
444
|
+
if (error instanceof Response) {
|
|
445
|
+
// A thrown Response (middleware short-circuit — redirect / auth
|
|
446
|
+
// gate) is a COMPLETED request, not an error: emit request.end
|
|
447
|
+
// (parity with match()), never request.error. Rethrow so the
|
|
448
|
+
// caller drives the redirect. Carry the Response's status so a
|
|
449
|
+
// sink can tell a 3xx short-circuit from a 2xx completion.
|
|
450
|
+
emitter.end(0, false, error.status);
|
|
451
|
+
throw error;
|
|
409
452
|
}
|
|
410
|
-
|
|
453
|
+
emitter.error(
|
|
454
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
455
|
+
actionContext ? "action" : "revalidation",
|
|
456
|
+
);
|
|
411
457
|
callOnError(error, actionContext ? "action" : "revalidation", {
|
|
412
458
|
request,
|
|
413
459
|
url: ctx.url,
|
|
@@ -105,6 +105,10 @@ import { getRouterContext } from "../router-context.js";
|
|
|
105
105
|
import type { GeneratorMiddleware } from "./cache-lookup.js";
|
|
106
106
|
import { debugLog, debugWarn, getOrCreateRequestId } from "../logging.js";
|
|
107
107
|
import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
|
|
108
|
+
import {
|
|
109
|
+
runWithRequestContext,
|
|
110
|
+
type RequestContext,
|
|
111
|
+
} from "../../server/request-context.js";
|
|
108
112
|
|
|
109
113
|
/**
|
|
110
114
|
* Creates background revalidation middleware
|
|
@@ -183,33 +187,46 @@ export function withBackgroundRevalidation<TEnv>(
|
|
|
183
187
|
const freshLoaderPromises = new Map<string, Promise<any>>();
|
|
184
188
|
setupLoaderAccess(freshHandlerContext, freshLoaderPromises);
|
|
185
189
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
190
|
+
// Re-establish the request-context ALS around the re-render. ctx.Store
|
|
191
|
+
// is a different ALS (DSL build context); on workerd a waitUntil task
|
|
192
|
+
// runs detached from the request's I/O context, so a handler/component
|
|
193
|
+
// that reads the ambient getRequestContext() during this background
|
|
194
|
+
// re-render would otherwise throw "called outside of a request context".
|
|
195
|
+
const freshSegments = await runWithRequestContext(
|
|
196
|
+
requestCtx as RequestContext<TEnv>,
|
|
197
|
+
() =>
|
|
198
|
+
ctx.Store.run(() =>
|
|
199
|
+
resolveAllSegments(
|
|
200
|
+
ctx.entries,
|
|
201
|
+
ctx.routeKey,
|
|
202
|
+
ctx.matched.params,
|
|
203
|
+
freshHandlerContext,
|
|
204
|
+
freshLoaderPromises,
|
|
205
|
+
{ skipLoaders: true },
|
|
206
|
+
),
|
|
207
|
+
),
|
|
195
208
|
);
|
|
196
209
|
|
|
197
210
|
let freshInterceptSegments: ResolvedSegment[] = [];
|
|
198
211
|
if (ctx.interceptResult) {
|
|
199
|
-
freshInterceptSegments = await
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
ctx.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
212
|
+
freshInterceptSegments = await runWithRequestContext(
|
|
213
|
+
requestCtx as RequestContext<TEnv>,
|
|
214
|
+
() =>
|
|
215
|
+
ctx.Store.run(() =>
|
|
216
|
+
resolveInterceptEntry(
|
|
217
|
+
ctx.interceptResult!.intercept,
|
|
218
|
+
ctx.interceptResult!.entry,
|
|
219
|
+
ctx.matched.params,
|
|
220
|
+
freshHandlerContext,
|
|
221
|
+
true,
|
|
222
|
+
undefined,
|
|
223
|
+
// Skip intercept middleware: this is a post-response background
|
|
224
|
+
// re-render to refresh a stale cached route. The foreground
|
|
225
|
+
// already ran the middleware; re-running it would double its
|
|
226
|
+
// side effects and a short-circuit Response would abort the write.
|
|
227
|
+
{ skipMiddleware: true },
|
|
228
|
+
),
|
|
229
|
+
),
|
|
213
230
|
);
|
|
214
231
|
}
|
|
215
232
|
|