@fishenv/http 0.0.0-stage → 0.0.3

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.
Files changed (106) hide show
  1. package/dist/contract/builder.d.ts +87 -0
  2. package/dist/contract/builder.js +89 -0
  3. package/dist/contract/index.d.ts +4 -0
  4. package/dist/contract/index.js +2 -0
  5. package/dist/contract/types.d.ts +47 -0
  6. package/dist/contract/types.js +0 -0
  7. package/dist/contract/ws.d.ts +178 -0
  8. package/dist/contract/ws.js +164 -0
  9. package/dist/core/compose.d.ts +22 -0
  10. package/dist/core/compose.js +87 -0
  11. package/dist/core/cors.d.ts +38 -0
  12. package/dist/core/cors.js +96 -0
  13. package/dist/core/error.d.ts +73 -0
  14. package/dist/core/error.js +142 -0
  15. package/dist/core/index.d.ts +20 -0
  16. package/dist/core/index.js +23 -0
  17. package/dist/core/matcher.d.ts +17 -0
  18. package/dist/core/matcher.js +110 -0
  19. package/dist/core/request.d.ts +28 -0
  20. package/dist/core/request.js +106 -0
  21. package/dist/core/route-builder.d.ts +58 -0
  22. package/dist/core/route-builder.js +122 -0
  23. package/dist/core/router.d.ts +162 -0
  24. package/dist/core/router.js +327 -0
  25. package/dist/core/transport.d.ts +39 -0
  26. package/dist/core/transport.js +68 -0
  27. package/dist/core/types.d.ts +86 -0
  28. package/dist/core/types.js +0 -0
  29. package/dist/core/upgrade.d.ts +85 -0
  30. package/dist/core/upgrade.js +110 -0
  31. package/dist/core/utils/headers.d.ts +1 -0
  32. package/dist/core/utils/headers.js +2 -0
  33. package/dist/core/utils/url.d.ts +27 -0
  34. package/dist/core/utils/url.js +130 -0
  35. package/dist/core/validation.d.ts +13 -0
  36. package/dist/core/validation.js +24 -0
  37. package/dist/sse/connection.d.ts +30 -0
  38. package/dist/sse/connection.js +104 -0
  39. package/dist/sse/encode.d.ts +10 -0
  40. package/dist/sse/encode.js +27 -0
  41. package/dist/sse/hub.d.ts +33 -0
  42. package/dist/sse/hub.js +115 -0
  43. package/dist/sse/index.d.ts +38 -0
  44. package/dist/sse/index.js +40 -0
  45. package/dist/sse/response.d.ts +10 -0
  46. package/dist/sse/response.js +50 -0
  47. package/dist/sse/route-builder.d.ts +44 -0
  48. package/dist/sse/route-builder.js +65 -0
  49. package/dist/sse/router.d.ts +11 -0
  50. package/dist/sse/router.js +16 -0
  51. package/dist/sse/types.d.ts +43 -0
  52. package/dist/sse/types.js +4 -0
  53. package/dist/static/index.d.ts +27 -0
  54. package/dist/static/index.js +25 -0
  55. package/dist/static/negotiate.d.ts +23 -0
  56. package/dist/static/negotiate.js +85 -0
  57. package/dist/static/options.d.ts +44 -0
  58. package/dist/static/options.js +190 -0
  59. package/dist/static/resolve.d.ts +56 -0
  60. package/dist/static/resolve.js +113 -0
  61. package/dist/static/response.d.ts +53 -0
  62. package/dist/static/response.js +146 -0
  63. package/dist/static/router.d.ts +59 -0
  64. package/dist/static/router.js +217 -0
  65. package/dist/static/rules.d.ts +21 -0
  66. package/dist/static/rules.js +49 -0
  67. package/dist/static/types.d.ts +76 -0
  68. package/dist/static/types.js +0 -0
  69. package/dist/ws/channels/channel.d.ts +175 -0
  70. package/dist/ws/channels/channel.js +125 -0
  71. package/dist/ws/channels/handler.d.ts +33 -0
  72. package/dist/ws/channels/handler.js +486 -0
  73. package/dist/ws/channels/protocol.d.ts +96 -0
  74. package/dist/ws/channels/protocol.js +94 -0
  75. package/dist/ws/channels/target.d.ts +36 -0
  76. package/dist/ws/channels/target.js +56 -0
  77. package/dist/ws/connection.d.ts +56 -0
  78. package/dist/ws/connection.js +172 -0
  79. package/dist/ws/helpers.d.ts +17 -0
  80. package/dist/ws/helpers.js +48 -0
  81. package/dist/ws/hub.d.ts +172 -0
  82. package/dist/ws/hub.js +489 -0
  83. package/dist/ws/index.d.ts +65 -0
  84. package/dist/ws/index.js +66 -0
  85. package/dist/ws/lifecycle.d.ts +21 -0
  86. package/dist/ws/lifecycle.js +97 -0
  87. package/dist/ws/limits.d.ts +32 -0
  88. package/dist/ws/limits.js +67 -0
  89. package/dist/ws/message.d.ts +19 -0
  90. package/dist/ws/message.js +58 -0
  91. package/dist/ws/origin.d.ts +40 -0
  92. package/dist/ws/origin.js +152 -0
  93. package/dist/ws/presence.d.ts +96 -0
  94. package/dist/ws/presence.js +420 -0
  95. package/dist/ws/route-builder.d.ts +136 -0
  96. package/dist/ws/route-builder.js +265 -0
  97. package/dist/ws/router.d.ts +21 -0
  98. package/dist/ws/router.js +36 -0
  99. package/dist/ws/tickets.d.ts +82 -0
  100. package/dist/ws/tickets.js +161 -0
  101. package/dist/ws/typed.d.ts +54 -0
  102. package/dist/ws/typed.js +104 -0
  103. package/dist/ws/types.d.ts +69 -0
  104. package/dist/ws/types.js +70 -0
  105. package/package.json +54 -4
  106. package/README.md +0 -3
@@ -0,0 +1,87 @@
1
+ import type { AnySchema, BasePathParams, ErrorTypes, HttpMethod, InferOutput, InputKind, InputOptions, MergeParam, RouteMeta } from "./types.ts";
2
+ /**
3
+ * Plain-data snapshot of a contract endpoint.
4
+ *
5
+ * This is the runtime shape consumers (the API's `router.contract()`, an
6
+ * OpenAPI generator, a typed client, ...) read from. It intentionally has no
7
+ * generic parameters — the compile-time shape lives on `ContractBuilder`
8
+ * itself and is recovered via `ContractShape<C>`.
9
+ */
10
+ export interface ContractDefinition {
11
+ method: HttpMethod;
12
+ path: string;
13
+ paramSchemas: Map<string, AnySchema>;
14
+ inputKind: InputKind;
15
+ inputOptions: InputOptions<InputKind>;
16
+ outputSchema?: AnySchema;
17
+ errorTypes?: ErrorTypes;
18
+ meta?: RouteMeta;
19
+ }
20
+ /**
21
+ * Fluent, immutable contract endpoint builder.
22
+ *
23
+ * Mirrors `RouteBuilder` from `@fishenv/http` (method/path/param/input/output)
24
+ * minus everything server-only (middleware, handlers, DI). A finished
25
+ * `ContractBuilder` is portable data: it can be handed to
26
+ * `router.contract(endpoint)` on the API side, or read by an OpenAPI
27
+ * generator / typed client on the consumer side.
28
+ */
29
+ export declare class ContractBuilder<M extends HttpMethod, P extends string, K extends InputKind = "none", O extends InputOptions<K> = InputOptions<K>, Params extends Record<string, unknown> = Record<string, unknown>, Output = undefined> {
30
+ #private;
31
+ readonly method: M;
32
+ readonly path: P;
33
+ constructor(method: M, path: P, paramSchemas?: Map<string, AnySchema>, inputKind?: InputKind, inputOptions?: InputOptions<InputKind>, outputSchema?: AnySchema, errorTypes?: ErrorTypes, meta?: RouteMeta);
34
+ /** Set contract metadata (title, tags, etc.) — returns new builder */
35
+ meta(data: RouteMeta): ContractBuilder<M, P, K, O, Params, Output>;
36
+ /** Narrow/coerce a path param with a Valibot schema — returns new builder */
37
+ param<Name extends string & keyof Params, S extends AnySchema>(name: Name, schema: S): ContractBuilder<M, P, K, O, MergeParam<Params, Name, S>, Output>;
38
+ /** Set input kind and options — returns new builder */
39
+ input<NK extends InputKind, NO extends InputOptions<NK>>(kind: NK, options?: NO): ContractBuilder<M, P, NK, NO extends InputOptions<NK> ? NO : InputOptions<NK>, Params, Output>;
40
+ /** Set output schema + optional error types — returns new builder */
41
+ output<S extends AnySchema>(schema: S, errors?: ErrorTypes): ContractBuilder<M, P, K, O, Params, InferOutput<S>>;
42
+ /**
43
+ * Runtime snapshot of this endpoint. Used by consumers such as
44
+ * `router.contract()`, OpenAPI generation, or a typed client — not meant
45
+ * to be read directly by application code.
46
+ */
47
+ get _definition(): ContractDefinition;
48
+ }
49
+ /**
50
+ * Recovers the compile-time shape (input/output/params) of a `ContractBuilder`.
51
+ * Used by consumers to derive their own generics from a contract endpoint.
52
+ */
53
+ export type ContractShape<C> = C extends ContractBuilder<infer M, infer P, infer K, infer O, infer Params, infer Output> ? {
54
+ method: M;
55
+ path: P;
56
+ inputKind: K;
57
+ inputOptions: O;
58
+ params: Params;
59
+ output: Output;
60
+ } : never;
61
+ /** Loosest possible `ContractBuilder` — use as a generic constraint. */
62
+ export type AnyContractBuilder = ContractBuilder<any, any, any, any, any, any>;
63
+ /**
64
+ * Entry point for defining contract endpoints, e.g.:
65
+ *
66
+ * ```ts
67
+ * export const contract = {
68
+ * users: {
69
+ * create: c.post("/users").input("json", { body: CreateUserSchema }).output(UserSchema),
70
+ * get: c.get("/users/:id").output(UserSchema),
71
+ * },
72
+ * };
73
+ * ```
74
+ */
75
+ export declare const c: {
76
+ ws: <P extends string>(path: P) => import("./ws.ts").WsContract<P, BasePathParams<P>, "raw", [], undefined, undefined>;
77
+ channel: <N extends string>(name: N) => import("./ws.ts").ChannelContract<N, BasePathParams<N>, {
78
+ payload: undefined;
79
+ reply: undefined;
80
+ }, Record<never, never>, Record<never, never>, undefined>;
81
+ get: <P extends string>(path: P) => ContractBuilder<"GET", P, "none", InputOptions<"none">, BasePathParams<P>, undefined>;
82
+ post: <P extends string>(path: P) => ContractBuilder<"POST", P, "none", InputOptions<"none">, BasePathParams<P>, undefined>;
83
+ put: <P extends string>(path: P) => ContractBuilder<"PUT", P, "none", InputOptions<"none">, BasePathParams<P>, undefined>;
84
+ patch: <P extends string>(path: P) => ContractBuilder<"PATCH", P, "none", InputOptions<"none">, BasePathParams<P>, undefined>;
85
+ delete: <P extends string>(path: P) => ContractBuilder<"DELETE", P, "none", InputOptions<"none">, BasePathParams<P>, undefined>;
86
+ options: <P extends string>(path: P) => ContractBuilder<"OPTIONS", P, "none", InputOptions<"none">, BasePathParams<P>, undefined>;
87
+ };
@@ -0,0 +1,89 @@
1
+ import { wsContractFactories } from "./ws.js";
2
+ /**
3
+ * Fluent, immutable contract endpoint builder.
4
+ *
5
+ * Mirrors `RouteBuilder` from `@fishenv/http` (method/path/param/input/output)
6
+ * minus everything server-only (middleware, handlers, DI). A finished
7
+ * `ContractBuilder` is portable data: it can be handed to
8
+ * `router.contract(endpoint)` on the API side, or read by an OpenAPI
9
+ * generator / typed client on the consumer side.
10
+ */
11
+ export class ContractBuilder {
12
+ method;
13
+ path;
14
+ #paramSchemas;
15
+ #inputKind;
16
+ #inputOptions;
17
+ #outputSchema;
18
+ #errorTypes;
19
+ #meta;
20
+ constructor(method, path, paramSchemas = new Map(), inputKind = "none", inputOptions = {}, outputSchema, errorTypes, meta) {
21
+ this.method = method;
22
+ this.path = path;
23
+ this.#paramSchemas = paramSchemas;
24
+ this.#inputKind = inputKind;
25
+ this.#inputOptions = inputOptions;
26
+ this.#outputSchema = outputSchema;
27
+ this.#errorTypes = errorTypes;
28
+ this.#meta = meta;
29
+ }
30
+ /** Set contract metadata (title, tags, etc.) — returns new builder */
31
+ meta(data) {
32
+ return new ContractBuilder(this.method, this.path, new Map(this.#paramSchemas), this.#inputKind, this.#inputOptions, this.#outputSchema, this.#errorTypes ? [...this.#errorTypes] : undefined, { ...this.#meta, ...data });
33
+ }
34
+ /** Narrow/coerce a path param with a Valibot schema — returns new builder */
35
+ param(name, schema) {
36
+ const newSchemas = new Map(this.#paramSchemas);
37
+ newSchemas.set(name, schema);
38
+ return new ContractBuilder(this.method, this.path, newSchemas, this.#inputKind, this.#inputOptions, this.#outputSchema, this.#errorTypes ? [...this.#errorTypes] : undefined, this.#meta ? { ...this.#meta } : undefined);
39
+ }
40
+ /** Set input kind and options — returns new builder */
41
+ input(kind, options) {
42
+ return new ContractBuilder(this.method, this.path, new Map(this.#paramSchemas), kind, (options ?? {}), this.#outputSchema, this.#errorTypes ? [...this.#errorTypes] : undefined, this.#meta ? { ...this.#meta } : undefined);
43
+ }
44
+ /** Set output schema + optional error types — returns new builder */
45
+ output(schema, errors) {
46
+ return new ContractBuilder(this.method, this.path, new Map(this.#paramSchemas), this.#inputKind, this.#inputOptions, schema, errors, this.#meta ? { ...this.#meta } : undefined);
47
+ }
48
+ /**
49
+ * Runtime snapshot of this endpoint. Used by consumers such as
50
+ * `router.contract()`, OpenAPI generation, or a typed client — not meant
51
+ * to be read directly by application code.
52
+ */
53
+ get _definition() {
54
+ return {
55
+ method: this.method,
56
+ path: this.path,
57
+ paramSchemas: this.#paramSchemas,
58
+ inputKind: this.#inputKind,
59
+ inputOptions: this.#inputOptions,
60
+ outputSchema: this.#outputSchema,
61
+ errorTypes: this.#errorTypes,
62
+ meta: this.#meta,
63
+ };
64
+ }
65
+ }
66
+ function endpoint(method) {
67
+ return (path) => new ContractBuilder(method, path);
68
+ }
69
+ /**
70
+ * Entry point for defining contract endpoints, e.g.:
71
+ *
72
+ * ```ts
73
+ * export const contract = {
74
+ * users: {
75
+ * create: c.post("/users").input("json", { body: CreateUserSchema }).output(UserSchema),
76
+ * get: c.get("/users/:id").output(UserSchema),
77
+ * },
78
+ * };
79
+ * ```
80
+ */
81
+ export const c = {
82
+ ...wsContractFactories,
83
+ get: endpoint("GET"),
84
+ post: endpoint("POST"),
85
+ put: endpoint("PUT"),
86
+ patch: endpoint("PATCH"),
87
+ delete: endpoint("DELETE"),
88
+ options: endpoint("OPTIONS"),
89
+ };
@@ -0,0 +1,4 @@
1
+ export { type AnyContractBuilder, ContractBuilder, type ContractDefinition, type ContractShape, c, } from "./builder.ts";
2
+ export type { AnySchema, BasePathParams, ErrorTypes, ExtractParams, HttpMethod, InferBody, InferCookies, InferHeaders, InferOutput, InferQuery, InputKind, InputOptions, MergeParam, RouteMeta, ValibotSchema, } from "./types.ts";
3
+ export type { AnyChannelContract, AnyWsContract, ChannelContractDefinition, ChannelEventDefinition, EventSchemas, InferChannel, InferSocket, JoinSchemas, SchemaInput, SchemaOutput, WsContractDefinition, WsContractKind, } from "./ws.ts";
4
+ export { CHANNELS_PROTOCOL, ChannelContract, WsContract } from "./ws.ts";
@@ -0,0 +1,2 @@
1
+ export { ContractBuilder, c, } from "./builder.js";
2
+ export { CHANNELS_PROTOCOL, ChannelContract, WsContract } from "./ws.js";
@@ -0,0 +1,47 @@
1
+ import type * as v from "valibot";
2
+ /**
3
+ * `@fishenv/http-contract` — the "shape" types shared between an API and its
4
+ * consumers (the API itself, an OpenAPI generator, a typed client, ...).
5
+ *
6
+ * This module has zero server-runtime dependencies (no DI, no middleware, no
7
+ * `Bun.serve`) — it only describes *what* an endpoint looks like: method,
8
+ * path, input, output. `@fishenv/http` (the `core` package) depends on this
9
+ * package to build its route registration API; this package must never
10
+ * depend back on `core`.
11
+ */
12
+ export type ValibotSchema<O = unknown> = v.BaseSchema<unknown, O, v.BaseIssue<unknown>>;
13
+ export type AnySchema = ValibotSchema<unknown>;
14
+ export type InferOutput<S extends AnySchema> = v.InferOutput<S>;
15
+ export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "HEAD";
16
+ export type InputKind = "json" | "multipart" | "urlencoded" | "blob" | "text" | "none";
17
+ export interface InputOptions<K extends InputKind> {
18
+ body?: K extends "blob" | "text" ? never : AnySchema;
19
+ headers?: AnySchema;
20
+ query?: AnySchema;
21
+ cookies?: AnySchema;
22
+ maxSize?: K extends "blob" | "multipart" ? number : never;
23
+ }
24
+ export type InferBody<K extends InputKind, O extends InputOptions<K>> = K extends "blob" ? Blob : K extends "text" ? string : K extends "none" ? undefined : O["body"] extends AnySchema ? InferOutput<O["body"]> : K extends "multipart" | "urlencoded" ? FormData : unknown;
25
+ export type InferHeaders<O extends InputOptions<InputKind>> = O["headers"] extends AnySchema ? InferOutput<O["headers"]> : Record<string, string>;
26
+ export type InferQuery<O extends InputOptions<InputKind>> = O["query"] extends AnySchema ? InferOutput<O["query"]> : Record<string, string | string[]>;
27
+ export type InferCookies<O extends InputOptions<InputKind>> = O["cookies"] extends AnySchema ? InferOutput<O["cookies"]> : Record<string, string>;
28
+ type ParamName<Param extends string> = Param extends `${infer Name}?` ? Name : Param extends `${infer Name}*` ? Name : Param;
29
+ export type ExtractParams<P extends string> = P extends `${string}:${infer Param}/${infer Rest}` ? ParamName<Param> | ExtractParams<`/${Rest}`> : P extends `${string}:${infer Param}` ? ParamName<Param> : never;
30
+ export type BasePathParams<P extends string> = {
31
+ [K in ExtractParams<P>]: string;
32
+ };
33
+ export type MergeParam<Base extends Record<string, unknown>, Name extends string, Schema extends AnySchema> = Omit<Base, Name> & Record<Name, InferOutput<Schema>>;
34
+ export interface RouteMeta {
35
+ title?: string;
36
+ description?: string;
37
+ tags?: string[];
38
+ deprecated?: boolean;
39
+ operationId?: string;
40
+ }
41
+ /**
42
+ * Error classes a handler may throw — documentation and client typing only
43
+ * (`.output(schema, errors)`, WebSocket replies). `any[]` so that classes with
44
+ * typed constructor params (`ForbiddenError(message?: string)`) fit.
45
+ */
46
+ export type ErrorTypes = (abstract new (...args: any[]) => Error)[];
47
+ export {};
File without changes
@@ -0,0 +1,178 @@
1
+ /**
2
+ * WebSocket contracts — portable data describing sockets and channels, shared
3
+ * by the server (`@fishenv/http/ws`) and typed clients.
4
+ *
5
+ * ```ts
6
+ * export const room = c
7
+ * .channel("rooms/:roomId")
8
+ * .param("roomId", v.pipe(v.string(), v.uuid()))
9
+ * .join(JoinPayload, JoinReply, [ForbiddenError])
10
+ * .clientEvent("message", MessageIn, MessageAck)
11
+ * .serverEvent("message", Message)
12
+ * .presence(PresenceMeta, { clientUpdates: true });
13
+ *
14
+ * export const socket = c.ws("/socket").channels(room);
15
+ * ```
16
+ *
17
+ * Like HTTP contracts, builders are immutable and expose a plain
18
+ * `_definition` snapshot; the compile-time shape is recovered with
19
+ * {@link InferChannel} / {@link InferSocket}.
20
+ */
21
+ import type * as v from "valibot";
22
+ import type { AnySchema, BasePathParams, ErrorTypes, MergeParam, RouteMeta } from "./types.ts";
23
+ /** The channel protocol's subprotocol name. */
24
+ export declare const CHANNELS_PROTOCOL = "fishenv.channels.v1";
25
+ type Maybe<S> = S extends AnySchema ? S : undefined;
26
+ /** What the sender passes in (before schema transforms). */
27
+ export type SchemaInput<S> = S extends AnySchema ? v.InferInput<S> : undefined;
28
+ /** What the receiver gets (after validation and transforms). */
29
+ export type SchemaOutput<S> = S extends AnySchema ? v.InferOutput<S> : undefined;
30
+ export interface EventSchemas {
31
+ payload: AnySchema;
32
+ reply: AnySchema | undefined;
33
+ }
34
+ export interface JoinSchemas {
35
+ payload: AnySchema | undefined;
36
+ reply: AnySchema | undefined;
37
+ }
38
+ export interface ChannelEventDefinition {
39
+ payload: AnySchema;
40
+ reply?: AnySchema;
41
+ errors?: ErrorTypes;
42
+ }
43
+ export interface ChannelContractDefinition {
44
+ /** Pattern using router path syntax, e.g. `rooms/:roomId`. */
45
+ name: string;
46
+ paramSchemas: Map<string, AnySchema>;
47
+ join: {
48
+ payload?: AnySchema;
49
+ reply?: AnySchema;
50
+ errors?: ErrorTypes;
51
+ };
52
+ clientEvents: Map<string, ChannelEventDefinition>;
53
+ serverEvents: Map<string, AnySchema>;
54
+ presence?: {
55
+ meta: AnySchema;
56
+ clientUpdates: boolean;
57
+ };
58
+ meta?: RouteMeta;
59
+ }
60
+ export declare class ChannelContract<N extends string, Params extends Record<string, unknown> = BasePathParams<N>, J extends JoinSchemas = {
61
+ payload: undefined;
62
+ reply: undefined;
63
+ }, CE extends Record<string, EventSchemas> = Record<never, never>, SE extends Record<string, AnySchema> = Record<never, never>, PM extends AnySchema | undefined = undefined> {
64
+ #private;
65
+ readonly name: N;
66
+ constructor(name: N, def?: ChannelContractDefinition);
67
+ meta(data: RouteMeta): ChannelContract<N, Params, J, CE, SE, PM>;
68
+ /** Narrow/coerce a channel-name param. */
69
+ param<Name extends string & keyof Params, S extends AnySchema>(name: Name, schema: S): ChannelContract<N, MergeParam<Params, Name, S>, J, CE, SE, PM>;
70
+ /** Join payload, join reply and documented errors. All optional. */
71
+ join<P extends AnySchema | undefined = undefined, R extends AnySchema | undefined = undefined>(payload?: P, reply?: R, errors?: ErrorTypes): ChannelContract<N, Params, {
72
+ payload: Maybe<P>;
73
+ reply: Maybe<R>;
74
+ }, CE, SE, PM>;
75
+ /** Client → server event. Without `reply` it is fire-and-forget. */
76
+ clientEvent<E extends string, P extends AnySchema, R extends AnySchema | undefined = undefined>(name: E, payload: P, reply?: R, errors?: ErrorTypes): ChannelContract<N, Params, J, CE & Record<E, {
77
+ payload: P;
78
+ reply: Maybe<R>;
79
+ }>, SE, PM>;
80
+ /** Server → client event. */
81
+ serverEvent<E extends string, P extends AnySchema>(name: E, payload: P): ChannelContract<N, Params, J, CE, SE & Record<E, P>, PM>;
82
+ /**
83
+ * Enable presence for this channel. `clientUpdates` lets clients change
84
+ * their own meta (rate-limited server-side).
85
+ */
86
+ presence<M extends AnySchema>(meta: M, opts?: {
87
+ clientUpdates?: boolean;
88
+ }): ChannelContract<N, Params, J, CE, SE, M>;
89
+ get _definition(): ChannelContractDefinition;
90
+ }
91
+ export type AnyChannelContract = ChannelContract<any, any, any, any, any, any>;
92
+ /** Compile-time shape of a channel contract, from both sides' perspective. */
93
+ export type InferChannel<C> = C extends ChannelContract<infer N, infer Params, infer J, infer CE, infer SE, infer PM> ? {
94
+ name: N;
95
+ params: Params;
96
+ join: {
97
+ /** What the client sends / the server receives. */
98
+ payloadIn: SchemaInput<J["payload"]>;
99
+ payload: SchemaOutput<J["payload"]>;
100
+ /** What the server returns / the client receives. */
101
+ replyIn: SchemaInput<J["reply"]>;
102
+ reply: SchemaOutput<J["reply"]>;
103
+ };
104
+ clientEvents: {
105
+ [K in keyof CE]: {
106
+ payloadIn: SchemaInput<CE[K]["payload"]>;
107
+ payload: SchemaOutput<CE[K]["payload"]>;
108
+ replyIn: SchemaInput<CE[K]["reply"]>;
109
+ reply: SchemaOutput<CE[K]["reply"]>;
110
+ };
111
+ };
112
+ serverEvents: {
113
+ [K in keyof SE]: {
114
+ payloadIn: SchemaInput<SE[K]>;
115
+ payload: SchemaOutput<SE[K]>;
116
+ };
117
+ };
118
+ presenceMeta: PM extends AnySchema ? {
119
+ in: SchemaInput<PM>;
120
+ out: SchemaOutput<PM>;
121
+ } : never;
122
+ } : never;
123
+ export type WsContractKind = "raw" | "messages" | "channels";
124
+ export interface WsContractDefinition {
125
+ path: string;
126
+ paramSchemas: Map<string, AnySchema>;
127
+ kind: WsContractKind;
128
+ protocols?: string[];
129
+ channels: AnyChannelContract[];
130
+ messages?: {
131
+ client: AnySchema;
132
+ server: AnySchema;
133
+ };
134
+ meta?: RouteMeta;
135
+ }
136
+ export declare class WsContract<P extends string, Params extends Record<string, unknown> = BasePathParams<P>, K extends WsContractKind = "raw", Channels extends readonly AnyChannelContract[] = [], Client extends AnySchema | undefined = undefined, Server extends AnySchema | undefined = undefined> {
137
+ #private;
138
+ readonly path: P;
139
+ constructor(path: P, def?: WsContractDefinition);
140
+ meta(data: RouteMeta): WsContract<P, Params, K, Channels, Client, Server>;
141
+ param<Name extends string & keyof Params, S extends AnySchema>(name: Name, schema: S): WsContract<P, MergeParam<Params, Name, S>, K, Channels, Client, Server>;
142
+ /** Subprotocols for raw/message sockets (channels use their own). */
143
+ protocols(list: string[]): WsContract<P, Params, K, Channels, Client, Server>;
144
+ /** A typed socket exchanging one JSON message type in each direction. */
145
+ messages<CS extends AnySchema, SS extends AnySchema>(schemas: {
146
+ client: CS;
147
+ server: SS;
148
+ }): WsContract<P, Params, "messages", [], CS, SS>;
149
+ /** A socket speaking the channel protocol with exactly these channels. */
150
+ channels<const Cs extends readonly AnyChannelContract[]>(...channels: Cs): WsContract<P, Params, "channels", Cs, undefined, undefined>;
151
+ get _definition(): WsContractDefinition;
152
+ }
153
+ export type AnyWsContract = WsContract<any, any, any, any, any, any>;
154
+ export type InferSocket<C> = C extends WsContract<infer P, infer Params, infer K, infer Channels, infer Client, infer Server> ? {
155
+ path: P;
156
+ params: Params;
157
+ kind: K;
158
+ channels: {
159
+ [I in keyof Channels]: InferChannel<Channels[I]>;
160
+ };
161
+ channelNames: Channels[number]["name"];
162
+ /** Client → server message (raw `messages()` sockets). */
163
+ clientMessageIn: SchemaInput<Client>;
164
+ clientMessage: SchemaOutput<Client>;
165
+ /** Server → client message. */
166
+ serverMessageIn: SchemaInput<Server>;
167
+ serverMessage: SchemaOutput<Server>;
168
+ } : never;
169
+ export declare const wsContractFactories: {
170
+ /** A WebSocket endpoint. */
171
+ ws: <P extends string>(path: P) => WsContract<P, BasePathParams<P>, "raw", [], undefined, undefined>;
172
+ /** A channel, named with router path syntax (`rooms/:roomId`). */
173
+ channel: <N extends string>(name: N) => ChannelContract<N, BasePathParams<N>, {
174
+ payload: undefined;
175
+ reply: undefined;
176
+ }, Record<never, never>, Record<never, never>, undefined>;
177
+ };
178
+ export {};
@@ -0,0 +1,164 @@
1
+ /**
2
+ * WebSocket contracts — portable data describing sockets and channels, shared
3
+ * by the server (`@fishenv/http/ws`) and typed clients.
4
+ *
5
+ * ```ts
6
+ * export const room = c
7
+ * .channel("rooms/:roomId")
8
+ * .param("roomId", v.pipe(v.string(), v.uuid()))
9
+ * .join(JoinPayload, JoinReply, [ForbiddenError])
10
+ * .clientEvent("message", MessageIn, MessageAck)
11
+ * .serverEvent("message", Message)
12
+ * .presence(PresenceMeta, { clientUpdates: true });
13
+ *
14
+ * export const socket = c.ws("/socket").channels(room);
15
+ * ```
16
+ *
17
+ * Like HTTP contracts, builders are immutable and expose a plain
18
+ * `_definition` snapshot; the compile-time shape is recovered with
19
+ * {@link InferChannel} / {@link InferSocket}.
20
+ */
21
+ /** The channel protocol's subprotocol name. */
22
+ export const CHANNELS_PROTOCOL = "fishenv.channels.v1";
23
+ export class ChannelContract {
24
+ name;
25
+ #def;
26
+ constructor(name, def) {
27
+ if (name.startsWith("/")) {
28
+ throw new Error(`Channel name "${name}" must not start with "/"`);
29
+ }
30
+ this.name = name;
31
+ this.#def = def ?? {
32
+ name,
33
+ paramSchemas: new Map(),
34
+ join: {},
35
+ clientEvents: new Map(),
36
+ serverEvents: new Map(),
37
+ };
38
+ }
39
+ #next(patch) {
40
+ const def = this.#def;
41
+ return new ChannelContract(this.name, {
42
+ ...def,
43
+ paramSchemas: new Map(def.paramSchemas),
44
+ join: { ...def.join },
45
+ clientEvents: new Map(def.clientEvents),
46
+ serverEvents: new Map(def.serverEvents),
47
+ presence: def.presence ? { ...def.presence } : undefined,
48
+ meta: def.meta ? { ...def.meta } : undefined,
49
+ ...patch,
50
+ });
51
+ }
52
+ meta(data) {
53
+ return this.#next({ meta: { ...this.#def.meta, ...data } });
54
+ }
55
+ /** Narrow/coerce a channel-name param. */
56
+ param(name, schema) {
57
+ const paramSchemas = new Map(this.#def.paramSchemas);
58
+ paramSchemas.set(name, schema);
59
+ return this.#next({ paramSchemas });
60
+ }
61
+ /** Join payload, join reply and documented errors. All optional. */
62
+ join(payload, reply, errors) {
63
+ return this.#next({ join: { payload, reply, errors } });
64
+ }
65
+ /** Client → server event. Without `reply` it is fire-and-forget. */
66
+ clientEvent(name, payload, reply, errors) {
67
+ if (this.#def.clientEvents.has(name)) {
68
+ throw new Error(`clientEvent "${name}" is already defined`);
69
+ }
70
+ const clientEvents = new Map(this.#def.clientEvents);
71
+ clientEvents.set(name, { payload, reply, errors });
72
+ return this.#next({ clientEvents });
73
+ }
74
+ /** Server → client event. */
75
+ serverEvent(name, payload) {
76
+ if (this.#def.serverEvents.has(name)) {
77
+ throw new Error(`serverEvent "${name}" is already defined`);
78
+ }
79
+ const serverEvents = new Map(this.#def.serverEvents);
80
+ serverEvents.set(name, payload);
81
+ return this.#next({ serverEvents });
82
+ }
83
+ /**
84
+ * Enable presence for this channel. `clientUpdates` lets clients change
85
+ * their own meta (rate-limited server-side).
86
+ */
87
+ presence(meta, opts = {}) {
88
+ return this.#next({
89
+ presence: { meta, clientUpdates: opts.clientUpdates ?? false },
90
+ });
91
+ }
92
+ get _definition() {
93
+ return this.#def;
94
+ }
95
+ }
96
+ export class WsContract {
97
+ path;
98
+ #def;
99
+ constructor(path, def) {
100
+ this.path = path;
101
+ this.#def = def ?? {
102
+ path,
103
+ paramSchemas: new Map(),
104
+ kind: "raw",
105
+ channels: [],
106
+ };
107
+ }
108
+ #next(patch) {
109
+ const def = this.#def;
110
+ return new WsContract(this.path, {
111
+ ...def,
112
+ paramSchemas: new Map(def.paramSchemas),
113
+ channels: [...def.channels],
114
+ protocols: def.protocols ? [...def.protocols] : undefined,
115
+ ...patch,
116
+ });
117
+ }
118
+ meta(data) {
119
+ return this.#next({ meta: { ...this.#def.meta, ...data } });
120
+ }
121
+ param(name, schema) {
122
+ const paramSchemas = new Map(this.#def.paramSchemas);
123
+ paramSchemas.set(name, schema);
124
+ return this.#next({ paramSchemas });
125
+ }
126
+ /** Subprotocols for raw/message sockets (channels use their own). */
127
+ protocols(list) {
128
+ return this.#next({ protocols: [...list] });
129
+ }
130
+ /** A typed socket exchanging one JSON message type in each direction. */
131
+ messages(schemas) {
132
+ if (this.#def.kind === "channels") {
133
+ throw new Error("messages() and channels() are mutually exclusive");
134
+ }
135
+ return this.#next({ kind: "messages", messages: { ...schemas } });
136
+ }
137
+ /** A socket speaking the channel protocol with exactly these channels. */
138
+ channels(...channels) {
139
+ if (this.#def.kind === "messages") {
140
+ throw new Error("messages() and channels() are mutually exclusive");
141
+ }
142
+ const names = new Set();
143
+ for (const ch of channels) {
144
+ if (names.has(ch.name)) {
145
+ throw new Error(`Channel "${ch.name}" is listed twice`);
146
+ }
147
+ names.add(ch.name);
148
+ }
149
+ return this.#next({
150
+ kind: "channels",
151
+ channels: [...channels],
152
+ protocols: [CHANNELS_PROTOCOL],
153
+ });
154
+ }
155
+ get _definition() {
156
+ return this.#def;
157
+ }
158
+ }
159
+ export const wsContractFactories = {
160
+ /** A WebSocket endpoint. */
161
+ ws: (path) => new WsContract(path),
162
+ /** A channel, named with router path syntax (`rooms/:roomId`). */
163
+ channel: (name) => new ChannelContract(name),
164
+ };
@@ -0,0 +1,22 @@
1
+ import type { DIContainer, ErrorHandlerFn, InterceptorPair, MiddlewareFn, RouteDefinition } from "./types.ts";
2
+ /**
3
+ * Run the middleware pipeline, accumulating context via object spread.
4
+ * Each middleware receives the accumulated ctx and returns new fields to merge.
5
+ */
6
+ export declare function runMiddlewarePipeline(req: Request, middlewares: MiddlewareFn<Record<string, unknown>, Record<string, unknown>>[], container: DIContainer): Promise<Record<string, unknown>>;
7
+ /**
8
+ * Run before interceptors, the handler, then after interceptors (in reverse).
9
+ * Before and after share a mutable `state` object created per-request.
10
+ */
11
+ export declare function runInterceptors(interceptors: InterceptorPair<Record<string, unknown>>[], req: Request, ctx: Record<string, unknown>, handlerFn: () => Promise<Response>): Promise<Response>;
12
+ /**
13
+ * Coerce a handler's return value into a Response.
14
+ * - If already a Response, pass through.
15
+ * - If output schema is defined, serialize as JSON.
16
+ * - Otherwise throw — handler contract violation.
17
+ */
18
+ export declare function coerceToResponse(raw: unknown, def: RouteDefinition): Response;
19
+ /**
20
+ * Full request dispatch: middleware → input parsing → interceptors + handler → error chain.
21
+ */
22
+ export declare function dispatch(req: Request, definition: RouteDefinition, params: Record<string, string>, container: DIContainer, errorHandlers: ErrorHandlerFn[]): Promise<Response>;