@richie-rpc/server 1.2.3 → 1.2.5

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.
@@ -0,0 +1,212 @@
1
+ // @bun
2
+ // packages/server/websocket.ts
3
+ import { matchPath, parseQuery } from "@richie-rpc/core";
4
+
5
+ class WebSocketValidationError extends Error {
6
+ messageType;
7
+ issues;
8
+ constructor(messageType, issues) {
9
+ super(`Validation failed for WebSocket message type: ${messageType}`);
10
+ this.messageType = messageType;
11
+ this.issues = issues;
12
+ this.name = "WebSocketValidationError";
13
+ }
14
+ }
15
+ function createTypedWebSocket(ws) {
16
+ return {
17
+ get raw() {
18
+ return ws;
19
+ },
20
+ send(type, payload) {
21
+ ws.send(JSON.stringify({ type, payload }));
22
+ },
23
+ subscribe(topic) {
24
+ ws.subscribe(topic);
25
+ },
26
+ unsubscribe(topic) {
27
+ ws.unsubscribe(topic);
28
+ },
29
+ publish(topic, type, payload) {
30
+ ws.publish(topic, JSON.stringify({ type, payload }));
31
+ },
32
+ close(code, reason) {
33
+ ws.close(code, reason);
34
+ },
35
+ get data() {
36
+ return ws.data;
37
+ }
38
+ };
39
+ }
40
+
41
+ class WebSocketRouter {
42
+ contract;
43
+ handlers;
44
+ basePath;
45
+ contextFactory;
46
+ constructor(contract, handlers, options) {
47
+ this.contract = contract;
48
+ this.handlers = handlers;
49
+ const bp = options?.basePath || "";
50
+ if (bp) {
51
+ this.basePath = bp.startsWith("/") ? bp : `/${bp}`;
52
+ this.basePath = this.basePath.endsWith("/") ? this.basePath.slice(0, -1) : this.basePath;
53
+ } else {
54
+ this.basePath = "";
55
+ }
56
+ this.contextFactory = options?.context;
57
+ }
58
+ findEndpoint(path) {
59
+ for (const [name, endpoint] of Object.entries(this.contract)) {
60
+ const params = matchPath(endpoint.path, path);
61
+ if (params !== null) {
62
+ return {
63
+ name,
64
+ endpoint,
65
+ params
66
+ };
67
+ }
68
+ }
69
+ return null;
70
+ }
71
+ parseUpgradeParams(request, endpoint, pathParams) {
72
+ const url = new URL(request.url);
73
+ let params = pathParams;
74
+ if (endpoint.params) {
75
+ const result = endpoint.params.safeParse(pathParams);
76
+ if (!result.success) {
77
+ throw new Error(`Invalid path params: ${result.error.message}`);
78
+ }
79
+ params = result.data;
80
+ }
81
+ let query = {};
82
+ if (endpoint.query) {
83
+ const queryData = parseQuery(url.searchParams);
84
+ const result = endpoint.query.safeParse(queryData);
85
+ if (!result.success) {
86
+ throw new Error(`Invalid query params: ${result.error.message}`);
87
+ }
88
+ query = result.data;
89
+ }
90
+ let headers = {};
91
+ if (endpoint.headers) {
92
+ const headersObj = {};
93
+ request.headers.forEach((value, key) => {
94
+ headersObj[key] = value;
95
+ });
96
+ const result = endpoint.headers.safeParse(headersObj);
97
+ if (!result.success) {
98
+ throw new Error(`Invalid headers: ${result.error.message}`);
99
+ }
100
+ headers = result.data;
101
+ }
102
+ return { params, query, headers };
103
+ }
104
+ async matchAndPrepareUpgrade(request) {
105
+ const url = new URL(request.url);
106
+ let path = url.pathname;
107
+ if (this.basePath && path.startsWith(this.basePath)) {
108
+ path = path.slice(this.basePath.length) || "/";
109
+ }
110
+ const match = this.findEndpoint(path);
111
+ if (!match) {
112
+ return null;
113
+ }
114
+ const { name, endpoint, params: rawParams } = match;
115
+ try {
116
+ const { params, query, headers } = this.parseUpgradeParams(request, endpoint, rawParams);
117
+ const context = this.contextFactory ? await this.contextFactory(request, String(name), endpoint) : undefined;
118
+ return {
119
+ endpointName: String(name),
120
+ endpoint,
121
+ params,
122
+ query,
123
+ headers,
124
+ context,
125
+ state: {}
126
+ };
127
+ } catch (err) {
128
+ console.error("WebSocket upgrade validation failed:", err);
129
+ return null;
130
+ }
131
+ }
132
+ validateMessage(endpoint, rawMessage) {
133
+ const messageStr = typeof rawMessage === "string" ? rawMessage : new TextDecoder().decode(rawMessage);
134
+ const parsed = JSON.parse(messageStr);
135
+ const { type, payload } = parsed;
136
+ const messageDef = endpoint.clientMessages[type];
137
+ if (!messageDef) {
138
+ throw new WebSocketValidationError(type, [
139
+ {
140
+ code: "custom",
141
+ path: ["type"],
142
+ message: `Unknown message type: ${type}`
143
+ }
144
+ ]);
145
+ }
146
+ const result = messageDef.payload.safeParse(payload);
147
+ if (!result.success) {
148
+ throw new WebSocketValidationError(type, result.error.issues);
149
+ }
150
+ return { type, payload: result.data };
151
+ }
152
+ get websocketHandler() {
153
+ return {
154
+ open: async (ws) => {
155
+ const data = ws.data;
156
+ const endpointHandlers = this.handlers[data.endpointName];
157
+ if (!endpointHandlers)
158
+ return;
159
+ const typedWs = createTypedWebSocket(ws);
160
+ if (endpointHandlers.open) {
161
+ await endpointHandlers.open(typedWs, data.context);
162
+ }
163
+ },
164
+ message: async (ws, message) => {
165
+ const data = ws.data;
166
+ const endpointHandlers = this.handlers[data.endpointName];
167
+ if (!endpointHandlers)
168
+ return;
169
+ const typedWs = createTypedWebSocket(ws);
170
+ try {
171
+ const validatedMessage = this.validateMessage(data.endpoint, typeof message === "string" ? message : message.buffer);
172
+ await endpointHandlers.message(typedWs, validatedMessage, data.context);
173
+ } catch (err) {
174
+ if (err instanceof WebSocketValidationError) {
175
+ if (endpointHandlers.validationError) {
176
+ endpointHandlers.validationError(typedWs, err, data.context);
177
+ } else {
178
+ typedWs.send("error", {
179
+ code: "VALIDATION_ERROR",
180
+ message: err.message,
181
+ issues: err.issues
182
+ });
183
+ }
184
+ } else {
185
+ console.error("WebSocket message handler error:", err);
186
+ }
187
+ }
188
+ },
189
+ close: (ws, _code, _reason) => {
190
+ const data = ws.data;
191
+ const endpointHandlers = this.handlers[data.endpointName];
192
+ if (!endpointHandlers)
193
+ return;
194
+ const typedWs = createTypedWebSocket(ws);
195
+ if (endpointHandlers.close) {
196
+ endpointHandlers.close(typedWs, data.context);
197
+ }
198
+ },
199
+ drain: () => {}
200
+ };
201
+ }
202
+ }
203
+ function createWebSocketRouter(contract, handlers, options) {
204
+ return new WebSocketRouter(contract, handlers, options);
205
+ }
206
+ export {
207
+ createWebSocketRouter,
208
+ WebSocketValidationError,
209
+ WebSocketRouter
210
+ };
211
+
212
+ //# debugId=47CE29C068793DD164756E2164756E21
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../websocket.ts"],
4
+ "sourcesContent": [
5
+ "import type {\n ExtractClientMessage,\n ExtractWSHeaders,\n ExtractWSParams,\n ExtractWSQuery,\n WebSocketContract,\n WebSocketContractDefinition,\n} from '@richie-rpc/core';\nimport { matchPath, parseQuery } from '@richie-rpc/core';\nimport type { z } from 'zod';\n\n/**\n * Validation error for WebSocket messages\n */\nexport class WebSocketValidationError extends Error {\n constructor(\n public messageType: string,\n public issues: z.ZodIssue[],\n ) {\n super(`Validation failed for WebSocket message type: ${messageType}`);\n this.name = 'WebSocketValidationError';\n }\n}\n\n/**\n * Data attached to WebSocket connections for routing\n */\nexport interface WebSocketData<\n T extends WebSocketContractDefinition = WebSocketContractDefinition,\n S = unknown,\n> {\n endpointName: string;\n endpoint: T;\n params: ExtractWSParams<T>;\n query: ExtractWSQuery<T>;\n headers: ExtractWSHeaders<T>;\n context: unknown;\n state: S;\n}\n\n/**\n * Typed WebSocket wrapper for sending messages\n */\nexport interface TypedServerWebSocket<T extends WebSocketContractDefinition, S = unknown> {\n /** The underlying Bun WebSocket */\n readonly raw: WebSocket;\n /** Send a typed message to the client */\n send<K extends keyof T['serverMessages']>(\n type: K,\n payload: z.infer<T['serverMessages'][K]['payload']>,\n ): void;\n /** Subscribe to a topic for pub/sub */\n subscribe(topic: string): void;\n /** Unsubscribe from a topic */\n unsubscribe(topic: string): void;\n /** Publish a message to a topic */\n publish<K extends keyof T['serverMessages']>(\n topic: string,\n type: K,\n payload: z.infer<T['serverMessages'][K]['payload']>,\n ): void;\n /** Close the connection */\n close(code?: number, reason?: string): void;\n /** Connection data */\n readonly data: WebSocketData<T, S>;\n}\n\n/**\n * Handler functions for a WebSocket endpoint\n */\nexport interface WebSocketEndpointHandlers<\n T extends WebSocketContractDefinition,\n C = unknown,\n S = unknown,\n> {\n /** Called when connection opens */\n open?(ws: TypedServerWebSocket<T, S>, ctx: C): void | Promise<void>;\n /** Called for each validated message */\n message(\n ws: TypedServerWebSocket<T, S>,\n message: ExtractClientMessage<T>,\n ctx: C,\n ): void | Promise<void>;\n /** Called when connection closes */\n close?(ws: TypedServerWebSocket<T, S>, ctx: C): void;\n /** Called when message validation fails */\n validationError?(ws: TypedServerWebSocket<T, S>, error: WebSocketValidationError, ctx: C): void;\n}\n\n/**\n * Contract handlers mapping for WebSocket endpoints\n */\nexport type WebSocketContractHandlers<T extends WebSocketContract, C = unknown, S = unknown> = {\n [K in keyof T]: WebSocketEndpointHandlers<T[K], C, S>;\n};\n\n/**\n * Upgrade data returned by matchAndPrepareUpgrade\n */\nexport interface UpgradeData<S = unknown> {\n endpointName: string;\n endpoint: WebSocketContractDefinition;\n params: Record<string, string>;\n query: Record<string, string | string[]>;\n headers: Record<string, string>;\n context: unknown;\n state: S;\n}\n\n/**\n * Options for WebSocket router\n */\nexport interface WebSocketRouterOptions<C = unknown, S = unknown> {\n basePath?: string;\n context?: (\n request: Request,\n endpointName: string,\n endpoint: WebSocketContractDefinition,\n ) => C | Promise<C>;\n /** Type hint for per-connection state. Use `{} as YourStateType` */\n state?: S;\n}\n\n/**\n * Bun WebSocket handler type (subset of Bun's types)\n */\nexport interface BunWebSocketHandler<T = unknown> {\n open(ws: Bun.ServerWebSocket<T>): void | Promise<void>;\n message(ws: Bun.ServerWebSocket<T>, message: string | Buffer<ArrayBuffer>): void | Promise<void>;\n close(ws: Bun.ServerWebSocket<T>, code: number, reason: string): void;\n drain(ws: Bun.ServerWebSocket<T>): void;\n}\n\n/**\n * Create a typed WebSocket wrapper\n */\nfunction createTypedWebSocket<T extends WebSocketContractDefinition, S = unknown>(\n ws: WebSocket & { data: WebSocketData<T, S> },\n): TypedServerWebSocket<T, S> {\n return {\n get raw() {\n return ws;\n },\n send(type, payload) {\n ws.send(JSON.stringify({ type, payload }));\n },\n subscribe(topic) {\n (ws as any).subscribe(topic);\n },\n unsubscribe(topic) {\n (ws as any).unsubscribe(topic);\n },\n publish(topic, type, payload) {\n (ws as any).publish(topic, JSON.stringify({ type, payload }));\n },\n close(code, reason) {\n ws.close(code, reason);\n },\n get data() {\n return ws.data;\n },\n };\n}\n\n/**\n * WebSocket router for managing WebSocket contract endpoints\n */\nexport class WebSocketRouter<T extends WebSocketContract, C = unknown, S = unknown> {\n private basePath: string;\n private contextFactory?: (\n request: Request,\n endpointName: string,\n endpoint: WebSocketContractDefinition,\n ) => C | Promise<C>;\n\n constructor(\n private contract: T,\n private handlers: WebSocketContractHandlers<T, C, S>,\n options?: WebSocketRouterOptions<C, S>,\n ) {\n // Normalize basePath\n const bp = options?.basePath || '';\n if (bp) {\n this.basePath = bp.startsWith('/') ? bp : `/${bp}`;\n this.basePath = this.basePath.endsWith('/') ? this.basePath.slice(0, -1) : this.basePath;\n } else {\n this.basePath = '';\n }\n this.contextFactory = options?.context;\n }\n\n /**\n * Find matching endpoint for a path\n */\n private findEndpoint(path: string): {\n name: keyof T;\n endpoint: WebSocketContractDefinition;\n params: Record<string, string>;\n } | null {\n for (const [name, endpoint] of Object.entries(this.contract)) {\n const params = matchPath(endpoint.path, path);\n if (params !== null) {\n return {\n name,\n endpoint: endpoint as WebSocketContractDefinition,\n params,\n };\n }\n }\n return null;\n }\n\n /**\n * Parse and validate upgrade request parameters\n */\n private parseUpgradeParams(\n request: Request,\n endpoint: WebSocketContractDefinition,\n pathParams: Record<string, string>,\n ): { params: any; query: any; headers: any } {\n const url = new URL(request.url);\n\n // Parse and validate path params\n let params: any = pathParams;\n if (endpoint.params) {\n const result = endpoint.params.safeParse(pathParams);\n if (!result.success) {\n throw new Error(`Invalid path params: ${result.error.message}`);\n }\n params = result.data;\n }\n\n // Parse and validate query params\n let query: any = {};\n if (endpoint.query) {\n const queryData = parseQuery(url.searchParams);\n const result = endpoint.query.safeParse(queryData);\n if (!result.success) {\n throw new Error(`Invalid query params: ${result.error.message}`);\n }\n query = result.data;\n }\n\n // Parse and validate headers\n let headers: any = {};\n if (endpoint.headers) {\n const headersObj: Record<string, string> = {};\n request.headers.forEach((value, key) => {\n headersObj[key] = value;\n });\n const result = endpoint.headers.safeParse(headersObj);\n if (!result.success) {\n throw new Error(`Invalid headers: ${result.error.message}`);\n }\n headers = result.data;\n }\n\n return { params, query, headers };\n }\n\n /**\n * Match a request and prepare upgrade data\n * Returns null if no match, or UpgradeData for server.upgrade()\n */\n async matchAndPrepareUpgrade(request: Request): Promise<UpgradeData<S> | null> {\n const url = new URL(request.url);\n let path = url.pathname;\n\n // Strip basePath if configured\n if (this.basePath && path.startsWith(this.basePath)) {\n path = path.slice(this.basePath.length) || '/';\n }\n\n const match = this.findEndpoint(path);\n if (!match) {\n return null;\n }\n\n const { name, endpoint, params: rawParams } = match;\n\n try {\n const { params, query, headers } = this.parseUpgradeParams(request, endpoint, rawParams);\n\n // Create context if factory provided\n const context = this.contextFactory\n ? await this.contextFactory(request, String(name), endpoint)\n : (undefined as C);\n\n return {\n endpointName: String(name),\n endpoint,\n params,\n query,\n headers,\n context,\n state: {} as S,\n };\n } catch (err) {\n // Validation failed during upgrade\n console.error('WebSocket upgrade validation failed:', err);\n return null;\n }\n }\n\n /**\n * Validate an incoming client message\n */\n private validateMessage(\n endpoint: WebSocketContractDefinition,\n rawMessage: string | ArrayBuffer,\n ): ExtractClientMessage<typeof endpoint> {\n // Parse message\n const messageStr =\n typeof rawMessage === 'string' ? rawMessage : new TextDecoder().decode(rawMessage);\n const parsed = JSON.parse(messageStr) as { type: string; payload: unknown };\n\n const { type, payload } = parsed;\n\n // Find the schema for this message type\n const messageDef = endpoint.clientMessages[type];\n if (!messageDef) {\n throw new WebSocketValidationError(type, [\n {\n code: 'custom',\n path: ['type'],\n message: `Unknown message type: ${type}`,\n },\n ]);\n }\n\n // Validate payload\n const result = messageDef.payload.safeParse(payload);\n if (!result.success) {\n throw new WebSocketValidationError(type, result.error.issues);\n }\n\n return { type, payload: result.data } as ExtractClientMessage<typeof endpoint>;\n }\n\n /**\n * Get Bun-compatible WebSocket handler\n */\n get websocketHandler(): BunWebSocketHandler<UpgradeData<S>> {\n return {\n open: async (ws) => {\n const data = ws.data;\n const endpointHandlers = this.handlers[data.endpointName as keyof T];\n if (!endpointHandlers) return;\n\n // Create typed wrapper with properly typed data\n const typedWs = createTypedWebSocket(\n ws as unknown as WebSocket & {\n data: WebSocketData<WebSocketContractDefinition, S>;\n },\n );\n\n if (endpointHandlers.open) {\n await endpointHandlers.open(typedWs as any, data.context as C);\n }\n },\n\n message: async (ws, message) => {\n const data = ws.data;\n const endpointHandlers = this.handlers[data.endpointName as keyof T];\n if (!endpointHandlers) return;\n\n const typedWs = createTypedWebSocket(\n ws as unknown as WebSocket & {\n data: WebSocketData<WebSocketContractDefinition, S>;\n },\n );\n\n try {\n // Validate the message\n const validatedMessage = this.validateMessage(\n data.endpoint,\n typeof message === 'string' ? message : message.buffer,\n );\n\n // Call handler with validated message\n await endpointHandlers.message(\n typedWs as any,\n validatedMessage as any,\n data.context as C,\n );\n } catch (err) {\n if (err instanceof WebSocketValidationError) {\n // Call validation error handler if provided\n if (endpointHandlers.validationError) {\n endpointHandlers.validationError(typedWs as any, err, data.context as C);\n } else {\n // Default: send error message back\n typedWs.send(\n 'error' as any,\n {\n code: 'VALIDATION_ERROR',\n message: err.message,\n issues: err.issues,\n } as any,\n );\n }\n } else {\n console.error('WebSocket message handler error:', err);\n }\n }\n },\n\n close: (ws, _code, _reason) => {\n const data = ws.data;\n const endpointHandlers = this.handlers[data.endpointName as keyof T];\n if (!endpointHandlers) return;\n\n const typedWs = createTypedWebSocket(\n ws as unknown as WebSocket & {\n data: WebSocketData<WebSocketContractDefinition, S>;\n },\n );\n\n if (endpointHandlers.close) {\n endpointHandlers.close(typedWs as any, data.context as C);\n }\n },\n drain: () => {\n // not used\n },\n };\n }\n}\n\n/**\n * Create a WebSocket router from a contract and handlers\n */\nexport function createWebSocketRouter<T extends WebSocketContract, C = unknown, S = unknown>(\n contract: T,\n handlers: WebSocketContractHandlers<T, C, S>,\n options?: WebSocketRouterOptions<C, S>,\n): WebSocketRouter<T, C, S> {\n return new WebSocketRouter(contract, handlers, options);\n}\n"
6
+ ],
7
+ "mappings": ";;AAQA;AAAA;AAMO,MAAM,iCAAiC,MAAM;AAAA,EAEzC;AAAA,EACA;AAAA,EAFT,WAAW,CACF,aACA,QACP;AAAA,IACA,MAAM,iDAAiD,aAAa;AAAA,IAH7D;AAAA,IACA;AAAA,IAGP,KAAK,OAAO;AAAA;AAEhB;AAkHA,SAAS,oBAAwE,CAC/E,IAC4B;AAAA,EAC5B,OAAO;AAAA,QACD,GAAG,GAAG;AAAA,MACR,OAAO;AAAA;AAAA,IAET,IAAI,CAAC,MAAM,SAAS;AAAA,MAClB,GAAG,KAAK,KAAK,UAAU,EAAE,MAAM,QAAQ,CAAC,CAAC;AAAA;AAAA,IAE3C,SAAS,CAAC,OAAO;AAAA,MACd,GAAW,UAAU,KAAK;AAAA;AAAA,IAE7B,WAAW,CAAC,OAAO;AAAA,MAChB,GAAW,YAAY,KAAK;AAAA;AAAA,IAE/B,OAAO,CAAC,OAAO,MAAM,SAAS;AAAA,MAC3B,GAAW,QAAQ,OAAO,KAAK,UAAU,EAAE,MAAM,QAAQ,CAAC,CAAC;AAAA;AAAA,IAE9D,KAAK,CAAC,MAAM,QAAQ;AAAA,MAClB,GAAG,MAAM,MAAM,MAAM;AAAA;AAAA,QAEnB,IAAI,GAAG;AAAA,MACT,OAAO,GAAG;AAAA;AAAA,EAEd;AAAA;AAAA;AAMK,MAAM,gBAAuE;AAAA,EASxE;AAAA,EACA;AAAA,EATF;AAAA,EACA;AAAA,EAMR,WAAW,CACD,UACA,UACR,SACA;AAAA,IAHQ;AAAA,IACA;AAAA,IAIR,MAAM,KAAK,SAAS,YAAY;AAAA,IAChC,IAAI,IAAI;AAAA,MACN,KAAK,WAAW,GAAG,WAAW,GAAG,IAAI,KAAK,IAAI;AAAA,MAC9C,KAAK,WAAW,KAAK,SAAS,SAAS,GAAG,IAAI,KAAK,SAAS,MAAM,GAAG,EAAE,IAAI,KAAK;AAAA,IAClF,EAAO;AAAA,MACL,KAAK,WAAW;AAAA;AAAA,IAElB,KAAK,iBAAiB,SAAS;AAAA;AAAA,EAMzB,YAAY,CAAC,MAIZ;AAAA,IACP,YAAY,MAAM,aAAa,OAAO,QAAQ,KAAK,QAAQ,GAAG;AAAA,MAC5D,MAAM,SAAS,UAAU,SAAS,MAAM,IAAI;AAAA,MAC5C,IAAI,WAAW,MAAM;AAAA,QACnB,OAAO;AAAA,UACL;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,IACA,OAAO;AAAA;AAAA,EAMD,kBAAkB,CACxB,SACA,UACA,YAC2C;AAAA,IAC3C,MAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAAA,IAG/B,IAAI,SAAc;AAAA,IAClB,IAAI,SAAS,QAAQ;AAAA,MACnB,MAAM,SAAS,SAAS,OAAO,UAAU,UAAU;AAAA,MACnD,IAAI,CAAC,OAAO,SAAS;AAAA,QACnB,MAAM,IAAI,MAAM,wBAAwB,OAAO,MAAM,SAAS;AAAA,MAChE;AAAA,MACA,SAAS,OAAO;AAAA,IAClB;AAAA,IAGA,IAAI,QAAa,CAAC;AAAA,IAClB,IAAI,SAAS,OAAO;AAAA,MAClB,MAAM,YAAY,WAAW,IAAI,YAAY;AAAA,MAC7C,MAAM,SAAS,SAAS,MAAM,UAAU,SAAS;AAAA,MACjD,IAAI,CAAC,OAAO,SAAS;AAAA,QACnB,MAAM,IAAI,MAAM,yBAAyB,OAAO,MAAM,SAAS;AAAA,MACjE;AAAA,MACA,QAAQ,OAAO;AAAA,IACjB;AAAA,IAGA,IAAI,UAAe,CAAC;AAAA,IACpB,IAAI,SAAS,SAAS;AAAA,MACpB,MAAM,aAAqC,CAAC;AAAA,MAC5C,QAAQ,QAAQ,QAAQ,CAAC,OAAO,QAAQ;AAAA,QACtC,WAAW,OAAO;AAAA,OACnB;AAAA,MACD,MAAM,SAAS,SAAS,QAAQ,UAAU,UAAU;AAAA,MACpD,IAAI,CAAC,OAAO,SAAS;AAAA,QACnB,MAAM,IAAI,MAAM,oBAAoB,OAAO,MAAM,SAAS;AAAA,MAC5D;AAAA,MACA,UAAU,OAAO;AAAA,IACnB;AAAA,IAEA,OAAO,EAAE,QAAQ,OAAO,QAAQ;AAAA;AAAA,OAO5B,uBAAsB,CAAC,SAAkD;AAAA,IAC7E,MAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAAA,IAC/B,IAAI,OAAO,IAAI;AAAA,IAGf,IAAI,KAAK,YAAY,KAAK,WAAW,KAAK,QAAQ,GAAG;AAAA,MACnD,OAAO,KAAK,MAAM,KAAK,SAAS,MAAM,KAAK;AAAA,IAC7C;AAAA,IAEA,MAAM,QAAQ,KAAK,aAAa,IAAI;AAAA,IACpC,IAAI,CAAC,OAAO;AAAA,MACV,OAAO;AAAA,IACT;AAAA,IAEA,QAAQ,MAAM,UAAU,QAAQ,cAAc;AAAA,IAE9C,IAAI;AAAA,MACF,QAAQ,QAAQ,OAAO,YAAY,KAAK,mBAAmB,SAAS,UAAU,SAAS;AAAA,MAGvF,MAAM,UAAU,KAAK,iBACjB,MAAM,KAAK,eAAe,SAAS,OAAO,IAAI,GAAG,QAAQ,IACxD;AAAA,MAEL,OAAO;AAAA,QACL,cAAc,OAAO,IAAI;AAAA,QACzB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,OAAO,CAAC;AAAA,MACV;AAAA,MACA,OAAO,KAAK;AAAA,MAEZ,QAAQ,MAAM,wCAAwC,GAAG;AAAA,MACzD,OAAO;AAAA;AAAA;AAAA,EAOH,eAAe,CACrB,UACA,YACuC;AAAA,IAEvC,MAAM,aACJ,OAAO,eAAe,WAAW,aAAa,IAAI,YAAY,EAAE,OAAO,UAAU;AAAA,IACnF,MAAM,SAAS,KAAK,MAAM,UAAU;AAAA,IAEpC,QAAQ,MAAM,YAAY;AAAA,IAG1B,MAAM,aAAa,SAAS,eAAe;AAAA,IAC3C,IAAI,CAAC,YAAY;AAAA,MACf,MAAM,IAAI,yBAAyB,MAAM;AAAA,QACvC;AAAA,UACE,MAAM;AAAA,UACN,MAAM,CAAC,MAAM;AAAA,UACb,SAAS,yBAAyB;AAAA,QACpC;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IAGA,MAAM,SAAS,WAAW,QAAQ,UAAU,OAAO;AAAA,IACnD,IAAI,CAAC,OAAO,SAAS;AAAA,MACnB,MAAM,IAAI,yBAAyB,MAAM,OAAO,MAAM,MAAM;AAAA,IAC9D;AAAA,IAEA,OAAO,EAAE,MAAM,SAAS,OAAO,KAAK;AAAA;AAAA,MAMlC,gBAAgB,GAAwC;AAAA,IAC1D,OAAO;AAAA,MACL,MAAM,OAAO,OAAO;AAAA,QAClB,MAAM,OAAO,GAAG;AAAA,QAChB,MAAM,mBAAmB,KAAK,SAAS,KAAK;AAAA,QAC5C,IAAI,CAAC;AAAA,UAAkB;AAAA,QAGvB,MAAM,UAAU,qBACd,EAGF;AAAA,QAEA,IAAI,iBAAiB,MAAM;AAAA,UACzB,MAAM,iBAAiB,KAAK,SAAgB,KAAK,OAAY;AAAA,QAC/D;AAAA;AAAA,MAGF,SAAS,OAAO,IAAI,YAAY;AAAA,QAC9B,MAAM,OAAO,GAAG;AAAA,QAChB,MAAM,mBAAmB,KAAK,SAAS,KAAK;AAAA,QAC5C,IAAI,CAAC;AAAA,UAAkB;AAAA,QAEvB,MAAM,UAAU,qBACd,EAGF;AAAA,QAEA,IAAI;AAAA,UAEF,MAAM,mBAAmB,KAAK,gBAC5B,KAAK,UACL,OAAO,YAAY,WAAW,UAAU,QAAQ,MAClD;AAAA,UAGA,MAAM,iBAAiB,QACrB,SACA,kBACA,KAAK,OACP;AAAA,UACA,OAAO,KAAK;AAAA,UACZ,IAAI,eAAe,0BAA0B;AAAA,YAE3C,IAAI,iBAAiB,iBAAiB;AAAA,cACpC,iBAAiB,gBAAgB,SAAgB,KAAK,KAAK,OAAY;AAAA,YACzE,EAAO;AAAA,cAEL,QAAQ,KACN,SACA;AAAA,gBACE,MAAM;AAAA,gBACN,SAAS,IAAI;AAAA,gBACb,QAAQ,IAAI;AAAA,cACd,CACF;AAAA;AAAA,UAEJ,EAAO;AAAA,YACL,QAAQ,MAAM,oCAAoC,GAAG;AAAA;AAAA;AAAA;AAAA,MAK3D,OAAO,CAAC,IAAI,OAAO,YAAY;AAAA,QAC7B,MAAM,OAAO,GAAG;AAAA,QAChB,MAAM,mBAAmB,KAAK,SAAS,KAAK;AAAA,QAC5C,IAAI,CAAC;AAAA,UAAkB;AAAA,QAEvB,MAAM,UAAU,qBACd,EAGF;AAAA,QAEA,IAAI,iBAAiB,OAAO;AAAA,UAC1B,iBAAiB,MAAM,SAAgB,KAAK,OAAY;AAAA,QAC1D;AAAA;AAAA,MAEF,OAAO,MAAM;AAAA,IAGf;AAAA;AAEJ;AAKO,SAAS,qBAA4E,CAC1F,UACA,UACA,SAC0B;AAAA,EAC1B,OAAO,IAAI,gBAAgB,UAAU,UAAU,OAAO;AAAA;",
8
+ "debugId": "47CE29C068793DD164756E2164756E21",
9
+ "names": []
10
+ }
@@ -1,8 +1,8 @@
1
- import type { Contract, EndpointDefinition, ExtractBody, ExtractHeaders, ExtractParams, ExtractQuery } from '@richie-rpc/core';
1
+ import type { Contract, DownloadEndpointDefinition, EndpointDefinition, ExtractBody, ExtractChunk, ExtractFinalResponse, ExtractHeaders, ExtractParams, ExtractQuery, ExtractSSEEventData, SSEEndpointDefinition, StandardEndpointDefinition, StreamingEndpointDefinition } from '@richie-rpc/core';
2
2
  import { Status } from '@richie-rpc/core';
3
3
  import type { z } from 'zod';
4
4
  export { Status };
5
- export type HandlerInput<T extends EndpointDefinition, C = unknown> = {
5
+ export type HandlerInput<T extends StandardEndpointDefinition, C = unknown> = {
6
6
  params: ExtractParams<T>;
7
7
  query: ExtractQuery<T>;
8
8
  headers: ExtractHeaders<T>;
@@ -10,16 +10,106 @@ export type HandlerInput<T extends EndpointDefinition, C = unknown> = {
10
10
  request: Request;
11
11
  context: C;
12
12
  };
13
- export type HandlerResponse<T extends EndpointDefinition> = {
13
+ export type HandlerResponse<T extends StandardEndpointDefinition> = {
14
14
  [Status in keyof T['responses']]: {
15
15
  status: Status;
16
16
  body: T['responses'][Status] extends z.ZodTypeAny ? z.infer<T['responses'][Status]> : never;
17
17
  headers?: Record<string, string>;
18
18
  };
19
19
  }[keyof T['responses']];
20
- export type Handler<T extends EndpointDefinition, C = unknown> = (input: HandlerInput<T, C>) => Promise<HandlerResponse<T>> | HandlerResponse<T>;
20
+ export type Handler<T extends StandardEndpointDefinition, C = unknown> = (input: HandlerInput<T, C>) => Promise<HandlerResponse<T>> | HandlerResponse<T>;
21
+ /**
22
+ * Emitter for streaming responses - push-based API
23
+ */
24
+ export interface StreamEmitter<T extends StreamingEndpointDefinition> {
25
+ /** Send a chunk to the client */
26
+ send(chunk: ExtractChunk<T>): void;
27
+ /** Close the stream with optional final response */
28
+ close(final?: ExtractFinalResponse<T>): void;
29
+ /** Check if stream is still open */
30
+ readonly isOpen: boolean;
31
+ }
32
+ /**
33
+ * Handler input for streaming endpoints
34
+ */
35
+ export type StreamingHandlerInput<T extends StreamingEndpointDefinition, C = unknown> = {
36
+ params: ExtractParams<T>;
37
+ query: ExtractQuery<T>;
38
+ headers: ExtractHeaders<T>;
39
+ body: ExtractBody<T>;
40
+ request: Request;
41
+ context: C;
42
+ stream: StreamEmitter<T>;
43
+ };
44
+ /**
45
+ * Handler function type for streaming endpoints
46
+ */
47
+ export type StreamingHandler<T extends StreamingEndpointDefinition, C = unknown> = (input: StreamingHandlerInput<T, C>) => void | Promise<void>;
48
+ /**
49
+ * Emitter for SSE responses
50
+ */
51
+ export interface SSEEmitter<T extends SSEEndpointDefinition> {
52
+ /** Send an event to the client */
53
+ send<K extends keyof T['events']>(event: K, data: ExtractSSEEventData<T, K>, options?: {
54
+ id?: string;
55
+ }): void;
56
+ /** Close the connection */
57
+ close(): void;
58
+ /** Check if connection is still open */
59
+ readonly isOpen: boolean;
60
+ }
61
+ /**
62
+ * Handler input for SSE endpoints
63
+ */
64
+ export type SSEHandlerInput<T extends SSEEndpointDefinition, C = unknown> = {
65
+ params: ExtractParams<T>;
66
+ query: ExtractQuery<T>;
67
+ headers: ExtractHeaders<T>;
68
+ request: Request;
69
+ context: C;
70
+ emitter: SSEEmitter<T>;
71
+ /** AbortSignal for detecting client disconnect */
72
+ signal: AbortSignal;
73
+ };
74
+ /**
75
+ * Handler function type for SSE endpoints
76
+ * Returns an optional cleanup function
77
+ */
78
+ export type SSEHandler<T extends SSEEndpointDefinition, C = unknown> = (input: SSEHandlerInput<T, C>) => void | (() => void) | Promise<void | (() => void)>;
79
+ /**
80
+ * Handler input for download endpoints
81
+ */
82
+ export type DownloadHandlerInput<T extends DownloadEndpointDefinition, C = unknown> = {
83
+ params: ExtractParams<T>;
84
+ query: ExtractQuery<T>;
85
+ headers: ExtractHeaders<T>;
86
+ request: Request;
87
+ context: C;
88
+ };
89
+ /**
90
+ * Handler response for download endpoints
91
+ * Success (200) returns File, errors return typed response
92
+ */
93
+ export type DownloadHandlerResponse<T extends DownloadEndpointDefinition> = {
94
+ status: 200;
95
+ body: File;
96
+ headers?: Record<string, string>;
97
+ } | (T['errorResponses'] extends Record<number, z.ZodTypeAny> ? {
98
+ [S in keyof T['errorResponses']]: {
99
+ status: S;
100
+ body: T['errorResponses'][S] extends z.ZodTypeAny ? z.infer<T['errorResponses'][S]> : never;
101
+ headers?: Record<string, string>;
102
+ };
103
+ }[keyof T['errorResponses']] : never);
104
+ /**
105
+ * Handler function type for download endpoints
106
+ */
107
+ export type DownloadHandler<T extends DownloadEndpointDefinition, C = unknown> = (input: DownloadHandlerInput<T, C>) => Promise<DownloadHandlerResponse<T>> | DownloadHandlerResponse<T>;
108
+ /**
109
+ * Contract handlers mapping - conditionally applies handler type based on endpoint type
110
+ */
21
111
  export type ContractHandlers<T extends Contract, C = unknown> = {
22
- [K in keyof T]: Handler<T[K], C>;
112
+ [K in keyof T]: T[K] extends StandardEndpointDefinition ? Handler<T[K], C> : T[K] extends StreamingEndpointDefinition ? StreamingHandler<T[K], C> : T[K] extends SSEEndpointDefinition ? SSEHandler<T[K], C> : T[K] extends DownloadEndpointDefinition ? DownloadHandler<T[K], C> : never;
23
113
  };
24
114
  export declare class ValidationError extends Error {
25
115
  field: string;
@@ -64,3 +154,4 @@ export declare class Router<T extends Contract, C = unknown> {
64
154
  * Create a router from a contract and handlers
65
155
  */
66
156
  export declare function createRouter<T extends Contract, C = unknown>(contract: T, handlers: ContractHandlers<T, C>, options?: RouterOptions<C>): Router<T, C>;
157
+ export { createWebSocketRouter } from './websocket';
@@ -0,0 +1,125 @@
1
+ import type { ExtractClientMessage, ExtractWSHeaders, ExtractWSParams, ExtractWSQuery, WebSocketContract, WebSocketContractDefinition } from '@richie-rpc/core';
2
+ import type { z } from 'zod';
3
+ /**
4
+ * Validation error for WebSocket messages
5
+ */
6
+ export declare class WebSocketValidationError extends Error {
7
+ messageType: string;
8
+ issues: z.ZodIssue[];
9
+ constructor(messageType: string, issues: z.ZodIssue[]);
10
+ }
11
+ /**
12
+ * Data attached to WebSocket connections for routing
13
+ */
14
+ export interface WebSocketData<T extends WebSocketContractDefinition = WebSocketContractDefinition, S = unknown> {
15
+ endpointName: string;
16
+ endpoint: T;
17
+ params: ExtractWSParams<T>;
18
+ query: ExtractWSQuery<T>;
19
+ headers: ExtractWSHeaders<T>;
20
+ context: unknown;
21
+ state: S;
22
+ }
23
+ /**
24
+ * Typed WebSocket wrapper for sending messages
25
+ */
26
+ export interface TypedServerWebSocket<T extends WebSocketContractDefinition, S = unknown> {
27
+ /** The underlying Bun WebSocket */
28
+ readonly raw: WebSocket;
29
+ /** Send a typed message to the client */
30
+ send<K extends keyof T['serverMessages']>(type: K, payload: z.infer<T['serverMessages'][K]['payload']>): void;
31
+ /** Subscribe to a topic for pub/sub */
32
+ subscribe(topic: string): void;
33
+ /** Unsubscribe from a topic */
34
+ unsubscribe(topic: string): void;
35
+ /** Publish a message to a topic */
36
+ publish<K extends keyof T['serverMessages']>(topic: string, type: K, payload: z.infer<T['serverMessages'][K]['payload']>): void;
37
+ /** Close the connection */
38
+ close(code?: number, reason?: string): void;
39
+ /** Connection data */
40
+ readonly data: WebSocketData<T, S>;
41
+ }
42
+ /**
43
+ * Handler functions for a WebSocket endpoint
44
+ */
45
+ export interface WebSocketEndpointHandlers<T extends WebSocketContractDefinition, C = unknown, S = unknown> {
46
+ /** Called when connection opens */
47
+ open?(ws: TypedServerWebSocket<T, S>, ctx: C): void | Promise<void>;
48
+ /** Called for each validated message */
49
+ message(ws: TypedServerWebSocket<T, S>, message: ExtractClientMessage<T>, ctx: C): void | Promise<void>;
50
+ /** Called when connection closes */
51
+ close?(ws: TypedServerWebSocket<T, S>, ctx: C): void;
52
+ /** Called when message validation fails */
53
+ validationError?(ws: TypedServerWebSocket<T, S>, error: WebSocketValidationError, ctx: C): void;
54
+ }
55
+ /**
56
+ * Contract handlers mapping for WebSocket endpoints
57
+ */
58
+ export type WebSocketContractHandlers<T extends WebSocketContract, C = unknown, S = unknown> = {
59
+ [K in keyof T]: WebSocketEndpointHandlers<T[K], C, S>;
60
+ };
61
+ /**
62
+ * Upgrade data returned by matchAndPrepareUpgrade
63
+ */
64
+ export interface UpgradeData<S = unknown> {
65
+ endpointName: string;
66
+ endpoint: WebSocketContractDefinition;
67
+ params: Record<string, string>;
68
+ query: Record<string, string | string[]>;
69
+ headers: Record<string, string>;
70
+ context: unknown;
71
+ state: S;
72
+ }
73
+ /**
74
+ * Options for WebSocket router
75
+ */
76
+ export interface WebSocketRouterOptions<C = unknown, S = unknown> {
77
+ basePath?: string;
78
+ context?: (request: Request, endpointName: string, endpoint: WebSocketContractDefinition) => C | Promise<C>;
79
+ /** Type hint for per-connection state. Use `{} as YourStateType` */
80
+ state?: S;
81
+ }
82
+ /**
83
+ * Bun WebSocket handler type (subset of Bun's types)
84
+ */
85
+ export interface BunWebSocketHandler<T = unknown> {
86
+ open(ws: Bun.ServerWebSocket<T>): void | Promise<void>;
87
+ message(ws: Bun.ServerWebSocket<T>, message: string | Buffer<ArrayBuffer>): void | Promise<void>;
88
+ close(ws: Bun.ServerWebSocket<T>, code: number, reason: string): void;
89
+ drain(ws: Bun.ServerWebSocket<T>): void;
90
+ }
91
+ /**
92
+ * WebSocket router for managing WebSocket contract endpoints
93
+ */
94
+ export declare class WebSocketRouter<T extends WebSocketContract, C = unknown, S = unknown> {
95
+ private contract;
96
+ private handlers;
97
+ private basePath;
98
+ private contextFactory?;
99
+ constructor(contract: T, handlers: WebSocketContractHandlers<T, C, S>, options?: WebSocketRouterOptions<C, S>);
100
+ /**
101
+ * Find matching endpoint for a path
102
+ */
103
+ private findEndpoint;
104
+ /**
105
+ * Parse and validate upgrade request parameters
106
+ */
107
+ private parseUpgradeParams;
108
+ /**
109
+ * Match a request and prepare upgrade data
110
+ * Returns null if no match, or UpgradeData for server.upgrade()
111
+ */
112
+ matchAndPrepareUpgrade(request: Request): Promise<UpgradeData<S> | null>;
113
+ /**
114
+ * Validate an incoming client message
115
+ */
116
+ private validateMessage;
117
+ /**
118
+ * Get Bun-compatible WebSocket handler
119
+ */
120
+ get websocketHandler(): BunWebSocketHandler<UpgradeData<S>>;
121
+ }
122
+ /**
123
+ * Create a WebSocket router from a contract and handlers
124
+ */
125
+ export declare function createWebSocketRouter<T extends WebSocketContract, C = unknown, S = unknown>(contract: T, handlers: WebSocketContractHandlers<T, C, S>, options?: WebSocketRouterOptions<C, S>): WebSocketRouter<T, C, S>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@richie-rpc/server",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "main": "./dist/cjs/index.cjs",
5
5
  "exports": {
6
6
  ".": {
@@ -10,7 +10,7 @@
10
10
  }
11
11
  },
12
12
  "peerDependencies": {
13
- "@richie-rpc/core": "^1.2.2",
13
+ "@richie-rpc/core": "^1.2.4",
14
14
  "typescript": "^5",
15
15
  "zod": "^4.1.12"
16
16
  },