@beignet/core
Version:
Core framework primitives for Beignet
518 lines (487 loc) • 14.2 kB
text/typescript
import type {
HttpContractConfig,
InferHeaderSchemaOutput,
InferOutput,
StandardSchema,
} from "../contracts/index.js";
import type { AnyPorts } from "../ports/index.js";
import type {
TrustedProxyConfig,
TrustedRequestInfo,
} from "./trusted-proxy.js";
/**
* Framework-neutral request shape consumed by Beignet server adapters.
*
* Platform adapters should convert their native request into this shape before
* passing it to `server.api(...)` or a single route handler.
*/
export interface HttpRequestLike {
/**
* HTTP method as received from the platform.
*/
method: string;
/**
* Absolute request URL.
*/
url: string;
/**
* Request headers.
*/
headers: Headers;
/**
* The platform request when an adapter has one available.
*
* Use this as an escape hatch for platform-specific APIs. Prefer the
* framework-agnostic methods below when possible.
*/
raw?: Request;
/**
* Parse the request body as JSON.
*/
json(): Promise<unknown>;
/**
* Parse the request body as text.
*/
text(): Promise<string>;
/**
* Parse the request body as an array buffer when the platform supports it.
*/
arrayBuffer?(): Promise<ArrayBuffer>;
/**
* Parse the request body as a Blob when the platform supports it.
*/
blob?(): Promise<Blob>;
/**
* Parse the request body as form data when the platform supports it.
*/
formData?(): Promise<FormData>;
/**
* Clone the request when the platform supports replaying the body.
*/
clone?(): HttpRequestLike;
}
/**
* Framework-neutral response object returned by route handlers and hooks.
*/
export interface HttpResponseLike {
/**
* HTTP status code.
*/
status: number;
/**
* Response headers.
*/
headers?: Record<string, string>;
/**
* JSON-serializable body or an adapter-specific body value.
*/
body?: unknown;
}
/**
* Response accepted by Beignet handlers.
*
* Use `HttpResponseLike` for framework-neutral responses. Return a native
* `Response` only when the current adapter can pass it through unchanged.
*/
export type HttpResponse = HttpResponseLike | Response;
/**
* Framework-neutral Beignet API handler consumed by HTTP adapters.
*/
export type HttpAdapterApiHandler = (
req: HttpRequestLike,
) => Promise<HttpResponse>;
/**
* Native handler shape produced by an HTTP adapter.
*/
export type HttpAdapterHandler<NativeRequest, NativeResponse> = (
req: NativeRequest,
) => Promise<NativeResponse>;
/**
* Contract implemented by packages that adapt Beignet's framework-neutral
* server runtime to a platform HTTP API.
*
* Core owns request parsing, hooks, route matching, validation, error mapping,
* response ownership, and provider lifecycle. Adapters own only the conversion
* between the platform request/response types and Beignet's `HttpRequestLike`
* / `HttpResponse` boundary.
*/
export interface HttpAdapter<NativeRequest, NativeResponse> {
/**
* Human-readable adapter name for diagnostics and documentation.
*/
name: string;
/**
* Convert a platform request into Beignet's framework-neutral request shape.
*/
toRequestLike(req: NativeRequest): HttpRequestLike;
/**
* Convert a Beignet response into the platform response type.
*/
toNativeResponse(res: HttpResponse): NativeResponse | Promise<NativeResponse>;
/**
* Wrap a Beignet API handler in the platform's native handler shape.
*/
createHandler(
handler: HttpAdapterApiHandler,
): HttpAdapterHandler<NativeRequest, NativeResponse>;
}
type InferSchemaOrFallback<
T extends StandardSchema | null,
Fallback,
> = T extends StandardSchema ? InferOutput<T> : Fallback;
/**
* Infer the handler path parameter type for a contract.
*/
export type InferPath<C extends HttpContractConfig> = InferSchemaOrFallback<
C["pathParams"],
Record<string, string>
>;
/**
* Infer the handler query parameter type for a contract.
*/
export type InferQuery<C extends HttpContractConfig> = InferSchemaOrFallback<
C["query"],
Record<string, string | string[]>
>;
/**
* Infer the handler request body type for a contract.
*/
export type InferBody<C extends HttpContractConfig> = InferSchemaOrFallback<
C["body"],
unknown
>;
/**
* Infer the merged request header type for a contract.
*/
export type InferHeaders<C extends HttpContractConfig> =
InferHeaderSchemaOutput<Exclude<C["headers"], undefined>> extends undefined
? Record<string, string>
: InferHeaderSchemaOutput<Exclude<C["headers"], undefined>>;
/**
* Arguments passed to a route handler after request parsing and validation.
*/
export interface HandlerArgs<Ctx, C extends HttpContractConfig> {
/**
* Framework-neutral request.
*/
req: HttpRequestLike;
/**
* Application context assembled by the server context blueprint.
*/
ctx: Ctx;
/**
* Matched contract config.
*/
contract: C;
/**
* Parsed path parameters.
*/
path: InferPath<C>;
/**
* Parsed query parameters.
*/
query: InferQuery<C>;
/**
* Parsed request headers.
*/
headers: InferHeaders<C>;
/**
* Parsed request body.
*/
body: InferBody<C>;
}
/**
* Route handler function for a contract.
*/
export type Handler<Ctx, C extends HttpContractConfig> = (
args: HandlerArgs<Ctx, C>,
) => Promise<HttpResponse> | HttpResponse;
/**
* Value or promise of that value.
*/
export type MaybePromise<T> = T | Promise<T>;
/**
* Arguments passed to a route-scoped hook after request parsing and context
* creation.
*/
export type RouteHookArgs<
Ctx,
C extends HttpContractConfig = HttpContractConfig,
> = HandlerArgs<Ctx, C>;
/**
* Hook that runs only for the route or route group where it is attached.
*
* Route hooks are for scoped policy and context enrichment such as
* authentication, tenant resolution, feature gates, and idempotency. They add
* fields to the handler context instead of replacing the app context.
*
* Hook additions must not include `gate`: the server re-attaches the gate
* declared by the context blueprint after every hook, so identity changes are
* picked up automatically.
*/
export interface RouteHook<
Ctx,
AddedCtx extends object & { gate?: never } = Record<string, never>,
> {
/**
* Optional name used in diagnostics and devtools.
*/
name?: string;
/**
* Resolve additional context for this route or throw to stop handling.
*/
resolve: (args: RouteHookArgs<Ctx>) => MaybePromise<AddedCtx | undefined>;
}
type AddedCtxFromHook<Hook> =
Hook extends RouteHook<infer _Ctx, infer AddedCtx> ? AddedCtx : unknown;
type UnionToIntersection<Union> = (
Union extends unknown
? (value: Union) => void
: never
) extends (value: infer Intersection) => void
? Intersection
: never;
/**
* Intersection of the context fields added by a route hook list.
*/
export type AddedCtxFromHooks<Hooks extends readonly unknown[]> =
Hooks extends readonly []
? unknown
: UnionToIntersection<AddedCtxFromHook<Hooks[number]>>;
/**
* Hook that runs after a route is matched but before request parsing and
* context creation.
*
* Returning a response short-circuits the rest of the request pipeline.
*/
export type OnRequestHook<
Ports extends AnyPorts = AnyPorts,
C extends HttpContractConfig = HttpContractConfig,
> = (args: {
req: HttpRequestLike;
requestInfo: TrustedRequestInfo;
ports: Ports;
contract: C;
params: Record<string, string>;
}) => MaybePromise<HttpResponse | undefined>;
/**
* Result from a `beforeHandle` hook.
*
* Returning a plain response short-circuits the handler. Returning an object can
* replace the context, short-circuit with a response, or do both.
*/
export type BeforeHandleResult<Ctx> =
| undefined
| HttpResponse
| {
ctx?: Ctx;
response?: HttpResponse;
};
/**
* Hook that runs after request parsing/context creation and before the handler.
*/
export type BeforeHandleHook<
Ctx,
C extends HttpContractConfig = HttpContractConfig,
> = (args: {
req: HttpRequestLike;
requestInfo: TrustedRequestInfo;
ctx: Ctx;
contract: C;
path: InferPath<C>;
query: InferQuery<C>;
headers: InferHeaders<C>;
body: InferBody<C>;
}) => MaybePromise<BeforeHandleResult<Ctx>>;
/**
* Hook that runs before the response is returned.
*
* Return a response to replace or decorate the outgoing response. Hooks run in
* declaration order. For native web `Response` results the hook receives a
* headers-only view and only header changes are applied.
*/
export type BeforeSendHook<
Ctx,
C extends HttpContractConfig = HttpContractConfig,
> = (args: {
req: HttpRequestLike;
requestInfo: TrustedRequestInfo;
ctx?: Ctx;
contract: C;
path?: InferPath<C>;
query?: InferQuery<C>;
headers?: InferHeaders<C>;
body?: InferBody<C>;
response: HttpResponseLike;
error?: unknown;
/**
* True when the route returned a native web Response. The response argument
* is a headers-only view ({ status, headers }); the body is not readable and
* returned body/status changes are ignored. Header changes are merged onto
* the native Response.
*/
native?: boolean;
}) => MaybePromise<HttpResponseLike | undefined>;
/**
* Per-stage timing breakdown of one request, in milliseconds.
*
* Stages are measured around the pipeline phases in execution order:
* `onRequest` hooks, request parsing/validation, context creation, route
* hooks plus `beforeHandle` hooks, the route handler, and response
* preparation (`beforeSend`, response validation, and finalizers). Stages
* that did not run for a request — parsing on raw routes, the handler after
* a hook short-circuit — report `0`. The stages do not sum exactly to the
* request `durationMs`; routing and bookkeeping live in the gaps.
*/
export type RequestStageTimings = {
onRequestMs: number;
parseMs: number;
contextMs: number;
beforeHandleMs: number;
handlerMs: number;
sendMs: number;
};
/**
* Hook that runs after the response has been prepared.
*
* This is for logging and observability. Errors thrown by `afterSend` hooks are
* ignored by the server pipeline.
*/
export type AfterSendHook<
Ctx,
C extends HttpContractConfig = HttpContractConfig,
> = (args: {
req: HttpRequestLike;
requestInfo: TrustedRequestInfo;
ctx?: Ctx;
contract: C;
path?: InferPath<C>;
query?: InferQuery<C>;
headers?: InferHeaders<C>;
body?: InferBody<C>;
response: HttpResponseLike;
error?: unknown;
durationMs: number;
/**
* Per-stage timing breakdown of this request.
*/
stages: RequestStageTimings;
}) => MaybePromise<void>;
/**
* Hook notified when the framework catches an error while handling a request.
*/
export type ServerCaughtErrorHook<
Ctx,
C extends HttpContractConfig = HttpContractConfig,
> = (args: {
err: unknown;
req: HttpRequestLike;
requestInfo?: TrustedRequestInfo;
ctx?: Ctx;
contract: C;
path?: InferPath<C>;
query?: InferQuery<C>;
headers?: InferHeaders<C>;
body?: InferBody<C>;
}) => MaybePromise<void>;
/**
* Hook that may map an unexpected error to a custom response.
*
* Return `undefined` to let the server's default unhandled-error mapper create
* the response.
*/
export type ServerUnhandledErrorMapper<
Ctx,
C extends HttpContractConfig = HttpContractConfig,
> = (args: {
err: unknown;
req: HttpRequestLike;
requestInfo?: TrustedRequestInfo;
ctx?: Ctx;
contract: C;
path?: InferPath<C>;
query?: InferQuery<C>;
headers?: InferHeaders<C>;
body?: InferBody<C>;
}) => MaybePromise<HttpResponse | undefined>;
/**
* Server lifecycle hook collection.
*
* Hooks run in the order they are registered within each server-hook phase.
* `onRequest` can short-circuit before context creation; route hooks resolve
* route-scoped context before server `beforeHandle`; `beforeHandle` can replace
* context or short-circuit; `beforeSend` can replace the outgoing response, and
* route-owned replacements are contract-validated before send; `afterSend`
* observes the final response.
*/
export interface ServerHook<Ctx, Ports extends AnyPorts = AnyPorts> {
/**
* Optional name used in diagnostics and devtools.
*/
name?: string;
/**
* Validates the hook configuration against the registered contracts.
*
* Invoked once at startup, right after `createServer(...)` collects the
* contracts from the `routes` option and before provider setup. Throw to
* fail startup instead of degrading silently at request time. Contracts
* registered later through `server.route(...)` are not seen here, so hooks
* that need full coverage should keep a runtime check as a backstop.
*/
validate?: (args: {
contracts: readonly HttpContractConfig[];
/**
* Server-level trusted-proxy policy. Request phases receive its resolved
* value as `requestInfo`.
*/
trustedProxy?: TrustedProxyConfig;
}) => void;
/**
* Runs after route matching and before body/query/header parsing.
*/
onRequest?: OnRequestHook<Ports>;
/**
* Runs after request parsing, context creation, and route hook resolution.
*/
beforeHandle?: BeforeHandleHook<Ctx>;
/**
* Runs before the response is returned. Native web `Response` results get a
* headers-only view with `native: true`.
*/
beforeSend?: BeforeSendHook<Ctx>;
/**
* Observes the final response after send preparation.
*/
afterSend?: AfterSendHook<Ctx>;
/**
* Observes framework-caught errors.
*/
onCaughtError?: ServerCaughtErrorHook<Ctx>;
/**
* Maps unexpected errors to responses.
*/
mapUnhandledError?: ServerUnhandledErrorMapper<Ctx>;
}
/**
* Compiled route entry used by server internals and adapter helpers.
*/
export type ResolvedRoute<_Ctx, C extends HttpContractConfig> = {
/**
* Contract config for the route.
*/
contract: C;
/**
* Handler that receives the raw request and optional path params.
*/
handler: (
req: HttpRequestLike,
params?: Record<string, string>,
) => Promise<HttpResponse>;
/**
* Test whether the route matches an incoming method and pathname.
*/
match: (
method: string,
pathname: string,
) => { matched: true } | { matched: false };
};