UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

1,250 lines (1,191 loc) 38.8 kB
import type { HttpContractConfig } from "../contracts/index.js"; import { httpErrors, isAppError, toErrorResponseBody, } from "../errors/index.js"; import { IdempotencyConflictError, IdempotencyInProgressError, } from "../idempotency/index.js"; import { type MemoInstrumentationEvent, runWithMemoScope, } from "../memo/index.js"; import type { AnyPorts } from "../ports/index.js"; import { AuthUnauthorizedError, EntitlementRequiredError, GateAuthorizationError, TenantRequiredError, } from "../ports/index.js"; import { createProviderInstrumentation, type ProviderInstrumentationTarget, resolveProviderInstrumentationPort, } from "../providers/index.js"; import { parseTraceparent, resolveTracingPort, runWithTracing, type TraceContextInput, } from "../tracing/index.js"; import type { ContextSeed } from "./context.js"; import type { Handler, HandlerArgs, HttpRequestLike, HttpResponse, RequestStageTimings, RouteHook, ServerCaughtErrorHook, ServerHook, ServerUnhandledErrorMapper, } from "./http.js"; import type { ServerInstrumentationOptions } from "./instrumentation.js"; import { getResponseFinalizerHook, type ResponseFinalizerValidationState, } from "./internal-hooks.js"; import { readContextActor, readContextTenant, setActiveRequestIdentity, } from "./request-context.js"; import { prepareRequestInputs, type RequestBodyOptions, requestBodyLimit, requestHeadersToRecord, } from "./request-preparation.js"; import { defaultErrorResponse, errorResponse, finalizeResponse, isHttpResponseLike, isWebResponse, mergeNativeResponseHeaders, normalizeHttpResponse, normalizeResponse, ResponseContractViolationError, type ResponseOwner, responseForHooks, responseOwnerFor, toContractViolationResponse, withContractLifecycleHeaders, withFrameworkErrorOwnerHeader, } from "./response-finalization.js"; import { type CompiledPath, compilePath, decodeMatchedParams, PathDecodeError, } from "./route-matching.js"; import type { TrustedRequestInfo } from "./trusted-proxy.js"; import { InvalidRequestUrlError } from "./trusted-proxy-internal.js"; import { UseCaseRouteInputValidationError } from "./use-case-route.js"; function withoutHeadResponseBody( response: HttpResponse, requestMethod: string, ): HttpResponse { if (requestMethod.toUpperCase() !== "HEAD") return response; if (isWebResponse(response)) { if (response.body === null) return response; void response.body.cancel().catch(() => {}); return new Response(null, { status: response.status, statusText: response.statusText, headers: response.headers, }); } const { body: _body, ...headResponse } = response; return headResponse; } type RequestExecutorOptions<Ctx> = { instrumentation?: ServerInstrumentationOptions<Ctx> | false; validateResponses?: boolean; requestBody?: RequestBodyOptions; onCaughtError?: ServerCaughtErrorHook<Ctx>; mapUnhandledError?: ServerUnhandledErrorMapper<Ctx>; }; type ExecutionResult<Ctx> = { ctx?: Ctx; response: HttpResponse; error?: unknown; owner?: ResponseOwner; }; type ExecutionTarget<Ctx, C extends HttpContractConfig> = { contract: C; /** * Compiled route pattern. Only required when the executor is invoked * without pre-matched params. */ compiled?: CompiledPath; handler: Handler<Ctx, C>; /** * Success status whose response schema validation is skipped because the * use case bound to this route already validated its output against the * same schema object. */ responseValidationExemptStatus?: number; }; function copyTrustedRequestInfo( requestInfo: TrustedRequestInfo, ): TrustedRequestInfo; function copyTrustedRequestInfo(requestInfo: undefined): undefined; function copyTrustedRequestInfo( requestInfo: TrustedRequestInfo | undefined, ): TrustedRequestInfo | undefined; function copyTrustedRequestInfo( requestInfo: TrustedRequestInfo | undefined, ): TrustedRequestInfo | undefined { if (!requestInfo) return undefined; return { ...requestInfo, url: new URL(requestInfo.url), }; } /** * Build the per-request execution pipeline once. * * The returned executor takes the contract, compiled pattern, and user handler * per invocation so fallback responses (404/405) can reuse a single pipeline * across requests instead of rebuilding it per unmatched request. */ export function createRequestExecutor< Ctx, FinalPorts extends AnyPorts, C extends HttpContractConfig, >( options: RequestExecutorOptions<Ctx>, finalPorts: FinalPorts, contextRuntime: { createRequestContext: ( req: HttpRequestLike, contract: HttpContractConfig, requestInfo: TrustedRequestInfo, ) => Promise<Ctx>; finalizeContext: (seed: ContextSeed<Ctx> | Ctx) => Ctx; resolveRequestInfo: (req: HttpRequestLike) => TrustedRequestInfo; }, hooks: ServerHook<Ctx, FinalPorts>[], routeHooks: readonly RouteHook<unknown, object>[] = [], optionsOverrides?: { skipRoutePreparation?: boolean; /** * Run the route through the full hook pipeline without contract * preparation: no query/path/header/body parsing or validation, and the * request body is left unconsumed for the handler. */ rawRoute?: boolean; }, ): ( target: ExecutionTarget<Ctx, C>, req: HttpRequestLike, preMatchedParams?: Record<string, string>, ) => Promise<HttpResponse> { const warnedNativeReplacementHooks = new WeakSet<object>(); const maxRequestBodyBytes = requestBodyLimit(options.requestBody); const executeRequest = async ( target: ExecutionTarget<Ctx, C>, req: HttpRequestLike, preMatchedParams?: Record<string, string>, ) => { const { contract, handler: userHandler } = target; let requestInfo: TrustedRequestInfo | undefined; let baseCtx: Ctx | undefined; let pathValue: HandlerArgs<Ctx, C>["path"] | undefined; let queryValue: HandlerArgs<Ctx, C>["query"] | undefined; let headersValue: HandlerArgs<Ctx, C>["headers"] | undefined; let bodyValue: HandlerArgs<Ctx, C>["body"] | undefined; const startedAt = Date.now(); // Stage timings accumulate because retries re-enter the send phase and // beforeHandle spans the route-hook and server-hook loops. const stages: RequestStageTimings = { onRequestMs: 0, parseMs: 0, contextMs: 0, beforeHandleMs: 0, handlerMs: 0, sendMs: 0, }; const timeStage = async <T>( stage: keyof RequestStageTimings, run: () => Promise<T> | T, ): Promise<T> => { const stageStartedAt = performance.now(); try { return await run(); } finally { stages[stage] += performance.now() - stageStartedAt; } }; const resolveErrorResult = async ( error: unknown, ctx?: Ctx, path?: HandlerArgs<Ctx, C>["path"], query?: HandlerArgs<Ctx, C>["query"], headers?: HandlerArgs<Ctx, C>["headers"], body?: HandlerArgs<Ctx, C>["body"], resultOptions?: { owner?: ResponseOwner; }, ): Promise<ExecutionResult<Ctx>> => { let currentError = error; const notifyCaughtError = async (caught: unknown) => { const args = { err: caught, req, requestInfo: copyTrustedRequestInfo(requestInfo), ctx, contract, path, query, headers, body, }; for (const hook of hooks) { if (!hook.onCaughtError) continue; try { await hook.onCaughtError(args); } catch { // Observers must not change response behavior. } } if (options.onCaughtError) { try { await options.onCaughtError(args); } catch { // Observers must not change response behavior. } } }; await notifyCaughtError(currentError); if (currentError instanceof ResponseContractViolationError) { return { ctx, response: toContractViolationResponse(currentError), error: currentError, owner: "framework", }; } if (currentError instanceof UseCaseRouteInputValidationError) { return { ctx, response: errorResponse( 500, currentError.code, currentError.message, { contractName: currentError.contractName, useCaseName: currentError.useCaseName, location: "useCaseInput", }, ), error: currentError, owner: "framework", }; } if (currentError instanceof InvalidRequestUrlError) { return { ctx, response: errorResponse( 400, "INVALID_REQUEST_URL", "Malformed request URL", ), error: currentError, owner: "framework", }; } if (isAppError(currentError)) { return { ctx, response: { status: currentError.status, ...(currentError.headers ? { headers: { ...currentError.headers } } : {}), body: toErrorResponseBody(currentError), }, error: currentError, owner: resultOptions?.owner ?? "route", }; } if (currentError instanceof AuthUnauthorizedError) { return { ctx, response: errorResponse(401, currentError.code, currentError.message), error: currentError, owner: "framework", }; } if (currentError instanceof TenantRequiredError) { return { ctx, response: errorResponse( currentError.status, currentError.code, currentError.message, ), error: currentError, owner: "framework", }; } if (currentError instanceof IdempotencyConflictError) { return { ctx, response: errorResponse( httpErrors.IdempotencyConflict.status, httpErrors.IdempotencyConflict.code, currentError.message, { namespace: currentError.namespace, key: currentError.key, }, ), error: currentError, owner: "framework", }; } if (currentError instanceof IdempotencyInProgressError) { return { ctx, response: errorResponse( httpErrors.IdempotencyInProgress.status, httpErrors.IdempotencyInProgress.code, currentError.message, { namespace: currentError.namespace, key: currentError.key, }, ), error: currentError, owner: "framework", }; } if (currentError instanceof GateAuthorizationError) { return { ctx, response: errorResponse( currentError.status, currentError.code, currentError.message, currentError.details, ), error: currentError, owner: "framework", }; } if (currentError instanceof EntitlementRequiredError) { return { ctx, response: errorResponse( currentError.status, currentError.code, currentError.message, currentError.details, ), error: currentError, owner: "framework", }; } for (const hook of hooks) { if (!hook.mapUnhandledError) continue; try { const handled = await hook.mapUnhandledError({ err: currentError, req, requestInfo: copyTrustedRequestInfo(requestInfo), ctx, contract, path, query, headers, body, }); if (handled) { const response = normalizeHttpResponse(handled); return { ctx, response, error: currentError, owner: responseOwnerFor(response, "framework"), }; } } catch (hookError) { currentError = hookError; await notifyCaughtError(currentError); } } if (options.mapUnhandledError) { try { const handled = await options.mapUnhandledError({ err: currentError, req, requestInfo: copyTrustedRequestInfo(requestInfo), ctx, contract, path, query, headers, body, }); if (handled) { const response = normalizeHttpResponse(handled); return { ctx, response, error: currentError, owner: responseOwnerFor(response, "framework"), }; } } catch (hookError) { currentError = hookError; await notifyCaughtError(currentError); } } return { ctx, response: defaultErrorResponse(currentError, ctx), error: currentError, owner: "framework", }; }; try { const resolvedRequestInfo = contextRuntime.resolveRequestInfo(req); requestInfo = resolvedRequestInfo; const url = new URL(req.url); let matchedParams: Record<string, string>; if (preMatchedParams) { matchedParams = preMatchedParams; } else { const compiled = target.compiled; const match = compiled ? compiled.pattern.exec(url.pathname) : null; const contractMethod = contract.method.toUpperCase(); const requestMethod = req.method.toUpperCase(); const methodMatches = contractMethod === requestMethod || (contractMethod === "GET" && requestMethod === "HEAD"); if (!compiled || !match || !methodMatches) { return errorResponse(404, "NOT_FOUND", "Not found"); } try { matchedParams = decodeMatchedParams(compiled.keys, match); } catch (error) { if (error instanceof PathDecodeError) { return errorResponse(400, "INVALID_PATH", "Malformed URL path"); } throw error; } } const rawHeaders = requestHeadersToRecord(req.headers); const runNativeBeforeSend = async ( initialResult: ExecutionResult<Ctx>, nativeResponse: Response, ): Promise<Response> => { const originalView = responseForHooks(nativeResponse); const originalHeaders = originalView.headers ?? {}; let transformed = originalView; for (const hook of hooks) { if (!hook.beforeSend) continue; const nextResponse = await hook.beforeSend({ req, requestInfo: copyTrustedRequestInfo(resolvedRequestInfo), ctx: initialResult.ctx, contract, path: pathValue, query: queryValue, headers: headersValue, body: bodyValue, response: transformed, error: initialResult.error, native: true, }); if (nextResponse) { if ( (nextResponse.status !== nativeResponse.status || nextResponse.body !== undefined) && !warnedNativeReplacementHooks.has(hook) && process.env.NODE_ENV !== "production" ) { warnedNativeReplacementHooks.add(hook); console.warn( `[beignet] beforeSend hook "${hook.name ?? "(anonymous)"}" returned a replacement status or body for a native Response on ${contract.method} ${contract.path}. Native responses are headers-only in beforeSend; status and body changes are ignored.`, ); } transformed = { status: nativeResponse.status, headers: nextResponse.headers, }; } } return mergeNativeResponseHeaders( nativeResponse, originalHeaders, transformed.headers ?? {}, ); }; const applyTransformHooks = async ( initialResult: ExecutionResult<Ctx>, allowRetry: boolean, ): Promise<ExecutionResult<Ctx>> => { try { if (isWebResponse(initialResult.response)) { return { ...initialResult, response: await runNativeBeforeSend( initialResult, initialResult.response, ), }; } let transformed = normalizeResponse(initialResult.response); for (const hook of hooks) { if (!hook.beforeSend) continue; const nextResponse = await hook.beforeSend({ req, requestInfo: copyTrustedRequestInfo(resolvedRequestInfo), ctx: initialResult.ctx, contract, path: pathValue, query: queryValue, headers: headersValue, body: bodyValue, response: transformed, error: initialResult.error, }); if (nextResponse) { transformed = normalizeResponse(nextResponse); } } return { ...initialResult, response: transformed, }; } catch (error) { const mapped = await resolveErrorResult( error, initialResult.ctx, pathValue, queryValue, headersValue, bodyValue, { owner: "framework" }, ); if (!allowRetry) { return mapped; } return applyTransformHooks(mapped, false); } }; const applyResponseFinalizerHooks = async ( initialResult: ExecutionResult<Ctx>, allowRetry: boolean, responseValidation: ResponseFinalizerValidationState, ): Promise<ExecutionResult<Ctx>> => { const response = normalizeHttpResponse(initialResult.response); const native = isWebResponse(response); const owner = responseOwnerFor(response, initialResult.owner); try { for (const hook of hooks) { const finalizer = getResponseFinalizerHook<Ctx>(hook); if (!finalizer) continue; await finalizer({ req, ctx: initialResult.ctx, contract, path: pathValue, query: queryValue, headers: headersValue, body: bodyValue, response: responseForHooks(response), error: initialResult.error, native: native ? true : undefined, owner, responseValidation, }); } return { ...initialResult, response, }; } catch (error) { const mapped = await resolveErrorResult( error, initialResult.ctx, pathValue, queryValue, headersValue, bodyValue, { owner: "framework" }, ); if (!allowRetry) { return mapped; } const transformed = await applyTransformHooks(mapped, true); return applyResponseFinalizerHooks( transformed, false, "not-applicable", ); } }; let result: ExecutionResult<Ctx> | undefined; const onRequestStartedAt = performance.now(); for (const hook of hooks) { if (!hook.onRequest) continue; try { const hookResult = await hook.onRequest({ req, requestInfo: copyTrustedRequestInfo(resolvedRequestInfo), ports: finalPorts, contract, params: matchedParams, }); if (hookResult) { const response = normalizeHttpResponse(hookResult); result = { response, owner: responseOwnerFor(response, "framework"), }; break; } } catch (error) { result = await resolveErrorResult( error, undefined, undefined, undefined, undefined, undefined, { owner: "framework" }, ); break; } } stages.onRequestMs = performance.now() - onRequestStartedAt; if (!result) { if (optionsOverrides?.skipRoutePreparation) { let createdCtx!: Ctx; try { createdCtx = await timeStage("contextMs", () => contextRuntime.createRequestContext( req, contract, resolvedRequestInfo, ), ); baseCtx = createdCtx; } catch (error) { result = await resolveErrorResult( error, undefined, undefined, undefined, undefined, undefined, { owner: "framework" }, ); } if (!result) { try { result = { ctx: createdCtx, response: normalizeHttpResponse( await timeStage("handlerMs", () => userHandler({ req, ctx: createdCtx, contract, path: {} as HandlerArgs<Ctx, C>["path"], query: {} as HandlerArgs<Ctx, C>["query"], headers: rawHeaders as HandlerArgs<Ctx, C>["headers"], body: undefined as HandlerArgs<Ctx, C>["body"], }), ), ), owner: "framework", }; } catch (error) { result = await resolveErrorResult( error, createdCtx, undefined, undefined, undefined, undefined, { owner: "framework" }, ); } } } else { const parseStartedAt = performance.now(); let path = undefined as HandlerArgs<Ctx, C>["path"] | undefined; let query = undefined as HandlerArgs<Ctx, C>["query"] | undefined; let headers = undefined as HandlerArgs<Ctx, C>["headers"] | undefined; let body = undefined as HandlerArgs<Ctx, C>["body"] | undefined; const prepared = await prepareRequestInputs({ contract, req, url, rawHeaders, matchedParams, maxRequestBodyBytes, rawRoute: optionsOverrides?.rawRoute, }); if (!prepared.ok) { result = { response: prepared.response, owner: "framework", }; } else { path = prepared.inputs.path as HandlerArgs<Ctx, C>["path"]; query = prepared.inputs.query as HandlerArgs<Ctx, C>["query"]; headers = prepared.inputs.headers as HandlerArgs<Ctx, C>["headers"]; body = prepared.inputs.body as HandlerArgs<Ctx, C>["body"]; } stages.parseMs = performance.now() - parseStartedAt; if (!result) { pathValue = path; queryValue = query; headersValue = headers; bodyValue = body; let createdCtx!: Ctx; try { createdCtx = await timeStage("contextMs", () => contextRuntime.createRequestContext( req, contract, resolvedRequestInfo, ), ); baseCtx = createdCtx; } catch (error) { result = await resolveErrorResult( error, undefined, pathValue, queryValue, headersValue, bodyValue, { owner: "framework" }, ); } if (!result) { const baseArgs: HandlerArgs<Ctx, C> = { req, ctx: createdCtx, contract, path: path as HandlerArgs<Ctx, C>["path"], query: query as HandlerArgs<Ctx, C>["query"], headers: headers as HandlerArgs<Ctx, C>["headers"], body: body as HandlerArgs<Ctx, C>["body"], }; let currentCtx = createdCtx; const beforeHandleStartedAt = performance.now(); for (const hook of routeHooks) { try { const additions = await hook.resolve({ req, ctx: currentCtx, contract, path, query, headers, body, }); if (additions && typeof additions === "object") { currentCtx = contextRuntime.finalizeContext({ ...(currentCtx as object), ...additions, } as Ctx); } } catch (error) { result = await resolveErrorResult( error, currentCtx, pathValue, queryValue, headersValue, bodyValue, { owner: "framework" }, ); break; } } if (!result) { for (const hook of hooks) { if (!hook.beforeHandle) continue; try { const hookResult = await hook.beforeHandle({ req, requestInfo: copyTrustedRequestInfo(resolvedRequestInfo), ctx: currentCtx, contract, path, query, headers, body, }); if (isWebResponse(hookResult)) { result = { ctx: currentCtx, response: hookResult, owner: "transport", }; break; } if (isHttpResponseLike(hookResult)) { result = { ctx: currentCtx, response: normalizeResponse(hookResult), owner: "framework", }; break; } if (hookResult?.ctx !== undefined) { currentCtx = contextRuntime.finalizeContext( hookResult.ctx, ); } if (hookResult?.response) { const response = normalizeHttpResponse( hookResult.response, ); result = { ctx: currentCtx, response, owner: responseOwnerFor(response, "framework"), }; break; } } catch (error) { result = await resolveErrorResult( error, currentCtx, pathValue, queryValue, headersValue, bodyValue, { owner: "framework" }, ); break; } } } stages.beforeHandleMs = performance.now() - beforeHandleStartedAt; if (!result) { // Hooks may have elevated the actor or resolved a tenant. // Refresh the ambient request context so record-time // consumers such as createAmbientAuditLog see the finalized // identity. setActiveRequestIdentity({ actor: readContextActor(currentCtx), tenant: readContextTenant(currentCtx), }); try { result = { ctx: currentCtx, response: normalizeHttpResponse( await timeStage("handlerMs", () => userHandler({ ...baseArgs, ctx: currentCtx }), ), ), }; } catch (error) { result = await resolveErrorResult( error, currentCtx, pathValue, queryValue, headersValue, bodyValue, ); } } } } } } const sendStartedAt = performance.now(); result = await applyTransformHooks(result, true); let finalResponse = normalizeHttpResponse(result.response); let finalError = result.error; let finalOwner = responseOwnerFor(finalResponse, result.owner); let responseValidation: ResponseFinalizerValidationState = "not-applicable"; if (finalOwner === "route" && !isWebResponse(finalResponse)) { const validateContract = options.validateResponses ?? true; try { finalResponse = await finalizeResponse( contract, finalResponse, target.responseValidationExemptStatus, { validateContract }, ); result = { ...result, response: finalResponse, }; responseValidation = validateContract ? "validated" : "disabled"; } catch (error) { if (error instanceof ResponseContractViolationError) { result = { ctx: result.ctx, response: toContractViolationResponse(error), error, owner: "framework", }; } else { result = await resolveErrorResult( error, result.ctx, pathValue, queryValue, headersValue, bodyValue, { owner: "framework" }, ); } finalResponse = normalizeHttpResponse(result.response); finalError = result.error; finalOwner = responseOwnerFor(finalResponse, result.owner); result = await applyTransformHooks(result, true); finalResponse = normalizeHttpResponse(result.response); finalError = result.error; finalOwner = responseOwnerFor(finalResponse, result.owner); responseValidation = "not-applicable"; } } result = await applyResponseFinalizerHooks( result, true, responseValidation, ); finalResponse = normalizeHttpResponse(result.response); finalError = result.error; finalOwner = responseOwnerFor(finalResponse, result.owner); if (!isWebResponse(finalResponse)) { finalResponse = withFrameworkErrorOwnerHeader( finalResponse, finalOwner, ); } finalResponse = withContractLifecycleHeaders(finalResponse, contract); finalResponse = withoutHeadResponseBody(finalResponse, req.method); stages.sendMs = performance.now() - sendStartedAt; const durationMs = Date.now() - startedAt; const stageTimings = roundStageTimings(stages); for (const hook of hooks) { if (!hook.afterSend) continue; try { await hook.afterSend({ req, requestInfo: copyTrustedRequestInfo(resolvedRequestInfo), ctx: result.ctx, contract, path: pathValue, query: queryValue, headers: headersValue, body: bodyValue, response: responseForHooks(finalResponse), error: finalError, durationMs, stages: stageTimings, }); } catch { // Ignore after-response hook failures; they should never change the response. } } return finalResponse; } catch (error) { const result = await resolveErrorResult( error, baseCtx, pathValue, queryValue, headersValue, bodyValue, { owner: "framework", }, ); const response = withoutHeadResponseBody( withContractLifecycleHeaders( normalizeHttpResponse(result.response), contract, ), req.method, ); if (isWebResponse(response)) { return response; } return withFrameworkErrorOwnerHeader( response, responseOwnerFor(response, result.owner), ); } }; return async ( target: ExecutionTarget<Ctx, C>, req: HttpRequestLike, preMatchedParams?: Record<string, string>, ) => { const tracing = resolveTracingPort(finalPorts); const instrumentationOptions = options.instrumentation === false ? undefined : options.instrumentation; const pathname = (() => { try { return new URL(req.url).pathname; } catch { return req.url; } })(); const ignored = ( instrumentationOptions?.ignorePaths ?? ["/api/devtools"] ).some((prefix) => { const normalized = prefix.replace(/\/+$/, ""); return pathname === normalized || pathname.startsWith(`${normalized}/`); }); if (!tracing || ignored) { return executeRequest(target, req, preMatchedParams); } const traceContextHeader = instrumentationOptions?.traceContextHeader ?? "traceparent"; const active = tracing.current(); const parsedTraceparent = traceContextHeader === false ? undefined : parseTraceparent(req.headers.get(traceContextHeader)); const parent: TraceContextInput | undefined = active ? undefined : !parsedTraceparent ? undefined : { traceparent: parsedTraceparent.traceparent, tracestate: req.headers.get("tracestate") ?? undefined, }; const traceAttributes = { "beignet.contract.name": target.contract.name, "http.request.method": req.method.toUpperCase(), "http.route": target.contract.path, } as const; return await runWithTracing( tracing, { name: `beignet.request ${target.contract.name}`, type: "request", kind: active ? "internal" : "server", parent, attributes: traceAttributes, metricAttributes: traceAttributes, }, async (span) => { const response = await executeRequest(target, req, preMatchedParams); span?.setAttribute("http.response.status_code", response.status); if (response.status >= 500) span?.setStatus("error"); return response; }, ); }; } /** * Build a memo instrumentation recorder from the app ports, or undefined when * no instrumentation sink is wired. * * Resolved per execution rather than at route-build time because providers * contribute `ports.instrumentation`/`ports.devtools` during setup, after * routes are registered. */ export function createMemoScopeRecorder( ports: AnyPorts, ): ((event: MemoInstrumentationEvent) => void) | undefined { const target = ports as ProviderInstrumentationTarget; if (!resolveProviderInstrumentationPort(target)) return undefined; const instrumentation = createProviderInstrumentation(target, { providerName: "memo", watcher: "memo", }); return (event) => { instrumentation.custom({ name: `memo.${event.kind}`, label: event.kind === "hit" ? "Memo hit" : "Memo fill", summary: `Memo ${event.kind} for ${event.memo}`, details: { memo: event.memo, key: event.key, ...(event.durationMs !== undefined ? { durationMs: event.durationMs } : {}), ...(event.failed ? { failed: true } : {}), }, }); }; } export function buildHandler< Ctx, FinalPorts extends AnyPorts, C extends HttpContractConfig, >( options: RequestExecutorOptions<Ctx>, finalPorts: FinalPorts, contextRuntime: { createRequestContext: ( req: HttpRequestLike, contract: HttpContractConfig, requestInfo: TrustedRequestInfo, ) => Promise<Ctx>; finalizeContext: (seed: ContextSeed<Ctx> | Ctx) => Ctx; resolveRequestInfo: (req: HttpRequestLike) => TrustedRequestInfo; }, contract: C, userHandler: Handler<Ctx, C>, hooks: ServerHook<Ctx, FinalPorts>[], routeHooks: readonly RouteHook<unknown, object>[] = [], optionsOverrides?: { skipRoutePreparation?: boolean; /** * Run the route through the full hook pipeline without contract * preparation: no query/path/header/body parsing or validation, and the * request body is left unconsumed for the handler. */ rawRoute?: boolean; }, responseValidationExemptStatus?: number, ): ( req: HttpRequestLike, preMatchedParams?: Record<string, string>, ) => Promise<HttpResponse> { const execute = createRequestExecutor<Ctx, FinalPorts, C>( options, finalPorts, contextRuntime, hooks, routeHooks, optionsOverrides, ); const executionTarget: ExecutionTarget<Ctx, C> = { contract, compiled: compilePath(contract.path), handler: userHandler, responseValidationExemptStatus, }; // Every HTTP execution runs inside a fresh memo scope so createMemo(...) // wrappers dedupe lookups for exactly one request. return (req, preMatchedParams) => runWithMemoScope({ record: createMemoScopeRecorder(finalPorts) }, () => execute(executionTarget, req, preMatchedParams), ); } function roundStageTimings(stages: RequestStageTimings): RequestStageTimings { const round = (value: number) => Math.round(value * 100) / 100; return { onRequestMs: round(stages.onRequestMs), parseMs: round(stages.parseMs), contextMs: round(stages.contextMs), beforeHandleMs: round(stages.beforeHandleMs), handlerMs: round(stages.handlerMs), sendMs: round(stages.sendMs), }; }