@nmtjs/client 0.15.2 → 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.
Files changed (56) hide show
  1. package/dist/client.d.ts +64 -0
  2. package/dist/client.js +97 -0
  3. package/dist/client.js.map +1 -0
  4. package/dist/clients/runtime.d.ts +6 -12
  5. package/dist/clients/runtime.js +58 -57
  6. package/dist/clients/runtime.js.map +1 -1
  7. package/dist/clients/static.d.ts +4 -9
  8. package/dist/clients/static.js +20 -20
  9. package/dist/clients/static.js.map +1 -1
  10. package/dist/core.d.ts +33 -83
  11. package/dist/core.js +305 -690
  12. package/dist/core.js.map +1 -1
  13. package/dist/events.d.ts +0 -1
  14. package/dist/events.js +74 -11
  15. package/dist/events.js.map +1 -1
  16. package/dist/index.d.ts +4 -0
  17. package/dist/index.js +4 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/layers/ping.d.ts +6 -0
  20. package/dist/layers/ping.js +65 -0
  21. package/dist/layers/ping.js.map +1 -0
  22. package/dist/layers/rpc.d.ts +19 -0
  23. package/dist/layers/rpc.js +521 -0
  24. package/dist/layers/rpc.js.map +1 -0
  25. package/dist/layers/streams.d.ts +20 -0
  26. package/dist/layers/streams.js +194 -0
  27. package/dist/layers/streams.js.map +1 -0
  28. package/dist/plugins/browser.js +28 -9
  29. package/dist/plugins/browser.js.map +1 -1
  30. package/dist/plugins/heartbeat.js +10 -10
  31. package/dist/plugins/heartbeat.js.map +1 -1
  32. package/dist/plugins/index.d.ts +1 -1
  33. package/dist/plugins/index.js +0 -1
  34. package/dist/plugins/index.js.map +1 -1
  35. package/dist/plugins/reconnect.js +11 -94
  36. package/dist/plugins/reconnect.js.map +1 -1
  37. package/dist/plugins/types.d.ts +27 -11
  38. package/dist/transport.d.ts +49 -31
  39. package/dist/types.d.ts +21 -5
  40. package/package.json +10 -10
  41. package/src/client.ts +216 -0
  42. package/src/clients/runtime.ts +93 -79
  43. package/src/clients/static.ts +46 -38
  44. package/src/core.ts +394 -901
  45. package/src/events.ts +113 -14
  46. package/src/index.ts +4 -0
  47. package/src/layers/ping.ts +99 -0
  48. package/src/layers/rpc.ts +725 -0
  49. package/src/layers/streams.ts +277 -0
  50. package/src/plugins/browser.ts +39 -9
  51. package/src/plugins/heartbeat.ts +10 -10
  52. package/src/plugins/index.ts +8 -1
  53. package/src/plugins/reconnect.ts +12 -119
  54. package/src/plugins/types.ts +30 -13
  55. package/src/transport.ts +75 -46
  56. package/src/types.ts +33 -8
package/dist/core.js CHANGED
@@ -1,381 +1,255 @@
1
- import { anyAbortSignal, createFuture, MAX_UINT32, noopFn, withTimeout, } from '@nmtjs/common';
2
- import { ClientMessageType, ConnectionType, ErrorCode, ProtocolBlob, ServerMessageType, } from '@nmtjs/protocol';
3
- import { ProtocolError, ProtocolServerBlobStream, ProtocolServerRPCStream, ProtocolServerStream, versions, } from '@nmtjs/protocol/client';
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 DEFAULT_RECONNECT_REASON = 'connect_error';
11
- /**
12
- * @todo Add error logging in ClientStreamPull rejection handler for easier debugging
13
- * @todo Consider edge case where callId/streamId overflow at MAX_UINT32 with existing entries
14
- */
15
- export class BaseClient extends EventEmitter {
16
- options;
17
- transportFactory;
18
- transportOptions;
19
- _;
20
- calls = new Map();
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
- messageContext;
24
- clientStreams = new ClientStreams();
25
- serverStreams = new ServerStreams();
26
- rpcStreams = new ServerStreams();
27
- callId = 0;
28
- streamId = 0;
29
- cab = null;
30
- connecting = null;
31
- _state = 'disconnected';
32
- _lastDisconnectReason = 'server';
33
- _disposed = false;
34
- pingNonce = 0;
35
- pendingPings = new Map();
36
- plugins = [];
37
- clientDisconnectAsReconnect = false;
38
- clientDisconnectOverrideReason = null;
39
- authValue;
40
- constructor(options, transportFactory, transportOptions) {
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.options = options;
43
- this.transportFactory = transportFactory;
44
- this.transportOptions = transportOptions;
53
+ this.transport = transport;
45
54
  this.protocol = versions[options.protocol];
46
- const { format, protocol } = this.options;
47
- this.transport = this.transportFactory({ protocol, format }, this.transportOptions);
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._state;
59
+ return this.#state;
62
60
  }
63
61
  get lastDisconnectReason() {
64
- return this._lastDisconnectReason;
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._disposed;
71
+ return this.#disposed;
71
72
  }
72
- requestReconnect(reason) {
73
- return this.disconnect({ reconnect: true, reason });
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
- get auth() {
76
- return this.authValue;
81
+ setMessageContextFactory(factory) {
82
+ this.#messageContextFactory = factory;
77
83
  }
78
- set auth(value) {
79
- this.authValue = value;
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._state === 'connected')
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
- const _connect = async () => {
89
- if (this.transport.type === ConnectionType.Bidirectional) {
90
- const client = this;
91
- this.cab = new AbortController();
92
- const protocol = this.protocol;
93
- const serverStreams = this.serverStreams;
94
- const transport = {
95
- send: (buffer) => {
96
- this.send(buffer).catch(noopFn);
97
- },
98
- };
99
- this.messageContext = {
100
- transport,
101
- encoder: this.options.format,
102
- decoder: this.options.format,
103
- addClientStream: (blob) => {
104
- const streamId = this.getStreamId();
105
- return this.clientStreams.add(blob.source, streamId, blob.metadata);
106
- },
107
- addServerStream(streamId, metadata) {
108
- const stream = new ProtocolServerBlobStream(metadata, {
109
- pull: (controller) => {
110
- client.emitStreamEvent({
111
- direction: 'outgoing',
112
- streamType: 'server_blob',
113
- action: 'pull',
114
- streamId,
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
- if (this.transport.type === ConnectionType.Bidirectional) {
158
- emitDisconnectOnFailure = DEFAULT_RECONNECT_REASON;
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.connecting = null;
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.connecting;
157
+ return this.#connecting;
171
158
  }
172
- async disconnect(options = {}) {
173
- if (this.transport.type === ConnectionType.Bidirectional) {
174
- // Ensure connect() won't short-circuit while the transport is closing.
175
- this._state = 'disconnected';
176
- this._lastDisconnectReason = 'client';
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
- blob(source, metadata) {
192
- return ProtocolBlob.from(source, metadata);
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
- else {
225
- if (signal) {
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
- const transformedPayload = this.transformer.encode(procedure, payload);
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 (error) {
251
- this.emitClientEvent({
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
- const result = call.promise.then((value) => {
262
- if (value instanceof ProtocolServerRPCStream) {
263
- return value.createAsyncIterable(() => {
264
- controller.abort();
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
- else {
285
- return await result.finally(() => {
286
- this.calls.delete(callId);
287
- });
185
+ catch (error) {
186
+ await this.#handleDisconnected(reason);
187
+ throw error;
288
188
  }
289
189
  }
290
- async onConnect() {
291
- this._state = 'connected';
292
- this._lastDisconnectReason = 'server';
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.emit('connected');
194
+ this.#clientDisconnectAsReconnect = true;
195
+ this.#clientDisconnectOverrideReason = reason;
196
+ return this.disconnect('client');
304
197
  }
305
- async onDisconnect(reason) {
306
- const effectiveReason = reason === 'client' && this.clientDisconnectAsReconnect
307
- ? (this.clientDisconnectOverrideReason ?? 'server')
308
- : reason;
309
- this.clientDisconnectAsReconnect = false;
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.stopAllPendingPings(effectiveReason);
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.cab.abort(reason);
206
+ this.#cab.abort('dispose');
332
207
  }
333
208
  catch {
334
- this.cab.abort();
209
+ this.#cab.abort();
335
210
  }
336
- this.cab = null;
337
211
  }
338
- this.emit('disconnected', effectiveReason);
339
- for (let i = this.plugins.length - 1; i >= 0; i--) {
340
- await this.plugins[i].onDisconnect?.(effectiveReason);
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
- nextPingNonce() {
347
- if (this.pingNonce >= MAX_UINT32)
348
- this.pingNonce = 0;
349
- return this.pingNonce++;
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
- ping(timeout, signal) {
352
- if (!this.messageContext ||
353
- this.transport.type !== ConnectionType.Bidirectional) {
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
- const nonce = this.nextPingNonce();
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
- stopAllPendingPings(reason) {
367
- if (!this.pendingPings.size)
368
- return;
369
- const error = new Error('Heartbeat stopped', { cause: reason });
370
- for (const pending of this.pendingPings.values())
371
- pending.reject(error);
372
- this.pendingPings.clear();
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.plugins) {
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
- switch (message.type) {
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
- handleRPCResponseMessage(message) {
527
- const { callId, result, error } = message;
528
- const call = this.calls.get(callId);
529
- if (!call)
530
- return;
531
- if (error) {
532
- this.emitClientEvent({
533
- kind: 'rpc_error',
534
- timestamp: Date.now(),
535
- callId,
536
- procedure: call.procedure,
537
- error,
538
- });
539
- call.reject(new ProtocolError(error.code, error.message, error.data));
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
- handleRPCStreamResponseMessage(message) {
566
- const call = this.calls.get(message.callId);
567
- if (message.error) {
568
- if (!call)
569
- return;
570
- this.emitClientEvent({
571
- kind: 'rpc_error',
572
- timestamp: Date.now(),
573
- callId: message.callId,
574
- procedure: call.procedure,
575
- error: message.error,
576
- });
577
- call.reject(new ProtocolError(message.error.code, message.error.message, message.error.data));
578
- }
579
- else {
580
- if (call) {
581
- const { procedure, signal } = call;
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
- handleCallResponse(callId, response) {
628
- const call = this.calls.get(callId);
629
- if (response.type === 'rpc_stream') {
630
- if (call) {
631
- this.emitClientEvent({
632
- kind: 'rpc_response',
633
- timestamp: Date.now(),
634
- callId,
635
- procedure: call.procedure,
636
- stream: true,
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
- else if (response.type === 'rpc') {
712
- if (!call)
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
- send(buffer, signal) {
741
- if (this.transport.type === ConnectionType.Unidirectional)
742
- throw new Error('Invalid transport type for send');
743
- return this.transport.send(buffer, { signal });
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: 'stream_event',
326
+ kind: 'state_changed',
748
327
  timestamp: Date.now(),
749
- ...event,
328
+ state: next,
329
+ previous,
750
330
  });
331
+ this.emit('state_changed', next, previous);
751
332
  }
752
- getStreamId() {
753
- if (this.streamId >= MAX_UINT32) {
754
- this.streamId = 0;
755
- }
756
- return this.streamId++;
333
+ #shouldReconnect(reason) {
334
+ return (!this.#disposed &&
335
+ !!this.#reconnectConfig &&
336
+ this.transport.type === ConnectionType.Bidirectional &&
337
+ reason !== 'client');
757
338
  }
758
- getCallId() {
759
- if (this.callId >= MAX_UINT32) {
760
- this.callId = 0;
761
- }
762
- return this.callId++;
339
+ #cancelReconnectLoop() {
340
+ this.#reconnectImmediate = false;
341
+ this.#reconnectController?.abort();
342
+ this.#reconnectController = null;
343
+ this.#reconnectPromise = null;
763
344
  }
764
- emitClientEvent(event) {
765
- for (const plugin of this.plugins) {
766
- try {
767
- const result = plugin.onClientEvent?.(event);
768
- Promise.resolve(result).catch(noopFn);
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
- catch { }
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