@richie-rpc/client 1.2.4 → 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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@richie-rpc/client",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "type": "commonjs"
5
5
  }
@@ -0,0 +1,178 @@
1
+ // @bun @bun-cjs
2
+ (function(exports, require, module, __filename, __dirname) {var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __moduleCache = /* @__PURE__ */ new WeakMap;
7
+ var __toCommonJS = (from) => {
8
+ var entry = __moduleCache.get(from), desc;
9
+ if (entry)
10
+ return entry;
11
+ entry = __defProp({}, "__esModule", { value: true });
12
+ if (from && typeof from === "object" || typeof from === "function")
13
+ __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
14
+ get: () => from[key],
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ }));
17
+ __moduleCache.set(from, entry);
18
+ return entry;
19
+ };
20
+ var __export = (target, all) => {
21
+ for (var name in all)
22
+ __defProp(target, name, {
23
+ get: all[name],
24
+ enumerable: true,
25
+ configurable: true,
26
+ set: (newValue) => all[name] = () => newValue
27
+ });
28
+ };
29
+
30
+ // packages/client/websocket.ts
31
+ var exports_websocket = {};
32
+ __export(exports_websocket, {
33
+ createWebSocketClient: () => createWebSocketClient,
34
+ WebSocketClientValidationError: () => WebSocketClientValidationError
35
+ });
36
+ module.exports = __toCommonJS(exports_websocket);
37
+ var import_core = require("@richie-rpc/core");
38
+
39
+ class WebSocketClientValidationError extends Error {
40
+ messageType;
41
+ issues;
42
+ constructor(messageType, issues) {
43
+ super(`Validation failed for WebSocket message type: ${messageType}`);
44
+ this.messageType = messageType;
45
+ this.issues = issues;
46
+ this.name = "WebSocketClientValidationError";
47
+ }
48
+ }
49
+ function createTypedWebSocket(endpoint, url) {
50
+ let ws = null;
51
+ let isConnected = false;
52
+ const messageListeners = new Set;
53
+ const typedListeners = {};
54
+ const stateListeners = new Set;
55
+ const errorListeners = new Set;
56
+ for (const type of Object.keys(endpoint.serverMessages)) {
57
+ typedListeners[type] = new Set;
58
+ }
59
+ function notifyStateChange(connected) {
60
+ isConnected = connected;
61
+ stateListeners.forEach((h) => h(connected));
62
+ }
63
+ function notifyError(error) {
64
+ errorListeners.forEach((h) => h(error));
65
+ }
66
+ function handleMessage(event) {
67
+ try {
68
+ const message = JSON.parse(event.data);
69
+ messageListeners.forEach((h) => h(message));
70
+ const { type, payload } = message;
71
+ if (typedListeners[type]) {
72
+ typedListeners[type].forEach((h) => h(payload));
73
+ }
74
+ } catch (err) {
75
+ notifyError(new Error(`Failed to parse WebSocket message: ${err.message}`));
76
+ }
77
+ }
78
+ return {
79
+ connect() {
80
+ if (ws) {
81
+ return () => {
82
+ ws?.close();
83
+ ws = null;
84
+ };
85
+ }
86
+ ws = new WebSocket(url);
87
+ ws.onopen = () => {
88
+ notifyStateChange(true);
89
+ };
90
+ ws.onclose = () => {
91
+ notifyStateChange(false);
92
+ ws = null;
93
+ };
94
+ ws.onerror = () => {
95
+ notifyError(new Error("WebSocket connection error"));
96
+ };
97
+ ws.onmessage = handleMessage;
98
+ return () => {
99
+ ws?.close();
100
+ ws = null;
101
+ };
102
+ },
103
+ send(type, payload) {
104
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
105
+ throw new Error("WebSocket is not connected");
106
+ }
107
+ const messageDef = endpoint.clientMessages[type];
108
+ if (messageDef && messageDef.payload) {
109
+ const result = messageDef.payload.safeParse(payload);
110
+ if (!result.success) {
111
+ throw new WebSocketClientValidationError(type, result.error.issues);
112
+ }
113
+ }
114
+ ws.send(JSON.stringify({ type, payload }));
115
+ },
116
+ on(type, handler) {
117
+ const typeStr = type;
118
+ if (!typedListeners[typeStr]) {
119
+ typedListeners[typeStr] = new Set;
120
+ }
121
+ typedListeners[typeStr].add(handler);
122
+ return () => typedListeners[typeStr]?.delete(handler);
123
+ },
124
+ onMessage(handler) {
125
+ messageListeners.add(handler);
126
+ return () => messageListeners.delete(handler);
127
+ },
128
+ onStateChange(handler) {
129
+ stateListeners.add(handler);
130
+ return () => stateListeners.delete(handler);
131
+ },
132
+ onError(handler) {
133
+ errorListeners.add(handler);
134
+ return () => errorListeners.delete(handler);
135
+ },
136
+ get connected() {
137
+ return isConnected;
138
+ }
139
+ };
140
+ }
141
+ function resolveWebSocketUrl(baseUrl) {
142
+ if (baseUrl.startsWith("ws://") || baseUrl.startsWith("wss://")) {
143
+ return baseUrl;
144
+ }
145
+ if (baseUrl.startsWith("http://")) {
146
+ return `ws://${baseUrl.slice(7)}`;
147
+ }
148
+ if (baseUrl.startsWith("https://")) {
149
+ return `wss://${baseUrl.slice(8)}`;
150
+ }
151
+ if (baseUrl.startsWith("/")) {
152
+ const g = globalThis;
153
+ if (g?.location) {
154
+ const protocol = g.location.protocol === "https:" ? "wss:" : "ws:";
155
+ return `${protocol}//${g.location.host}${baseUrl}`;
156
+ }
157
+ return `ws://localhost${baseUrl}`;
158
+ }
159
+ return `ws://${baseUrl}`;
160
+ }
161
+ function createWebSocketClient(contract, config) {
162
+ const resolvedBaseUrl = resolveWebSocketUrl(config.baseUrl);
163
+ const client = {};
164
+ for (const [name, endpoint] of Object.entries(contract)) {
165
+ client[name] = (options = {}) => {
166
+ let path = endpoint.path;
167
+ if (options.params) {
168
+ path = import_core.interpolatePath(path, options.params);
169
+ }
170
+ const url = import_core.buildUrl(resolvedBaseUrl, path, options.query);
171
+ return createTypedWebSocket(endpoint, url);
172
+ };
173
+ }
174
+ return client;
175
+ }
176
+ })
177
+
178
+ //# debugId=15DE3AD0993419A564756E2164756E21
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../websocket.ts"],
4
+ "sourcesContent": [
5
+ "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport type {\n ExtractClientMessagePayload,\n ExtractServerMessage,\n ExtractServerMessagePayload,\n ExtractWSHeaders,\n ExtractWSParams,\n ExtractWSQuery,\n WebSocketContract,\n WebSocketContractDefinition,\n} from '@richie-rpc/core';\nimport { buildUrl, interpolatePath } from '@richie-rpc/core';\n\n/**\n * Validation error for WebSocket messages\n */\nexport class WebSocketClientValidationError extends Error {\n constructor(\n public messageType: string,\n public issues: unknown[],\n ) {\n super(`Validation failed for WebSocket message type: ${messageType}`);\n this.name = 'WebSocketClientValidationError';\n }\n}\n\n/**\n * Options for creating a WebSocket connection\n */\nexport type WebSocketConnectionOptions<T extends WebSocketContractDefinition> = {\n params?: ExtractWSParams<T> extends never ? never : ExtractWSParams<T>;\n query?: ExtractWSQuery<T> extends never ? never : ExtractWSQuery<T>;\n headers?: ExtractWSHeaders<T> extends never ? never : ExtractWSHeaders<T>;\n};\n\n/**\n * Typed WebSocket connection interface\n */\nexport interface TypedWebSocket<T extends WebSocketContractDefinition> {\n /** Connect to WebSocket server, returns disconnect function */\n connect(): () => void;\n\n /** Send a typed message (validates before sending) */\n send<K extends keyof T['clientMessages']>(\n type: K,\n payload: ExtractClientMessagePayload<T, K>,\n ): void;\n\n /** Subscribe to specific message type, returns unsubscribe function */\n on<K extends keyof T['serverMessages']>(\n type: K,\n handler: (payload: ExtractServerMessagePayload<T, K>) => void,\n ): () => void;\n\n /** Subscribe to all messages, returns unsubscribe function */\n onMessage(handler: (message: ExtractServerMessage<T>) => void): () => void;\n\n /** Subscribe to connection state changes */\n onStateChange(handler: (connected: boolean) => void): () => void;\n\n /** Subscribe to connection errors (network failures, etc.) */\n onError(handler: (error: Error) => void): () => void;\n\n /** Current connection state */\n readonly connected: boolean;\n}\n\n/**\n * WebSocket client type for a contract\n */\nexport type WebSocketClient<T extends WebSocketContract> = {\n [K in keyof T]: (options?: WebSocketConnectionOptions<T[K]>) => TypedWebSocket<T[K]>;\n};\n\n/**\n * WebSocket client configuration\n */\nexport interface WebSocketClientConfig {\n /** Base URL for WebSocket connections (ws:// or wss://) */\n baseUrl: string;\n}\n\n/**\n * Create a typed WebSocket connection for a specific endpoint\n */\nfunction createTypedWebSocket<T extends WebSocketContractDefinition>(\n endpoint: T,\n url: string,\n): TypedWebSocket<T> {\n let ws: WebSocket | null = null;\n let isConnected = false;\n\n type MessageHandler = (message: ExtractServerMessage<T>) => void;\n type TypedHandler<K extends keyof T['serverMessages']> = (\n payload: ExtractServerMessagePayload<T, K>,\n ) => void;\n type StateHandler = (connected: boolean) => void;\n type ErrorHandler = (error: Error) => void;\n\n const messageListeners = new Set<MessageHandler>();\n const typedListeners: Record<string, Set<TypedHandler<any>>> = {};\n const stateListeners = new Set<StateHandler>();\n const errorListeners = new Set<ErrorHandler>();\n\n // Initialize typed listener sets for each server message type\n for (const type of Object.keys(endpoint.serverMessages)) {\n typedListeners[type] = new Set();\n }\n\n function notifyStateChange(connected: boolean) {\n isConnected = connected;\n stateListeners.forEach((h) => h(connected));\n }\n\n function notifyError(error: Error) {\n errorListeners.forEach((h) => h(error));\n }\n\n function handleMessage(event: MessageEvent) {\n try {\n const message = JSON.parse(event.data) as ExtractServerMessage<T>;\n\n // Notify all-message listeners\n messageListeners.forEach((h) => h(message));\n\n // Notify type-specific listeners\n const { type, payload } = message as { type: string; payload: unknown };\n if (typedListeners[type]) {\n typedListeners[type].forEach((h) => h(payload as any));\n }\n } catch (err) {\n notifyError(new Error(`Failed to parse WebSocket message: ${(err as Error).message}`));\n }\n }\n\n return {\n connect() {\n if (ws) {\n // Already connected or connecting\n return () => {\n ws?.close();\n ws = null;\n };\n }\n\n ws = new WebSocket(url);\n\n ws.onopen = () => {\n notifyStateChange(true);\n };\n\n ws.onclose = () => {\n notifyStateChange(false);\n ws = null;\n };\n\n ws.onerror = () => {\n notifyError(new Error('WebSocket connection error'));\n };\n\n ws.onmessage = handleMessage;\n\n // Return disconnect function\n return () => {\n ws?.close();\n ws = null;\n };\n },\n\n send(type, payload) {\n if (!ws || ws.readyState !== WebSocket.OPEN) {\n throw new Error('WebSocket is not connected');\n }\n\n // Validate payload against schema\n const messageDef = endpoint.clientMessages[type as string];\n if (messageDef && messageDef.payload) {\n const result = messageDef.payload.safeParse(payload);\n if (!result.success) {\n throw new WebSocketClientValidationError(type as string, result.error.issues);\n }\n }\n\n // Send message\n ws.send(JSON.stringify({ type, payload }));\n },\n\n on(type, handler) {\n const typeStr = type as string;\n if (!typedListeners[typeStr]) {\n typedListeners[typeStr] = new Set();\n }\n typedListeners[typeStr].add(handler);\n return () => typedListeners[typeStr]?.delete(handler);\n },\n\n onMessage(handler) {\n messageListeners.add(handler);\n return () => messageListeners.delete(handler);\n },\n\n onStateChange(handler) {\n stateListeners.add(handler);\n return () => stateListeners.delete(handler);\n },\n\n onError(handler) {\n errorListeners.add(handler);\n return () => errorListeners.delete(handler);\n },\n\n get connected() {\n return isConnected;\n },\n };\n}\n\n/**\n * Resolve HTTP URL to WebSocket URL\n */\nfunction resolveWebSocketUrl(baseUrl: string): string {\n // If already a WebSocket URL, return as-is\n if (baseUrl.startsWith('ws://') || baseUrl.startsWith('wss://')) {\n return baseUrl;\n }\n\n // Convert http:// to ws:// and https:// to wss://\n if (baseUrl.startsWith('http://')) {\n return `ws://${baseUrl.slice(7)}`;\n }\n if (baseUrl.startsWith('https://')) {\n return `wss://${baseUrl.slice(8)}`;\n }\n\n // If relative URL, resolve using window.location\n if (baseUrl.startsWith('/')) {\n const g = globalThis as unknown as { location?: { protocol?: string; host?: string } };\n if (g?.location) {\n const protocol = g.location.protocol === 'https:' ? 'wss:' : 'ws:';\n return `${protocol}//${g.location.host}${baseUrl}`;\n }\n return `ws://localhost${baseUrl}`;\n }\n\n // Assume ws:// by default\n return `ws://${baseUrl}`;\n}\n\n/**\n * Create a typed WebSocket client for a contract\n *\n * @param contract - The WebSocket contract definition\n * @param config - Client configuration with baseUrl\n * @returns Client object with methods for each endpoint\n *\n * @example\n * ```typescript\n * const wsContract = defineWebSocketContract({\n * chat: {\n * path: '/ws/chat/:roomId',\n * params: z.object({ roomId: z.string() }),\n * clientMessages: {\n * sendMessage: { payload: z.object({ text: z.string() }) },\n * },\n * serverMessages: {\n * message: { payload: z.object({ userId: z.string(), text: z.string() }) },\n * },\n * },\n * });\n *\n * const wsClient = createWebSocketClient(wsContract, { baseUrl: 'ws://localhost:3000' });\n *\n * // Create connection instance\n * const chat = wsClient.chat({ params: { roomId: 'room1' } });\n *\n * // Connect and get disconnect function\n * const disconnect = chat.connect();\n *\n * // Subscribe to state changes\n * chat.onStateChange((connected) => {\n * console.log('Connected:', connected);\n * });\n *\n * // Subscribe to specific message types\n * chat.on('message', (payload) => {\n * console.log(`${payload.userId}: ${payload.text}`);\n * });\n *\n * // Send messages\n * chat.send('sendMessage', { text: 'Hello!' });\n *\n * // Disconnect when done\n * disconnect();\n * ```\n */\nexport function createWebSocketClient<T extends WebSocketContract>(\n contract: T,\n config: WebSocketClientConfig,\n): WebSocketClient<T> {\n const resolvedBaseUrl = resolveWebSocketUrl(config.baseUrl);\n\n const client: Record<string, unknown> = {};\n\n for (const [name, endpoint] of Object.entries(contract)) {\n client[name] = (options: WebSocketConnectionOptions<WebSocketContractDefinition> = {}) => {\n // Build URL\n let path = endpoint.path;\n if (options.params) {\n path = interpolatePath(path, options.params as Record<string, string | number>);\n }\n\n const url = buildUrl(\n resolvedBaseUrl,\n path,\n options.query as Record<string, string | number | boolean | string[]> | undefined,\n );\n\n return createTypedWebSocket(endpoint, url);\n };\n }\n\n return client as WebSocketClient<T>;\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "15DE3AD0993419A564756E2164756E21",
9
+ "names": []
10
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@richie-rpc/client",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "type": "module"
5
5
  }
@@ -0,0 +1,147 @@
1
+ // @bun
2
+ // packages/client/websocket.ts
3
+ import { buildUrl, interpolatePath } from "@richie-rpc/core";
4
+
5
+ class WebSocketClientValidationError 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 = "WebSocketClientValidationError";
13
+ }
14
+ }
15
+ function createTypedWebSocket(endpoint, url) {
16
+ let ws = null;
17
+ let isConnected = false;
18
+ const messageListeners = new Set;
19
+ const typedListeners = {};
20
+ const stateListeners = new Set;
21
+ const errorListeners = new Set;
22
+ for (const type of Object.keys(endpoint.serverMessages)) {
23
+ typedListeners[type] = new Set;
24
+ }
25
+ function notifyStateChange(connected) {
26
+ isConnected = connected;
27
+ stateListeners.forEach((h) => h(connected));
28
+ }
29
+ function notifyError(error) {
30
+ errorListeners.forEach((h) => h(error));
31
+ }
32
+ function handleMessage(event) {
33
+ try {
34
+ const message = JSON.parse(event.data);
35
+ messageListeners.forEach((h) => h(message));
36
+ const { type, payload } = message;
37
+ if (typedListeners[type]) {
38
+ typedListeners[type].forEach((h) => h(payload));
39
+ }
40
+ } catch (err) {
41
+ notifyError(new Error(`Failed to parse WebSocket message: ${err.message}`));
42
+ }
43
+ }
44
+ return {
45
+ connect() {
46
+ if (ws) {
47
+ return () => {
48
+ ws?.close();
49
+ ws = null;
50
+ };
51
+ }
52
+ ws = new WebSocket(url);
53
+ ws.onopen = () => {
54
+ notifyStateChange(true);
55
+ };
56
+ ws.onclose = () => {
57
+ notifyStateChange(false);
58
+ ws = null;
59
+ };
60
+ ws.onerror = () => {
61
+ notifyError(new Error("WebSocket connection error"));
62
+ };
63
+ ws.onmessage = handleMessage;
64
+ return () => {
65
+ ws?.close();
66
+ ws = null;
67
+ };
68
+ },
69
+ send(type, payload) {
70
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
71
+ throw new Error("WebSocket is not connected");
72
+ }
73
+ const messageDef = endpoint.clientMessages[type];
74
+ if (messageDef && messageDef.payload) {
75
+ const result = messageDef.payload.safeParse(payload);
76
+ if (!result.success) {
77
+ throw new WebSocketClientValidationError(type, result.error.issues);
78
+ }
79
+ }
80
+ ws.send(JSON.stringify({ type, payload }));
81
+ },
82
+ on(type, handler) {
83
+ const typeStr = type;
84
+ if (!typedListeners[typeStr]) {
85
+ typedListeners[typeStr] = new Set;
86
+ }
87
+ typedListeners[typeStr].add(handler);
88
+ return () => typedListeners[typeStr]?.delete(handler);
89
+ },
90
+ onMessage(handler) {
91
+ messageListeners.add(handler);
92
+ return () => messageListeners.delete(handler);
93
+ },
94
+ onStateChange(handler) {
95
+ stateListeners.add(handler);
96
+ return () => stateListeners.delete(handler);
97
+ },
98
+ onError(handler) {
99
+ errorListeners.add(handler);
100
+ return () => errorListeners.delete(handler);
101
+ },
102
+ get connected() {
103
+ return isConnected;
104
+ }
105
+ };
106
+ }
107
+ function resolveWebSocketUrl(baseUrl) {
108
+ if (baseUrl.startsWith("ws://") || baseUrl.startsWith("wss://")) {
109
+ return baseUrl;
110
+ }
111
+ if (baseUrl.startsWith("http://")) {
112
+ return `ws://${baseUrl.slice(7)}`;
113
+ }
114
+ if (baseUrl.startsWith("https://")) {
115
+ return `wss://${baseUrl.slice(8)}`;
116
+ }
117
+ if (baseUrl.startsWith("/")) {
118
+ const g = globalThis;
119
+ if (g?.location) {
120
+ const protocol = g.location.protocol === "https:" ? "wss:" : "ws:";
121
+ return `${protocol}//${g.location.host}${baseUrl}`;
122
+ }
123
+ return `ws://localhost${baseUrl}`;
124
+ }
125
+ return `ws://${baseUrl}`;
126
+ }
127
+ function createWebSocketClient(contract, config) {
128
+ const resolvedBaseUrl = resolveWebSocketUrl(config.baseUrl);
129
+ const client = {};
130
+ for (const [name, endpoint] of Object.entries(contract)) {
131
+ client[name] = (options = {}) => {
132
+ let path = endpoint.path;
133
+ if (options.params) {
134
+ path = interpolatePath(path, options.params);
135
+ }
136
+ const url = buildUrl(resolvedBaseUrl, path, options.query);
137
+ return createTypedWebSocket(endpoint, url);
138
+ };
139
+ }
140
+ return client;
141
+ }
142
+ export {
143
+ createWebSocketClient,
144
+ WebSocketClientValidationError
145
+ };
146
+
147
+ //# debugId=0C267D8CB4E1D91364756E2164756E21
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../websocket.ts"],
4
+ "sourcesContent": [
5
+ "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport type {\n ExtractClientMessagePayload,\n ExtractServerMessage,\n ExtractServerMessagePayload,\n ExtractWSHeaders,\n ExtractWSParams,\n ExtractWSQuery,\n WebSocketContract,\n WebSocketContractDefinition,\n} from '@richie-rpc/core';\nimport { buildUrl, interpolatePath } from '@richie-rpc/core';\n\n/**\n * Validation error for WebSocket messages\n */\nexport class WebSocketClientValidationError extends Error {\n constructor(\n public messageType: string,\n public issues: unknown[],\n ) {\n super(`Validation failed for WebSocket message type: ${messageType}`);\n this.name = 'WebSocketClientValidationError';\n }\n}\n\n/**\n * Options for creating a WebSocket connection\n */\nexport type WebSocketConnectionOptions<T extends WebSocketContractDefinition> = {\n params?: ExtractWSParams<T> extends never ? never : ExtractWSParams<T>;\n query?: ExtractWSQuery<T> extends never ? never : ExtractWSQuery<T>;\n headers?: ExtractWSHeaders<T> extends never ? never : ExtractWSHeaders<T>;\n};\n\n/**\n * Typed WebSocket connection interface\n */\nexport interface TypedWebSocket<T extends WebSocketContractDefinition> {\n /** Connect to WebSocket server, returns disconnect function */\n connect(): () => void;\n\n /** Send a typed message (validates before sending) */\n send<K extends keyof T['clientMessages']>(\n type: K,\n payload: ExtractClientMessagePayload<T, K>,\n ): void;\n\n /** Subscribe to specific message type, returns unsubscribe function */\n on<K extends keyof T['serverMessages']>(\n type: K,\n handler: (payload: ExtractServerMessagePayload<T, K>) => void,\n ): () => void;\n\n /** Subscribe to all messages, returns unsubscribe function */\n onMessage(handler: (message: ExtractServerMessage<T>) => void): () => void;\n\n /** Subscribe to connection state changes */\n onStateChange(handler: (connected: boolean) => void): () => void;\n\n /** Subscribe to connection errors (network failures, etc.) */\n onError(handler: (error: Error) => void): () => void;\n\n /** Current connection state */\n readonly connected: boolean;\n}\n\n/**\n * WebSocket client type for a contract\n */\nexport type WebSocketClient<T extends WebSocketContract> = {\n [K in keyof T]: (options?: WebSocketConnectionOptions<T[K]>) => TypedWebSocket<T[K]>;\n};\n\n/**\n * WebSocket client configuration\n */\nexport interface WebSocketClientConfig {\n /** Base URL for WebSocket connections (ws:// or wss://) */\n baseUrl: string;\n}\n\n/**\n * Create a typed WebSocket connection for a specific endpoint\n */\nfunction createTypedWebSocket<T extends WebSocketContractDefinition>(\n endpoint: T,\n url: string,\n): TypedWebSocket<T> {\n let ws: WebSocket | null = null;\n let isConnected = false;\n\n type MessageHandler = (message: ExtractServerMessage<T>) => void;\n type TypedHandler<K extends keyof T['serverMessages']> = (\n payload: ExtractServerMessagePayload<T, K>,\n ) => void;\n type StateHandler = (connected: boolean) => void;\n type ErrorHandler = (error: Error) => void;\n\n const messageListeners = new Set<MessageHandler>();\n const typedListeners: Record<string, Set<TypedHandler<any>>> = {};\n const stateListeners = new Set<StateHandler>();\n const errorListeners = new Set<ErrorHandler>();\n\n // Initialize typed listener sets for each server message type\n for (const type of Object.keys(endpoint.serverMessages)) {\n typedListeners[type] = new Set();\n }\n\n function notifyStateChange(connected: boolean) {\n isConnected = connected;\n stateListeners.forEach((h) => h(connected));\n }\n\n function notifyError(error: Error) {\n errorListeners.forEach((h) => h(error));\n }\n\n function handleMessage(event: MessageEvent) {\n try {\n const message = JSON.parse(event.data) as ExtractServerMessage<T>;\n\n // Notify all-message listeners\n messageListeners.forEach((h) => h(message));\n\n // Notify type-specific listeners\n const { type, payload } = message as { type: string; payload: unknown };\n if (typedListeners[type]) {\n typedListeners[type].forEach((h) => h(payload as any));\n }\n } catch (err) {\n notifyError(new Error(`Failed to parse WebSocket message: ${(err as Error).message}`));\n }\n }\n\n return {\n connect() {\n if (ws) {\n // Already connected or connecting\n return () => {\n ws?.close();\n ws = null;\n };\n }\n\n ws = new WebSocket(url);\n\n ws.onopen = () => {\n notifyStateChange(true);\n };\n\n ws.onclose = () => {\n notifyStateChange(false);\n ws = null;\n };\n\n ws.onerror = () => {\n notifyError(new Error('WebSocket connection error'));\n };\n\n ws.onmessage = handleMessage;\n\n // Return disconnect function\n return () => {\n ws?.close();\n ws = null;\n };\n },\n\n send(type, payload) {\n if (!ws || ws.readyState !== WebSocket.OPEN) {\n throw new Error('WebSocket is not connected');\n }\n\n // Validate payload against schema\n const messageDef = endpoint.clientMessages[type as string];\n if (messageDef && messageDef.payload) {\n const result = messageDef.payload.safeParse(payload);\n if (!result.success) {\n throw new WebSocketClientValidationError(type as string, result.error.issues);\n }\n }\n\n // Send message\n ws.send(JSON.stringify({ type, payload }));\n },\n\n on(type, handler) {\n const typeStr = type as string;\n if (!typedListeners[typeStr]) {\n typedListeners[typeStr] = new Set();\n }\n typedListeners[typeStr].add(handler);\n return () => typedListeners[typeStr]?.delete(handler);\n },\n\n onMessage(handler) {\n messageListeners.add(handler);\n return () => messageListeners.delete(handler);\n },\n\n onStateChange(handler) {\n stateListeners.add(handler);\n return () => stateListeners.delete(handler);\n },\n\n onError(handler) {\n errorListeners.add(handler);\n return () => errorListeners.delete(handler);\n },\n\n get connected() {\n return isConnected;\n },\n };\n}\n\n/**\n * Resolve HTTP URL to WebSocket URL\n */\nfunction resolveWebSocketUrl(baseUrl: string): string {\n // If already a WebSocket URL, return as-is\n if (baseUrl.startsWith('ws://') || baseUrl.startsWith('wss://')) {\n return baseUrl;\n }\n\n // Convert http:// to ws:// and https:// to wss://\n if (baseUrl.startsWith('http://')) {\n return `ws://${baseUrl.slice(7)}`;\n }\n if (baseUrl.startsWith('https://')) {\n return `wss://${baseUrl.slice(8)}`;\n }\n\n // If relative URL, resolve using window.location\n if (baseUrl.startsWith('/')) {\n const g = globalThis as unknown as { location?: { protocol?: string; host?: string } };\n if (g?.location) {\n const protocol = g.location.protocol === 'https:' ? 'wss:' : 'ws:';\n return `${protocol}//${g.location.host}${baseUrl}`;\n }\n return `ws://localhost${baseUrl}`;\n }\n\n // Assume ws:// by default\n return `ws://${baseUrl}`;\n}\n\n/**\n * Create a typed WebSocket client for a contract\n *\n * @param contract - The WebSocket contract definition\n * @param config - Client configuration with baseUrl\n * @returns Client object with methods for each endpoint\n *\n * @example\n * ```typescript\n * const wsContract = defineWebSocketContract({\n * chat: {\n * path: '/ws/chat/:roomId',\n * params: z.object({ roomId: z.string() }),\n * clientMessages: {\n * sendMessage: { payload: z.object({ text: z.string() }) },\n * },\n * serverMessages: {\n * message: { payload: z.object({ userId: z.string(), text: z.string() }) },\n * },\n * },\n * });\n *\n * const wsClient = createWebSocketClient(wsContract, { baseUrl: 'ws://localhost:3000' });\n *\n * // Create connection instance\n * const chat = wsClient.chat({ params: { roomId: 'room1' } });\n *\n * // Connect and get disconnect function\n * const disconnect = chat.connect();\n *\n * // Subscribe to state changes\n * chat.onStateChange((connected) => {\n * console.log('Connected:', connected);\n * });\n *\n * // Subscribe to specific message types\n * chat.on('message', (payload) => {\n * console.log(`${payload.userId}: ${payload.text}`);\n * });\n *\n * // Send messages\n * chat.send('sendMessage', { text: 'Hello!' });\n *\n * // Disconnect when done\n * disconnect();\n * ```\n */\nexport function createWebSocketClient<T extends WebSocketContract>(\n contract: T,\n config: WebSocketClientConfig,\n): WebSocketClient<T> {\n const resolvedBaseUrl = resolveWebSocketUrl(config.baseUrl);\n\n const client: Record<string, unknown> = {};\n\n for (const [name, endpoint] of Object.entries(contract)) {\n client[name] = (options: WebSocketConnectionOptions<WebSocketContractDefinition> = {}) => {\n // Build URL\n let path = endpoint.path;\n if (options.params) {\n path = interpolatePath(path, options.params as Record<string, string | number>);\n }\n\n const url = buildUrl(\n resolvedBaseUrl,\n path,\n options.query as Record<string, string | number | boolean | string[]> | undefined,\n );\n\n return createTypedWebSocket(endpoint, url);\n };\n }\n\n return client as WebSocketClient<T>;\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "0C267D8CB4E1D91364756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,97 @@
1
+ import type { ExtractClientMessagePayload, ExtractServerMessage, ExtractServerMessagePayload, ExtractWSHeaders, ExtractWSParams, ExtractWSQuery, WebSocketContract, WebSocketContractDefinition } from '@richie-rpc/core';
2
+ /**
3
+ * Validation error for WebSocket messages
4
+ */
5
+ export declare class WebSocketClientValidationError extends Error {
6
+ messageType: string;
7
+ issues: unknown[];
8
+ constructor(messageType: string, issues: unknown[]);
9
+ }
10
+ /**
11
+ * Options for creating a WebSocket connection
12
+ */
13
+ export type WebSocketConnectionOptions<T extends WebSocketContractDefinition> = {
14
+ params?: ExtractWSParams<T> extends never ? never : ExtractWSParams<T>;
15
+ query?: ExtractWSQuery<T> extends never ? never : ExtractWSQuery<T>;
16
+ headers?: ExtractWSHeaders<T> extends never ? never : ExtractWSHeaders<T>;
17
+ };
18
+ /**
19
+ * Typed WebSocket connection interface
20
+ */
21
+ export interface TypedWebSocket<T extends WebSocketContractDefinition> {
22
+ /** Connect to WebSocket server, returns disconnect function */
23
+ connect(): () => void;
24
+ /** Send a typed message (validates before sending) */
25
+ send<K extends keyof T['clientMessages']>(type: K, payload: ExtractClientMessagePayload<T, K>): void;
26
+ /** Subscribe to specific message type, returns unsubscribe function */
27
+ on<K extends keyof T['serverMessages']>(type: K, handler: (payload: ExtractServerMessagePayload<T, K>) => void): () => void;
28
+ /** Subscribe to all messages, returns unsubscribe function */
29
+ onMessage(handler: (message: ExtractServerMessage<T>) => void): () => void;
30
+ /** Subscribe to connection state changes */
31
+ onStateChange(handler: (connected: boolean) => void): () => void;
32
+ /** Subscribe to connection errors (network failures, etc.) */
33
+ onError(handler: (error: Error) => void): () => void;
34
+ /** Current connection state */
35
+ readonly connected: boolean;
36
+ }
37
+ /**
38
+ * WebSocket client type for a contract
39
+ */
40
+ export type WebSocketClient<T extends WebSocketContract> = {
41
+ [K in keyof T]: (options?: WebSocketConnectionOptions<T[K]>) => TypedWebSocket<T[K]>;
42
+ };
43
+ /**
44
+ * WebSocket client configuration
45
+ */
46
+ export interface WebSocketClientConfig {
47
+ /** Base URL for WebSocket connections (ws:// or wss://) */
48
+ baseUrl: string;
49
+ }
50
+ /**
51
+ * Create a typed WebSocket client for a contract
52
+ *
53
+ * @param contract - The WebSocket contract definition
54
+ * @param config - Client configuration with baseUrl
55
+ * @returns Client object with methods for each endpoint
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * const wsContract = defineWebSocketContract({
60
+ * chat: {
61
+ * path: '/ws/chat/:roomId',
62
+ * params: z.object({ roomId: z.string() }),
63
+ * clientMessages: {
64
+ * sendMessage: { payload: z.object({ text: z.string() }) },
65
+ * },
66
+ * serverMessages: {
67
+ * message: { payload: z.object({ userId: z.string(), text: z.string() }) },
68
+ * },
69
+ * },
70
+ * });
71
+ *
72
+ * const wsClient = createWebSocketClient(wsContract, { baseUrl: 'ws://localhost:3000' });
73
+ *
74
+ * // Create connection instance
75
+ * const chat = wsClient.chat({ params: { roomId: 'room1' } });
76
+ *
77
+ * // Connect and get disconnect function
78
+ * const disconnect = chat.connect();
79
+ *
80
+ * // Subscribe to state changes
81
+ * chat.onStateChange((connected) => {
82
+ * console.log('Connected:', connected);
83
+ * });
84
+ *
85
+ * // Subscribe to specific message types
86
+ * chat.on('message', (payload) => {
87
+ * console.log(`${payload.userId}: ${payload.text}`);
88
+ * });
89
+ *
90
+ * // Send messages
91
+ * chat.send('sendMessage', { text: 'Hello!' });
92
+ *
93
+ * // Disconnect when done
94
+ * disconnect();
95
+ * ```
96
+ */
97
+ export declare function createWebSocketClient<T extends WebSocketContract>(contract: T, config: WebSocketClientConfig): WebSocketClient<T>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@richie-rpc/client",
3
- "version": "1.2.4",
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.3",
13
+ "@richie-rpc/core": "^1.2.4",
14
14
  "typescript": "^5",
15
15
  "zod": "^4.1.12"
16
16
  },