@onebun/core 0.1.1 → 0.1.2

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,129 @@
1
+ /**
2
+ * WebSocket Client Types
3
+ *
4
+ * Type definitions for the typed WebSocket client.
5
+ */
6
+
7
+ import type { WsServiceDefinition, WsGatewayDefinition } from './ws-service-definition';
8
+
9
+ /**
10
+ * Options for WebSocket client
11
+ */
12
+ export interface WsClientOptions {
13
+ /** WebSocket server URL */
14
+ url: string;
15
+ /** Authentication options */
16
+ auth?: {
17
+ /** Bearer token */
18
+ token?: string;
19
+ /** Custom auth payload getter */
20
+ getAuth?: () => Record<string, unknown>;
21
+ };
22
+ /** Enable automatic reconnection */
23
+ reconnect?: boolean;
24
+ /** Reconnection interval in milliseconds */
25
+ reconnectInterval?: number;
26
+ /** Maximum reconnection attempts */
27
+ maxReconnectAttempts?: number;
28
+ /** Timeout for requests in milliseconds */
29
+ timeout?: number;
30
+ /** Socket.IO specific: transports to use */
31
+ transports?: ('websocket' | 'polling')[];
32
+ /** Namespace to connect to */
33
+ namespace?: string;
34
+ }
35
+
36
+ /**
37
+ * Connection state
38
+ */
39
+ export enum WsConnectionState {
40
+ DISCONNECTED = 'disconnected',
41
+ CONNECTING = 'connecting',
42
+ CONNECTED = 'connected',
43
+ RECONNECTING = 'reconnecting',
44
+ }
45
+
46
+ /**
47
+ * Event listener type
48
+ */
49
+ export type WsEventListener<T = unknown> = (data: T, params?: Record<string, string>) => void;
50
+
51
+ /**
52
+ * Client event types
53
+ */
54
+ export type WsClientEvent =
55
+ | 'connect'
56
+ | 'disconnect'
57
+ | 'error'
58
+ | 'reconnect'
59
+ | 'reconnect_attempt'
60
+ | 'reconnect_failed';
61
+
62
+ /**
63
+ * Client event listener types
64
+ */
65
+ export interface WsClientEventListeners {
66
+ connect: () => void;
67
+ disconnect: (reason: string) => void;
68
+ error: (error: Error) => void;
69
+ reconnect: (attempt: number) => void;
70
+ reconnect_attempt: (attempt: number) => void;
71
+ reconnect_failed: () => void;
72
+ }
73
+
74
+ /**
75
+ * Gateway client interface
76
+ */
77
+ export interface WsGatewayClient {
78
+ /** Send an event and wait for acknowledgement */
79
+ emit<T = unknown>(event: string, data?: unknown): Promise<T>;
80
+ /** Subscribe to events */
81
+ on<T = unknown>(event: string, listener: WsEventListener<T>): void;
82
+ /** Unsubscribe from events */
83
+ off(event: string, listener?: WsEventListener): void;
84
+ /** Send event without waiting for response */
85
+ send(event: string, data?: unknown): void;
86
+ }
87
+
88
+ /**
89
+ * Main WebSocket client interface
90
+ */
91
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
92
+ export interface WsClient<TDef extends WsServiceDefinition = WsServiceDefinition> {
93
+ /** Connect to WebSocket server */
94
+ connect(): Promise<void>;
95
+ /** Disconnect from WebSocket server */
96
+ disconnect(): void;
97
+ /** Check if connected */
98
+ isConnected(): boolean;
99
+ /** Get current connection state */
100
+ getState(): WsConnectionState;
101
+ /** Subscribe to client events */
102
+ on<E extends WsClientEvent>(event: E, listener: WsClientEventListeners[E]): void;
103
+ /** Unsubscribe from client events */
104
+ off<E extends WsClientEvent>(event: E, listener?: WsClientEventListeners[E]): void;
105
+ /** Access gateway by name */
106
+ [gatewayName: string]: WsGatewayClient | unknown;
107
+ }
108
+
109
+ /**
110
+ * Extract gateway names from service definition
111
+ */
112
+ export type ExtractGatewayNames<TDef extends WsServiceDefinition> =
113
+ TDef['_gateways'] extends Map<infer K, WsGatewayDefinition> ? K : never;
114
+
115
+ /**
116
+ * Typed service client
117
+ */
118
+ export type TypedWsClient<TDef extends WsServiceDefinition> = WsClient<TDef> & {
119
+ [K in ExtractGatewayNames<TDef> & string]: WsGatewayClient;
120
+ };
121
+
122
+ /**
123
+ * Pending request for acknowledgement
124
+ */
125
+ export interface PendingRequest {
126
+ resolve: (value: unknown) => void;
127
+ reject: (error: Error) => void;
128
+ timeout: ReturnType<typeof setTimeout>;
129
+ }
@@ -0,0 +1,331 @@
1
+ /**
2
+ * WebSocket Decorators Tests
3
+ */
4
+
5
+ import {
6
+ describe,
7
+ it,
8
+ expect,
9
+ } from 'bun:test';
10
+
11
+ import { BaseWebSocketGateway } from './ws-base-gateway';
12
+ import {
13
+ WebSocketGateway,
14
+ OnConnect,
15
+ OnDisconnect,
16
+ OnJoinRoom,
17
+ OnLeaveRoom,
18
+ OnMessage,
19
+ Client,
20
+ Socket,
21
+ MessageData,
22
+ RoomName,
23
+ PatternParams,
24
+ WsServer,
25
+ getGatewayMetadata,
26
+ isWebSocketGateway,
27
+ getWsHandlers,
28
+ getWsParamMetadata,
29
+ } from './ws-decorators';
30
+ import { WsHandlerType, WsParamType } from './ws.types';
31
+
32
+ describe('ws-decorators', () => {
33
+ describe('@WebSocketGateway', () => {
34
+ it('should mark class as WebSocket gateway', () => {
35
+ @WebSocketGateway()
36
+ class TestGateway extends BaseWebSocketGateway {}
37
+
38
+ expect(isWebSocketGateway(TestGateway)).toBe(true);
39
+ });
40
+
41
+ it('should store path option', () => {
42
+ @WebSocketGateway({ path: '/ws' })
43
+ class TestGateway extends BaseWebSocketGateway {}
44
+
45
+ const metadata = getGatewayMetadata(TestGateway);
46
+ expect(metadata?.path).toBe('/ws');
47
+ });
48
+
49
+ it('should store namespace option', () => {
50
+ @WebSocketGateway({ path: '/ws', namespace: 'chat' })
51
+ class TestGateway extends BaseWebSocketGateway {}
52
+
53
+ const metadata = getGatewayMetadata(TestGateway);
54
+ expect(metadata?.namespace).toBe('chat');
55
+ });
56
+
57
+ it('should use default path if not provided', () => {
58
+ @WebSocketGateway()
59
+ class TestGateway extends BaseWebSocketGateway {}
60
+
61
+ const metadata = getGatewayMetadata(TestGateway);
62
+ expect(metadata?.path).toBe('/');
63
+ });
64
+ });
65
+
66
+ describe('method decorators', () => {
67
+ describe('@OnConnect', () => {
68
+ it('should register connect handler', () => {
69
+ @WebSocketGateway()
70
+ class TestGateway extends BaseWebSocketGateway {
71
+ @OnConnect()
72
+ handleConnect() {}
73
+ }
74
+
75
+ const handlers = getWsHandlers(TestGateway);
76
+ const connectHandler = handlers.find((h) => h.type === WsHandlerType.CONNECT);
77
+
78
+ expect(connectHandler).toBeDefined();
79
+ expect(connectHandler?.handler).toBe('handleConnect');
80
+ });
81
+ });
82
+
83
+ describe('@OnDisconnect', () => {
84
+ it('should register disconnect handler', () => {
85
+ @WebSocketGateway()
86
+ class TestGateway extends BaseWebSocketGateway {
87
+ @OnDisconnect()
88
+ handleDisconnect() {}
89
+ }
90
+
91
+ const handlers = getWsHandlers(TestGateway);
92
+ const handler = handlers.find((h) => h.type === WsHandlerType.DISCONNECT);
93
+
94
+ expect(handler).toBeDefined();
95
+ expect(handler?.handler).toBe('handleDisconnect');
96
+ });
97
+ });
98
+
99
+ describe('@OnJoinRoom', () => {
100
+ it('should register join room handler without pattern', () => {
101
+ @WebSocketGateway()
102
+ class TestGateway extends BaseWebSocketGateway {
103
+ @OnJoinRoom()
104
+ handleJoin() {}
105
+ }
106
+
107
+ const handlers = getWsHandlers(TestGateway);
108
+ const handler = handlers.find((h) => h.type === WsHandlerType.JOIN_ROOM);
109
+
110
+ expect(handler).toBeDefined();
111
+ expect(handler?.pattern).toBeUndefined();
112
+ });
113
+
114
+ it('should register join room handler with pattern', () => {
115
+ @WebSocketGateway()
116
+ class TestGateway extends BaseWebSocketGateway {
117
+ @OnJoinRoom('room:{roomId}')
118
+ handleJoin() {}
119
+ }
120
+
121
+ const handlers = getWsHandlers(TestGateway);
122
+ const handler = handlers.find((h) => h.type === WsHandlerType.JOIN_ROOM);
123
+
124
+ expect(handler?.pattern).toBe('room:{roomId}');
125
+ });
126
+ });
127
+
128
+ describe('@OnLeaveRoom', () => {
129
+ it('should register leave room handler', () => {
130
+ @WebSocketGateway()
131
+ class TestGateway extends BaseWebSocketGateway {
132
+ @OnLeaveRoom('room:*')
133
+ handleLeave() {}
134
+ }
135
+
136
+ const handlers = getWsHandlers(TestGateway);
137
+ const handler = handlers.find((h) => h.type === WsHandlerType.LEAVE_ROOM);
138
+
139
+ expect(handler).toBeDefined();
140
+ expect(handler?.pattern).toBe('room:*');
141
+ });
142
+ });
143
+
144
+ describe('@OnMessage', () => {
145
+ it('should register message handler with pattern', () => {
146
+ @WebSocketGateway()
147
+ class TestGateway extends BaseWebSocketGateway {
148
+ @OnMessage('chat:message')
149
+ handleMessage() {}
150
+ }
151
+
152
+ const handlers = getWsHandlers(TestGateway);
153
+ const handler = handlers.find((h) => h.type === WsHandlerType.MESSAGE);
154
+
155
+ expect(handler).toBeDefined();
156
+ expect(handler?.pattern).toBe('chat:message');
157
+ });
158
+
159
+ it('should support wildcard patterns', () => {
160
+ @WebSocketGateway()
161
+ class TestGateway extends BaseWebSocketGateway {
162
+ @OnMessage('chat:*')
163
+ handleMessage() {}
164
+ }
165
+
166
+ const handlers = getWsHandlers(TestGateway);
167
+ const handler = handlers.find((h) => h.type === WsHandlerType.MESSAGE);
168
+
169
+ expect(handler?.pattern).toBe('chat:*');
170
+ });
171
+
172
+ it('should support parameterized patterns', () => {
173
+ @WebSocketGateway()
174
+ class TestGateway extends BaseWebSocketGateway {
175
+ @OnMessage('chat:{roomId}:message')
176
+ handleMessage() {}
177
+ }
178
+
179
+ const handlers = getWsHandlers(TestGateway);
180
+ const handler = handlers.find((h) => h.type === WsHandlerType.MESSAGE);
181
+
182
+ expect(handler?.pattern).toBe('chat:{roomId}:message');
183
+ });
184
+ });
185
+ });
186
+
187
+ describe('parameter decorators', () => {
188
+ it('should register @Client parameter', () => {
189
+ @WebSocketGateway()
190
+ class TestGateway extends BaseWebSocketGateway {
191
+ @OnMessage('test')
192
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
193
+ handleMessage(@Client() client: unknown) {}
194
+ }
195
+
196
+ const params = getWsParamMetadata(TestGateway.prototype, 'handleMessage');
197
+ const clientParam = params.find((p) => p.type === WsParamType.CLIENT);
198
+
199
+ expect(clientParam).toBeDefined();
200
+ expect(clientParam?.index).toBe(0);
201
+ });
202
+
203
+ it('should register @Socket parameter', () => {
204
+ @WebSocketGateway()
205
+ class TestGateway extends BaseWebSocketGateway {
206
+ @OnMessage('test')
207
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
208
+ handleMessage(@Socket() socket: unknown) {}
209
+ }
210
+
211
+ const params = getWsParamMetadata(TestGateway.prototype, 'handleMessage');
212
+ const socketParam = params.find((p) => p.type === WsParamType.SOCKET);
213
+
214
+ expect(socketParam).toBeDefined();
215
+ });
216
+
217
+ it('should register @MessageData parameter', () => {
218
+ @WebSocketGateway()
219
+ class TestGateway extends BaseWebSocketGateway {
220
+ @OnMessage('test')
221
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
222
+ handleMessage(@MessageData() data: unknown) {}
223
+ }
224
+
225
+ const params = getWsParamMetadata(TestGateway.prototype, 'handleMessage');
226
+ const dataParam = params.find((p) => p.type === WsParamType.MESSAGE_DATA);
227
+
228
+ expect(dataParam).toBeDefined();
229
+ });
230
+
231
+ it('should register @MessageData with property path', () => {
232
+ @WebSocketGateway()
233
+ class TestGateway extends BaseWebSocketGateway {
234
+ @OnMessage('test')
235
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
236
+ handleMessage(@MessageData('text') text: string) {}
237
+ }
238
+
239
+ const params = getWsParamMetadata(TestGateway.prototype, 'handleMessage');
240
+ const dataParam = params.find((p) => p.type === WsParamType.MESSAGE_DATA);
241
+
242
+ expect(dataParam?.property).toBe('text');
243
+ });
244
+
245
+ it('should register @RoomName parameter', () => {
246
+ @WebSocketGateway()
247
+ class TestGateway extends BaseWebSocketGateway {
248
+ @OnJoinRoom()
249
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
250
+ handleJoin(@RoomName() room: string) {}
251
+ }
252
+
253
+ const params = getWsParamMetadata(TestGateway.prototype, 'handleJoin');
254
+ const roomParam = params.find((p) => p.type === WsParamType.ROOM_NAME);
255
+
256
+ expect(roomParam).toBeDefined();
257
+ });
258
+
259
+ it('should register @PatternParams parameter', () => {
260
+ @WebSocketGateway()
261
+ class TestGateway extends BaseWebSocketGateway {
262
+ @OnMessage('chat:{roomId}')
263
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
264
+ handleMessage(@PatternParams() params: { roomId: string }) {}
265
+ }
266
+
267
+ const params = getWsParamMetadata(TestGateway.prototype, 'handleMessage');
268
+ const patternParam = params.find((p) => p.type === WsParamType.PATTERN_PARAMS);
269
+
270
+ expect(patternParam).toBeDefined();
271
+ });
272
+
273
+ it('should register @WsServer parameter', () => {
274
+ @WebSocketGateway()
275
+ class TestGateway extends BaseWebSocketGateway {
276
+ @OnMessage('test')
277
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
278
+ handleMessage(@WsServer() server: unknown) {}
279
+ }
280
+
281
+ const params = getWsParamMetadata(TestGateway.prototype, 'handleMessage');
282
+ const serverParam = params.find((p) => p.type === WsParamType.SERVER);
283
+
284
+ expect(serverParam).toBeDefined();
285
+ });
286
+
287
+ it('should register multiple parameters in correct order', () => {
288
+ @WebSocketGateway()
289
+ class TestGateway extends BaseWebSocketGateway {
290
+ @OnMessage('chat:{roomId}')
291
+ handleMessage(
292
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
293
+ @Client() client: unknown,
294
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
295
+ @MessageData() data: unknown,
296
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
297
+ @PatternParams() params: unknown,
298
+ ) {}
299
+ }
300
+
301
+ const params = getWsParamMetadata(TestGateway.prototype, 'handleMessage');
302
+
303
+ expect(params).toHaveLength(3);
304
+ expect(params.find((p) => p.index === 0)?.type).toBe(WsParamType.CLIENT);
305
+ expect(params.find((p) => p.index === 1)?.type).toBe(WsParamType.MESSAGE_DATA);
306
+ expect(params.find((p) => p.index === 2)?.type).toBe(WsParamType.PATTERN_PARAMS);
307
+ });
308
+ });
309
+
310
+ describe('multiple handlers', () => {
311
+ it('should support multiple handlers in one gateway', () => {
312
+ @WebSocketGateway()
313
+ class TestGateway extends BaseWebSocketGateway {
314
+ @OnConnect()
315
+ handleConnect() {}
316
+
317
+ @OnDisconnect()
318
+ handleDisconnect() {}
319
+
320
+ @OnMessage('chat:*')
321
+ handleChat() {}
322
+
323
+ @OnMessage('admin:*')
324
+ handleAdmin() {}
325
+ }
326
+
327
+ const handlers = getWsHandlers(TestGateway);
328
+ expect(handlers).toHaveLength(4);
329
+ });
330
+ });
331
+ });