@nmtjs/client 0.15.0-beta.9 → 0.15.0

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 (51) hide show
  1. package/README.md +31 -1
  2. package/dist/clients/runtime.d.ts +2 -2
  3. package/dist/clients/runtime.js +1 -1
  4. package/dist/clients/runtime.js.map +1 -1
  5. package/dist/clients/static.d.ts +2 -2
  6. package/dist/clients/static.js +6 -3
  7. package/dist/clients/static.js.map +1 -1
  8. package/dist/core.d.ts +38 -8
  9. package/dist/core.js +414 -66
  10. package/dist/core.js.map +1 -1
  11. package/dist/events.d.ts +1 -1
  12. package/dist/events.js.map +1 -1
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.js +6 -5
  15. package/dist/index.js.map +1 -1
  16. package/dist/plugins/browser.d.ts +2 -0
  17. package/dist/plugins/browser.js +41 -0
  18. package/dist/plugins/browser.js.map +1 -0
  19. package/dist/plugins/heartbeat.d.ts +6 -0
  20. package/dist/plugins/heartbeat.js +86 -0
  21. package/dist/plugins/heartbeat.js.map +1 -0
  22. package/dist/plugins/index.d.ts +5 -0
  23. package/dist/plugins/index.js +6 -0
  24. package/dist/plugins/index.js.map +1 -0
  25. package/dist/plugins/logging.d.ts +9 -0
  26. package/dist/plugins/logging.js +30 -0
  27. package/dist/plugins/logging.js.map +1 -0
  28. package/dist/plugins/reconnect.d.ts +6 -0
  29. package/dist/plugins/reconnect.js +98 -0
  30. package/dist/plugins/reconnect.js.map +1 -0
  31. package/dist/plugins/types.d.ts +63 -0
  32. package/dist/plugins/types.js +2 -0
  33. package/dist/plugins/types.js.map +1 -0
  34. package/dist/streams.d.ts +3 -3
  35. package/dist/streams.js.map +1 -1
  36. package/dist/transformers.js.map +1 -1
  37. package/dist/types.d.ts +1 -4
  38. package/dist/types.js.map +1 -1
  39. package/package.json +27 -17
  40. package/src/clients/runtime.ts +4 -4
  41. package/src/clients/static.ts +9 -13
  42. package/src/core.ts +476 -77
  43. package/src/index.ts +1 -0
  44. package/src/plugins/browser.ts +61 -0
  45. package/src/plugins/heartbeat.ts +111 -0
  46. package/src/plugins/index.ts +5 -0
  47. package/src/plugins/logging.ts +42 -0
  48. package/src/plugins/reconnect.ts +130 -0
  49. package/src/plugins/types.ts +72 -0
  50. package/src/streams.ts +3 -3
  51. package/src/types.ts +1 -17
package/dist/core.js CHANGED
@@ -1,14 +1,13 @@
1
- import { anyAbortSignal, createFuture, MAX_UINT32, noopFn } from '@nmtjs/common';
1
+ import { anyAbortSignal, createFuture, MAX_UINT32, noopFn, withTimeout, } from '@nmtjs/common';
2
2
  import { ClientMessageType, ConnectionType, ErrorCode, ProtocolBlob, ServerMessageType, } from '@nmtjs/protocol';
3
3
  import { ProtocolError, ProtocolServerBlobStream, ProtocolServerRPCStream, ProtocolServerStream, versions, } from '@nmtjs/protocol/client';
4
- import { EventEmitter } from "./events.js";
5
- import { ClientStreams, ServerStreams } from "./streams.js";
4
+ import { EventEmitter } from './events.js';
5
+ import { ClientStreams, ServerStreams } from './streams.js';
6
6
  export { ErrorCode, ProtocolBlob, } from '@nmtjs/protocol';
7
- export * from "./types.js";
7
+ export * from './types.js';
8
8
  export class ClientError extends ProtocolError {
9
9
  }
10
- const DEFAULT_RECONNECT_TIMEOUT = 1000;
11
- const DEFAULT_MAX_RECONNECT_TIMEOUT = 60000;
10
+ const DEFAULT_RECONNECT_REASON = 'connect_error';
12
11
  /**
13
12
  * @todo Add error logging in ClientStreamPull rejection handler for easier debugging
14
13
  * @todo Consider edge case where callId/streamId overflow at MAX_UINT32 with existing entries
@@ -28,10 +27,16 @@ export class BaseClient extends EventEmitter {
28
27
  callId = 0;
29
28
  streamId = 0;
30
29
  cab = null;
31
- reconnectTimeout = DEFAULT_RECONNECT_TIMEOUT;
32
30
  connecting = null;
33
- state = 'disconnected';
34
- #auth;
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;
35
40
  constructor(options, transportFactory, transportOptions) {
36
41
  super();
37
42
  this.options = options;
@@ -40,46 +45,55 @@ export class BaseClient extends EventEmitter {
40
45
  this.protocol = versions[options.protocol];
41
46
  const { format, protocol } = this.options;
42
47
  this.transport = this.transportFactory({ protocol, format }, this.transportOptions);
43
- if (this.transport.type === ConnectionType.Bidirectional &&
44
- this.options.autoreconnect) {
45
- this.on('disconnected', async (reason) => {
46
- while (this.state === 'disconnected') {
47
- const timeout = new Promise((resolve) => setTimeout(resolve, this.reconnectTimeout));
48
- this.reconnectTimeout = Math.min(this.reconnectTimeout * 2, DEFAULT_MAX_RECONNECT_TIMEOUT);
49
- await timeout;
50
- await this.connect().catch(noopFn);
51
- }
52
- });
53
- this.on('connected', () => {
54
- this.reconnectTimeout = DEFAULT_RECONNECT_TIMEOUT;
55
- });
56
- if (globalThis.window) {
57
- globalThis.window.addEventListener('pageshow', () => {
58
- if (this.state === 'disconnected')
59
- this.connect();
60
- });
61
- }
48
+ this.plugins = this.options.plugins?.map((plugin) => plugin(this)) ?? [];
49
+ for (const plugin of this.plugins) {
50
+ plugin.onInit?.();
62
51
  }
63
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
+ }
59
+ }
60
+ get state() {
61
+ return this._state;
62
+ }
63
+ get lastDisconnectReason() {
64
+ return this._lastDisconnectReason;
65
+ }
66
+ get transportType() {
67
+ return this.transport.type;
68
+ }
69
+ isDisposed() {
70
+ return this._disposed;
71
+ }
72
+ requestReconnect(reason) {
73
+ return this.disconnect({ reconnect: true, reason });
74
+ }
64
75
  get auth() {
65
- return this.#auth;
76
+ return this.authValue;
66
77
  }
67
78
  set auth(value) {
68
- this.#auth = value;
79
+ this.authValue = value;
69
80
  }
70
81
  connect() {
71
- if (this.state === 'connected')
82
+ if (this._state === 'connected')
72
83
  return Promise.resolve();
73
84
  if (this.connecting)
74
85
  return this.connecting;
86
+ if (this._disposed)
87
+ return Promise.reject(new Error('Client is disposed'));
75
88
  const _connect = async () => {
76
89
  if (this.transport.type === ConnectionType.Bidirectional) {
90
+ const client = this;
77
91
  this.cab = new AbortController();
78
92
  const protocol = this.protocol;
79
93
  const serverStreams = this.serverStreams;
80
94
  const transport = {
81
95
  send: (buffer) => {
82
- this.#send(buffer).catch(noopFn);
96
+ this.send(buffer).catch(noopFn);
83
97
  },
84
98
  };
85
99
  this.messageContext = {
@@ -87,12 +101,19 @@ export class BaseClient extends EventEmitter {
87
101
  encoder: this.options.format,
88
102
  decoder: this.options.format,
89
103
  addClientStream: (blob) => {
90
- const streamId = this.#getStreamId();
104
+ const streamId = this.getStreamId();
91
105
  return this.clientStreams.add(blob.source, streamId, blob.metadata);
92
106
  },
93
107
  addServerStream(streamId, metadata) {
94
108
  const stream = new ProtocolServerBlobStream(metadata, {
95
109
  pull: (controller) => {
110
+ client.emitStreamEvent({
111
+ direction: 'outgoing',
112
+ streamType: 'server_blob',
113
+ action: 'pull',
114
+ streamId,
115
+ byteLength: 65535,
116
+ });
96
117
  transport.send(protocol.encodeMessage(this, ClientMessageType.ServerStreamPull, { streamId, size: 65535 /* 64kb by default */ }));
97
118
  },
98
119
  close: () => {
@@ -104,13 +125,19 @@ export class BaseClient extends EventEmitter {
104
125
  return ({ signal } = {}) => {
105
126
  if (signal)
106
127
  signal.addEventListener('abort', () => {
128
+ client.emitStreamEvent({
129
+ direction: 'outgoing',
130
+ streamType: 'server_blob',
131
+ action: 'abort',
132
+ streamId,
133
+ });
107
134
  transport.send(protocol.encodeMessage(this, ClientMessageType.ServerStreamAbort, { streamId }));
108
135
  serverStreams.abort(streamId);
109
136
  }, { once: true });
110
137
  return stream;
111
138
  };
112
139
  },
113
- streamId: this.#getStreamId.bind(this),
140
+ streamId: this.getStreamId.bind(this),
114
141
  };
115
142
  return this.transport.connect({
116
143
  auth: this.auth,
@@ -121,17 +148,40 @@ export class BaseClient extends EventEmitter {
121
148
  });
122
149
  }
123
150
  };
151
+ let emitDisconnectOnFailure = null;
124
152
  this.connecting = _connect()
125
153
  .then(() => {
126
- this.state = 'connected';
154
+ this._state = 'connected';
155
+ })
156
+ .catch((error) => {
157
+ if (this.transport.type === ConnectionType.Bidirectional) {
158
+ emitDisconnectOnFailure = DEFAULT_RECONNECT_REASON;
159
+ }
160
+ throw error;
127
161
  })
128
162
  .finally(() => {
129
163
  this.connecting = null;
164
+ if (emitDisconnectOnFailure && !this._disposed) {
165
+ this._state = 'disconnected';
166
+ this._lastDisconnectReason = emitDisconnectOnFailure;
167
+ void this.onDisconnect(emitDisconnectOnFailure);
168
+ }
130
169
  });
131
170
  return this.connecting;
132
171
  }
133
- async disconnect() {
172
+ async disconnect(options = {}) {
134
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
+ }
135
185
  this.cab.abort();
136
186
  await this.transport.disconnect();
137
187
  this.messageContext = null;
@@ -153,11 +203,18 @@ export class BaseClient extends EventEmitter {
153
203
  if (this.cab?.signal)
154
204
  signals.push(this.cab.signal);
155
205
  const signal = signals.length ? anyAbortSignal(...signals) : undefined;
156
- const callId = this.#getCallId();
206
+ const callId = this.getCallId();
157
207
  const call = createFuture();
158
208
  call.procedure = procedure;
159
209
  call.signal = signal;
160
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
+ });
161
218
  // Check if signal is already aborted before proceeding
162
219
  if (signal?.aborted) {
163
220
  this.calls.delete(callId);
@@ -171,7 +228,7 @@ export class BaseClient extends EventEmitter {
171
228
  if (this.transport.type === ConnectionType.Bidirectional &&
172
229
  this.messageContext) {
173
230
  const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.RpcAbort, { callId });
174
- this.#send(buffer).catch(noopFn);
231
+ this.send(buffer).catch(noopFn);
175
232
  }
176
233
  }, { once: true });
177
234
  }
@@ -179,7 +236,7 @@ export class BaseClient extends EventEmitter {
179
236
  const transformedPayload = this.transformer.encode(procedure, payload);
180
237
  if (this.transport.type === ConnectionType.Bidirectional) {
181
238
  const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.Rpc, { callId, procedure, payload: transformedPayload });
182
- await this.#send(buffer, signal);
239
+ await this.send(buffer, signal);
183
240
  }
184
241
  else {
185
242
  const response = await this.transport.call({
@@ -187,10 +244,17 @@ export class BaseClient extends EventEmitter {
187
244
  format: this.options.format,
188
245
  auth: this.auth,
189
246
  }, { callId, procedure, payload: transformedPayload }, { signal, _stream_response: options._stream_response });
190
- this.#handleCallResponse(callId, response);
247
+ this.handleCallResponse(callId, response);
191
248
  }
192
249
  }
193
250
  catch (error) {
251
+ this.emitClientEvent({
252
+ kind: 'rpc_error',
253
+ timestamp: Date.now(),
254
+ callId,
255
+ procedure,
256
+ error,
257
+ });
194
258
  call.reject(error);
195
259
  }
196
260
  }
@@ -200,6 +264,9 @@ export class BaseClient extends EventEmitter {
200
264
  controller.abort();
201
265
  });
202
266
  }
267
+ if (options._stream_response && typeof value === 'function') {
268
+ return value;
269
+ }
203
270
  controller.abort();
204
271
  return value;
205
272
  }, (err) => {
@@ -221,97 +288,304 @@ export class BaseClient extends EventEmitter {
221
288
  }
222
289
  }
223
290
  async onConnect() {
224
- this.state = 'connected';
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?.();
302
+ }
225
303
  this.emit('connected');
226
304
  }
227
305
  async onDisconnect(reason) {
228
- this.state = 'disconnected';
229
- this.emit('disconnected', reason);
230
- this.clientStreams.clear(reason);
231
- this.serverStreams.clear(reason);
232
- this.rpcStreams.clear(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.
319
+ 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) {
330
+ try {
331
+ this.cab.abort(reason);
332
+ }
333
+ catch {
334
+ this.cab.abort();
335
+ }
336
+ this.cab = null;
337
+ }
338
+ this.emit('disconnected', effectiveReason);
339
+ for (let i = this.plugins.length - 1; i >= 0; i--) {
340
+ await this.plugins[i].onDisconnect?.(effectiveReason);
341
+ }
342
+ void this.clientStreams.clear(effectiveReason);
343
+ void this.serverStreams.clear(effectiveReason);
344
+ void this.rpcStreams.clear(effectiveReason);
345
+ }
346
+ nextPingNonce() {
347
+ if (this.pingNonce >= MAX_UINT32)
348
+ this.pingNonce = 0;
349
+ return this.pingNonce++;
350
+ }
351
+ ping(timeout, signal) {
352
+ if (!this.messageContext ||
353
+ this.transport.type !== ConnectionType.Bidirectional) {
354
+ return Promise.reject(new Error('Client is not connected'));
355
+ }
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
+ });
365
+ }
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();
233
373
  }
234
374
  async onMessage(buffer) {
235
375
  if (!this.messageContext)
236
376
  return;
237
377
  const message = this.protocol.decodeMessage(this.messageContext, buffer);
378
+ for (const plugin of this.plugins) {
379
+ plugin.onServerMessage?.(message, buffer);
380
+ }
381
+ this.emitClientEvent({
382
+ kind: 'server_message',
383
+ timestamp: Date.now(),
384
+ messageType: message.type,
385
+ rawByteLength: buffer.byteLength,
386
+ body: message,
387
+ });
238
388
  switch (message.type) {
239
389
  case ServerMessageType.RpcResponse:
240
- this.#handleRPCResponseMessage(message);
390
+ this.handleRPCResponseMessage(message);
241
391
  break;
242
392
  case ServerMessageType.RpcStreamResponse:
243
- this.#handleRPCStreamResponseMessage(message);
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
+ }
244
409
  break;
410
+ }
245
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
+ });
246
419
  this.rpcStreams.push(message.callId, message.chunk);
247
420
  break;
248
421
  case ServerMessageType.RpcStreamEnd:
422
+ this.emitStreamEvent({
423
+ direction: 'incoming',
424
+ streamType: 'rpc',
425
+ action: 'end',
426
+ callId: message.callId,
427
+ });
249
428
  this.rpcStreams.end(message.callId);
250
429
  this.calls.delete(message.callId);
251
430
  break;
252
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
+ });
253
439
  this.rpcStreams.abort(message.callId);
254
440
  this.calls.delete(message.callId);
255
441
  break;
256
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
+ });
257
450
  this.serverStreams.push(message.streamId, message.chunk);
258
451
  break;
259
452
  case ServerMessageType.ServerStreamEnd:
453
+ this.emitStreamEvent({
454
+ direction: 'incoming',
455
+ streamType: 'server_blob',
456
+ action: 'end',
457
+ streamId: message.streamId,
458
+ });
260
459
  this.serverStreams.end(message.streamId);
261
460
  break;
262
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
+ });
263
469
  this.serverStreams.abort(message.streamId);
264
470
  break;
265
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
+ });
266
479
  this.clientStreams.pull(message.streamId, message.size).then((chunk) => {
267
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
+ });
268
488
  const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.ClientStreamPush, { streamId: message.streamId, chunk });
269
- this.#send(buffer).catch(noopFn);
489
+ this.send(buffer).catch(noopFn);
270
490
  }
271
491
  else {
492
+ this.emitStreamEvent({
493
+ direction: 'outgoing',
494
+ streamType: 'client_blob',
495
+ action: 'end',
496
+ streamId: message.streamId,
497
+ });
272
498
  const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.ClientStreamEnd, { streamId: message.streamId });
273
- this.#send(buffer).catch(noopFn);
499
+ this.send(buffer).catch(noopFn);
274
500
  this.clientStreams.end(message.streamId);
275
501
  }
276
502
  }, () => {
503
+ this.emitStreamEvent({
504
+ direction: 'outgoing',
505
+ streamType: 'client_blob',
506
+ action: 'abort',
507
+ streamId: message.streamId,
508
+ });
277
509
  const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.ClientStreamAbort, { streamId: message.streamId });
278
- this.#send(buffer).catch(noopFn);
510
+ this.send(buffer).catch(noopFn);
279
511
  this.clientStreams.remove(message.streamId);
280
512
  });
281
513
  break;
282
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
+ });
283
522
  this.clientStreams.abort(message.streamId);
284
523
  break;
285
524
  }
286
525
  }
287
- #handleRPCResponseMessage(message) {
526
+ handleRPCResponseMessage(message) {
288
527
  const { callId, result, error } = message;
289
528
  const call = this.calls.get(callId);
290
529
  if (!call)
291
530
  return;
292
531
  if (error) {
532
+ this.emitClientEvent({
533
+ kind: 'rpc_error',
534
+ timestamp: Date.now(),
535
+ callId,
536
+ procedure: call.procedure,
537
+ error,
538
+ });
293
539
  call.reject(new ProtocolError(error.code, error.message, error.data));
294
540
  }
295
541
  else {
296
542
  try {
297
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
+ });
298
551
  call.resolve(transformed);
299
552
  }
300
553
  catch (error) {
554
+ this.emitClientEvent({
555
+ kind: 'rpc_error',
556
+ timestamp: Date.now(),
557
+ callId,
558
+ procedure: call.procedure,
559
+ error,
560
+ });
301
561
  call.reject(new ProtocolError(ErrorCode.ClientRequestError, 'Unable to decode response', error));
302
562
  }
303
563
  }
304
564
  }
305
- #handleRPCStreamResponseMessage(message) {
565
+ handleRPCStreamResponseMessage(message) {
306
566
  const call = this.calls.get(message.callId);
307
567
  if (message.error) {
308
568
  if (!call)
309
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
+ });
310
577
  call.reject(new ProtocolError(message.error.code, message.error.message, message.error.data));
311
578
  }
312
579
  else {
313
580
  if (call) {
314
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
+ });
315
589
  const stream = new ProtocolServerRPCStream({
316
590
  start: (controller) => {
317
591
  if (signal) {
@@ -325,7 +599,7 @@ export class BaseClient extends EventEmitter {
325
599
  this.calls.delete(message.callId);
326
600
  if (this.messageContext) {
327
601
  const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.RpcAbort, { callId: message.callId, reason: signal.reason });
328
- this.#send(buffer).catch(noopFn);
602
+ this.send(buffer).catch(noopFn);
329
603
  }
330
604
  }
331
605
  }, { once: true });
@@ -334,10 +608,6 @@ export class BaseClient extends EventEmitter {
334
608
  transform: (chunk) => {
335
609
  return this.transformer.decode(procedure, this.options.format.decode(chunk));
336
610
  },
337
- pull: () => {
338
- const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.RpcPull, { callId: message.callId });
339
- this.#send(buffer).catch(noopFn);
340
- },
341
611
  readableStrategy: { highWaterMark: 0 },
342
612
  });
343
613
  this.rpcStreams.add(message.callId, stream);
@@ -349,15 +619,22 @@ export class BaseClient extends EventEmitter {
349
619
  // Need to send an abort for the stream to avoid resource leaks from server side
350
620
  if (this.messageContext) {
351
621
  const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.RpcAbort, { callId: message.callId });
352
- this.#send(buffer).catch(noopFn);
622
+ this.send(buffer).catch(noopFn);
353
623
  }
354
624
  }
355
625
  }
356
626
  }
357
- #handleCallResponse(callId, response) {
627
+ handleCallResponse(callId, response) {
358
628
  const call = this.calls.get(callId);
359
629
  if (response.type === 'rpc_stream') {
360
630
  if (call) {
631
+ this.emitClientEvent({
632
+ kind: 'rpc_response',
633
+ timestamp: Date.now(),
634
+ callId,
635
+ procedure: call.procedure,
636
+ stream: true,
637
+ });
361
638
  const stream = new ProtocolServerStream({
362
639
  transform: (chunk) => {
363
640
  return this.transformer.decode(call.procedure, this.options.format.decode(chunk));
@@ -365,7 +642,38 @@ export class BaseClient extends EventEmitter {
365
642
  });
366
643
  this.rpcStreams.add(callId, stream);
367
644
  call.resolve(({ signal }) => {
368
- response.stream.pipeTo(stream.writable, { signal }).catch(noopFn);
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
+ })();
369
677
  return stream;
370
678
  });
371
679
  }
@@ -378,9 +686,16 @@ export class BaseClient extends EventEmitter {
378
686
  }
379
687
  else if (response.type === 'blob') {
380
688
  if (call) {
689
+ this.emitClientEvent({
690
+ kind: 'rpc_response',
691
+ timestamp: Date.now(),
692
+ callId,
693
+ procedure: call.procedure,
694
+ stream: true,
695
+ });
381
696
  const { metadata, source } = response;
382
697
  const stream = new ProtocolServerBlobStream(metadata);
383
- this.serverStreams.add(this.#getStreamId(), stream);
698
+ this.serverStreams.add(this.getStreamId(), stream);
384
699
  call.resolve(({ signal }) => {
385
700
  source.pipeTo(stream.writable, { signal }).catch(noopFn);
386
701
  return stream;
@@ -397,30 +712,63 @@ export class BaseClient extends EventEmitter {
397
712
  if (!call)
398
713
  return;
399
714
  try {
400
- const transformed = this.transformer.decode(call.procedure, response.result);
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
+ });
401
726
  call.resolve(transformed);
402
727
  }
403
728
  catch (error) {
729
+ this.emitClientEvent({
730
+ kind: 'rpc_error',
731
+ timestamp: Date.now(),
732
+ callId,
733
+ procedure: call.procedure,
734
+ error,
735
+ });
404
736
  call.reject(new ProtocolError(ErrorCode.ClientRequestError, 'Unable to decode response', error));
405
737
  }
406
738
  }
407
739
  }
408
- #send(buffer, signal) {
740
+ send(buffer, signal) {
409
741
  if (this.transport.type === ConnectionType.Unidirectional)
410
742
  throw new Error('Invalid transport type for send');
411
743
  return this.transport.send(buffer, { signal });
412
744
  }
413
- #getStreamId() {
745
+ emitStreamEvent(event) {
746
+ this.emitClientEvent({
747
+ kind: 'stream_event',
748
+ timestamp: Date.now(),
749
+ ...event,
750
+ });
751
+ }
752
+ getStreamId() {
414
753
  if (this.streamId >= MAX_UINT32) {
415
754
  this.streamId = 0;
416
755
  }
417
756
  return this.streamId++;
418
757
  }
419
- #getCallId() {
758
+ getCallId() {
420
759
  if (this.callId >= MAX_UINT32) {
421
760
  this.callId = 0;
422
761
  }
423
762
  return this.callId++;
424
763
  }
764
+ emitClientEvent(event) {
765
+ for (const plugin of this.plugins) {
766
+ try {
767
+ const result = plugin.onClientEvent?.(event);
768
+ Promise.resolve(result).catch(noopFn);
769
+ }
770
+ catch { }
771
+ }
772
+ }
425
773
  }
426
774
  //# sourceMappingURL=core.js.map