@nmtjs/client 0.15.3 → 0.16.0-beta.1
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 +64 -0
- package/dist/client.js +97 -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 +33 -83
- package/dist/core.js +305 -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 +521 -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 +216 -0
- package/src/clients/runtime.ts +93 -79
- package/src/clients/static.ts +46 -38
- package/src/core.ts +394 -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 +725 -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,255 @@
|
|
|
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
|
-
cab = null;
|
|
30
|
-
connecting = null;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
format;
|
|
33
|
+
application;
|
|
34
|
+
auth;
|
|
35
|
+
messageContext = null;
|
|
36
|
+
#state = 'idle';
|
|
37
|
+
#messageContextFactory = null;
|
|
38
|
+
#cab = null;
|
|
39
|
+
#connecting = null;
|
|
40
|
+
#disposed = false;
|
|
41
|
+
#plugins = [];
|
|
42
|
+
#lastDisconnectReason = 'server';
|
|
43
|
+
#clientDisconnectAsReconnect = false;
|
|
44
|
+
#clientDisconnectOverrideReason = null;
|
|
45
|
+
#reconnectConfig = null;
|
|
46
|
+
#reconnectPauseReasons = new Set();
|
|
47
|
+
#reconnectController = null;
|
|
48
|
+
#reconnectPromise = null;
|
|
49
|
+
#reconnectTimeout = DEFAULT_RECONNECT_TIMEOUT;
|
|
50
|
+
#reconnectImmediate = false;
|
|
51
|
+
constructor(options, transport) {
|
|
41
52
|
super();
|
|
42
|
-
this.
|
|
43
|
-
this.transportFactory = transportFactory;
|
|
44
|
-
this.transportOptions = transportOptions;
|
|
53
|
+
this.transport = transport;
|
|
45
54
|
this.protocol = versions[options.protocol];
|
|
46
|
-
|
|
47
|
-
this.
|
|
48
|
-
this.plugins = this.options.plugins?.map((plugin) => plugin(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
|
-
}
|
|
55
|
+
this.format = options.format;
|
|
56
|
+
this.application = options.application;
|
|
59
57
|
}
|
|
60
58
|
get state() {
|
|
61
|
-
return this
|
|
59
|
+
return this.#state;
|
|
62
60
|
}
|
|
63
61
|
get lastDisconnectReason() {
|
|
64
|
-
return this
|
|
62
|
+
return this.#lastDisconnectReason;
|
|
65
63
|
}
|
|
66
64
|
get transportType() {
|
|
67
65
|
return this.transport.type;
|
|
68
66
|
}
|
|
67
|
+
get connectionSignal() {
|
|
68
|
+
return this.#cab?.signal;
|
|
69
|
+
}
|
|
69
70
|
isDisposed() {
|
|
70
|
-
return this
|
|
71
|
+
return this.#disposed;
|
|
71
72
|
}
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
initPlugins(plugins = [], context) {
|
|
74
|
+
if (this.#plugins.length > 0)
|
|
75
|
+
return;
|
|
76
|
+
this.#plugins = plugins.map((plugin) => plugin(context));
|
|
77
|
+
for (const plugin of this.#plugins) {
|
|
78
|
+
plugin.onInit?.();
|
|
79
|
+
}
|
|
74
80
|
}
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
setMessageContextFactory(factory) {
|
|
82
|
+
this.#messageContextFactory = factory;
|
|
77
83
|
}
|
|
78
|
-
|
|
79
|
-
this
|
|
84
|
+
configureReconnect(config) {
|
|
85
|
+
this.#reconnectConfig = config;
|
|
86
|
+
this.#reconnectTimeout = config?.initialTimeout ?? DEFAULT_RECONNECT_TIMEOUT;
|
|
87
|
+
this.#reconnectImmediate = false;
|
|
88
|
+
if (!config) {
|
|
89
|
+
this.#cancelReconnectLoop();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (this.transport.type === ConnectionType.Bidirectional &&
|
|
93
|
+
this.#state === 'disconnected' &&
|
|
94
|
+
this.#lastDisconnectReason !== 'client') {
|
|
95
|
+
this.#ensureReconnectLoop();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
setReconnectPauseReason(reason, active) {
|
|
99
|
+
if (active) {
|
|
100
|
+
this.#reconnectPauseReasons.add(reason);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
this.#reconnectPauseReasons.delete(reason);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
triggerReconnect() {
|
|
107
|
+
if (this.#disposed ||
|
|
108
|
+
!this.#reconnectConfig ||
|
|
109
|
+
this.transport.type !== ConnectionType.Bidirectional) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
this.#reconnectImmediate = true;
|
|
113
|
+
if (this.#state === 'disconnected' || this.#state === 'idle') {
|
|
114
|
+
this.#ensureReconnectLoop();
|
|
115
|
+
}
|
|
80
116
|
}
|
|
81
117
|
connect() {
|
|
82
|
-
if (this
|
|
83
|
-
return Promise.resolve();
|
|
84
|
-
if (this.connecting)
|
|
85
|
-
return this.connecting;
|
|
86
|
-
if (this._disposed)
|
|
118
|
+
if (this.#disposed) {
|
|
87
119
|
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';
|
|
120
|
+
}
|
|
121
|
+
if (this.#state === 'connected')
|
|
122
|
+
return Promise.resolve();
|
|
123
|
+
if (this.#connecting)
|
|
124
|
+
return this.#connecting;
|
|
125
|
+
if (this.transport.type === ConnectionType.Unidirectional) {
|
|
126
|
+
return this.#handleConnected();
|
|
127
|
+
}
|
|
128
|
+
if (!this.#messageContextFactory) {
|
|
129
|
+
return Promise.reject(new Error('Message context factory is not configured'));
|
|
130
|
+
}
|
|
131
|
+
this.#setState('connecting');
|
|
132
|
+
this.#cab = new AbortController();
|
|
133
|
+
this.messageContext = this.#messageContextFactory();
|
|
134
|
+
this.#connecting = this.transport
|
|
135
|
+
.connect({
|
|
136
|
+
auth: this.auth,
|
|
137
|
+
application: this.application,
|
|
138
|
+
onMessage: (message) => {
|
|
139
|
+
void this.#onMessage(message);
|
|
140
|
+
},
|
|
141
|
+
onConnect: () => {
|
|
142
|
+
void this.#handleConnected();
|
|
143
|
+
},
|
|
144
|
+
onDisconnect: (reason) => {
|
|
145
|
+
void this.#handleDisconnected(reason);
|
|
146
|
+
},
|
|
155
147
|
})
|
|
156
|
-
.catch((error) => {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
148
|
+
.catch(async (error) => {
|
|
149
|
+
this.messageContext = null;
|
|
150
|
+
this.#cab = null;
|
|
151
|
+
await this.#handleDisconnected(DEFAULT_CONNECT_ERROR_REASON);
|
|
160
152
|
throw error;
|
|
161
153
|
})
|
|
162
154
|
.finally(() => {
|
|
163
|
-
this
|
|
164
|
-
if (emitDisconnectOnFailure && !this._disposed) {
|
|
165
|
-
this._state = 'disconnected';
|
|
166
|
-
this._lastDisconnectReason = emitDisconnectOnFailure;
|
|
167
|
-
void this.onDisconnect(emitDisconnectOnFailure);
|
|
168
|
-
}
|
|
155
|
+
this.#connecting = null;
|
|
169
156
|
});
|
|
170
|
-
return this
|
|
157
|
+
return this.#connecting;
|
|
171
158
|
}
|
|
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;
|
|
159
|
+
async disconnect(reason = 'client') {
|
|
160
|
+
this.#cancelReconnectLoop();
|
|
161
|
+
if (this.transport.type === ConnectionType.Unidirectional) {
|
|
162
|
+
await this.#handleDisconnected(reason);
|
|
163
|
+
return;
|
|
189
164
|
}
|
|
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);
|
|
165
|
+
if (this.#state === 'idle' || this.#state === 'disconnected') {
|
|
166
|
+
this.#lastDisconnectReason = reason;
|
|
167
|
+
this.#setState('disconnected');
|
|
168
|
+
return;
|
|
223
169
|
}
|
|
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
|
-
}
|
|
170
|
+
this.#setState('disconnecting');
|
|
171
|
+
if (this.#cab && !this.#cab.signal.aborted) {
|
|
235
172
|
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
|
-
}
|
|
173
|
+
this.#cab.abort(reason);
|
|
249
174
|
}
|
|
250
|
-
catch
|
|
251
|
-
this.
|
|
252
|
-
kind: 'rpc_error',
|
|
253
|
-
timestamp: Date.now(),
|
|
254
|
-
callId,
|
|
255
|
-
procedure,
|
|
256
|
-
error,
|
|
257
|
-
});
|
|
258
|
-
call.reject(error);
|
|
175
|
+
catch {
|
|
176
|
+
this.#cab.abort();
|
|
259
177
|
}
|
|
260
178
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
if (options._stream_response && typeof value === 'function') {
|
|
268
|
-
return value;
|
|
179
|
+
try {
|
|
180
|
+
await this.transport.disconnect();
|
|
181
|
+
if (this.#state === 'disconnecting') {
|
|
182
|
+
await this.#handleDisconnected(reason);
|
|
269
183
|
}
|
|
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
184
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
});
|
|
185
|
+
catch (error) {
|
|
186
|
+
await this.#handleDisconnected(reason);
|
|
187
|
+
throw error;
|
|
288
188
|
}
|
|
289
189
|
}
|
|
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?.();
|
|
190
|
+
requestReconnect(reason = 'server') {
|
|
191
|
+
if (this.transport.type !== ConnectionType.Bidirectional) {
|
|
192
|
+
return Promise.resolve();
|
|
302
193
|
}
|
|
303
|
-
this
|
|
194
|
+
this.#clientDisconnectAsReconnect = true;
|
|
195
|
+
this.#clientDisconnectOverrideReason = reason;
|
|
196
|
+
return this.disconnect('client');
|
|
304
197
|
}
|
|
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.
|
|
198
|
+
dispose() {
|
|
199
|
+
if (this.#disposed)
|
|
200
|
+
return;
|
|
201
|
+
this.#disposed = true;
|
|
202
|
+
this.#cancelReconnectLoop();
|
|
319
203
|
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) {
|
|
204
|
+
if (this.#cab && !this.#cab.signal.aborted) {
|
|
330
205
|
try {
|
|
331
|
-
this
|
|
206
|
+
this.#cab.abort('dispose');
|
|
332
207
|
}
|
|
333
208
|
catch {
|
|
334
|
-
this
|
|
209
|
+
this.#cab.abort();
|
|
335
210
|
}
|
|
336
|
-
this.cab = null;
|
|
337
211
|
}
|
|
338
|
-
this.
|
|
339
|
-
|
|
340
|
-
|
|
212
|
+
if (this.transport.type === ConnectionType.Bidirectional &&
|
|
213
|
+
(this.#state === 'connecting' || this.#state === 'connected')) {
|
|
214
|
+
void this.transport.disconnect().catch(noopFn);
|
|
215
|
+
}
|
|
216
|
+
for (let i = this.#plugins.length - 1; i >= 0; i--) {
|
|
217
|
+
this.#plugins[i].dispose?.();
|
|
341
218
|
}
|
|
342
|
-
void this.clientStreams.clear(effectiveReason);
|
|
343
|
-
void this.serverStreams.clear(effectiveReason);
|
|
344
|
-
void this.rpcStreams.clear(effectiveReason);
|
|
345
219
|
}
|
|
346
|
-
|
|
347
|
-
if (this.
|
|
348
|
-
|
|
349
|
-
|
|
220
|
+
send(buffer, signal) {
|
|
221
|
+
if (this.transport.type !== ConnectionType.Bidirectional) {
|
|
222
|
+
throw new Error('Invalid transport type for send');
|
|
223
|
+
}
|
|
224
|
+
return this.transport.send(buffer, { signal });
|
|
350
225
|
}
|
|
351
|
-
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
return Promise.reject(new Error('Client is not connected'));
|
|
226
|
+
transportCall(context, rpc, options) {
|
|
227
|
+
if (this.transport.type !== ConnectionType.Unidirectional) {
|
|
228
|
+
throw new Error('Invalid transport type for call');
|
|
355
229
|
}
|
|
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
|
-
});
|
|
230
|
+
return this.transport.call(context, rpc, options);
|
|
365
231
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
232
|
+
emitClientEvent(event) {
|
|
233
|
+
for (const plugin of this.#plugins) {
|
|
234
|
+
try {
|
|
235
|
+
const result = plugin.onClientEvent?.(event);
|
|
236
|
+
Promise.resolve(result).catch(noopFn);
|
|
237
|
+
}
|
|
238
|
+
catch { }
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
emitStreamEvent(event) {
|
|
242
|
+
this.emitClientEvent({
|
|
243
|
+
kind: 'stream_event',
|
|
244
|
+
timestamp: Date.now(),
|
|
245
|
+
...event,
|
|
246
|
+
});
|
|
373
247
|
}
|
|
374
|
-
async onMessage(buffer) {
|
|
248
|
+
async #onMessage(buffer) {
|
|
375
249
|
if (!this.messageContext)
|
|
376
250
|
return;
|
|
377
251
|
const message = this.protocol.decodeMessage(this.messageContext, buffer);
|
|
378
|
-
for (const plugin of this
|
|
252
|
+
for (const plugin of this.#plugins) {
|
|
379
253
|
plugin.onServerMessage?.(message, buffer);
|
|
380
254
|
}
|
|
381
255
|
this.emitClientEvent({
|
|
@@ -385,390 +259,131 @@ export class BaseClient extends EventEmitter {
|
|
|
385
259
|
rawByteLength: buffer.byteLength,
|
|
386
260
|
body: message,
|
|
387
261
|
});
|
|
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
|
-
}
|
|
262
|
+
this.emit('message', message, buffer);
|
|
525
263
|
}
|
|
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
|
-
}
|
|
264
|
+
async #handleConnected() {
|
|
265
|
+
this.#reconnectTimeout =
|
|
266
|
+
this.#reconnectConfig?.initialTimeout ?? DEFAULT_RECONNECT_TIMEOUT;
|
|
267
|
+
this.#reconnectImmediate = false;
|
|
268
|
+
this.#setState('connected');
|
|
269
|
+
this.#lastDisconnectReason = 'server';
|
|
270
|
+
this.emitClientEvent({
|
|
271
|
+
kind: 'connected',
|
|
272
|
+
timestamp: Date.now(),
|
|
273
|
+
transportType: this.transport.type === ConnectionType.Bidirectional
|
|
274
|
+
? 'bidirectional'
|
|
275
|
+
: 'unidirectional',
|
|
276
|
+
});
|
|
277
|
+
for (const plugin of this.#plugins) {
|
|
278
|
+
await plugin.onConnect?.();
|
|
563
279
|
}
|
|
280
|
+
this.emit('connected');
|
|
564
281
|
}
|
|
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);
|
|
282
|
+
async #handleDisconnected(reason) {
|
|
283
|
+
const effectiveReason = reason === 'client' && this.#clientDisconnectAsReconnect
|
|
284
|
+
? (this.#clientDisconnectOverrideReason ?? 'server')
|
|
285
|
+
: reason;
|
|
286
|
+
this.#clientDisconnectAsReconnect = false;
|
|
287
|
+
this.#clientDisconnectOverrideReason = null;
|
|
288
|
+
const shouldSkip = this.#state === 'disconnected' &&
|
|
289
|
+
this.messageContext === null &&
|
|
290
|
+
this.#lastDisconnectReason === effectiveReason;
|
|
291
|
+
this.messageContext = null;
|
|
292
|
+
if (this.#cab) {
|
|
293
|
+
if (!this.#cab.signal.aborted) {
|
|
294
|
+
try {
|
|
295
|
+
this.#cab.abort(reason);
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
this.#cab.abort();
|
|
623
299
|
}
|
|
624
300
|
}
|
|
301
|
+
this.#cab = null;
|
|
625
302
|
}
|
|
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
|
-
}
|
|
303
|
+
if (shouldSkip)
|
|
304
|
+
return;
|
|
305
|
+
this.#lastDisconnectReason = effectiveReason;
|
|
306
|
+
this.#setState('disconnected');
|
|
307
|
+
this.emitClientEvent({
|
|
308
|
+
kind: 'disconnected',
|
|
309
|
+
timestamp: Date.now(),
|
|
310
|
+
reason: effectiveReason,
|
|
311
|
+
});
|
|
312
|
+
this.emit('disconnected', effectiveReason);
|
|
313
|
+
for (let i = this.#plugins.length - 1; i >= 0; i--) {
|
|
314
|
+
await this.#plugins[i].onDisconnect?.(effectiveReason);
|
|
710
315
|
}
|
|
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
|
-
}
|
|
316
|
+
if (this.#shouldReconnect(effectiveReason)) {
|
|
317
|
+
this.#ensureReconnectLoop();
|
|
738
318
|
}
|
|
739
319
|
}
|
|
740
|
-
|
|
741
|
-
if (
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
emitStreamEvent(event) {
|
|
320
|
+
#setState(next) {
|
|
321
|
+
if (next === this.#state)
|
|
322
|
+
return;
|
|
323
|
+
const previous = this.#state;
|
|
324
|
+
this.#state = next;
|
|
746
325
|
this.emitClientEvent({
|
|
747
|
-
kind: '
|
|
326
|
+
kind: 'state_changed',
|
|
748
327
|
timestamp: Date.now(),
|
|
749
|
-
|
|
328
|
+
state: next,
|
|
329
|
+
previous,
|
|
750
330
|
});
|
|
331
|
+
this.emit('state_changed', next, previous);
|
|
751
332
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
this
|
|
755
|
-
|
|
756
|
-
|
|
333
|
+
#shouldReconnect(reason) {
|
|
334
|
+
return (!this.#disposed &&
|
|
335
|
+
!!this.#reconnectConfig &&
|
|
336
|
+
this.transport.type === ConnectionType.Bidirectional &&
|
|
337
|
+
reason !== 'client');
|
|
757
338
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
339
|
+
#cancelReconnectLoop() {
|
|
340
|
+
this.#reconnectImmediate = false;
|
|
341
|
+
this.#reconnectController?.abort();
|
|
342
|
+
this.#reconnectController = null;
|
|
343
|
+
this.#reconnectPromise = null;
|
|
763
344
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
345
|
+
#ensureReconnectLoop() {
|
|
346
|
+
if (this.#reconnectPromise || !this.#reconnectConfig)
|
|
347
|
+
return;
|
|
348
|
+
const signal = new AbortController();
|
|
349
|
+
this.#reconnectController = signal;
|
|
350
|
+
this.#reconnectPromise = (async () => {
|
|
351
|
+
while (!signal.signal.aborted &&
|
|
352
|
+
!this.#disposed &&
|
|
353
|
+
this.#reconnectConfig &&
|
|
354
|
+
(this.#state === 'disconnected' || this.#state === 'idle') &&
|
|
355
|
+
this.#lastDisconnectReason !== 'client') {
|
|
356
|
+
if (this.#reconnectPauseReasons.size) {
|
|
357
|
+
await sleep(1000, signal.signal);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const delay = this.#reconnectImmediate
|
|
361
|
+
? 0
|
|
362
|
+
: computeReconnectDelay(this.#reconnectTimeout);
|
|
363
|
+
this.#reconnectImmediate = false;
|
|
364
|
+
if (delay > 0) {
|
|
365
|
+
await sleep(delay, signal.signal);
|
|
366
|
+
}
|
|
367
|
+
const currentState = this.state;
|
|
368
|
+
if (signal.signal.aborted ||
|
|
369
|
+
this.#disposed ||
|
|
370
|
+
!this.#reconnectConfig ||
|
|
371
|
+
currentState === 'connected' ||
|
|
372
|
+
currentState === 'connecting') {
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
const previousTimeout = this.#reconnectTimeout;
|
|
376
|
+
await this.connect().catch(noopFn);
|
|
377
|
+
if (this.state !== 'connected' && this.#reconnectConfig) {
|
|
378
|
+
this.#reconnectTimeout = Math.min(previousTimeout * 2, this.#reconnectConfig.maxTimeout ?? DEFAULT_MAX_RECONNECT_TIMEOUT);
|
|
379
|
+
}
|
|
769
380
|
}
|
|
770
|
-
|
|
771
|
-
|
|
381
|
+
})().finally(() => {
|
|
382
|
+
if (this.#reconnectController === signal) {
|
|
383
|
+
this.#reconnectController = null;
|
|
384
|
+
}
|
|
385
|
+
this.#reconnectPromise = null;
|
|
386
|
+
});
|
|
772
387
|
}
|
|
773
388
|
}
|
|
774
389
|
//# sourceMappingURL=core.js.map
|