@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.
Files changed (56) hide show
  1. package/dist/client.d.ts +65 -0
  2. package/dist/client.js +98 -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 +36 -83
  11. package/dist/core.js +315 -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 +564 -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 +218 -0
  42. package/src/clients/runtime.ts +93 -79
  43. package/src/clients/static.ts +46 -38
  44. package/src/core.ts +408 -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 +778 -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,265 @@
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
+ 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.options = options;
43
- this.transportFactory = transportFactory;
44
- this.transportOptions = transportOptions;
54
+ this.transport = transport;
45
55
  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
- }
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._state;
61
+ return this.#state;
62
62
  }
63
63
  get lastDisconnectReason() {
64
- return this._lastDisconnectReason;
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._disposed;
73
+ return this.#disposed;
71
74
  }
72
- requestReconnect(reason) {
73
- return this.disconnect({ reconnect: true, reason });
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
- get auth() {
76
- return this.authValue;
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
- set auth(value) {
79
- this.authValue = value;
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._state === 'connected')
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
- 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';
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
- if (this.transport.type === ConnectionType.Bidirectional) {
158
- emitDisconnectOnFailure = DEFAULT_RECONNECT_REASON;
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.connecting = null;
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.connecting;
167
+ return this.#connecting;
171
168
  }
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;
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
- 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);
175
+ if (this.#state === 'idle' || this.#state === 'disconnected') {
176
+ this.#lastDisconnectReason = reason;
177
+ this.#setState('disconnected');
178
+ return;
223
179
  }
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
- }
180
+ this.#setState('disconnecting');
181
+ if (this.#cab && !this.#cab.signal.aborted) {
235
182
  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
- }
183
+ this.#cab.abort(reason);
249
184
  }
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);
185
+ catch {
186
+ this.#cab.abort();
259
187
  }
260
188
  }
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;
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
- else {
285
- return await result.finally(() => {
286
- this.calls.delete(callId);
287
- });
195
+ catch (error) {
196
+ await this.#handleDisconnected(reason);
197
+ throw error;
288
198
  }
289
199
  }
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?.();
200
+ requestReconnect(reason = 'server') {
201
+ if (this.transport.type !== ConnectionType.Bidirectional) {
202
+ return Promise.resolve();
302
203
  }
303
- this.emit('connected');
204
+ this.#clientDisconnectAsReconnect = true;
205
+ this.#clientDisconnectOverrideReason = reason;
206
+ return this.disconnect('client');
304
207
  }
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.
208
+ dispose() {
209
+ if (this.#disposed)
210
+ return;
211
+ this.#disposed = true;
212
+ this.#cancelReconnectLoop();
319
213
  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) {
214
+ if (this.#cab && !this.#cab.signal.aborted) {
330
215
  try {
331
- this.cab.abort(reason);
216
+ this.#cab.abort('dispose');
332
217
  }
333
218
  catch {
334
- this.cab.abort();
219
+ this.#cab.abort();
335
220
  }
336
- this.cab = null;
337
221
  }
338
- this.emit('disconnected', effectiveReason);
339
- for (let i = this.plugins.length - 1; i >= 0; i--) {
340
- await this.plugins[i].onDisconnect?.(effectiveReason);
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
- nextPingNonce() {
347
- if (this.pingNonce >= MAX_UINT32)
348
- this.pingNonce = 0;
349
- return this.pingNonce++;
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
- ping(timeout, signal) {
352
- if (!this.messageContext ||
353
- this.transport.type !== ConnectionType.Bidirectional) {
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
- 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
- });
240
+ return this.transport.call(context, rpc, options);
365
241
  }
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();
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
- async onMessage(buffer) {
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.plugins) {
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
- 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
- }
272
+ this.emit('message', message, buffer);
525
273
  }
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
- }
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
- 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);
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
- 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
- }
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
- 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
- }
326
+ if (this.#shouldReconnect(effectiveReason)) {
327
+ this.#ensureReconnectLoop();
738
328
  }
739
329
  }
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) {
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: 'stream_event',
336
+ kind: 'state_changed',
748
337
  timestamp: Date.now(),
749
- ...event,
338
+ state: next,
339
+ previous,
750
340
  });
341
+ this.emit('state_changed', next, previous);
751
342
  }
752
- getStreamId() {
753
- if (this.streamId >= MAX_UINT32) {
754
- this.streamId = 0;
755
- }
756
- return this.streamId++;
343
+ #shouldReconnect(reason) {
344
+ return (!this.#disposed &&
345
+ !!this.#reconnectConfig &&
346
+ this.transport.type === ConnectionType.Bidirectional &&
347
+ reason !== 'client');
757
348
  }
758
- getCallId() {
759
- if (this.callId >= MAX_UINT32) {
760
- this.callId = 0;
761
- }
762
- return this.callId++;
349
+ #cancelReconnectLoop() {
350
+ this.#reconnectImmediate = false;
351
+ this.#reconnectController?.abort();
352
+ this.#reconnectController = null;
353
+ this.#reconnectPromise = null;
763
354
  }
764
- emitClientEvent(event) {
765
- for (const plugin of this.plugins) {
766
- try {
767
- const result = plugin.onClientEvent?.(event);
768
- Promise.resolve(result).catch(noopFn);
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
- catch { }
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