@roj-ai/transport 0.1.27 → 0.1.29
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/dist/client/client-connection.d.ts +2 -1
- package/dist/client/client-connection.d.ts.map +1 -1
- package/dist/client/client-connection.js +17 -15
- package/dist/client/client-connection.js.map +1 -1
- package/package.json +1 -1
- package/src/client/client-connection.test.ts +146 -0
- package/src/client/client-connection.ts +18 -16
|
@@ -38,10 +38,11 @@ export declare class ClientConnection<TReceive extends ProtocolDef, TSend extend
|
|
|
38
38
|
hasReconnect(): boolean;
|
|
39
39
|
protected handleOpen(): void;
|
|
40
40
|
protected handleClose(code: number, reason: string): void;
|
|
41
|
-
protected handleConnectionLost(): void;
|
|
41
|
+
protected handleConnectionLost(cause?: Error): void;
|
|
42
42
|
private doConnect;
|
|
43
43
|
private calculateReconnectDelay;
|
|
44
44
|
private stopReconnect;
|
|
45
|
+
private rejectConnect;
|
|
45
46
|
private clearConnectPromise;
|
|
46
47
|
}
|
|
47
48
|
export declare function createClientConnection<TReceive extends ProtocolDef, TSend extends ProtocolDef>(config: ClientConnectionConfig<TReceive, TSend>): ClientConnection<TReceive, TSend>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-connection.d.ts","sourceRoot":"","sources":["../../src/client/client-connection.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAExD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAG7D;;GAEG;AACH,MAAM,WAAW,sBAAsB,CAAC,QAAQ,SAAS,WAAW,EAAE,KAAK,SAAS,WAAW,CAAE,SAAQ,gBAAgB,CAAC,QAAQ,EAAE,KAAK,CAAC;IACzI,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,iBAAiB,CAAA;IAC5B,SAAS,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAA;CACrC;AAED;;GAEG;AACH,qBAAa,gBAAgB,CAAC,QAAQ,SAAS,WAAW,EAAE,KAAK,SAAS,WAAW,CAAE,SAAQ,UAAU,CAAC,QAAQ,EAAE,KAAK,CAAC;IACzH,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;IAC5B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmB;IAG7C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAyB;IAC1D,OAAO,CAAC,iBAAiB,CAAI;IAC7B,OAAO,CAAC,cAAc,CAA6C;IACnE,OAAO,CAAC,gBAAgB,CAAQ;IAGhC,OAAO,CAAC,cAAc,CAA6B;IACnD,OAAO,CAAC,cAAc,CAA4B;IAClD,OAAO,CAAC,aAAa,CAAwC;IAC7D,OAAO,CAAC,qBAAqB,CAAQ;gBAEzB,MAAM,EAAE,sBAAsB,CAAC,QAAQ,EAAE,KAAK,CAAC;IASrD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAgBxB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"client-connection.d.ts","sourceRoot":"","sources":["../../src/client/client-connection.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAExD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAG7D;;GAEG;AACH,MAAM,WAAW,sBAAsB,CAAC,QAAQ,SAAS,WAAW,EAAE,KAAK,SAAS,WAAW,CAAE,SAAQ,gBAAgB,CAAC,QAAQ,EAAE,KAAK,CAAC;IACzI,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,iBAAiB,CAAA;IAC5B,SAAS,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAA;CACrC;AAED;;GAEG;AACH,qBAAa,gBAAgB,CAAC,QAAQ,SAAS,WAAW,EAAE,KAAK,SAAS,WAAW,CAAE,SAAQ,UAAU,CAAC,QAAQ,EAAE,KAAK,CAAC;IACzH,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;IAC5B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmB;IAG7C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAyB;IAC1D,OAAO,CAAC,iBAAiB,CAAI;IAC7B,OAAO,CAAC,cAAc,CAA6C;IACnE,OAAO,CAAC,gBAAgB,CAAQ;IAGhC,OAAO,CAAC,cAAc,CAA6B;IACnD,OAAO,CAAC,cAAc,CAA4B;IAClD,OAAO,CAAC,aAAa,CAAwC;IAC7D,OAAO,CAAC,qBAAqB,CAAQ;gBAEzB,MAAM,EAAE,sBAAsB,CAAC,QAAQ,EAAE,KAAK,CAAC;IASrD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAgBxB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAejC,oBAAoB,IAAI,MAAM;IAI9B,YAAY,IAAI,OAAO;cAKJ,UAAU,IAAI,IAAI;cAmBlB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;cAgB/C,oBAAoB,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI;IAkC5D,OAAO,CAAC,SAAS;IAYjB,OAAO,CAAC,uBAAuB;IAS/B,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,mBAAmB;CAK3B;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,SAAS,WAAW,EAAE,KAAK,SAAS,WAAW,EAC7F,MAAM,EAAE,sBAAsB,CAAC,QAAQ,EAAE,KAAK,CAAC,GAC7C,gBAAgB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAEnC"}
|
|
@@ -54,10 +54,7 @@ export class ClientConnection extends Connection {
|
|
|
54
54
|
}
|
|
55
55
|
this.ws = null;
|
|
56
56
|
this.setState('disconnected');
|
|
57
|
-
|
|
58
|
-
this.connectReject(new Error('Disconnected'));
|
|
59
|
-
this.clearConnectPromise();
|
|
60
|
-
}
|
|
57
|
+
this.rejectConnect(new Error('Disconnected'));
|
|
61
58
|
}
|
|
62
59
|
getReconnectAttempts() {
|
|
63
60
|
return this.reconnectAttempts;
|
|
@@ -87,13 +84,14 @@ export class ClientConnection extends Connection {
|
|
|
87
84
|
this.setState('disconnected');
|
|
88
85
|
return;
|
|
89
86
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
87
|
+
// Don't reject the pending connect() here. With reconnect enabled the initial
|
|
88
|
+
// connect must stay pending across failed attempts and settle only on open
|
|
89
|
+
// (resolve) or give-up (reject) — see handleConnectionLost. Rejecting on the
|
|
90
|
+
// first pre-open close crashed callers that await connect() under an
|
|
91
|
+
// infinite-reconnect policy.
|
|
92
|
+
this.handleConnectionLost(new Error(`Connection closed: ${code} ${reason}`));
|
|
95
93
|
}
|
|
96
|
-
handleConnectionLost() {
|
|
94
|
+
handleConnectionLost(cause) {
|
|
97
95
|
this.cleanup();
|
|
98
96
|
if (this.intentionalDisconnect) {
|
|
99
97
|
this.setState('disconnected');
|
|
@@ -101,11 +99,13 @@ export class ClientConnection extends Connection {
|
|
|
101
99
|
}
|
|
102
100
|
if (!this.reconnectOptions || this.reconnectStopped) {
|
|
103
101
|
this.setState('disconnected');
|
|
102
|
+
this.rejectConnect(cause ?? new Error('Connection lost'));
|
|
104
103
|
return;
|
|
105
104
|
}
|
|
106
105
|
if (this.reconnectAttempts >= this.reconnectOptions.maxAttempts) {
|
|
107
106
|
this.emit('error', new Error('Max reconnection attempts reached'));
|
|
108
107
|
this.setState('disconnected');
|
|
108
|
+
this.rejectConnect(cause ?? new Error('Max reconnection attempts reached'));
|
|
109
109
|
return;
|
|
110
110
|
}
|
|
111
111
|
this.setState('reconnecting');
|
|
@@ -126,11 +126,7 @@ export class ClientConnection extends Connection {
|
|
|
126
126
|
ws.onopen = () => this.handleOpen();
|
|
127
127
|
}
|
|
128
128
|
catch (error) {
|
|
129
|
-
|
|
130
|
-
this.connectReject(error instanceof Error ? error : new Error('Connection failed'));
|
|
131
|
-
this.clearConnectPromise();
|
|
132
|
-
}
|
|
133
|
-
this.handleConnectionLost();
|
|
129
|
+
this.handleConnectionLost(error instanceof Error ? error : new Error('Connection failed'));
|
|
134
130
|
}
|
|
135
131
|
}
|
|
136
132
|
calculateReconnectDelay() {
|
|
@@ -149,6 +145,12 @@ export class ClientConnection extends Connection {
|
|
|
149
145
|
this.reconnectTimer = null;
|
|
150
146
|
}
|
|
151
147
|
}
|
|
148
|
+
rejectConnect(error) {
|
|
149
|
+
if (this.connectReject) {
|
|
150
|
+
this.connectReject(error);
|
|
151
|
+
this.clearConnectPromise();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
152
154
|
clearConnectPromise() {
|
|
153
155
|
this.connectPromise = null;
|
|
154
156
|
this.connectResolve = null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-connection.js","sourceRoot":"","sources":["../../src/client/client-connection.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAIlD,OAAO,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAA;AAE5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAW1D;;GAEG;AACH,MAAM,OAAO,gBAA0E,SAAQ,UAA2B;IACxG,GAAG,CAAQ;IACX,SAAS,CAAmB;IAE7C,eAAe;IACE,gBAAgB,CAAyB;IAClD,iBAAiB,GAAG,CAAC,CAAA;IACrB,cAAc,GAAyC,IAAI,CAAA;IAC3D,gBAAgB,GAAG,KAAK,CAAA;IAEhC,kBAAkB;IACV,cAAc,GAAyB,IAAI,CAAA;IAC3C,cAAc,GAAwB,IAAI,CAAA;IAC1C,aAAa,GAAoC,IAAI,CAAA;IACrD,qBAAqB,GAAG,KAAK,CAAA;IAErC,YAAY,MAA+C;QAC1D,KAAK,CAAC,MAAM,CAAC,CAAA;QACb,IAAI,CAAC,GAAG,GAAG,MAAM,CAAC,GAAG,CAAA;QACrB,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,SAAS,CAAA;QACjC,IAAI,CAAC,gBAAgB,GAAG,MAAM,CAAC,SAAS;YACvC,CAAC,CAAC,EAAE,GAAG,yBAAyB,EAAE,GAAG,MAAM,CAAC,SAAS,EAAE;YACvD,CAAC,CAAC,IAAI,CAAA;IACR,CAAC;IAED,KAAK,CAAC,OAAO;QACZ,IAAI,IAAI,CAAC,WAAW,EAAE;YAAE,OAAM;QAC9B,IAAI,IAAI,CAAC,cAAc;YAAE,OAAO,IAAI,CAAC,cAAc,CAAA;QAEnD,IAAI,CAAC,qBAAqB,GAAG,KAAK,CAAA;QAClC,IAAI,CAAC,gBAAgB,GAAG,KAAK,CAAA;QAE7B,IAAI,CAAC,cAAc,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YAC3D,IAAI,CAAC,cAAc,GAAG,OAAO,CAAA;YAC7B,IAAI,CAAC,aAAa,GAAG,MAAM,CAAA;YAC3B,IAAI,CAAC,SAAS,EAAE,CAAA;QACjB,CAAC,CAAC,CAAA;QAEF,OAAO,IAAI,CAAC,cAAc,CAAA;IAC3B,CAAC;IAED,KAAK,CAAC,UAAU;QACf,IAAI,CAAC,qBAAqB,GAAG,IAAI,CAAA;QACjC,IAAI,CAAC,aAAa,EAAE,CAAA;QACpB,IAAI,CAAC,OAAO,EAAE,CAAA;QAEd,IAAI,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC,UAAU,KAAK,mBAAmB,CAAC,MAAM,EAAE,CAAC;YAClE,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,mBAAmB,CAAC,CAAA;QACzC,CAAC;QAED,IAAI,CAAC,EAAE,GAAG,IAAI,CAAA;QACd,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAA;QAE7B,IAAI,
|
|
1
|
+
{"version":3,"file":"client-connection.js","sourceRoot":"","sources":["../../src/client/client-connection.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClientConnection reconnect / connect() settlement tests
|
|
3
|
+
*
|
|
4
|
+
* Regression guard: a WebSocket that closes *before it ever opens* must not
|
|
5
|
+
* reject the initial connect() promise while reconnect is still viable. The
|
|
6
|
+
* agent server awaits connect() during boot; rejecting on the first pre-open
|
|
7
|
+
* close crashed the whole process ("Process died") even though it was
|
|
8
|
+
* configured with an infinite-reconnect policy.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, it } from 'bun:test'
|
|
12
|
+
|
|
13
|
+
import { ClientConnection } from './client-connection.js'
|
|
14
|
+
import type { ICloseEvent, IWebSocket, IWebSocketFactory } from '../platform/types.js'
|
|
15
|
+
import { WebSocketReadyState } from '../platform/types.js'
|
|
16
|
+
|
|
17
|
+
class MockWebSocket implements IWebSocket {
|
|
18
|
+
readyState: WebSocketReadyState = WebSocketReadyState.CONNECTING
|
|
19
|
+
onopen: (() => void) | null = null
|
|
20
|
+
onclose: ((event: ICloseEvent) => void) | null = null
|
|
21
|
+
onerror: ((error: Error) => void) | null = null
|
|
22
|
+
onmessage: ((data: string) => void) | null = null
|
|
23
|
+
|
|
24
|
+
send(_data: string): void {}
|
|
25
|
+
|
|
26
|
+
close(code = 1000, reason = ''): void {
|
|
27
|
+
this.readyState = WebSocketReadyState.CLOSED
|
|
28
|
+
this.onclose?.({ code, reason })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
simulateOpen(): void {
|
|
32
|
+
this.readyState = WebSocketReadyState.OPEN
|
|
33
|
+
this.onopen?.()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
simulateClose(code: number, reason: string): void {
|
|
37
|
+
this.readyState = WebSocketReadyState.CLOSED
|
|
38
|
+
this.onclose?.({ code, reason })
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Factory that fires open/close on the next tick — after ClientConnection has
|
|
43
|
+
// wired ws.onopen / ws.onclose in doConnect(). 'close' never opens the socket,
|
|
44
|
+
// reproducing a rejected upgrade / immediate server close on connect.
|
|
45
|
+
class MockWsFactory implements IWebSocketFactory {
|
|
46
|
+
created = 0
|
|
47
|
+
constructor(private readonly behavior: 'close' | 'open') {}
|
|
48
|
+
|
|
49
|
+
create(_url: string): IWebSocket {
|
|
50
|
+
this.created++
|
|
51
|
+
const ws = new MockWebSocket()
|
|
52
|
+
if (this.behavior === 'close') {
|
|
53
|
+
setTimeout(() => ws.simulateClose(1006, 'handshake failed'), 0)
|
|
54
|
+
} else {
|
|
55
|
+
setTimeout(() => ws.simulateOpen(), 0)
|
|
56
|
+
}
|
|
57
|
+
return ws
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const tick = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
62
|
+
|
|
63
|
+
describe('ClientConnection connect() settlement on pre-open close', () => {
|
|
64
|
+
it('keeps connect() pending (no reject) across pre-open closes under infinite reconnect', async () => {
|
|
65
|
+
const factory = new MockWsFactory('close')
|
|
66
|
+
const client = new ClientConnection({
|
|
67
|
+
url: 'ws://mock',
|
|
68
|
+
wsFactory: factory,
|
|
69
|
+
reconnect: { baseDelayMs: 10, maxDelayMs: 20, maxAttempts: Infinity, jitterFactor: 0 },
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
let settled: 'resolved' | 'rejected' | null = null
|
|
73
|
+
const reconnecting: number[] = []
|
|
74
|
+
client.on((event, data) => {
|
|
75
|
+
if (event === 'reconnecting') reconnecting.push((data as { attempt: number }).attempt)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const p = client.connect().then(
|
|
79
|
+
() => { settled = 'resolved' },
|
|
80
|
+
() => { settled = 'rejected' },
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
await tick(80)
|
|
84
|
+
|
|
85
|
+
// The crash used to happen here: connect() must NOT have rejected yet.
|
|
86
|
+
expect(settled).toBeNull()
|
|
87
|
+
expect(factory.created).toBeGreaterThan(1) // kept retrying
|
|
88
|
+
expect(reconnecting.length).toBeGreaterThan(1)
|
|
89
|
+
|
|
90
|
+
// disconnect() is the only thing that settles a still-pending connect()
|
|
91
|
+
await client.disconnect()
|
|
92
|
+
await p
|
|
93
|
+
expect(settled).toBe('rejected')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('resolves connect() once a later attempt opens (recovers after transient closes)', async () => {
|
|
97
|
+
// Fails the first two attempts (pre-open close), then opens.
|
|
98
|
+
let attempt = 0
|
|
99
|
+
const factory: IWebSocketFactory = {
|
|
100
|
+
create: () => {
|
|
101
|
+
attempt++
|
|
102
|
+
const ws = new MockWebSocket()
|
|
103
|
+
if (attempt <= 2) {
|
|
104
|
+
setTimeout(() => ws.simulateClose(1006, 'handshake failed'), 0)
|
|
105
|
+
} else {
|
|
106
|
+
setTimeout(() => ws.simulateOpen(), 0)
|
|
107
|
+
}
|
|
108
|
+
return ws
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
const client = new ClientConnection({
|
|
112
|
+
url: 'ws://mock',
|
|
113
|
+
wsFactory: factory,
|
|
114
|
+
reconnect: { baseDelayMs: 10, maxDelayMs: 20, maxAttempts: Infinity, jitterFactor: 0 },
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
await client.connect()
|
|
118
|
+
expect(client.isConnected()).toBe(true)
|
|
119
|
+
|
|
120
|
+
await client.disconnect()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('rejects connect() after a finite reconnect budget is exhausted', async () => {
|
|
124
|
+
const factory = new MockWsFactory('close')
|
|
125
|
+
const client = new ClientConnection({
|
|
126
|
+
url: 'ws://mock',
|
|
127
|
+
wsFactory: factory,
|
|
128
|
+
reconnect: { baseDelayMs: 5, maxDelayMs: 10, maxAttempts: 2, jitterFactor: 0 },
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
await expect(client.connect()).rejects.toThrow(/Connection closed/)
|
|
132
|
+
expect(factory.created).toBe(3) // initial attempt + 2 reconnects
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('rejects connect() on the first pre-open close when reconnect is disabled', async () => {
|
|
136
|
+
const factory = new MockWsFactory('close')
|
|
137
|
+
const client = new ClientConnection({
|
|
138
|
+
url: 'ws://mock',
|
|
139
|
+
wsFactory: factory,
|
|
140
|
+
// no reconnect option → reconnect disabled
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
await expect(client.connect()).rejects.toThrow(/Connection closed/)
|
|
144
|
+
expect(factory.created).toBe(1)
|
|
145
|
+
})
|
|
146
|
+
})
|
|
@@ -78,10 +78,7 @@ export class ClientConnection<TReceive extends ProtocolDef, TSend extends Protoc
|
|
|
78
78
|
this.ws = null
|
|
79
79
|
this.setState('disconnected')
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
this.connectReject(new Error('Disconnected'))
|
|
83
|
-
this.clearConnectPromise()
|
|
84
|
-
}
|
|
81
|
+
this.rejectConnect(new Error('Disconnected'))
|
|
85
82
|
}
|
|
86
83
|
|
|
87
84
|
getReconnectAttempts(): number {
|
|
@@ -120,15 +117,15 @@ export class ClientConnection<TReceive extends ProtocolDef, TSend extends Protoc
|
|
|
120
117
|
return
|
|
121
118
|
}
|
|
122
119
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
this.handleConnectionLost()
|
|
120
|
+
// Don't reject the pending connect() here. With reconnect enabled the initial
|
|
121
|
+
// connect must stay pending across failed attempts and settle only on open
|
|
122
|
+
// (resolve) or give-up (reject) — see handleConnectionLost. Rejecting on the
|
|
123
|
+
// first pre-open close crashed callers that await connect() under an
|
|
124
|
+
// infinite-reconnect policy.
|
|
125
|
+
this.handleConnectionLost(new Error(`Connection closed: ${code} ${reason}`))
|
|
129
126
|
}
|
|
130
127
|
|
|
131
|
-
protected override handleConnectionLost(): void {
|
|
128
|
+
protected override handleConnectionLost(cause?: Error): void {
|
|
132
129
|
this.cleanup()
|
|
133
130
|
|
|
134
131
|
if (this.intentionalDisconnect) {
|
|
@@ -138,12 +135,14 @@ export class ClientConnection<TReceive extends ProtocolDef, TSend extends Protoc
|
|
|
138
135
|
|
|
139
136
|
if (!this.reconnectOptions || this.reconnectStopped) {
|
|
140
137
|
this.setState('disconnected')
|
|
138
|
+
this.rejectConnect(cause ?? new Error('Connection lost'))
|
|
141
139
|
return
|
|
142
140
|
}
|
|
143
141
|
|
|
144
142
|
if (this.reconnectAttempts >= this.reconnectOptions.maxAttempts) {
|
|
145
143
|
this.emit('error', new Error('Max reconnection attempts reached'))
|
|
146
144
|
this.setState('disconnected')
|
|
145
|
+
this.rejectConnect(cause ?? new Error('Max reconnection attempts reached'))
|
|
147
146
|
return
|
|
148
147
|
}
|
|
149
148
|
|
|
@@ -168,11 +167,7 @@ export class ClientConnection<TReceive extends ProtocolDef, TSend extends Protoc
|
|
|
168
167
|
this.setupWebSocket(ws)
|
|
169
168
|
ws.onopen = () => this.handleOpen()
|
|
170
169
|
} catch (error) {
|
|
171
|
-
|
|
172
|
-
this.connectReject(error instanceof Error ? error : new Error('Connection failed'))
|
|
173
|
-
this.clearConnectPromise()
|
|
174
|
-
}
|
|
175
|
-
this.handleConnectionLost()
|
|
170
|
+
this.handleConnectionLost(error instanceof Error ? error : new Error('Connection failed'))
|
|
176
171
|
}
|
|
177
172
|
}
|
|
178
173
|
|
|
@@ -193,6 +188,13 @@ export class ClientConnection<TReceive extends ProtocolDef, TSend extends Protoc
|
|
|
193
188
|
}
|
|
194
189
|
}
|
|
195
190
|
|
|
191
|
+
private rejectConnect(error: Error): void {
|
|
192
|
+
if (this.connectReject) {
|
|
193
|
+
this.connectReject(error)
|
|
194
|
+
this.clearConnectPromise()
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
196
198
|
private clearConnectPromise(): void {
|
|
197
199
|
this.connectPromise = null
|
|
198
200
|
this.connectResolve = null
|