import type { BinaryResponse, RawResponse } from "../binary.js"; import type { InferInput, InferOutput, StandardSchemaV1 } from "../schema/standard.js"; import type { Params, RouteSchema } from "./context.js"; import type { StatusResponse } from "./runtime-core.js"; /** * One route's input/output shape as the **client** will consume it. `query`/`body` * are `never` when the route declares no schema for them, so the client can detect * "this route takes no body" via `[body] extends [never]`. `output` is the * handler's raw return type (the client applies `Jsonify` when reading it). */ export interface RouteInfo { readonly params: object; readonly query: unknown; readonly body: unknown; readonly output: unknown; /** The complete inferred/declared status-keyed response map. */ readonly responses?: unknown; /** The route's declared error bodies as a status-keyed record (from `schema.errors`, e.g. * `{ 404: NotFound }`); `unknown` when the route declares none. The typed client turns this into * a status-discriminated failure union. */ readonly errors?: unknown; /** The SSE event payload type (from `schema.sse`, declared via `app.sse()`); `never` for ordinary * routes. The typed client keys `.subscribe()` availability and its event type off this. */ readonly sse?: unknown; /** The WebSocket message contract (from `app.ws()`'s `messageSchema`/`sendSchema`): `in` is what the * client may send, `out` what the server pushes. Present only on WS entries (method key `"WS"`); * the typed client keys `.ws()` availability and its frame types off this. */ readonly ws?: unknown; } /** The accumulated, type-level map of every route on a Server: path → method → RouteInfo. */ export type Registry = Record>; /** The empty registry (no routes). `NonNullable` is `{}` without tripping noBannedTypes. */ export type EmptyRegistry = NonNullable; /** The client-visible request inputs use the schema's INPUT side: the registry describes what a * caller must SEND over the wire (pre-validation), not what the handler receives after parsing. With * a Zod `.default()`/transform the two differ - `InferOutput` would force the client to supply * fields the schema fills in. (Same rule as the WS `in` channel below.) */ type RegistryBody = S extends { body: infer B extends StandardSchemaV1; } ? InferInput : never; type RegistryQuery = S extends { query: infer Q extends StandardSchemaV1; } ? InferInput : never; type StatusCodeOf = Output extends StatusResponse ? (number extends Code ? never : Code) : never; type StatusBodyAt = Output extends StatusResponse ? Code extends Actual ? Body : never : never; type IsSuccessCode = `${Code}` extends `2${string}` ? true : false; type EmptyResponseMap = NonNullable; type StatusBodies> = [Codes] extends [never] ? EmptyResponseMap : { [Code in Codes]: StatusBodyAt; }; /** A dynamic status code cannot be classified as success or failure; expose an opaque success arm * rather than guessing a status or body schema. Explicit route schemas still override this fallback. */ type DynamicStatusResponses = Output extends StatusResponse ? number extends Code ? { 200: unknown; } : EmptyResponseMap : EmptyResponseMap; type InferredResponses = (Exclude | Response> extends never ? EmptyResponseMap : { 200: Exclude | Response>; }) & StatusBodies & DynamicStatusResponses; type ExplicitResponses = (S extends { response: infer R extends StandardSchemaV1; } ? { 200: InferOutput; } : EmptyResponseMap) & (S extends { errors: infer E extends Record; } ? { [K in keyof E]: E[K] extends StandardSchemaV1 ? InferOutput : never; } : EmptyResponseMap); type MergeResponseMaps = Omit & Explicit; type ResponseMap = MergeResponseMaps, ExplicitResponses>; /** Public type seam for build-time consumers that need the complete route response map. */ export type ResponseMapFor = ResponseMap; type ResponseErrors = { [K in keyof M as K extends number ? (IsSuccessCode extends true ? never : K) : never]: M[K]; }; type ResponseSuccessBodies = { [K in keyof M]: K extends number ? (IsSuccessCode extends true ? M[K] : never) : never; }[keyof M]; type NonEmptyOrUnknown = keyof M extends never ? unknown : M; /** The client-visible output is the union of all inferred/declared 2xx bodies. */ type RegistryOutput = ResponseSuccessBodies>; /** The SSE event payload type from a route's `sse` schema; `never` for ordinary routes. */ type RegistrySse = S extends { sse: infer E extends StandardSchemaV1; } ? InferOutput : never; /** Build a {@link RouteInfo} from a route's path, schema, and handler output type. */ export type RouteInfoFor = { readonly params: Params; readonly query: RegistryQuery; readonly body: RegistryBody; readonly output: RegistryOutput; readonly responses?: ResponseMapFor; readonly errors: NonEmptyOrUnknown>>; readonly sse: RegistrySse; }; /** * The client-visible output of a handler: its awaited return, with an UNbranded raw `Response` removed * (a route returning a bare `Response` has no describable body). A response branded by `bytes()` * ({@link BinaryResponse}) or `raw()` ({@link RawResponse}) is KEPT, so the brand reaches the client's * `Jsonify` and types `data` as `Blob` / `Jsonify` rather than collapsing to `never`. */ export type OutputOf unknown> = KeepBrandedResponse>>; /** Preserve a `bytes()`/`raw()`-branded response; drop only a bare `Response`. Distributes over a * union return, so `Foo | BinaryResponse` keeps both arms. */ type KeepBrandedResponse = O extends BinaryResponse ? O : O extends RawResponse ? O : O extends Response ? never : O; /** * The registry entry for a WebSocket route (stored under the pseudo-method key `"WS"`, so WS routes * ride the same path → method map as HTTP routes - no extra Server generic). `in` is the client → * server frame type: the `messageSchema`'s INPUT side, since that is what goes on the wire before * validation transforms it. `out` is the server → client frame type from `sendSchema` (a type-level * contract; the server's own sends are not runtime-validated). Either is `unknown` when undeclared. */ export type WsRouteInfoFor = { readonly params: Params; readonly query: never; readonly body: never; readonly output: never; readonly errors: unknown; readonly sse: never; readonly ws: { readonly in: In extends StandardSchemaV1 ? InferInput : unknown; readonly out: Out extends StandardSchemaV1 ? InferOutput : unknown; }; }; /** * Merge a new route into the registry, combining methods that share a path. * * SCALING CEILING (measured - see many-routes.test-d.ts and the isolation study below): a single * fluent chain hits TS2589 at ~95-100 routes. This intersection is NOT the cause - in isolation it * accumulates 1000+ routes cleanly, and heavy per-route `Params`/schema inference alone * reaches 600+. The wall is an INTERACTION unique to the fluent builder: each `.get(path, handler)` * both (a) computes the handler's context type from the path (`c.params.id` inferred from `:id`) * AND (b) returns `Server, Ctx>`. Neither alone strains the compiler; the * PRODUCT - recomputing the handler context while re-threading the ever-larger registry at each of * N steps - exhausts TypeScript's per-expression instantiation budget around N≈95. It is therefore * O(N) and inherent to any builder that infers handler context AND accumulates a typed route * registry (Elysia/tRPC/hono's typed clients cap the same way); it is not fixable by reshaping * AddRoute. Past ~90 routes, use a path that DOESN'T form the product: split into domain groups and * `.merge()` them (each group is a short chain; a merge is one `R & R2` intersection with no * per-call context work), or contract-first `implement()` (the registry is one object type declared * upfront - no grow-R-per-call, so no ceiling at all). */ export type AddRoute = R & { [P in Path]: { [M in Method]: Info; }; }; export {}; //# sourceMappingURL=registry.d.ts.map