import type { ReactNode } from "react"; import { sanitizeError } from "../errors"; import type { ErrorInfo, ErrorPhase, MatchResult } from "../types"; import type { EntryData, InterceptEntry, InterceptSelectorContext, } from "../server/context"; import type { MatchApiDeps } from "./types.js"; import type { RouterContext } from "./router-context.js"; import { runWithRouterContext } from "./router-context.js"; import { type ActionContext, type MatchContext, createPipelineState, } from "./match-context.js"; import { createMatchPartialPipeline } from "./match-pipelines.js"; import { collectMatchResult } from "./match-result.js"; import { createMatchContextForFull as _createMatchContextForFull, createMatchContextForPartial as _createMatchContextForPartial, matchError as _matchError, } from "./match-api.js"; import { previewMatch as _previewMatch } from "./preview-match.js"; import { runWithRouterLogContext, withRouterLogScope, isRouterDebugEnabled, startRevalidationTrace, flushRevalidationTrace, } from "./logging.js"; import type { ErrorBoundaryHandler, NotFoundBoundaryHandler } from "../types"; import type { MiddlewareFn } from "./middleware.js"; import { type TelemetrySink, safeEmit, resolveSink, getRequestId, } from "./telemetry.js"; export interface MatchHandlerDeps { buildRouterContext: () => RouterContext; callOnError: (error: unknown, phase: ErrorPhase, context: any) => void; matchApiDeps: MatchApiDeps; defaultErrorBoundary: ReactNode | ErrorBoundaryHandler | undefined; findMatch: (pathname: string, ms?: any) => any; findInterceptForRoute: ( routeKey: string, parentEntry: EntryData | null, selectorContext: InterceptSelectorContext | null, isAction: boolean, ) => { intercept: InterceptEntry; entry: EntryData } | null; telemetry?: TelemetrySink; } export interface MatchHandlers { match: (request: Request, env: TEnv) => Promise; matchPartial: ( request: Request, context: TEnv, actionContext?: ActionContext, ) => Promise; matchError: ( request: Request, _context: TEnv, error: unknown, segmentType?: ErrorInfo["segmentType"], ) => Promise; previewMatch: ( request: Request, _context: TEnv, ) => Promise<{ routeMiddleware?: Array<{ handler: MiddlewareFn; params: Record; }>; responseType?: string; handler?: Function; params?: Record; negotiated?: boolean; manifestEntry?: EntryData; } | null>; createMatchContextForFull: ( request: Request, env: TEnv, ) => Promise | { type: "redirect"; redirectUrl: string }>; createMatchContextForPartial: ( request: Request, env: TEnv, actionContext?: { actionId?: string; actionUrl?: URL; actionResult?: any; formData?: FormData; }, ) => Promise | null>; } /** * Create match handler functions bound to router closure state. * These are the main request-handling entry points for SSR, navigation, * error recovery, and preview matching. */ export function createMatchHandlers( deps: MatchHandlerDeps, ): MatchHandlers { const { buildRouterContext, callOnError, matchApiDeps, defaultErrorBoundary, findInterceptForRoute, } = deps; const hasTelemetry = !!deps.telemetry; const telemetry = resolveSink(deps.telemetry); async function createMatchContextForFull( request: Request, env: TEnv, ): Promise | { type: "redirect"; redirectUrl: string }> { return _createMatchContextForFull( request, env, matchApiDeps, findInterceptForRoute, ); } async function createMatchContextForPartial( request: Request, env: TEnv, actionContext?: { actionId?: string; actionUrl?: URL; actionResult?: any; formData?: FormData; }, ): Promise | null> { return _createMatchContextForPartial( request, env, matchApiDeps, findInterceptForRoute, actionContext, ); } /** * Match request and return segments (document/SSR requests) * * Uses generator middleware pipeline for clean separation of concerns: * - cache-lookup: Check cache first * - segment-resolution: Resolve segments on cache miss * - cache-store: Store results in cache * - background-revalidation: SWR revalidation */ async function match(request: Request, env: TEnv): Promise { const requestId = hasTelemetry ? getRequestId(request) : undefined; return runWithRouterLogContext({ request, transaction: "match" }, () => { const routerCtx = buildRouterContext(); routerCtx.requestId = requestId; return runWithRouterContext(routerCtx, async () => withRouterLogScope("match", async () => { const matchStart = performance.now(); const pathname = new URL(request.url).pathname; if (hasTelemetry) { safeEmit(telemetry, { type: "request.start", timestamp: matchStart, requestId, method: request.method, pathname, transaction: "match", isPartial: false, }); } const result = await createMatchContextForFull(request, env); // Handle redirect case if ("type" in result && result.type === "redirect") { if (hasTelemetry) { safeEmit(telemetry, { type: "request.end", timestamp: performance.now(), requestId, method: request.method, pathname, transaction: "match", durationMs: performance.now() - matchStart, segmentCount: 0, cacheHit: false, }); } return { segments: [], matched: [], diff: [], resolvedIds: [], params: {}, redirect: result.redirectUrl, }; } const ctx = result as MatchContext; try { const state = createPipelineState(); const pipeline = createMatchPartialPipeline(ctx, state); const matchResult = await collectMatchResult(pipeline, ctx, state); if (hasTelemetry) { safeEmit(telemetry, { type: "cache.decision", timestamp: performance.now(), requestId, pathname, routeKey: ctx.routeKey, hit: state.cacheHit, shouldRevalidate: !!state.shouldRevalidate, source: state.cacheSource, }); safeEmit(telemetry, { type: "request.end", timestamp: performance.now(), requestId, method: request.method, pathname, transaction: "match", durationMs: performance.now() - matchStart, segmentCount: matchResult.segments.length, cacheHit: state.cacheHit, }); } return matchResult; } catch (error) { if (hasTelemetry) { const errorObj = error instanceof Error ? error : new Error(String(error)); safeEmit(telemetry, { type: "request.error", timestamp: performance.now(), requestId, method: request.method, pathname, transaction: "match", error: errorObj, phase: error instanceof Response ? "redirect" : "routing", durationMs: performance.now() - matchStart, }); } if (error instanceof Response) throw error; // Report unhandled errors during full match pipeline callOnError(error, "routing", { request, url: ctx.url, env, isPartial: false, handledByBoundary: false, }); throw sanitizeError(error); } }), ); }); } async function matchError( request: Request, _context: TEnv, error: unknown, segmentType: ErrorInfo["segmentType"] = "route", ): Promise { return runWithRouterLogContext({ request, transaction: "matchError" }, () => withRouterLogScope("matchError", () => _matchError( request, _context, error, matchApiDeps, defaultErrorBoundary, segmentType, ), ), ); } /** * Match partial request with revalidation * * Uses generator middleware pipeline for clean separation of concerns: * - cache-lookup: Check cache first * - segment-resolution: Resolve segments on cache miss * - intercept-resolution: Handle intercept routes * - cache-store: Store results in cache * - background-revalidation: SWR revalidation */ async function matchPartial( request: Request, context: TEnv, actionContext?: ActionContext, ): Promise { const partialRequestId = hasTelemetry ? getRequestId(request) : undefined; return runWithRouterLogContext( { request, transaction: "matchPartial" }, () => { const routerCtx = buildRouterContext(); routerCtx.requestId = partialRequestId; return runWithRouterContext(routerCtx, async () => withRouterLogScope("matchPartial", async () => { const matchStart = performance.now(); const pathname = new URL(request.url).pathname; if (hasTelemetry) { safeEmit(telemetry, { type: "request.start", timestamp: matchStart, requestId: partialRequestId, method: request.method, pathname, transaction: "matchPartial", isPartial: true, }); } const ctx = await createMatchContextForPartial( request, context, actionContext, ); if (!ctx) { if (hasTelemetry) { safeEmit(telemetry, { type: "request.end", timestamp: performance.now(), requestId: partialRequestId, method: request.method, pathname, transaction: "matchPartial", durationMs: performance.now() - matchStart, segmentCount: 0, cacheHit: false, }); } return null; } if (isRouterDebugEnabled()) { startRevalidationTrace({ method: request.method, prevUrl: ctx.prevUrl.href, nextUrl: ctx.url.href, routeKey: ctx.routeKey, isAction: !!actionContext, stale: ctx.stale || undefined, }); } try { const state = createPipelineState(); const pipeline = createMatchPartialPipeline(ctx, state); const matchResult = await collectMatchResult( pipeline, ctx, state, ); flushRevalidationTrace(); if (hasTelemetry) { safeEmit(telemetry, { type: "cache.decision", timestamp: performance.now(), requestId: partialRequestId, pathname, routeKey: ctx.routeKey, hit: state.cacheHit, shouldRevalidate: !!state.shouldRevalidate, source: state.cacheSource, }); safeEmit(telemetry, { type: "request.end", timestamp: performance.now(), requestId: partialRequestId, method: request.method, pathname, transaction: "matchPartial", durationMs: performance.now() - matchStart, segmentCount: matchResult.segments.length, cacheHit: state.cacheHit, }); } return matchResult; } catch (error) { flushRevalidationTrace(); if (hasTelemetry) { const errorObj = error instanceof Error ? error : new Error(String(error)); const phase = actionContext ? "action" : "revalidation"; safeEmit(telemetry, { type: "request.error", timestamp: performance.now(), requestId: partialRequestId, method: request.method, pathname, transaction: "matchPartial", error: errorObj, phase: error instanceof Response ? "redirect" : phase, durationMs: performance.now() - matchStart, }); } if (error instanceof Response) throw error; // Report unhandled errors during partial match pipeline callOnError(error, actionContext ? "action" : "revalidation", { request, url: ctx.url, env: context, actionId: actionContext?.actionId, isPartial: true, handledByBoundary: false, }); throw sanitizeError(error); } }), ); }, ); } async function previewMatch( request: Request, _context: TEnv, ): ReturnType { return _previewMatch(request, _context, { findMatch: deps.findMatch }); } return { match: match, matchPartial: matchPartial, matchError: matchError, previewMatch: previewMatch, createMatchContextForFull: createMatchContextForFull, createMatchContextForPartial: createMatchContextForPartial, }; }