@roj-ai/transport 0.0.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.
Files changed (64) hide show
  1. package/dist/client/client-connection.d.ts +48 -0
  2. package/dist/client/client-connection.d.ts.map +1 -0
  3. package/dist/client/client-connection.js +160 -0
  4. package/dist/client/index.d.ts +10 -0
  5. package/dist/client/index.d.ts.map +1 -0
  6. package/dist/client/index.js +7 -0
  7. package/dist/core/connection.d.ts +59 -0
  8. package/dist/core/connection.d.ts.map +1 -0
  9. package/dist/core/connection.js +165 -0
  10. package/dist/core/index.d.ts +16 -0
  11. package/dist/core/index.d.ts.map +1 -0
  12. package/dist/core/index.js +13 -0
  13. package/dist/core/protocol.d.ts +206 -0
  14. package/dist/core/protocol.d.ts.map +1 -0
  15. package/dist/core/protocol.js +93 -0
  16. package/dist/core/result.d.ts +26 -0
  17. package/dist/core/result.d.ts.map +1 -0
  18. package/dist/core/result.js +20 -0
  19. package/dist/core/router.d.ts +79 -0
  20. package/dist/core/router.d.ts.map +1 -0
  21. package/dist/core/router.js +119 -0
  22. package/dist/core/types.d.ts +44 -0
  23. package/dist/core/types.d.ts.map +1 -0
  24. package/dist/core/types.js +14 -0
  25. package/dist/index.d.ts +24 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +26 -0
  28. package/dist/platform/browser.d.ts +14 -0
  29. package/dist/platform/browser.d.ts.map +1 -0
  30. package/dist/platform/browser.js +65 -0
  31. package/dist/platform/bun.d.ts +69 -0
  32. package/dist/platform/bun.d.ts.map +1 -0
  33. package/dist/platform/bun.js +152 -0
  34. package/dist/platform/types.d.ts +83 -0
  35. package/dist/platform/types.d.ts.map +1 -0
  36. package/dist/platform/types.js +14 -0
  37. package/dist/server/connection-manager.d.ts +106 -0
  38. package/dist/server/connection-manager.d.ts.map +1 -0
  39. package/dist/server/connection-manager.js +234 -0
  40. package/dist/server/index.d.ts +12 -0
  41. package/dist/server/index.d.ts.map +1 -0
  42. package/dist/server/index.js +8 -0
  43. package/dist/server/server-connection.d.ts +50 -0
  44. package/dist/server/server-connection.d.ts.map +1 -0
  45. package/dist/server/server-connection.js +84 -0
  46. package/package.json +52 -0
  47. package/src/__tests__/integration.test.ts +443 -0
  48. package/src/client/client-connection.ts +207 -0
  49. package/src/client/index.ts +24 -0
  50. package/src/core/connection.ts +203 -0
  51. package/src/core/index.ts +52 -0
  52. package/src/core/protocol.test.ts +235 -0
  53. package/src/core/protocol.ts +348 -0
  54. package/src/core/result.ts +35 -0
  55. package/src/core/router.test.ts +248 -0
  56. package/src/core/router.ts +186 -0
  57. package/src/core/types.ts +85 -0
  58. package/src/index.ts +80 -0
  59. package/src/platform/browser.ts +79 -0
  60. package/src/platform/bun.ts +220 -0
  61. package/src/platform/types.ts +102 -0
  62. package/src/server/connection-manager.ts +298 -0
  63. package/src/server/index.ts +26 -0
  64. package/src/server/server-connection.ts +122 -0
@@ -0,0 +1,443 @@
1
+ /**
2
+ * Integration Tests for Transport Package
3
+ *
4
+ * These tests run actual WebSocket server and client to verify
5
+ * real-world communication scenarios with notification-only protocol.
6
+ */
7
+
8
+ import type { Server } from 'bun'
9
+ import { afterAll, afterEach, beforeAll, describe, expect, it } from 'bun:test'
10
+ import { z } from 'zod'
11
+
12
+ import { ClientConnection } from '../client/client-connection.js'
13
+ import { defineProtocol, notification } from '../core/protocol.js'
14
+ import { BunServerWebSocketAdapter, bunWebSocketFactory, createBunWebSocketHandlers } from '../platform/bun.js'
15
+ import type { IServerWebSocket } from '../platform/types.js'
16
+ import { ServerConnection } from '../server/server-connection.js'
17
+
18
+ // ============================================================================
19
+ // Test Protocols (notification-only)
20
+ // ============================================================================
21
+
22
+ // Client -> Server (notifications)
23
+ const clientProtocol = defineProtocol({
24
+ userMessage: notification({
25
+ input: z.object({ sessionId: z.string(), content: z.string() }),
26
+ }),
27
+ userStatus: notification({
28
+ input: z.object({ status: z.enum(['online', 'away', 'offline']) }),
29
+ }),
30
+ })
31
+
32
+ // Server -> Client (notifications)
33
+ const serverProtocol = defineProtocol({
34
+ serverEvent: notification({
35
+ input: z.object({ eventType: z.string(), data: z.unknown() }),
36
+ }),
37
+ agentMessage: notification({
38
+ input: z.object({ sessionId: z.string(), content: z.string() }),
39
+ }),
40
+ })
41
+
42
+ // ============================================================================
43
+ // Test Server Setup
44
+ // ============================================================================
45
+
46
+ interface TestServerState {
47
+ server: Server
48
+ port: number
49
+ connections: Map<string, ServerConnection<typeof clientProtocol._def, typeof serverProtocol._def>>
50
+ receivedNotifications: Array<{ type: string; input: unknown }>
51
+ }
52
+
53
+ function createTestServer(): Promise<TestServerState> {
54
+ return new Promise((resolve) => {
55
+ const connections = new Map<string, ServerConnection<typeof clientProtocol._def, typeof serverProtocol._def>>()
56
+ const receivedNotifications: Array<{ type: string; input: unknown }> = []
57
+
58
+ const wsHandlers = createBunWebSocketHandlers({
59
+ onOpen: (ws: IServerWebSocket) => {
60
+ const serverConn = new ServerConnection({
61
+ receiveProtocol: clientProtocol,
62
+ sendProtocol: serverProtocol,
63
+ })
64
+
65
+ // Set up handlers for incoming notifications
66
+ serverConn.setHandlers({
67
+ userMessage: async (input) => {
68
+ receivedNotifications.push({ type: 'userMessage', input })
69
+ },
70
+ userStatus: async (input) => {
71
+ receivedNotifications.push({ type: 'userStatus', input })
72
+ },
73
+ })
74
+
75
+ serverConn.attach(ws)
76
+ connections.set(serverConn.getConnectionId(), serverConn)
77
+ ;(ws as unknown as { __serverConn: ServerConnection<typeof clientProtocol._def, typeof serverProtocol._def> }).__serverConn = serverConn
78
+ },
79
+ onClose: (ws: IServerWebSocket) => {
80
+ const serverConn = (ws as unknown as { __serverConn: ServerConnection<typeof clientProtocol._def, typeof serverProtocol._def> }).__serverConn
81
+ if (serverConn) {
82
+ connections.delete(serverConn.getConnectionId())
83
+ }
84
+ },
85
+ onMessage: (ws: IServerWebSocket, message: string) => {
86
+ // Forward message to the WebSocket's onmessage handler
87
+ ws.onmessage?.(message)
88
+ },
89
+ onError: () => {},
90
+ getData: () => ({}),
91
+ })
92
+
93
+ const server = Bun.serve({
94
+ port: 0, // Random available port
95
+ fetch(req, server) {
96
+ if (server.upgrade(req)) {
97
+ return
98
+ }
99
+ return new Response('WebSocket server', { status: 200 })
100
+ },
101
+ websocket: {
102
+ open: wsHandlers.open,
103
+ close: wsHandlers.close,
104
+ message: wsHandlers.message,
105
+ },
106
+ })
107
+
108
+ resolve({
109
+ server,
110
+ port: server.port,
111
+ connections,
112
+ receivedNotifications,
113
+ })
114
+ })
115
+ }
116
+
117
+ // ============================================================================
118
+ // Integration Tests
119
+ // ============================================================================
120
+
121
+ describe('Transport Integration Tests', () => {
122
+ let testServer: TestServerState
123
+
124
+ beforeAll(async () => {
125
+ testServer = await createTestServer()
126
+ })
127
+
128
+ afterAll(() => {
129
+ testServer.server.stop()
130
+ })
131
+
132
+ afterEach(() => {
133
+ testServer.receivedNotifications.length = 0
134
+ })
135
+
136
+ describe('Connection', () => {
137
+ it('should connect successfully', async () => {
138
+ const client = new ClientConnection({
139
+ url: `ws://localhost:${testServer.port}`,
140
+ wsFactory: bunWebSocketFactory,
141
+ receiveProtocol: serverProtocol,
142
+ sendProtocol: clientProtocol,
143
+ })
144
+
145
+ await client.connect()
146
+
147
+ expect(client.isConnected()).toBe(true)
148
+ expect(client.getState()).toBe('connected')
149
+
150
+ await client.disconnect()
151
+ expect(client.isConnected()).toBe(false)
152
+ })
153
+
154
+ it('should emit state change events', async () => {
155
+ const stateChanges: Array<{ from: string; to: string }> = []
156
+
157
+ const client = new ClientConnection({
158
+ url: `ws://localhost:${testServer.port}`,
159
+ wsFactory: bunWebSocketFactory,
160
+ receiveProtocol: serverProtocol,
161
+ sendProtocol: clientProtocol,
162
+ })
163
+
164
+ client.on((event, data) => {
165
+ if (event === 'state_change') {
166
+ stateChanges.push(data as { from: string; to: string })
167
+ }
168
+ })
169
+
170
+ await client.connect()
171
+ await client.disconnect()
172
+
173
+ // Should have state changes: disconnected -> connecting -> connected -> disconnected
174
+ expect(stateChanges.length).toBeGreaterThan(2)
175
+ expect(stateChanges.some((s) => s.to === 'connected')).toBe(true)
176
+ })
177
+ })
178
+
179
+ describe('Client-to-Server Notifications', () => {
180
+ it('should send notification to server', async () => {
181
+ const client = new ClientConnection({
182
+ url: `ws://localhost:${testServer.port}`,
183
+ wsFactory: bunWebSocketFactory,
184
+ receiveProtocol: serverProtocol,
185
+ sendProtocol: clientProtocol,
186
+ })
187
+
188
+ await client.connect()
189
+
190
+ // Send notification
191
+ client.notify.userMessage({ sessionId: 'sess-1', content: 'Hello World' })
192
+
193
+ // Wait for notification to arrive
194
+ await new Promise((resolve) => setTimeout(resolve, 50))
195
+
196
+ // Verify server received the notification
197
+ expect(testServer.receivedNotifications).toContainEqual({
198
+ type: 'userMessage',
199
+ input: { sessionId: 'sess-1', content: 'Hello World' },
200
+ })
201
+
202
+ await client.disconnect()
203
+ })
204
+
205
+ it('should send multiple notifications', async () => {
206
+ const client = new ClientConnection({
207
+ url: `ws://localhost:${testServer.port}`,
208
+ wsFactory: bunWebSocketFactory,
209
+ receiveProtocol: serverProtocol,
210
+ sendProtocol: clientProtocol,
211
+ })
212
+
213
+ await client.connect()
214
+
215
+ // Send multiple notifications
216
+ client.notify.userMessage({ sessionId: 'sess-1', content: 'First message' })
217
+ client.notify.userStatus({ status: 'online' })
218
+ client.notify.userMessage({ sessionId: 'sess-1', content: 'Second message' })
219
+
220
+ // Wait for notifications to arrive
221
+ await new Promise((resolve) => setTimeout(resolve, 50))
222
+
223
+ expect(testServer.receivedNotifications.length).toBe(3)
224
+ expect(testServer.receivedNotifications[0]).toEqual({
225
+ type: 'userMessage',
226
+ input: { sessionId: 'sess-1', content: 'First message' },
227
+ })
228
+ expect(testServer.receivedNotifications[1]).toEqual({
229
+ type: 'userStatus',
230
+ input: { status: 'online' },
231
+ })
232
+ expect(testServer.receivedNotifications[2]).toEqual({
233
+ type: 'userMessage',
234
+ input: { sessionId: 'sess-1', content: 'Second message' },
235
+ })
236
+
237
+ await client.disconnect()
238
+ })
239
+ })
240
+
241
+ describe('Server-to-Client Notifications', () => {
242
+ it('should receive notifications from server', async () => {
243
+ const receivedNotifications: Array<{ eventType: string; data: unknown }> = []
244
+
245
+ const client = new ClientConnection({
246
+ url: `ws://localhost:${testServer.port}`,
247
+ wsFactory: bunWebSocketFactory,
248
+ receiveProtocol: serverProtocol,
249
+ sendProtocol: clientProtocol,
250
+ })
251
+
252
+ // Set up notification handler
253
+ client.setHandlers({
254
+ serverEvent: async (input) => {
255
+ receivedNotifications.push(input)
256
+ },
257
+ })
258
+
259
+ await client.connect()
260
+
261
+ // Get the server connection and send notification
262
+ await new Promise((resolve) => setTimeout(resolve, 50)) // Wait for server to register connection
263
+
264
+ const serverConnections = Array.from(testServer.connections.values())
265
+ expect(serverConnections.length).toBeGreaterThan(0)
266
+
267
+ const serverConn = serverConnections[0]
268
+ serverConn.notify.serverEvent({ eventType: 'test_event', data: { foo: 'bar' } })
269
+
270
+ // Wait for notification to arrive
271
+ await new Promise((resolve) => setTimeout(resolve, 50))
272
+
273
+ expect(receivedNotifications.length).toBe(1)
274
+ expect(receivedNotifications[0].eventType).toBe('test_event')
275
+ expect(receivedNotifications[0].data).toEqual({ foo: 'bar' })
276
+
277
+ await client.disconnect()
278
+ })
279
+
280
+ it('should receive agent messages from server', async () => {
281
+ const receivedMessages: Array<{ sessionId: string; content: string }> = []
282
+
283
+ const client = new ClientConnection({
284
+ url: `ws://localhost:${testServer.port}`,
285
+ wsFactory: bunWebSocketFactory,
286
+ receiveProtocol: serverProtocol,
287
+ sendProtocol: clientProtocol,
288
+ })
289
+
290
+ client.setHandlers({
291
+ agentMessage: async (input) => {
292
+ receivedMessages.push(input)
293
+ },
294
+ })
295
+
296
+ await client.connect()
297
+ await new Promise((resolve) => setTimeout(resolve, 50))
298
+
299
+ const serverConnections = Array.from(testServer.connections.values())
300
+ const serverConn = serverConnections[0]
301
+
302
+ serverConn.notify.agentMessage({ sessionId: 'sess-1', content: 'Hello from agent!' })
303
+
304
+ await new Promise((resolve) => setTimeout(resolve, 50))
305
+
306
+ expect(receivedMessages.length).toBe(1)
307
+ expect(receivedMessages[0]).toEqual({ sessionId: 'sess-1', content: 'Hello from agent!' })
308
+
309
+ await client.disconnect()
310
+ })
311
+ })
312
+
313
+ describe('Reconnection', () => {
314
+ it('should attempt reconnection when connection is lost', async () => {
315
+ const reconnectAttempts: number[] = []
316
+ const stateChanges: string[] = []
317
+
318
+ // Create a server that immediately closes connections
319
+ const tempServer = Bun.serve({
320
+ port: 0,
321
+ fetch(req, server) {
322
+ if (server.upgrade(req)) return
323
+ return new Response('ok')
324
+ },
325
+ websocket: {
326
+ open(ws) {
327
+ // Close connection after a brief delay to trigger reconnect
328
+ setTimeout(() => ws.close(1000, 'Server initiated close'), 50)
329
+ },
330
+ message() {},
331
+ close() {},
332
+ },
333
+ })
334
+
335
+ const client = new ClientConnection({
336
+ url: `ws://localhost:${tempServer.port}`,
337
+ wsFactory: bunWebSocketFactory,
338
+ receiveProtocol: serverProtocol,
339
+ sendProtocol: clientProtocol,
340
+ reconnect: {
341
+ baseDelayMs: 20,
342
+ maxDelayMs: 100,
343
+ maxAttempts: 2,
344
+ jitterFactor: 0,
345
+ },
346
+ })
347
+
348
+ client.on((event, data) => {
349
+ if (event === 'reconnecting') {
350
+ reconnectAttempts.push((data as { attempt: number }).attempt)
351
+ }
352
+ if (event === 'state_change') {
353
+ stateChanges.push((data as { to: string }).to)
354
+ }
355
+ })
356
+
357
+ await client.connect()
358
+
359
+ // Wait for server to close connection and client to attempt reconnect
360
+ await new Promise((resolve) => setTimeout(resolve, 200))
361
+
362
+ // Should have attempted reconnection
363
+ expect(reconnectAttempts.length).toBeGreaterThan(0)
364
+ expect(stateChanges).toContain('reconnecting')
365
+
366
+ await client.disconnect()
367
+ tempServer.stop()
368
+ })
369
+ })
370
+
371
+ describe('Multiple Clients', () => {
372
+ it('should handle multiple simultaneous clients', async () => {
373
+ const numClients = 5
374
+ const clients: ClientConnection<typeof serverProtocol._def, typeof clientProtocol._def>[] = []
375
+
376
+ // Connect multiple clients
377
+ for (let i = 0; i < numClients; i++) {
378
+ const client = new ClientConnection({
379
+ url: `ws://localhost:${testServer.port}`,
380
+ wsFactory: bunWebSocketFactory,
381
+ receiveProtocol: serverProtocol,
382
+ sendProtocol: clientProtocol,
383
+ })
384
+ await client.connect()
385
+ clients.push(client)
386
+ }
387
+
388
+ // All clients should be connected
389
+ expect(clients.every((c) => c.isConnected())).toBe(true)
390
+
391
+ // Each client sends a notification
392
+ clients.forEach((client, i) => {
393
+ client.notify.userMessage({ sessionId: `sess-${i}`, content: `Message from client ${i}` })
394
+ })
395
+
396
+ // Wait for notifications to arrive
397
+ await new Promise((resolve) => setTimeout(resolve, 100))
398
+
399
+ // All notifications should be received
400
+ expect(testServer.receivedNotifications.length).toBe(numClients)
401
+
402
+ // Disconnect all
403
+ await Promise.all(clients.map((c) => c.disconnect()))
404
+ })
405
+ })
406
+
407
+ describe('Bidirectional Communication', () => {
408
+ it('should handle simultaneous send and receive', async () => {
409
+ const clientReceivedMessages: unknown[] = []
410
+
411
+ const client = new ClientConnection({
412
+ url: `ws://localhost:${testServer.port}`,
413
+ wsFactory: bunWebSocketFactory,
414
+ receiveProtocol: serverProtocol,
415
+ sendProtocol: clientProtocol,
416
+ })
417
+
418
+ client.setHandlers({
419
+ agentMessage: async (input) => {
420
+ clientReceivedMessages.push(input)
421
+ },
422
+ })
423
+
424
+ await client.connect()
425
+ await new Promise((resolve) => setTimeout(resolve, 50))
426
+
427
+ // Client sends notification
428
+ client.notify.userMessage({ sessionId: 'sess-1', content: 'Client message' })
429
+
430
+ // Server sends notification simultaneously
431
+ const serverConnections = Array.from(testServer.connections.values())
432
+ serverConnections[0].notify.agentMessage({ sessionId: 'sess-1', content: 'Server message' })
433
+
434
+ await new Promise((resolve) => setTimeout(resolve, 100))
435
+
436
+ // Both sides should have received notifications
437
+ expect(testServer.receivedNotifications.some((n) => n.type === 'userMessage')).toBe(true)
438
+ expect(clientReceivedMessages.length).toBe(1)
439
+
440
+ await client.disconnect()
441
+ })
442
+ })
443
+ })
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Client Connection
3
+ *
4
+ * WebSocket client with automatic reconnection.
5
+ * Simplified: no handshake - connected immediately when WebSocket opens.
6
+ */
7
+
8
+ import { Connection } from '../core/connection.js'
9
+ import type { ConnectionConfig } from '../core/connection.js'
10
+ import type { ProtocolDef } from '../core/protocol.js'
11
+ import type { ReconnectOptions } from '../core/types.js'
12
+ import { DEFAULT_RECONNECT_OPTIONS } from '../core/types.js'
13
+ import type { IWebSocketFactory } from '../platform/types.js'
14
+ import { WebSocketReadyState } from '../platform/types.js'
15
+
16
+ /**
17
+ * Client connection configuration.
18
+ */
19
+ export interface ClientConnectionConfig<TReceive extends ProtocolDef, TSend extends ProtocolDef> extends ConnectionConfig<TReceive, TSend> {
20
+ url: string
21
+ wsFactory: IWebSocketFactory
22
+ reconnect?: Partial<ReconnectOptions>
23
+ }
24
+
25
+ /**
26
+ * WebSocket client with automatic reconnection and exponential backoff.
27
+ */
28
+ export class ClientConnection<TReceive extends ProtocolDef, TSend extends ProtocolDef> extends Connection<TReceive, TSend> {
29
+ private readonly url: string
30
+ private readonly wsFactory: IWebSocketFactory
31
+
32
+ // Reconnection
33
+ private readonly reconnectOptions: ReconnectOptions | null
34
+ private reconnectAttempts = 0
35
+ private reconnectTimer: ReturnType<typeof setTimeout> | null = null
36
+ private reconnectStopped = false
37
+
38
+ // Connect promise
39
+ private connectPromise: Promise<void> | null = null
40
+ private connectResolve: (() => void) | null = null
41
+ private connectReject: ((error: Error) => void) | null = null
42
+ private intentionalDisconnect = false
43
+
44
+ constructor(config: ClientConnectionConfig<TReceive, TSend>) {
45
+ super(config)
46
+ this.url = config.url
47
+ this.wsFactory = config.wsFactory
48
+ this.reconnectOptions = config.reconnect
49
+ ? { ...DEFAULT_RECONNECT_OPTIONS, ...config.reconnect }
50
+ : null
51
+ }
52
+
53
+ async connect(): Promise<void> {
54
+ if (this.isConnected()) return
55
+ if (this.connectPromise) return this.connectPromise
56
+
57
+ this.intentionalDisconnect = false
58
+ this.reconnectStopped = false
59
+
60
+ this.connectPromise = new Promise<void>((resolve, reject) => {
61
+ this.connectResolve = resolve
62
+ this.connectReject = reject
63
+ this.doConnect()
64
+ })
65
+
66
+ return this.connectPromise
67
+ }
68
+
69
+ async disconnect(): Promise<void> {
70
+ this.intentionalDisconnect = true
71
+ this.stopReconnect()
72
+ this.cleanup()
73
+
74
+ if (this.ws && this.ws.readyState !== WebSocketReadyState.CLOSED) {
75
+ this.ws.close(1000, 'Client disconnect')
76
+ }
77
+
78
+ this.ws = null
79
+ this.setState('disconnected')
80
+
81
+ if (this.connectReject) {
82
+ this.connectReject(new Error('Disconnected'))
83
+ this.clearConnectPromise()
84
+ }
85
+ }
86
+
87
+ getReconnectAttempts(): number {
88
+ return this.reconnectAttempts
89
+ }
90
+
91
+ hasReconnect(): boolean {
92
+ return this.reconnectOptions !== null
93
+ }
94
+
95
+ // Override to resolve connect promise and reset reconnect
96
+ protected override handleOpen(): void {
97
+ const wasReconnecting = this.reconnectAttempts > 0
98
+ super.handleOpen()
99
+ this.reconnectAttempts = 0
100
+
101
+ // Flush any messages buffered during disconnect
102
+ this.flushSendBuffer()
103
+
104
+ if (this.connectResolve) {
105
+ this.connectResolve()
106
+ this.clearConnectPromise()
107
+ }
108
+
109
+ // Emit reconnected event so application can restore subscriptions
110
+ if (wasReconnecting) {
111
+ this.emit('reconnected')
112
+ }
113
+ }
114
+
115
+ protected override handleClose(code: number, reason: string): void {
116
+ this.cleanup()
117
+
118
+ if (this.intentionalDisconnect) {
119
+ this.setState('disconnected')
120
+ return
121
+ }
122
+
123
+ if (this.connectReject) {
124
+ this.connectReject(new Error(`Connection closed: ${code} ${reason}`))
125
+ this.clearConnectPromise()
126
+ }
127
+
128
+ this.handleConnectionLost()
129
+ }
130
+
131
+ protected override handleConnectionLost(): void {
132
+ this.cleanup()
133
+
134
+ if (this.intentionalDisconnect) {
135
+ this.setState('disconnected')
136
+ return
137
+ }
138
+
139
+ if (!this.reconnectOptions || this.reconnectStopped) {
140
+ this.setState('disconnected')
141
+ return
142
+ }
143
+
144
+ if (this.reconnectAttempts >= this.reconnectOptions.maxAttempts) {
145
+ this.emit('error', new Error('Max reconnection attempts reached'))
146
+ this.setState('disconnected')
147
+ return
148
+ }
149
+
150
+ this.setState('reconnecting')
151
+ const delay = this.calculateReconnectDelay()
152
+ this.reconnectAttempts++
153
+
154
+ this.emit('reconnecting', { delay, attempt: this.reconnectAttempts })
155
+
156
+ this.reconnectTimer = setTimeout(() => {
157
+ this.reconnectTimer = null
158
+ this.setState('connecting')
159
+ this.doConnect()
160
+ }, delay)
161
+ }
162
+
163
+ private doConnect(): void {
164
+ this.setState('connecting')
165
+
166
+ try {
167
+ const ws = this.wsFactory.create(this.url)
168
+ this.setupWebSocket(ws)
169
+ ws.onopen = () => this.handleOpen()
170
+ } catch (error) {
171
+ if (this.connectReject) {
172
+ this.connectReject(error instanceof Error ? error : new Error('Connection failed'))
173
+ this.clearConnectPromise()
174
+ }
175
+ this.handleConnectionLost()
176
+ }
177
+ }
178
+
179
+ private calculateReconnectDelay(): number {
180
+ if (!this.reconnectOptions) return 0
181
+ const { baseDelayMs, maxDelayMs, jitterFactor } = this.reconnectOptions
182
+ const exponential = baseDelayMs * Math.pow(2, this.reconnectAttempts)
183
+ const capped = Math.min(exponential, maxDelayMs)
184
+ const jitter = capped * jitterFactor * Math.random()
185
+ return Math.floor(capped + jitter)
186
+ }
187
+
188
+ private stopReconnect(): void {
189
+ this.reconnectStopped = true
190
+ if (this.reconnectTimer) {
191
+ clearTimeout(this.reconnectTimer)
192
+ this.reconnectTimer = null
193
+ }
194
+ }
195
+
196
+ private clearConnectPromise(): void {
197
+ this.connectPromise = null
198
+ this.connectResolve = null
199
+ this.connectReject = null
200
+ }
201
+ }
202
+
203
+ export function createClientConnection<TReceive extends ProtocolDef, TSend extends ProtocolDef>(
204
+ config: ClientConnectionConfig<TReceive, TSend>,
205
+ ): ClientConnection<TReceive, TSend> {
206
+ return new ClientConnection(config)
207
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Client Connection Module
3
+ *
4
+ * WebSocket client for connecting to server endpoints.
5
+ */
6
+
7
+ export { ClientConnection, createClientConnection } from './client-connection.js'
8
+ export type { ClientConnectionConfig } from './client-connection.js'
9
+
10
+ // Re-export core types commonly used with client
11
+ export type {
12
+ ConnectionState,
13
+ HandlerContext,
14
+ Protocol,
15
+ ProtocolCaller,
16
+ ProtocolDef,
17
+ ProtocolHandlers,
18
+ ProtocolNotifier,
19
+ ReconnectOptions,
20
+ TransportEvent,
21
+ TransportEventListener,
22
+ } from '../core/index.js'
23
+
24
+ export { defineProtocol, method, notification } from '../core/index.js'