@nmtjs/client 0.15.3 → 0.16.0-beta.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/dist/client.d.ts +65 -0
- package/dist/client.js +98 -0
- package/dist/client.js.map +1 -0
- package/dist/clients/runtime.d.ts +6 -12
- package/dist/clients/runtime.js +58 -57
- package/dist/clients/runtime.js.map +1 -1
- package/dist/clients/static.d.ts +4 -9
- package/dist/clients/static.js +20 -20
- package/dist/clients/static.js.map +1 -1
- package/dist/core.d.ts +36 -83
- package/dist/core.js +315 -690
- package/dist/core.js.map +1 -1
- package/dist/events.d.ts +0 -1
- package/dist/events.js +74 -11
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/layers/ping.d.ts +6 -0
- package/dist/layers/ping.js +65 -0
- package/dist/layers/ping.js.map +1 -0
- package/dist/layers/rpc.d.ts +19 -0
- package/dist/layers/rpc.js +564 -0
- package/dist/layers/rpc.js.map +1 -0
- package/dist/layers/streams.d.ts +20 -0
- package/dist/layers/streams.js +194 -0
- package/dist/layers/streams.js.map +1 -0
- package/dist/plugins/browser.js +28 -9
- package/dist/plugins/browser.js.map +1 -1
- package/dist/plugins/heartbeat.js +10 -10
- package/dist/plugins/heartbeat.js.map +1 -1
- package/dist/plugins/index.d.ts +1 -1
- package/dist/plugins/index.js +0 -1
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/reconnect.js +11 -94
- package/dist/plugins/reconnect.js.map +1 -1
- package/dist/plugins/types.d.ts +27 -11
- package/dist/transport.d.ts +49 -31
- package/dist/types.d.ts +21 -5
- package/package.json +10 -10
- package/src/client.ts +218 -0
- package/src/clients/runtime.ts +93 -79
- package/src/clients/static.ts +46 -38
- package/src/core.ts +408 -901
- package/src/events.ts +113 -14
- package/src/index.ts +4 -0
- package/src/layers/ping.ts +99 -0
- package/src/layers/rpc.ts +778 -0
- package/src/layers/streams.ts +277 -0
- package/src/plugins/browser.ts +39 -9
- package/src/plugins/heartbeat.ts +10 -10
- package/src/plugins/index.ts +8 -1
- package/src/plugins/reconnect.ts +12 -119
- package/src/plugins/types.ts +30 -13
- package/src/transport.ts +75 -46
- package/src/types.ts +33 -8
package/dist/core.js
CHANGED
|
@@ -1,381 +1,265 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { ProtocolError,
|
|
1
|
+
import { noopFn } from '@nmtjs/common';
|
|
2
|
+
import { ConnectionType } from '@nmtjs/protocol';
|
|
3
|
+
import { ProtocolError, versions } from '@nmtjs/protocol/client';
|
|
4
4
|
import { EventEmitter } from './events.js';
|
|
5
|
-
import { ClientStreams, ServerStreams } from './streams.js';
|
|
6
5
|
export { ErrorCode, ProtocolBlob, } from '@nmtjs/protocol';
|
|
7
|
-
export * from './types.js';
|
|
8
6
|
export class ClientError extends ProtocolError {
|
|
9
7
|
}
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
8
|
+
const DEFAULT_RECONNECT_TIMEOUT = 1000;
|
|
9
|
+
const DEFAULT_MAX_RECONNECT_TIMEOUT = 60000;
|
|
10
|
+
const DEFAULT_CONNECT_ERROR_REASON = 'connect_error';
|
|
11
|
+
const sleep = (ms, signal) => {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
if (signal?.aborted)
|
|
14
|
+
return resolve();
|
|
15
|
+
const timer = setTimeout(resolve, ms);
|
|
16
|
+
signal?.addEventListener('abort', () => {
|
|
17
|
+
clearTimeout(timer);
|
|
18
|
+
resolve();
|
|
19
|
+
}, { once: true });
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
const computeReconnectDelay = (ms) => {
|
|
23
|
+
if (globalThis.window) {
|
|
24
|
+
const jitter = Math.floor(ms * 0.2 * Math.random());
|
|
25
|
+
return ms + jitter;
|
|
26
|
+
}
|
|
27
|
+
return ms;
|
|
28
|
+
};
|
|
29
|
+
export class ClientCore extends EventEmitter {
|
|
21
30
|
transport;
|
|
22
31
|
protocol;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
format;
|
|
33
|
+
application;
|
|
34
|
+
autoConnect;
|
|
35
|
+
auth;
|
|
36
|
+
messageContext = null;
|
|
37
|
+
#state = 'idle';
|
|
38
|
+
#messageContextFactory = null;
|
|
39
|
+
#cab = null;
|
|
40
|
+
#connecting = null;
|
|
41
|
+
#disposed = false;
|
|
42
|
+
#plugins = [];
|
|
43
|
+
#lastDisconnectReason = 'server';
|
|
44
|
+
#clientDisconnectAsReconnect = false;
|
|
45
|
+
#clientDisconnectOverrideReason = null;
|
|
46
|
+
#reconnectConfig = null;
|
|
47
|
+
#reconnectPauseReasons = new Set();
|
|
48
|
+
#reconnectController = null;
|
|
49
|
+
#reconnectPromise = null;
|
|
50
|
+
#reconnectTimeout = DEFAULT_RECONNECT_TIMEOUT;
|
|
51
|
+
#reconnectImmediate = false;
|
|
52
|
+
constructor(options, transport) {
|
|
41
53
|
super();
|
|
42
|
-
this.
|
|
43
|
-
this.transportFactory = transportFactory;
|
|
44
|
-
this.transportOptions = transportOptions;
|
|
54
|
+
this.transport = transport;
|
|
45
55
|
this.protocol = versions[options.protocol];
|
|
46
|
-
|
|
47
|
-
this.
|
|
48
|
-
this.
|
|
49
|
-
for (const plugin of this.plugins) {
|
|
50
|
-
plugin.onInit?.();
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
dispose() {
|
|
54
|
-
this._disposed = true;
|
|
55
|
-
this.stopAllPendingPings('dispose');
|
|
56
|
-
for (let i = this.plugins.length - 1; i >= 0; i--) {
|
|
57
|
-
this.plugins[i].dispose?.();
|
|
58
|
-
}
|
|
56
|
+
this.format = options.format;
|
|
57
|
+
this.application = options.application;
|
|
58
|
+
this.autoConnect = options.autoConnect ?? false;
|
|
59
59
|
}
|
|
60
60
|
get state() {
|
|
61
|
-
return this
|
|
61
|
+
return this.#state;
|
|
62
62
|
}
|
|
63
63
|
get lastDisconnectReason() {
|
|
64
|
-
return this
|
|
64
|
+
return this.#lastDisconnectReason;
|
|
65
65
|
}
|
|
66
66
|
get transportType() {
|
|
67
67
|
return this.transport.type;
|
|
68
68
|
}
|
|
69
|
+
get connectionSignal() {
|
|
70
|
+
return this.#cab?.signal;
|
|
71
|
+
}
|
|
69
72
|
isDisposed() {
|
|
70
|
-
return this
|
|
73
|
+
return this.#disposed;
|
|
71
74
|
}
|
|
72
|
-
|
|
73
|
-
return this.
|
|
75
|
+
shouldConnectOnCall() {
|
|
76
|
+
return (this.autoConnect &&
|
|
77
|
+
!this.#disposed &&
|
|
78
|
+
this.#lastDisconnectReason !== 'client' &&
|
|
79
|
+
(this.#state === 'idle' ||
|
|
80
|
+
this.#state === 'connecting' ||
|
|
81
|
+
this.#state === 'disconnected'));
|
|
74
82
|
}
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
initPlugins(plugins = [], context) {
|
|
84
|
+
if (this.#plugins.length > 0)
|
|
85
|
+
return;
|
|
86
|
+
this.#plugins = plugins.map((plugin) => plugin(context));
|
|
87
|
+
for (const plugin of this.#plugins) {
|
|
88
|
+
plugin.onInit?.();
|
|
89
|
+
}
|
|
77
90
|
}
|
|
78
|
-
|
|
79
|
-
this
|
|
91
|
+
setMessageContextFactory(factory) {
|
|
92
|
+
this.#messageContextFactory = factory;
|
|
93
|
+
}
|
|
94
|
+
configureReconnect(config) {
|
|
95
|
+
this.#reconnectConfig = config;
|
|
96
|
+
this.#reconnectTimeout = config?.initialTimeout ?? DEFAULT_RECONNECT_TIMEOUT;
|
|
97
|
+
this.#reconnectImmediate = false;
|
|
98
|
+
if (!config) {
|
|
99
|
+
this.#cancelReconnectLoop();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (this.transport.type === ConnectionType.Bidirectional &&
|
|
103
|
+
this.#state === 'disconnected' &&
|
|
104
|
+
this.#lastDisconnectReason !== 'client') {
|
|
105
|
+
this.#ensureReconnectLoop();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
setReconnectPauseReason(reason, active) {
|
|
109
|
+
if (active) {
|
|
110
|
+
this.#reconnectPauseReasons.add(reason);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
this.#reconnectPauseReasons.delete(reason);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
triggerReconnect() {
|
|
117
|
+
if (this.#disposed ||
|
|
118
|
+
!this.#reconnectConfig ||
|
|
119
|
+
this.transport.type !== ConnectionType.Bidirectional) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
this.#reconnectImmediate = true;
|
|
123
|
+
if (this.#state === 'disconnected' || this.#state === 'idle') {
|
|
124
|
+
this.#ensureReconnectLoop();
|
|
125
|
+
}
|
|
80
126
|
}
|
|
81
127
|
connect() {
|
|
82
|
-
if (this
|
|
83
|
-
return Promise.resolve();
|
|
84
|
-
if (this.connecting)
|
|
85
|
-
return this.connecting;
|
|
86
|
-
if (this._disposed)
|
|
128
|
+
if (this.#disposed) {
|
|
87
129
|
return Promise.reject(new Error('Client is disposed'));
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
byteLength: 65535,
|
|
116
|
-
});
|
|
117
|
-
transport.send(protocol.encodeMessage(this, ClientMessageType.ServerStreamPull, { streamId, size: 65535 /* 64kb by default */ }));
|
|
118
|
-
},
|
|
119
|
-
close: () => {
|
|
120
|
-
serverStreams.remove(streamId);
|
|
121
|
-
},
|
|
122
|
-
readableStrategy: { highWaterMark: 0 },
|
|
123
|
-
});
|
|
124
|
-
serverStreams.add(streamId, stream);
|
|
125
|
-
return ({ signal } = {}) => {
|
|
126
|
-
if (signal)
|
|
127
|
-
signal.addEventListener('abort', () => {
|
|
128
|
-
client.emitStreamEvent({
|
|
129
|
-
direction: 'outgoing',
|
|
130
|
-
streamType: 'server_blob',
|
|
131
|
-
action: 'abort',
|
|
132
|
-
streamId,
|
|
133
|
-
});
|
|
134
|
-
transport.send(protocol.encodeMessage(this, ClientMessageType.ServerStreamAbort, { streamId }));
|
|
135
|
-
serverStreams.abort(streamId);
|
|
136
|
-
}, { once: true });
|
|
137
|
-
return stream;
|
|
138
|
-
};
|
|
139
|
-
},
|
|
140
|
-
streamId: this.getStreamId.bind(this),
|
|
141
|
-
};
|
|
142
|
-
return this.transport.connect({
|
|
143
|
-
auth: this.auth,
|
|
144
|
-
application: this.options.application,
|
|
145
|
-
onMessage: this.onMessage.bind(this),
|
|
146
|
-
onConnect: this.onConnect.bind(this),
|
|
147
|
-
onDisconnect: this.onDisconnect.bind(this),
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
let emitDisconnectOnFailure = null;
|
|
152
|
-
this.connecting = _connect()
|
|
153
|
-
.then(() => {
|
|
154
|
-
this._state = 'connected';
|
|
130
|
+
}
|
|
131
|
+
if (this.#state === 'connected')
|
|
132
|
+
return Promise.resolve();
|
|
133
|
+
if (this.#connecting)
|
|
134
|
+
return this.#connecting;
|
|
135
|
+
if (this.transport.type === ConnectionType.Unidirectional) {
|
|
136
|
+
return this.#handleConnected();
|
|
137
|
+
}
|
|
138
|
+
if (!this.#messageContextFactory) {
|
|
139
|
+
return Promise.reject(new Error('Message context factory is not configured'));
|
|
140
|
+
}
|
|
141
|
+
this.#setState('connecting');
|
|
142
|
+
this.#cab = new AbortController();
|
|
143
|
+
this.messageContext = this.#messageContextFactory();
|
|
144
|
+
this.#connecting = this.transport
|
|
145
|
+
.connect({
|
|
146
|
+
auth: this.auth,
|
|
147
|
+
application: this.application,
|
|
148
|
+
onMessage: (message) => {
|
|
149
|
+
void this.#onMessage(message);
|
|
150
|
+
},
|
|
151
|
+
onConnect: () => {
|
|
152
|
+
void this.#handleConnected();
|
|
153
|
+
},
|
|
154
|
+
onDisconnect: (reason) => {
|
|
155
|
+
void this.#handleDisconnected(reason);
|
|
156
|
+
},
|
|
155
157
|
})
|
|
156
|
-
.catch((error) => {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
158
|
+
.catch(async (error) => {
|
|
159
|
+
this.messageContext = null;
|
|
160
|
+
this.#cab = null;
|
|
161
|
+
await this.#handleDisconnected(DEFAULT_CONNECT_ERROR_REASON);
|
|
160
162
|
throw error;
|
|
161
163
|
})
|
|
162
164
|
.finally(() => {
|
|
163
|
-
this
|
|
164
|
-
if (emitDisconnectOnFailure && !this._disposed) {
|
|
165
|
-
this._state = 'disconnected';
|
|
166
|
-
this._lastDisconnectReason = emitDisconnectOnFailure;
|
|
167
|
-
void this.onDisconnect(emitDisconnectOnFailure);
|
|
168
|
-
}
|
|
165
|
+
this.#connecting = null;
|
|
169
166
|
});
|
|
170
|
-
return this
|
|
167
|
+
return this.#connecting;
|
|
171
168
|
}
|
|
172
|
-
async disconnect(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
this
|
|
176
|
-
|
|
177
|
-
if (options.reconnect) {
|
|
178
|
-
this.clientDisconnectAsReconnect = true;
|
|
179
|
-
this.clientDisconnectOverrideReason = options.reason ?? 'server';
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
this.clientDisconnectAsReconnect = false;
|
|
183
|
-
this.clientDisconnectOverrideReason = null;
|
|
184
|
-
}
|
|
185
|
-
this.cab.abort();
|
|
186
|
-
await this.transport.disconnect();
|
|
187
|
-
this.messageContext = null;
|
|
188
|
-
this.cab = null;
|
|
169
|
+
async disconnect(reason = 'client') {
|
|
170
|
+
this.#cancelReconnectLoop();
|
|
171
|
+
if (this.transport.type === ConnectionType.Unidirectional) {
|
|
172
|
+
await this.#handleDisconnected(reason);
|
|
173
|
+
return;
|
|
189
174
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
async _call(procedure, payload, options = {}) {
|
|
195
|
-
const timeout = options.timeout ?? this.options.timeout;
|
|
196
|
-
const controller = new AbortController();
|
|
197
|
-
// attach all abort signals
|
|
198
|
-
const signals = [controller.signal];
|
|
199
|
-
if (timeout)
|
|
200
|
-
signals.push(AbortSignal.timeout(timeout));
|
|
201
|
-
if (options.signal)
|
|
202
|
-
signals.push(options.signal);
|
|
203
|
-
if (this.cab?.signal)
|
|
204
|
-
signals.push(this.cab.signal);
|
|
205
|
-
const signal = signals.length ? anyAbortSignal(...signals) : undefined;
|
|
206
|
-
const callId = this.getCallId();
|
|
207
|
-
const call = createFuture();
|
|
208
|
-
call.procedure = procedure;
|
|
209
|
-
call.signal = signal;
|
|
210
|
-
this.calls.set(callId, call);
|
|
211
|
-
this.emitClientEvent({
|
|
212
|
-
kind: 'rpc_request',
|
|
213
|
-
timestamp: Date.now(),
|
|
214
|
-
callId,
|
|
215
|
-
procedure,
|
|
216
|
-
body: payload,
|
|
217
|
-
});
|
|
218
|
-
// Check if signal is already aborted before proceeding
|
|
219
|
-
if (signal?.aborted) {
|
|
220
|
-
this.calls.delete(callId);
|
|
221
|
-
const error = new ProtocolError(ErrorCode.ClientRequestError, signal.reason);
|
|
222
|
-
call.reject(error);
|
|
175
|
+
if (this.#state === 'idle' || this.#state === 'disconnected') {
|
|
176
|
+
this.#lastDisconnectReason = reason;
|
|
177
|
+
this.#setState('disconnected');
|
|
178
|
+
return;
|
|
223
179
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
signal.addEventListener('abort', () => {
|
|
227
|
-
call.reject(new ProtocolError(ErrorCode.ClientRequestError, signal.reason));
|
|
228
|
-
if (this.transport.type === ConnectionType.Bidirectional &&
|
|
229
|
-
this.messageContext) {
|
|
230
|
-
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.RpcAbort, { callId });
|
|
231
|
-
this.send(buffer).catch(noopFn);
|
|
232
|
-
}
|
|
233
|
-
}, { once: true });
|
|
234
|
-
}
|
|
180
|
+
this.#setState('disconnecting');
|
|
181
|
+
if (this.#cab && !this.#cab.signal.aborted) {
|
|
235
182
|
try {
|
|
236
|
-
|
|
237
|
-
if (this.transport.type === ConnectionType.Bidirectional) {
|
|
238
|
-
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.Rpc, { callId, procedure, payload: transformedPayload });
|
|
239
|
-
await this.send(buffer, signal);
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
242
|
-
const response = await this.transport.call({
|
|
243
|
-
application: this.options.application,
|
|
244
|
-
format: this.options.format,
|
|
245
|
-
auth: this.auth,
|
|
246
|
-
}, { callId, procedure, payload: transformedPayload }, { signal, _stream_response: options._stream_response });
|
|
247
|
-
this.handleCallResponse(callId, response);
|
|
248
|
-
}
|
|
183
|
+
this.#cab.abort(reason);
|
|
249
184
|
}
|
|
250
|
-
catch
|
|
251
|
-
this.
|
|
252
|
-
kind: 'rpc_error',
|
|
253
|
-
timestamp: Date.now(),
|
|
254
|
-
callId,
|
|
255
|
-
procedure,
|
|
256
|
-
error,
|
|
257
|
-
});
|
|
258
|
-
call.reject(error);
|
|
185
|
+
catch {
|
|
186
|
+
this.#cab.abort();
|
|
259
187
|
}
|
|
260
188
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
if (options._stream_response && typeof value === 'function') {
|
|
268
|
-
return value;
|
|
189
|
+
try {
|
|
190
|
+
await this.transport.disconnect();
|
|
191
|
+
if (this.#state === 'disconnecting') {
|
|
192
|
+
await this.#handleDisconnected(reason);
|
|
269
193
|
}
|
|
270
|
-
controller.abort();
|
|
271
|
-
return value;
|
|
272
|
-
}, (err) => {
|
|
273
|
-
controller.abort();
|
|
274
|
-
throw err;
|
|
275
|
-
});
|
|
276
|
-
if (this.options.safe) {
|
|
277
|
-
return await result
|
|
278
|
-
.then((result) => ({ result }))
|
|
279
|
-
.catch((error) => ({ error }))
|
|
280
|
-
.finally(() => {
|
|
281
|
-
this.calls.delete(callId);
|
|
282
|
-
});
|
|
283
194
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
});
|
|
195
|
+
catch (error) {
|
|
196
|
+
await this.#handleDisconnected(reason);
|
|
197
|
+
throw error;
|
|
288
198
|
}
|
|
289
199
|
}
|
|
290
|
-
|
|
291
|
-
this.
|
|
292
|
-
|
|
293
|
-
this.emitClientEvent({
|
|
294
|
-
kind: 'connected',
|
|
295
|
-
timestamp: Date.now(),
|
|
296
|
-
transportType: this.transport.type === ConnectionType.Bidirectional
|
|
297
|
-
? 'bidirectional'
|
|
298
|
-
: 'unidirectional',
|
|
299
|
-
});
|
|
300
|
-
for (const plugin of this.plugins) {
|
|
301
|
-
await plugin.onConnect?.();
|
|
200
|
+
requestReconnect(reason = 'server') {
|
|
201
|
+
if (this.transport.type !== ConnectionType.Bidirectional) {
|
|
202
|
+
return Promise.resolve();
|
|
302
203
|
}
|
|
303
|
-
this
|
|
204
|
+
this.#clientDisconnectAsReconnect = true;
|
|
205
|
+
this.#clientDisconnectOverrideReason = reason;
|
|
206
|
+
return this.disconnect('client');
|
|
304
207
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
this
|
|
310
|
-
this.clientDisconnectOverrideReason = null;
|
|
311
|
-
this._state = 'disconnected';
|
|
312
|
-
this._lastDisconnectReason = effectiveReason;
|
|
313
|
-
this.emitClientEvent({
|
|
314
|
-
kind: 'disconnected',
|
|
315
|
-
timestamp: Date.now(),
|
|
316
|
-
reason: effectiveReason,
|
|
317
|
-
});
|
|
318
|
-
// Connection is gone, never keep old message context around.
|
|
208
|
+
dispose() {
|
|
209
|
+
if (this.#disposed)
|
|
210
|
+
return;
|
|
211
|
+
this.#disposed = true;
|
|
212
|
+
this.#cancelReconnectLoop();
|
|
319
213
|
this.messageContext = null;
|
|
320
|
-
this.
|
|
321
|
-
// Fail-fast: do not keep pending calls around across disconnects.
|
|
322
|
-
if (this.calls.size) {
|
|
323
|
-
const error = new ProtocolError(ErrorCode.ConnectionError, 'Disconnected', { reason: effectiveReason });
|
|
324
|
-
for (const call of this.calls.values()) {
|
|
325
|
-
call.reject(error);
|
|
326
|
-
}
|
|
327
|
-
this.calls.clear();
|
|
328
|
-
}
|
|
329
|
-
if (this.cab) {
|
|
214
|
+
if (this.#cab && !this.#cab.signal.aborted) {
|
|
330
215
|
try {
|
|
331
|
-
this
|
|
216
|
+
this.#cab.abort('dispose');
|
|
332
217
|
}
|
|
333
218
|
catch {
|
|
334
|
-
this
|
|
219
|
+
this.#cab.abort();
|
|
335
220
|
}
|
|
336
|
-
this.cab = null;
|
|
337
221
|
}
|
|
338
|
-
this.
|
|
339
|
-
|
|
340
|
-
|
|
222
|
+
if (this.transport.type === ConnectionType.Bidirectional &&
|
|
223
|
+
(this.#state === 'connecting' || this.#state === 'connected')) {
|
|
224
|
+
void this.transport.disconnect().catch(noopFn);
|
|
225
|
+
}
|
|
226
|
+
for (let i = this.#plugins.length - 1; i >= 0; i--) {
|
|
227
|
+
this.#plugins[i].dispose?.();
|
|
341
228
|
}
|
|
342
|
-
void this.clientStreams.clear(effectiveReason);
|
|
343
|
-
void this.serverStreams.clear(effectiveReason);
|
|
344
|
-
void this.rpcStreams.clear(effectiveReason);
|
|
345
229
|
}
|
|
346
|
-
|
|
347
|
-
if (this.
|
|
348
|
-
|
|
349
|
-
|
|
230
|
+
send(buffer, signal) {
|
|
231
|
+
if (this.transport.type !== ConnectionType.Bidirectional) {
|
|
232
|
+
throw new Error('Invalid transport type for send');
|
|
233
|
+
}
|
|
234
|
+
return this.transport.send(buffer, { signal });
|
|
350
235
|
}
|
|
351
|
-
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
return Promise.reject(new Error('Client is not connected'));
|
|
236
|
+
transportCall(context, rpc, options) {
|
|
237
|
+
if (this.transport.type !== ConnectionType.Unidirectional) {
|
|
238
|
+
throw new Error('Invalid transport type for call');
|
|
355
239
|
}
|
|
356
|
-
|
|
357
|
-
const future = createFuture();
|
|
358
|
-
this.pendingPings.set(nonce, future);
|
|
359
|
-
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.Ping, { nonce });
|
|
360
|
-
return this.send(buffer, signal)
|
|
361
|
-
.then(() => withTimeout(future.promise, timeout, new Error('Heartbeat timeout')))
|
|
362
|
-
.finally(() => {
|
|
363
|
-
this.pendingPings.delete(nonce);
|
|
364
|
-
});
|
|
240
|
+
return this.transport.call(context, rpc, options);
|
|
365
241
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
242
|
+
emitClientEvent(event) {
|
|
243
|
+
for (const plugin of this.#plugins) {
|
|
244
|
+
try {
|
|
245
|
+
const result = plugin.onClientEvent?.(event);
|
|
246
|
+
Promise.resolve(result).catch(noopFn);
|
|
247
|
+
}
|
|
248
|
+
catch { }
|
|
249
|
+
}
|
|
373
250
|
}
|
|
374
|
-
|
|
251
|
+
emitStreamEvent(event) {
|
|
252
|
+
this.emitClientEvent({
|
|
253
|
+
kind: 'stream_event',
|
|
254
|
+
timestamp: Date.now(),
|
|
255
|
+
...event,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
async #onMessage(buffer) {
|
|
375
259
|
if (!this.messageContext)
|
|
376
260
|
return;
|
|
377
261
|
const message = this.protocol.decodeMessage(this.messageContext, buffer);
|
|
378
|
-
for (const plugin of this
|
|
262
|
+
for (const plugin of this.#plugins) {
|
|
379
263
|
plugin.onServerMessage?.(message, buffer);
|
|
380
264
|
}
|
|
381
265
|
this.emitClientEvent({
|
|
@@ -385,390 +269,131 @@ export class BaseClient extends EventEmitter {
|
|
|
385
269
|
rawByteLength: buffer.byteLength,
|
|
386
270
|
body: message,
|
|
387
271
|
});
|
|
388
|
-
|
|
389
|
-
case ServerMessageType.RpcResponse:
|
|
390
|
-
this.handleRPCResponseMessage(message);
|
|
391
|
-
break;
|
|
392
|
-
case ServerMessageType.RpcStreamResponse:
|
|
393
|
-
this.handleRPCStreamResponseMessage(message);
|
|
394
|
-
break;
|
|
395
|
-
case ServerMessageType.Pong: {
|
|
396
|
-
const pending = this.pendingPings.get(message.nonce);
|
|
397
|
-
if (pending) {
|
|
398
|
-
this.pendingPings.delete(message.nonce);
|
|
399
|
-
pending.resolve();
|
|
400
|
-
}
|
|
401
|
-
this.emit('pong', message.nonce);
|
|
402
|
-
break;
|
|
403
|
-
}
|
|
404
|
-
case ServerMessageType.Ping: {
|
|
405
|
-
if (this.messageContext) {
|
|
406
|
-
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.Pong, { nonce: message.nonce });
|
|
407
|
-
this.send(buffer).catch(noopFn);
|
|
408
|
-
}
|
|
409
|
-
break;
|
|
410
|
-
}
|
|
411
|
-
case ServerMessageType.RpcStreamChunk:
|
|
412
|
-
this.emitStreamEvent({
|
|
413
|
-
direction: 'incoming',
|
|
414
|
-
streamType: 'rpc',
|
|
415
|
-
action: 'push',
|
|
416
|
-
callId: message.callId,
|
|
417
|
-
byteLength: message.chunk.byteLength,
|
|
418
|
-
});
|
|
419
|
-
this.rpcStreams.push(message.callId, message.chunk);
|
|
420
|
-
break;
|
|
421
|
-
case ServerMessageType.RpcStreamEnd:
|
|
422
|
-
this.emitStreamEvent({
|
|
423
|
-
direction: 'incoming',
|
|
424
|
-
streamType: 'rpc',
|
|
425
|
-
action: 'end',
|
|
426
|
-
callId: message.callId,
|
|
427
|
-
});
|
|
428
|
-
this.rpcStreams.end(message.callId);
|
|
429
|
-
this.calls.delete(message.callId);
|
|
430
|
-
break;
|
|
431
|
-
case ServerMessageType.RpcStreamAbort:
|
|
432
|
-
this.emitStreamEvent({
|
|
433
|
-
direction: 'incoming',
|
|
434
|
-
streamType: 'rpc',
|
|
435
|
-
action: 'abort',
|
|
436
|
-
callId: message.callId,
|
|
437
|
-
reason: message.reason,
|
|
438
|
-
});
|
|
439
|
-
this.rpcStreams.abort(message.callId);
|
|
440
|
-
this.calls.delete(message.callId);
|
|
441
|
-
break;
|
|
442
|
-
case ServerMessageType.ServerStreamPush:
|
|
443
|
-
this.emitStreamEvent({
|
|
444
|
-
direction: 'incoming',
|
|
445
|
-
streamType: 'server_blob',
|
|
446
|
-
action: 'push',
|
|
447
|
-
streamId: message.streamId,
|
|
448
|
-
byteLength: message.chunk.byteLength,
|
|
449
|
-
});
|
|
450
|
-
this.serverStreams.push(message.streamId, message.chunk);
|
|
451
|
-
break;
|
|
452
|
-
case ServerMessageType.ServerStreamEnd:
|
|
453
|
-
this.emitStreamEvent({
|
|
454
|
-
direction: 'incoming',
|
|
455
|
-
streamType: 'server_blob',
|
|
456
|
-
action: 'end',
|
|
457
|
-
streamId: message.streamId,
|
|
458
|
-
});
|
|
459
|
-
this.serverStreams.end(message.streamId);
|
|
460
|
-
break;
|
|
461
|
-
case ServerMessageType.ServerStreamAbort:
|
|
462
|
-
this.emitStreamEvent({
|
|
463
|
-
direction: 'incoming',
|
|
464
|
-
streamType: 'server_blob',
|
|
465
|
-
action: 'abort',
|
|
466
|
-
streamId: message.streamId,
|
|
467
|
-
reason: message.reason,
|
|
468
|
-
});
|
|
469
|
-
this.serverStreams.abort(message.streamId);
|
|
470
|
-
break;
|
|
471
|
-
case ServerMessageType.ClientStreamPull:
|
|
472
|
-
this.emitStreamEvent({
|
|
473
|
-
direction: 'incoming',
|
|
474
|
-
streamType: 'client_blob',
|
|
475
|
-
action: 'pull',
|
|
476
|
-
streamId: message.streamId,
|
|
477
|
-
byteLength: message.size,
|
|
478
|
-
});
|
|
479
|
-
this.clientStreams.pull(message.streamId, message.size).then((chunk) => {
|
|
480
|
-
if (chunk) {
|
|
481
|
-
this.emitStreamEvent({
|
|
482
|
-
direction: 'outgoing',
|
|
483
|
-
streamType: 'client_blob',
|
|
484
|
-
action: 'push',
|
|
485
|
-
streamId: message.streamId,
|
|
486
|
-
byteLength: chunk.byteLength,
|
|
487
|
-
});
|
|
488
|
-
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.ClientStreamPush, { streamId: message.streamId, chunk });
|
|
489
|
-
this.send(buffer).catch(noopFn);
|
|
490
|
-
}
|
|
491
|
-
else {
|
|
492
|
-
this.emitStreamEvent({
|
|
493
|
-
direction: 'outgoing',
|
|
494
|
-
streamType: 'client_blob',
|
|
495
|
-
action: 'end',
|
|
496
|
-
streamId: message.streamId,
|
|
497
|
-
});
|
|
498
|
-
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.ClientStreamEnd, { streamId: message.streamId });
|
|
499
|
-
this.send(buffer).catch(noopFn);
|
|
500
|
-
this.clientStreams.end(message.streamId);
|
|
501
|
-
}
|
|
502
|
-
}, () => {
|
|
503
|
-
this.emitStreamEvent({
|
|
504
|
-
direction: 'outgoing',
|
|
505
|
-
streamType: 'client_blob',
|
|
506
|
-
action: 'abort',
|
|
507
|
-
streamId: message.streamId,
|
|
508
|
-
});
|
|
509
|
-
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.ClientStreamAbort, { streamId: message.streamId });
|
|
510
|
-
this.send(buffer).catch(noopFn);
|
|
511
|
-
this.clientStreams.remove(message.streamId);
|
|
512
|
-
});
|
|
513
|
-
break;
|
|
514
|
-
case ServerMessageType.ClientStreamAbort:
|
|
515
|
-
this.emitStreamEvent({
|
|
516
|
-
direction: 'incoming',
|
|
517
|
-
streamType: 'client_blob',
|
|
518
|
-
action: 'abort',
|
|
519
|
-
streamId: message.streamId,
|
|
520
|
-
reason: message.reason,
|
|
521
|
-
});
|
|
522
|
-
this.clientStreams.abort(message.streamId);
|
|
523
|
-
break;
|
|
524
|
-
}
|
|
272
|
+
this.emit('message', message, buffer);
|
|
525
273
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
else {
|
|
542
|
-
try {
|
|
543
|
-
const transformed = this.transformer.decode(call.procedure, result);
|
|
544
|
-
this.emitClientEvent({
|
|
545
|
-
kind: 'rpc_response',
|
|
546
|
-
timestamp: Date.now(),
|
|
547
|
-
callId,
|
|
548
|
-
procedure: call.procedure,
|
|
549
|
-
body: transformed,
|
|
550
|
-
});
|
|
551
|
-
call.resolve(transformed);
|
|
552
|
-
}
|
|
553
|
-
catch (error) {
|
|
554
|
-
this.emitClientEvent({
|
|
555
|
-
kind: 'rpc_error',
|
|
556
|
-
timestamp: Date.now(),
|
|
557
|
-
callId,
|
|
558
|
-
procedure: call.procedure,
|
|
559
|
-
error,
|
|
560
|
-
});
|
|
561
|
-
call.reject(new ProtocolError(ErrorCode.ClientRequestError, 'Unable to decode response', error));
|
|
562
|
-
}
|
|
274
|
+
async #handleConnected() {
|
|
275
|
+
this.#reconnectTimeout =
|
|
276
|
+
this.#reconnectConfig?.initialTimeout ?? DEFAULT_RECONNECT_TIMEOUT;
|
|
277
|
+
this.#reconnectImmediate = false;
|
|
278
|
+
this.#setState('connected');
|
|
279
|
+
this.#lastDisconnectReason = 'server';
|
|
280
|
+
this.emitClientEvent({
|
|
281
|
+
kind: 'connected',
|
|
282
|
+
timestamp: Date.now(),
|
|
283
|
+
transportType: this.transport.type === ConnectionType.Bidirectional
|
|
284
|
+
? 'bidirectional'
|
|
285
|
+
: 'unidirectional',
|
|
286
|
+
});
|
|
287
|
+
for (const plugin of this.#plugins) {
|
|
288
|
+
await plugin.onConnect?.();
|
|
563
289
|
}
|
|
290
|
+
this.emit('connected');
|
|
564
291
|
}
|
|
565
|
-
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
this.emitClientEvent({
|
|
583
|
-
kind: 'rpc_response',
|
|
584
|
-
timestamp: Date.now(),
|
|
585
|
-
callId: message.callId,
|
|
586
|
-
procedure,
|
|
587
|
-
stream: true,
|
|
588
|
-
});
|
|
589
|
-
const stream = new ProtocolServerRPCStream({
|
|
590
|
-
start: (controller) => {
|
|
591
|
-
if (signal) {
|
|
592
|
-
if (signal.aborted)
|
|
593
|
-
controller.error(signal.reason);
|
|
594
|
-
else
|
|
595
|
-
signal.addEventListener('abort', () => {
|
|
596
|
-
controller.error(signal.reason);
|
|
597
|
-
if (this.rpcStreams.has(message.callId)) {
|
|
598
|
-
this.rpcStreams.remove(message.callId);
|
|
599
|
-
this.calls.delete(message.callId);
|
|
600
|
-
if (this.messageContext) {
|
|
601
|
-
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.RpcAbort, { callId: message.callId, reason: signal.reason });
|
|
602
|
-
this.send(buffer).catch(noopFn);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}, { once: true });
|
|
606
|
-
}
|
|
607
|
-
},
|
|
608
|
-
transform: (chunk) => {
|
|
609
|
-
return this.transformer.decode(procedure, this.options.format.decode(chunk));
|
|
610
|
-
},
|
|
611
|
-
readableStrategy: { highWaterMark: 0 },
|
|
612
|
-
});
|
|
613
|
-
this.rpcStreams.add(message.callId, stream);
|
|
614
|
-
call.resolve(stream);
|
|
615
|
-
}
|
|
616
|
-
else {
|
|
617
|
-
// Call not found, but stream response received
|
|
618
|
-
// This can happen if the call was aborted or timed out
|
|
619
|
-
// Need to send an abort for the stream to avoid resource leaks from server side
|
|
620
|
-
if (this.messageContext) {
|
|
621
|
-
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.RpcAbort, { callId: message.callId });
|
|
622
|
-
this.send(buffer).catch(noopFn);
|
|
292
|
+
async #handleDisconnected(reason) {
|
|
293
|
+
const effectiveReason = reason === 'client' && this.#clientDisconnectAsReconnect
|
|
294
|
+
? (this.#clientDisconnectOverrideReason ?? 'server')
|
|
295
|
+
: reason;
|
|
296
|
+
this.#clientDisconnectAsReconnect = false;
|
|
297
|
+
this.#clientDisconnectOverrideReason = null;
|
|
298
|
+
const shouldSkip = this.#state === 'disconnected' &&
|
|
299
|
+
this.messageContext === null &&
|
|
300
|
+
this.#lastDisconnectReason === effectiveReason;
|
|
301
|
+
this.messageContext = null;
|
|
302
|
+
if (this.#cab) {
|
|
303
|
+
if (!this.#cab.signal.aborted) {
|
|
304
|
+
try {
|
|
305
|
+
this.#cab.abort(reason);
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
this.#cab.abort();
|
|
623
309
|
}
|
|
624
310
|
}
|
|
311
|
+
this.#cab = null;
|
|
625
312
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
const stream = new ProtocolServerStream({
|
|
639
|
-
transform: (chunk) => {
|
|
640
|
-
return this.transformer.decode(call.procedure, this.options.format.decode(chunk));
|
|
641
|
-
},
|
|
642
|
-
});
|
|
643
|
-
this.rpcStreams.add(callId, stream);
|
|
644
|
-
call.resolve(({ signal }) => {
|
|
645
|
-
const reader = response.stream.getReader();
|
|
646
|
-
let onAbort;
|
|
647
|
-
if (signal) {
|
|
648
|
-
onAbort = () => {
|
|
649
|
-
reader.cancel(signal.reason).catch(noopFn);
|
|
650
|
-
this.rpcStreams.abort(callId).catch(noopFn);
|
|
651
|
-
};
|
|
652
|
-
if (signal.aborted)
|
|
653
|
-
onAbort();
|
|
654
|
-
else
|
|
655
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
656
|
-
}
|
|
657
|
-
void (async () => {
|
|
658
|
-
try {
|
|
659
|
-
while (true) {
|
|
660
|
-
const { done, value } = await reader.read();
|
|
661
|
-
if (done)
|
|
662
|
-
break;
|
|
663
|
-
await this.rpcStreams.push(callId, value);
|
|
664
|
-
}
|
|
665
|
-
await this.rpcStreams.end(callId);
|
|
666
|
-
}
|
|
667
|
-
catch {
|
|
668
|
-
await this.rpcStreams.abort(callId).catch(noopFn);
|
|
669
|
-
}
|
|
670
|
-
finally {
|
|
671
|
-
reader.releaseLock();
|
|
672
|
-
if (signal && onAbort) {
|
|
673
|
-
signal.removeEventListener('abort', onAbort);
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
})();
|
|
677
|
-
return stream;
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
else {
|
|
681
|
-
// Call not found, but stream response received
|
|
682
|
-
// This can happen if the call was aborted or timed out
|
|
683
|
-
// Need to cancel the stream to avoid resource leaks from server side
|
|
684
|
-
response.stream.cancel().catch(noopFn);
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
else if (response.type === 'blob') {
|
|
688
|
-
if (call) {
|
|
689
|
-
this.emitClientEvent({
|
|
690
|
-
kind: 'rpc_response',
|
|
691
|
-
timestamp: Date.now(),
|
|
692
|
-
callId,
|
|
693
|
-
procedure: call.procedure,
|
|
694
|
-
stream: true,
|
|
695
|
-
});
|
|
696
|
-
const { metadata, source } = response;
|
|
697
|
-
const stream = new ProtocolServerBlobStream(metadata);
|
|
698
|
-
this.serverStreams.add(this.getStreamId(), stream);
|
|
699
|
-
call.resolve(({ signal }) => {
|
|
700
|
-
source.pipeTo(stream.writable, { signal }).catch(noopFn);
|
|
701
|
-
return stream;
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
|
-
else {
|
|
705
|
-
// Call not found, but blob response received
|
|
706
|
-
// This can happen if the call was aborted or timed out
|
|
707
|
-
// Need to cancel the stream to avoid resource leaks from server side
|
|
708
|
-
response.source.cancel().catch(noopFn);
|
|
709
|
-
}
|
|
313
|
+
if (shouldSkip)
|
|
314
|
+
return;
|
|
315
|
+
this.#lastDisconnectReason = effectiveReason;
|
|
316
|
+
this.#setState('disconnected');
|
|
317
|
+
this.emitClientEvent({
|
|
318
|
+
kind: 'disconnected',
|
|
319
|
+
timestamp: Date.now(),
|
|
320
|
+
reason: effectiveReason,
|
|
321
|
+
});
|
|
322
|
+
this.emit('disconnected', effectiveReason);
|
|
323
|
+
for (let i = this.#plugins.length - 1; i >= 0; i--) {
|
|
324
|
+
await this.#plugins[i].onDisconnect?.(effectiveReason);
|
|
710
325
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
return;
|
|
714
|
-
try {
|
|
715
|
-
const decodedPayload = response.result.byteLength === 0
|
|
716
|
-
? undefined
|
|
717
|
-
: this.options.format.decode(response.result);
|
|
718
|
-
const transformed = this.transformer.decode(call.procedure, decodedPayload);
|
|
719
|
-
this.emitClientEvent({
|
|
720
|
-
kind: 'rpc_response',
|
|
721
|
-
timestamp: Date.now(),
|
|
722
|
-
callId,
|
|
723
|
-
procedure: call.procedure,
|
|
724
|
-
body: transformed,
|
|
725
|
-
});
|
|
726
|
-
call.resolve(transformed);
|
|
727
|
-
}
|
|
728
|
-
catch (error) {
|
|
729
|
-
this.emitClientEvent({
|
|
730
|
-
kind: 'rpc_error',
|
|
731
|
-
timestamp: Date.now(),
|
|
732
|
-
callId,
|
|
733
|
-
procedure: call.procedure,
|
|
734
|
-
error,
|
|
735
|
-
});
|
|
736
|
-
call.reject(new ProtocolError(ErrorCode.ClientRequestError, 'Unable to decode response', error));
|
|
737
|
-
}
|
|
326
|
+
if (this.#shouldReconnect(effectiveReason)) {
|
|
327
|
+
this.#ensureReconnectLoop();
|
|
738
328
|
}
|
|
739
329
|
}
|
|
740
|
-
|
|
741
|
-
if (
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
emitStreamEvent(event) {
|
|
330
|
+
#setState(next) {
|
|
331
|
+
if (next === this.#state)
|
|
332
|
+
return;
|
|
333
|
+
const previous = this.#state;
|
|
334
|
+
this.#state = next;
|
|
746
335
|
this.emitClientEvent({
|
|
747
|
-
kind: '
|
|
336
|
+
kind: 'state_changed',
|
|
748
337
|
timestamp: Date.now(),
|
|
749
|
-
|
|
338
|
+
state: next,
|
|
339
|
+
previous,
|
|
750
340
|
});
|
|
341
|
+
this.emit('state_changed', next, previous);
|
|
751
342
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
this
|
|
755
|
-
|
|
756
|
-
|
|
343
|
+
#shouldReconnect(reason) {
|
|
344
|
+
return (!this.#disposed &&
|
|
345
|
+
!!this.#reconnectConfig &&
|
|
346
|
+
this.transport.type === ConnectionType.Bidirectional &&
|
|
347
|
+
reason !== 'client');
|
|
757
348
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
349
|
+
#cancelReconnectLoop() {
|
|
350
|
+
this.#reconnectImmediate = false;
|
|
351
|
+
this.#reconnectController?.abort();
|
|
352
|
+
this.#reconnectController = null;
|
|
353
|
+
this.#reconnectPromise = null;
|
|
763
354
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
355
|
+
#ensureReconnectLoop() {
|
|
356
|
+
if (this.#reconnectPromise || !this.#reconnectConfig)
|
|
357
|
+
return;
|
|
358
|
+
const signal = new AbortController();
|
|
359
|
+
this.#reconnectController = signal;
|
|
360
|
+
this.#reconnectPromise = (async () => {
|
|
361
|
+
while (!signal.signal.aborted &&
|
|
362
|
+
!this.#disposed &&
|
|
363
|
+
this.#reconnectConfig &&
|
|
364
|
+
(this.#state === 'disconnected' || this.#state === 'idle') &&
|
|
365
|
+
this.#lastDisconnectReason !== 'client') {
|
|
366
|
+
if (this.#reconnectPauseReasons.size) {
|
|
367
|
+
await sleep(1000, signal.signal);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const delay = this.#reconnectImmediate
|
|
371
|
+
? 0
|
|
372
|
+
: computeReconnectDelay(this.#reconnectTimeout);
|
|
373
|
+
this.#reconnectImmediate = false;
|
|
374
|
+
if (delay > 0) {
|
|
375
|
+
await sleep(delay, signal.signal);
|
|
376
|
+
}
|
|
377
|
+
const currentState = this.state;
|
|
378
|
+
if (signal.signal.aborted ||
|
|
379
|
+
this.#disposed ||
|
|
380
|
+
!this.#reconnectConfig ||
|
|
381
|
+
currentState === 'connected' ||
|
|
382
|
+
currentState === 'connecting') {
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
const previousTimeout = this.#reconnectTimeout;
|
|
386
|
+
await this.connect().catch(noopFn);
|
|
387
|
+
if (this.state !== 'connected' && this.#reconnectConfig) {
|
|
388
|
+
this.#reconnectTimeout = Math.min(previousTimeout * 2, this.#reconnectConfig.maxTimeout ?? DEFAULT_MAX_RECONNECT_TIMEOUT);
|
|
389
|
+
}
|
|
769
390
|
}
|
|
770
|
-
|
|
771
|
-
|
|
391
|
+
})().finally(() => {
|
|
392
|
+
if (this.#reconnectController === signal) {
|
|
393
|
+
this.#reconnectController = null;
|
|
394
|
+
}
|
|
395
|
+
this.#reconnectPromise = null;
|
|
396
|
+
});
|
|
772
397
|
}
|
|
773
398
|
}
|
|
774
399
|
//# sourceMappingURL=core.js.map
|