@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.
- package/README.md +233 -0
- package/package.json +1 -1
- package/src/application.test.ts +119 -0
- package/src/application.ts +112 -5
- package/src/docs-examples.test.ts +753 -0
- package/src/index.ts +96 -0
- package/src/module.ts +10 -4
- package/src/redis-client.ts +502 -0
- package/src/shared-redis.ts +231 -0
- package/src/types.ts +50 -0
- package/src/ws-base-gateway.test.ts +479 -0
- package/src/ws-base-gateway.ts +514 -0
- package/src/ws-client.test.ts +511 -0
- package/src/ws-client.ts +628 -0
- package/src/ws-client.types.ts +129 -0
- package/src/ws-decorators.test.ts +331 -0
- package/src/ws-decorators.ts +417 -0
- package/src/ws-guards.test.ts +334 -0
- package/src/ws-guards.ts +298 -0
- package/src/ws-handler.ts +658 -0
- package/src/ws-integration.test.ts +517 -0
- package/src/ws-pattern-matcher.test.ts +152 -0
- package/src/ws-pattern-matcher.ts +240 -0
- package/src/ws-service-definition.ts +223 -0
- package/src/ws-socketio-protocol.test.ts +344 -0
- package/src/ws-socketio-protocol.ts +567 -0
- package/src/ws-storage-memory.test.ts +246 -0
- package/src/ws-storage-memory.ts +222 -0
- package/src/ws-storage-redis.ts +302 -0
- package/src/ws-storage.ts +210 -0
- package/src/ws.types.ts +342 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for ws-client.ts
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
describe,
|
|
7
|
+
it,
|
|
8
|
+
expect,
|
|
9
|
+
beforeEach,
|
|
10
|
+
afterEach,
|
|
11
|
+
mock,
|
|
12
|
+
} from 'bun:test';
|
|
13
|
+
|
|
14
|
+
import type { WsServiceDefinition } from './ws-service-definition';
|
|
15
|
+
|
|
16
|
+
import { createWsClient } from './ws-client';
|
|
17
|
+
import { WsConnectionState } from './ws-client.types';
|
|
18
|
+
import { WsHandlerType } from './ws.types';
|
|
19
|
+
|
|
20
|
+
// Mock WebSocket
|
|
21
|
+
class MockWebSocket {
|
|
22
|
+
static instances: MockWebSocket[] = [];
|
|
23
|
+
|
|
24
|
+
readyState = 1; // OPEN
|
|
25
|
+
onopen: ((event: Event) => void) | null = null;
|
|
26
|
+
onmessage: ((event: MessageEvent) => void) | null = null;
|
|
27
|
+
onclose: ((event: CloseEvent) => void) | null = null;
|
|
28
|
+
onerror: ((event: Event) => void) | null = null;
|
|
29
|
+
|
|
30
|
+
sentMessages: string[] = [];
|
|
31
|
+
|
|
32
|
+
constructor(public url: string) {
|
|
33
|
+
MockWebSocket.instances.push(this);
|
|
34
|
+
// Simulate async open
|
|
35
|
+
setTimeout(() => {
|
|
36
|
+
this.onopen?.(new Event('open'));
|
|
37
|
+
}, 0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
send(data: string): void {
|
|
41
|
+
this.sentMessages.push(data);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
close(code?: number, reason?: string): void {
|
|
45
|
+
this.onclose?.(new CloseEvent('close', { code: code || 1000, reason: reason || '' }));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Simulate receiving a message
|
|
49
|
+
receiveMessage(data: string): void {
|
|
50
|
+
this.onmessage?.(new MessageEvent('message', { data }));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Simulate error
|
|
54
|
+
triggerError(): void {
|
|
55
|
+
this.onerror?.(new Event('error'));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static reset(): void {
|
|
59
|
+
MockWebSocket.instances = [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static getLastInstance(): MockWebSocket | undefined {
|
|
63
|
+
return MockWebSocket.instances[MockWebSocket.instances.length - 1];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Mock service definition
|
|
68
|
+
function createMockDefinition(): WsServiceDefinition {
|
|
69
|
+
return {
|
|
70
|
+
_module: class TestModule {},
|
|
71
|
+
_endpoints: [],
|
|
72
|
+
_gateways: new Map([
|
|
73
|
+
['TestGateway', {
|
|
74
|
+
name: 'TestGateway',
|
|
75
|
+
path: '/ws',
|
|
76
|
+
namespace: undefined,
|
|
77
|
+
events: new Map([
|
|
78
|
+
['test:event', {
|
|
79
|
+
gateway: 'TestGateway',
|
|
80
|
+
event: 'test:event',
|
|
81
|
+
handler: 'handleTestEvent',
|
|
82
|
+
type: WsHandlerType.MESSAGE,
|
|
83
|
+
}],
|
|
84
|
+
['chat:*', {
|
|
85
|
+
gateway: 'TestGateway',
|
|
86
|
+
event: 'chat:*',
|
|
87
|
+
handler: 'handleChat',
|
|
88
|
+
type: WsHandlerType.MESSAGE,
|
|
89
|
+
}],
|
|
90
|
+
]),
|
|
91
|
+
}],
|
|
92
|
+
]),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe('WsClient', () => {
|
|
97
|
+
let originalWebSocket: typeof globalThis.WebSocket;
|
|
98
|
+
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
MockWebSocket.reset();
|
|
101
|
+
originalWebSocket = globalThis.WebSocket;
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
103
|
+
globalThis.WebSocket = MockWebSocket as any;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
afterEach(() => {
|
|
107
|
+
globalThis.WebSocket = originalWebSocket;
|
|
108
|
+
MockWebSocket.reset();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('createWsClient', () => {
|
|
112
|
+
it('should create client with default options', () => {
|
|
113
|
+
const definition = createMockDefinition();
|
|
114
|
+
const client = createWsClient(definition, { url: 'ws://localhost:3000' });
|
|
115
|
+
|
|
116
|
+
expect(client).toBeDefined();
|
|
117
|
+
expect(typeof client.connect).toBe('function');
|
|
118
|
+
expect(typeof client.disconnect).toBe('function');
|
|
119
|
+
expect(typeof client.isConnected).toBe('function');
|
|
120
|
+
expect(typeof client.getState).toBe('function');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('connect', () => {
|
|
125
|
+
it('should connect to WebSocket server', async () => {
|
|
126
|
+
const definition = createMockDefinition();
|
|
127
|
+
const client = createWsClient(definition, { url: 'ws://localhost:3000' });
|
|
128
|
+
|
|
129
|
+
await client.connect();
|
|
130
|
+
|
|
131
|
+
expect(client.isConnected()).toBe(true);
|
|
132
|
+
expect(client.getState()).toBe(WsConnectionState.CONNECTED);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should include auth token in URL', async () => {
|
|
136
|
+
const definition = createMockDefinition();
|
|
137
|
+
const client = createWsClient(definition, {
|
|
138
|
+
url: 'ws://localhost:3000',
|
|
139
|
+
auth: { token: 'test-token' },
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await client.connect();
|
|
143
|
+
|
|
144
|
+
const ws = MockWebSocket.getLastInstance();
|
|
145
|
+
expect(ws?.url).toContain('token=test-token');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should include namespace in URL', async () => {
|
|
149
|
+
const definition = createMockDefinition();
|
|
150
|
+
const client = createWsClient(definition, {
|
|
151
|
+
url: 'ws://localhost:3000',
|
|
152
|
+
namespace: 'chat',
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await client.connect();
|
|
156
|
+
|
|
157
|
+
const ws = MockWebSocket.getLastInstance();
|
|
158
|
+
expect(ws?.url).toContain('namespace=chat');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should return immediately if already connected', async () => {
|
|
162
|
+
const definition = createMockDefinition();
|
|
163
|
+
const client = createWsClient(definition, { url: 'ws://localhost:3000' });
|
|
164
|
+
|
|
165
|
+
await client.connect();
|
|
166
|
+
const instanceCount = MockWebSocket.instances.length;
|
|
167
|
+
|
|
168
|
+
await client.connect();
|
|
169
|
+
|
|
170
|
+
// Should not create a new WebSocket
|
|
171
|
+
expect(MockWebSocket.instances.length).toBe(instanceCount);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should emit connect event', async () => {
|
|
175
|
+
const definition = createMockDefinition();
|
|
176
|
+
const client = createWsClient(definition, { url: 'ws://localhost:3000' });
|
|
177
|
+
const connectHandler = mock(() => undefined);
|
|
178
|
+
|
|
179
|
+
client.on('connect', connectHandler);
|
|
180
|
+
await client.connect();
|
|
181
|
+
|
|
182
|
+
expect(connectHandler).toHaveBeenCalledTimes(1);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('disconnect', () => {
|
|
187
|
+
it('should disconnect from server', async () => {
|
|
188
|
+
const definition = createMockDefinition();
|
|
189
|
+
const client = createWsClient(definition, { url: 'ws://localhost:3000' });
|
|
190
|
+
|
|
191
|
+
await client.connect();
|
|
192
|
+
client.disconnect();
|
|
193
|
+
|
|
194
|
+
expect(client.isConnected()).toBe(false);
|
|
195
|
+
expect(client.getState()).toBe(WsConnectionState.DISCONNECTED);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should emit disconnect event', async () => {
|
|
199
|
+
const definition = createMockDefinition();
|
|
200
|
+
const client = createWsClient(definition, { url: 'ws://localhost:3000' });
|
|
201
|
+
const disconnectHandler = mock(() => undefined);
|
|
202
|
+
|
|
203
|
+
await client.connect();
|
|
204
|
+
client.on('disconnect', disconnectHandler);
|
|
205
|
+
client.disconnect();
|
|
206
|
+
|
|
207
|
+
expect(disconnectHandler).toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('event listeners', () => {
|
|
212
|
+
it('should subscribe to client events', async () => {
|
|
213
|
+
const definition = createMockDefinition();
|
|
214
|
+
const client = createWsClient(definition, { url: 'ws://localhost:3000' });
|
|
215
|
+
const handler = mock(() => undefined);
|
|
216
|
+
|
|
217
|
+
client.on('connect', handler);
|
|
218
|
+
await client.connect();
|
|
219
|
+
|
|
220
|
+
expect(handler).toHaveBeenCalled();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should unsubscribe specific listener', async () => {
|
|
224
|
+
const definition = createMockDefinition();
|
|
225
|
+
const client = createWsClient(definition, { url: 'ws://localhost:3000' });
|
|
226
|
+
const handler = mock(() => undefined);
|
|
227
|
+
|
|
228
|
+
client.on('connect', handler);
|
|
229
|
+
client.off('connect', handler);
|
|
230
|
+
await client.connect();
|
|
231
|
+
|
|
232
|
+
expect(handler).not.toHaveBeenCalled();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should unsubscribe all listeners for event', async () => {
|
|
236
|
+
const definition = createMockDefinition();
|
|
237
|
+
const client = createWsClient(definition, { url: 'ws://localhost:3000' });
|
|
238
|
+
const handler1 = mock(() => undefined);
|
|
239
|
+
const handler2 = mock(() => undefined);
|
|
240
|
+
|
|
241
|
+
client.on('connect', handler1);
|
|
242
|
+
client.on('connect', handler2);
|
|
243
|
+
client.off('connect');
|
|
244
|
+
await client.connect();
|
|
245
|
+
|
|
246
|
+
expect(handler1).not.toHaveBeenCalled();
|
|
247
|
+
expect(handler2).not.toHaveBeenCalled();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe('gateway access', () => {
|
|
252
|
+
it('should access gateway client through proxy', async () => {
|
|
253
|
+
const definition = createMockDefinition();
|
|
254
|
+
const client = createWsClient(definition, { url: 'ws://localhost:3000' });
|
|
255
|
+
|
|
256
|
+
await client.connect();
|
|
257
|
+
|
|
258
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
259
|
+
const gateway = (client as any).TestGateway;
|
|
260
|
+
expect(gateway).toBeDefined();
|
|
261
|
+
expect(gateway.emit).toBeFunction();
|
|
262
|
+
expect(gateway.send).toBeFunction();
|
|
263
|
+
expect(gateway.on).toBeFunction();
|
|
264
|
+
expect(gateway.off).toBeFunction();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should cache gateway client', async () => {
|
|
268
|
+
const definition = createMockDefinition();
|
|
269
|
+
const client = createWsClient(definition, { url: 'ws://localhost:3000' });
|
|
270
|
+
|
|
271
|
+
await client.connect();
|
|
272
|
+
|
|
273
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
274
|
+
const gateway1 = (client as any).TestGateway;
|
|
275
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
276
|
+
const gateway2 = (client as any).TestGateway;
|
|
277
|
+
|
|
278
|
+
expect(gateway1).toBe(gateway2);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe('message handling', () => {
|
|
283
|
+
it('should handle native format messages', async () => {
|
|
284
|
+
const definition = createMockDefinition();
|
|
285
|
+
const client = createWsClient(definition, { url: 'ws://localhost:3000' });
|
|
286
|
+
const handler = mock(() => undefined);
|
|
287
|
+
|
|
288
|
+
await client.connect();
|
|
289
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
290
|
+
(client as any).TestGateway.on('test:event', handler);
|
|
291
|
+
|
|
292
|
+
const ws = MockWebSocket.getLastInstance();
|
|
293
|
+
ws?.receiveMessage(JSON.stringify({ event: 'test:event', data: { foo: 'bar' } }));
|
|
294
|
+
|
|
295
|
+
expect(handler).toHaveBeenCalledWith({ foo: 'bar' });
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should match pattern events', async () => {
|
|
299
|
+
const definition = createMockDefinition();
|
|
300
|
+
const client = createWsClient(definition, { url: 'ws://localhost:3000' });
|
|
301
|
+
const handler = mock(() => undefined);
|
|
302
|
+
|
|
303
|
+
await client.connect();
|
|
304
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
305
|
+
(client as any).TestGateway.on('chat:*', handler);
|
|
306
|
+
|
|
307
|
+
const ws = MockWebSocket.getLastInstance();
|
|
308
|
+
ws?.receiveMessage(JSON.stringify({ event: 'chat:general', data: { text: 'hello' } }));
|
|
309
|
+
|
|
310
|
+
expect(handler).toHaveBeenCalled();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should handle Engine.IO PING packet', async () => {
|
|
314
|
+
const definition = createMockDefinition();
|
|
315
|
+
const client = createWsClient(definition, { url: 'ws://localhost:3000' });
|
|
316
|
+
|
|
317
|
+
await client.connect();
|
|
318
|
+
|
|
319
|
+
const ws = MockWebSocket.getLastInstance();
|
|
320
|
+
if (ws) {
|
|
321
|
+
ws.sentMessages.length = 0; // Clear previous messages
|
|
322
|
+
|
|
323
|
+
// Send PING (Engine.IO packet type 2)
|
|
324
|
+
ws.receiveMessage('2');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Should respond with PONG (Engine.IO packet type 3)
|
|
328
|
+
expect(ws?.sentMessages).toContain('3');
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe('send and emit', () => {
|
|
333
|
+
it('should send message without acknowledgement', async () => {
|
|
334
|
+
const definition = createMockDefinition();
|
|
335
|
+
const client = createWsClient(definition, { url: 'ws://localhost:3000' });
|
|
336
|
+
|
|
337
|
+
await client.connect();
|
|
338
|
+
|
|
339
|
+
const ws = MockWebSocket.getLastInstance();
|
|
340
|
+
if (ws) {
|
|
341
|
+
ws.sentMessages.length = 0;
|
|
342
|
+
|
|
343
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
344
|
+
(client as any).TestGateway.send('test:event', { foo: 'bar' });
|
|
345
|
+
|
|
346
|
+
expect(ws.sentMessages.length).toBe(1);
|
|
347
|
+
const sent = JSON.parse(ws.sentMessages[0]);
|
|
348
|
+
expect(sent.event).toBe('test:event');
|
|
349
|
+
expect(sent.data).toEqual({ foo: 'bar' });
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should throw when sending while disconnected', async () => {
|
|
354
|
+
const definition = createMockDefinition();
|
|
355
|
+
const client = createWsClient(definition, { url: 'ws://localhost:3000' });
|
|
356
|
+
|
|
357
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
358
|
+
expect(() => (client as any).TestGateway?.send?.('test:event', {})).toThrow();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('should emit message and wait for acknowledgement', async () => {
|
|
362
|
+
const definition = createMockDefinition();
|
|
363
|
+
const client = createWsClient(definition, {
|
|
364
|
+
url: 'ws://localhost:3000',
|
|
365
|
+
timeout: 1000,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
await client.connect();
|
|
369
|
+
|
|
370
|
+
const ws = MockWebSocket.getLastInstance();
|
|
371
|
+
|
|
372
|
+
// Start emit (returns promise)
|
|
373
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
374
|
+
const emitPromise = (client as any).TestGateway.emit('test:event', { foo: 'bar' });
|
|
375
|
+
|
|
376
|
+
// Simulate server acknowledgement
|
|
377
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
378
|
+
const sent = JSON.parse(ws!.sentMessages[ws!.sentMessages.length - 1]);
|
|
379
|
+
ws?.receiveMessage(JSON.stringify({
|
|
380
|
+
event: 'ack',
|
|
381
|
+
data: { result: 'ok' },
|
|
382
|
+
ack: sent.ack,
|
|
383
|
+
}));
|
|
384
|
+
|
|
385
|
+
const result = await emitPromise;
|
|
386
|
+
expect(result).toEqual({ result: 'ok' });
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('should timeout emit if no acknowledgement', async () => {
|
|
390
|
+
const definition = createMockDefinition();
|
|
391
|
+
const client = createWsClient(definition, {
|
|
392
|
+
url: 'ws://localhost:3000',
|
|
393
|
+
timeout: 50, // Short timeout for test
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
await client.connect();
|
|
397
|
+
|
|
398
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
399
|
+
const emitPromise = (client as any).TestGateway.emit('test:event', { foo: 'bar' });
|
|
400
|
+
|
|
401
|
+
await expect(emitPromise).rejects.toThrow('Request timeout');
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
describe('reconnection', () => {
|
|
406
|
+
it('should attempt reconnection on disconnect', async () => {
|
|
407
|
+
const definition = createMockDefinition();
|
|
408
|
+
const client = createWsClient(definition, {
|
|
409
|
+
url: 'ws://localhost:3000',
|
|
410
|
+
reconnect: true,
|
|
411
|
+
reconnectInterval: 10,
|
|
412
|
+
maxReconnectAttempts: 3,
|
|
413
|
+
});
|
|
414
|
+
const reconnectAttemptHandler = mock(() => undefined);
|
|
415
|
+
|
|
416
|
+
await client.connect();
|
|
417
|
+
client.on('reconnect_attempt', reconnectAttemptHandler);
|
|
418
|
+
|
|
419
|
+
// Simulate server disconnect
|
|
420
|
+
const ws = MockWebSocket.getLastInstance();
|
|
421
|
+
ws?.close(1006, 'Connection lost');
|
|
422
|
+
|
|
423
|
+
// Wait for reconnect attempt
|
|
424
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
425
|
+
|
|
426
|
+
expect(reconnectAttemptHandler).toHaveBeenCalled();
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('should track reconnect attempts', async () => {
|
|
430
|
+
const definition = createMockDefinition();
|
|
431
|
+
const client = createWsClient(definition, {
|
|
432
|
+
url: 'ws://localhost:3000',
|
|
433
|
+
reconnect: true,
|
|
434
|
+
reconnectInterval: 10,
|
|
435
|
+
maxReconnectAttempts: 3,
|
|
436
|
+
});
|
|
437
|
+
let reconnectAttemptCount = 0;
|
|
438
|
+
|
|
439
|
+
await client.connect();
|
|
440
|
+
client.on('reconnect_attempt', (attempt) => {
|
|
441
|
+
reconnectAttemptCount = attempt;
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// Simulate initial disconnect
|
|
445
|
+
const ws = MockWebSocket.getLastInstance();
|
|
446
|
+
ws?.close(1006, 'Connection lost');
|
|
447
|
+
|
|
448
|
+
// Wait for first reconnect attempt
|
|
449
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
450
|
+
|
|
451
|
+
expect(reconnectAttemptCount).toBeGreaterThanOrEqual(1);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('should not reconnect if reconnect is disabled', async () => {
|
|
455
|
+
const definition = createMockDefinition();
|
|
456
|
+
const client = createWsClient(definition, {
|
|
457
|
+
url: 'ws://localhost:3000',
|
|
458
|
+
reconnect: false,
|
|
459
|
+
});
|
|
460
|
+
const reconnectAttemptHandler = mock(() => undefined);
|
|
461
|
+
|
|
462
|
+
await client.connect();
|
|
463
|
+
client.on('reconnect_attempt', reconnectAttemptHandler);
|
|
464
|
+
|
|
465
|
+
const initialInstanceCount = MockWebSocket.instances.length;
|
|
466
|
+
|
|
467
|
+
// Simulate disconnect
|
|
468
|
+
const ws = MockWebSocket.getLastInstance();
|
|
469
|
+
ws?.close(1006, 'Connection lost');
|
|
470
|
+
|
|
471
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
472
|
+
|
|
473
|
+
expect(reconnectAttemptHandler).not.toHaveBeenCalled();
|
|
474
|
+
expect(MockWebSocket.instances.length).toBe(initialInstanceCount);
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
describe('connection state', () => {
|
|
479
|
+
it('should track connection state', async () => {
|
|
480
|
+
const definition = createMockDefinition();
|
|
481
|
+
const client = createWsClient(definition, { url: 'ws://localhost:3000' });
|
|
482
|
+
|
|
483
|
+
expect(client.getState()).toBe(WsConnectionState.DISCONNECTED);
|
|
484
|
+
|
|
485
|
+
const connectPromise = client.connect();
|
|
486
|
+
// State changes to CONNECTING synchronously
|
|
487
|
+
|
|
488
|
+
await connectPromise;
|
|
489
|
+
expect(client.getState()).toBe(WsConnectionState.CONNECTED);
|
|
490
|
+
|
|
491
|
+
client.disconnect();
|
|
492
|
+
expect(client.getState()).toBe(WsConnectionState.DISCONNECTED);
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
describe('error handling', () => {
|
|
497
|
+
it('should emit error event on WebSocket error', async () => {
|
|
498
|
+
const definition = createMockDefinition();
|
|
499
|
+
const client = createWsClient(definition, { url: 'ws://localhost:3000' });
|
|
500
|
+
const errorHandler = mock(() => undefined);
|
|
501
|
+
|
|
502
|
+
await client.connect();
|
|
503
|
+
client.on('error', errorHandler);
|
|
504
|
+
|
|
505
|
+
const ws = MockWebSocket.getLastInstance();
|
|
506
|
+
ws?.triggerError();
|
|
507
|
+
|
|
508
|
+
expect(errorHandler).toHaveBeenCalled();
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
});
|