@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
@@ -0,0 +1,725 @@
1
+ import type { Future } from '@nmtjs/common'
2
+ import type { ServerMessageTypePayload } from '@nmtjs/protocol/client'
3
+ import { anyAbortSignal, createFuture, MAX_UINT32, noopFn } from '@nmtjs/common'
4
+ import {
5
+ ClientMessageType,
6
+ ConnectionType,
7
+ ErrorCode,
8
+ ProtocolBlob,
9
+ ServerMessageType,
10
+ } from '@nmtjs/protocol'
11
+ import { ProtocolError, ProtocolServerRPCStream } from '@nmtjs/protocol/client'
12
+
13
+ import type { ClientCore } from '../core.ts'
14
+ import type { BaseClientTransformer } from '../transformers.ts'
15
+ import type { ClientCallOptions } from '../types.ts'
16
+ import type { StreamLayerApi } from './streams.ts'
17
+ import { ServerStreams } from '../streams.ts'
18
+ import { createServerBlobConsumer } from './streams.ts'
19
+
20
+ export type ProtocolClientCall = Future<any> & {
21
+ procedure: string
22
+ signal?: AbortSignal
23
+ cleanup?: () => void
24
+ }
25
+
26
+ export interface RpcLayerApi {
27
+ call(
28
+ procedure: string,
29
+ payload: any,
30
+ options?: ClientCallOptions,
31
+ ): Promise<any>
32
+ readonly pendingCallCount: number
33
+ readonly activeStreamCount: number
34
+ }
35
+
36
+ const toReasonString = (reason: unknown) => {
37
+ if (typeof reason === 'string') return reason
38
+ if (reason === undefined || reason === null) return undefined
39
+ return String(reason)
40
+ }
41
+
42
+ const waitForConnected = (core: ClientCore, signal?: AbortSignal) => {
43
+ if (core.state === 'connected') return Promise.resolve()
44
+
45
+ return new Promise<void>((resolve, reject) => {
46
+ if (signal?.aborted) {
47
+ reject(signal.reason)
48
+ return
49
+ }
50
+
51
+ const offConnected = core.once('connected', () => {
52
+ signal?.removeEventListener('abort', onAbort)
53
+ resolve()
54
+ })
55
+
56
+ const onAbort = () => {
57
+ offConnected()
58
+ reject(signal?.reason)
59
+ }
60
+
61
+ signal?.addEventListener('abort', onAbort, { once: true })
62
+ })
63
+ }
64
+
65
+ async function* reconnectingAsyncIterable<T>(
66
+ core: ClientCore,
67
+ initialIterable: AsyncIterable<T>,
68
+ callFn: () => Promise<AsyncIterable<T>>,
69
+ signal?: AbortSignal,
70
+ ): AsyncGenerator<T> {
71
+ let iterable: AsyncIterable<T> | null = initialIterable
72
+
73
+ while (!signal?.aborted) {
74
+ try {
75
+ const currentIterable = iterable ?? (await callFn())
76
+ iterable = null
77
+
78
+ for await (const item of currentIterable) {
79
+ yield item
80
+ }
81
+ return
82
+ } catch (error) {
83
+ iterable = null
84
+
85
+ if (signal?.aborted) throw error
86
+
87
+ if (
88
+ error instanceof ProtocolError &&
89
+ error.code === ErrorCode.ConnectionError
90
+ ) {
91
+ await waitForConnected(core, signal)
92
+ continue
93
+ }
94
+
95
+ throw error
96
+ }
97
+ }
98
+ }
99
+
100
+ const createManagedAsyncIterable = <T>(
101
+ iterable: AsyncIterable<T>,
102
+ options: {
103
+ onDone?: () => void
104
+ onReturn?: (value: unknown) => void
105
+ onThrow?: (error: unknown) => void
106
+ },
107
+ ): AsyncIterable<T> => {
108
+ return {
109
+ [Symbol.asyncIterator]() {
110
+ const iterator = iterable[Symbol.asyncIterator]()
111
+ let settled = false
112
+
113
+ const finish = () => {
114
+ if (settled) return
115
+ settled = true
116
+ options.onDone?.()
117
+ }
118
+
119
+ return {
120
+ async next() {
121
+ const result = await iterator.next()
122
+ if (result.done) {
123
+ finish()
124
+ }
125
+ return result
126
+ },
127
+ async return(value) {
128
+ options.onReturn?.(value)
129
+ finish()
130
+ return iterator.return?.(value) ?? { done: true, value }
131
+ },
132
+ async throw(error) {
133
+ options.onThrow?.(error)
134
+ finish()
135
+ return iterator.throw?.(error) ?? Promise.reject(error)
136
+ },
137
+ }
138
+ },
139
+ }
140
+ }
141
+
142
+ export const createRpcLayer = (
143
+ core: ClientCore,
144
+ streams: StreamLayerApi,
145
+ transformer: BaseClientTransformer,
146
+ options: { timeout?: number; safe?: boolean } = {},
147
+ ): RpcLayerApi => {
148
+ const calls = new Map<number, ProtocolClientCall>()
149
+ const rpcStreams = new ServerStreams<ProtocolServerRPCStream>()
150
+
151
+ let callId = 0
152
+
153
+ const nextCallId = () => {
154
+ if (callId >= MAX_UINT32) {
155
+ callId = 0
156
+ }
157
+
158
+ return callId++
159
+ }
160
+
161
+ const handleRPCResponseMessage = (
162
+ message: ServerMessageTypePayload[ServerMessageType.RpcResponse],
163
+ ) => {
164
+ const call = calls.get(message.callId)
165
+ if (!call) return
166
+
167
+ if (message.error) {
168
+ core.emitClientEvent({
169
+ kind: 'rpc_error',
170
+ timestamp: Date.now(),
171
+ callId: message.callId,
172
+ procedure: call.procedure,
173
+ error: message.error,
174
+ })
175
+
176
+ call.reject(
177
+ new ProtocolError(
178
+ message.error.code,
179
+ message.error.message,
180
+ message.error.data,
181
+ ),
182
+ )
183
+ return
184
+ }
185
+
186
+ try {
187
+ const transformed = transformer.decode(call.procedure, message.result)
188
+ core.emitClientEvent({
189
+ kind: 'rpc_response',
190
+ timestamp: Date.now(),
191
+ callId: message.callId,
192
+ procedure: call.procedure,
193
+ body: transformed,
194
+ })
195
+ call.resolve(transformed)
196
+ } catch (error) {
197
+ core.emitClientEvent({
198
+ kind: 'rpc_error',
199
+ timestamp: Date.now(),
200
+ callId: message.callId,
201
+ procedure: call.procedure,
202
+ error,
203
+ })
204
+ call.reject(
205
+ new ProtocolError(
206
+ ErrorCode.ClientRequestError,
207
+ 'Unable to decode response',
208
+ error,
209
+ ),
210
+ )
211
+ }
212
+ }
213
+
214
+ const handleRPCStreamResponseMessage = (
215
+ message: ServerMessageTypePayload[ServerMessageType.RpcStreamResponse],
216
+ ) => {
217
+ const call = calls.get(message.callId)
218
+
219
+ if (message.error) {
220
+ if (!call) return
221
+
222
+ core.emitClientEvent({
223
+ kind: 'rpc_error',
224
+ timestamp: Date.now(),
225
+ callId: message.callId,
226
+ procedure: call.procedure,
227
+ error: message.error,
228
+ })
229
+
230
+ call.reject(
231
+ new ProtocolError(
232
+ message.error.code,
233
+ message.error.message,
234
+ message.error.data,
235
+ ),
236
+ )
237
+ return
238
+ }
239
+
240
+ if (!call) {
241
+ if (!core.messageContext) return
242
+
243
+ const buffer = core.protocol.encodeMessage(
244
+ core.messageContext,
245
+ ClientMessageType.RpcAbort,
246
+ { callId: message.callId },
247
+ )
248
+
249
+ core.send(buffer).catch(noopFn)
250
+ return
251
+ }
252
+
253
+ core.emitClientEvent({
254
+ kind: 'rpc_response',
255
+ timestamp: Date.now(),
256
+ callId: message.callId,
257
+ procedure: call.procedure,
258
+ stream: true,
259
+ })
260
+
261
+ const { procedure, signal } = call
262
+ const stream = new ProtocolServerRPCStream({
263
+ start: (controller) => {
264
+ if (!signal) return
265
+
266
+ if (signal.aborted) {
267
+ controller.error(signal.reason)
268
+ return
269
+ }
270
+
271
+ const onAbort = () => {
272
+ controller.error(signal.reason)
273
+
274
+ if (rpcStreams.has(message.callId)) {
275
+ void rpcStreams.abort(message.callId).catch(noopFn)
276
+ if (core.messageContext) {
277
+ const buffer = core.protocol.encodeMessage(
278
+ core.messageContext,
279
+ ClientMessageType.RpcAbort,
280
+ {
281
+ callId: message.callId,
282
+ reason: toReasonString(signal.reason),
283
+ },
284
+ )
285
+ core.send(buffer).catch(noopFn)
286
+ }
287
+ }
288
+ }
289
+
290
+ signal.addEventListener('abort', onAbort, { once: true })
291
+ call.cleanup = () => {
292
+ signal.removeEventListener('abort', onAbort)
293
+ }
294
+ },
295
+ transform: (chunk) => {
296
+ return transformer.decode(procedure, core.format.decode(chunk))
297
+ },
298
+ readableStrategy: { highWaterMark: 0 },
299
+ })
300
+
301
+ rpcStreams.add(message.callId, stream)
302
+ call.resolve(stream)
303
+ }
304
+
305
+ const handleTransportErrorResponse = (
306
+ callId: number,
307
+ response: Extract<
308
+ Awaited<ReturnType<ClientCore['transportCall']>>,
309
+ { type: 'error' }
310
+ >,
311
+ ) => {
312
+ const call = calls.get(callId)
313
+ if (!call) return
314
+
315
+ let error: ProtocolError
316
+
317
+ try {
318
+ const decoded = core.format.decode(response.error) as {
319
+ code?: string
320
+ message?: string
321
+ data?: unknown
322
+ }
323
+
324
+ error = new ProtocolError(
325
+ decoded.code || ErrorCode.ClientRequestError,
326
+ decoded.message || response.statusText || 'Request failed',
327
+ decoded.data,
328
+ )
329
+ } catch {
330
+ error = new ProtocolError(
331
+ ErrorCode.ClientRequestError,
332
+ response.statusText
333
+ ? `HTTP ${response.status ?? ''}: ${response.statusText}`.trim()
334
+ : 'Request failed',
335
+ )
336
+ }
337
+
338
+ core.emitClientEvent({
339
+ kind: 'rpc_error',
340
+ timestamp: Date.now(),
341
+ callId,
342
+ procedure: call.procedure,
343
+ error,
344
+ })
345
+
346
+ call.reject(error)
347
+ }
348
+
349
+ const handleCallResponse = (
350
+ currentCallId: number,
351
+ response: Awaited<ReturnType<ClientCore['transportCall']>>,
352
+ ) => {
353
+ const call = calls.get(currentCallId)
354
+
355
+ if (response.type === 'error') {
356
+ handleTransportErrorResponse(currentCallId, response)
357
+ return
358
+ }
359
+
360
+ if (response.type === 'rpc_stream') {
361
+ if (!call) {
362
+ response.stream.cancel().catch(noopFn)
363
+ return
364
+ }
365
+
366
+ core.emitClientEvent({
367
+ kind: 'rpc_response',
368
+ timestamp: Date.now(),
369
+ callId: currentCallId,
370
+ procedure: call.procedure,
371
+ stream: true,
372
+ })
373
+
374
+ const reader = response.stream.getReader()
375
+ const { signal } = call
376
+ let onAbort: (() => void) | undefined
377
+
378
+ const stream = new ProtocolServerRPCStream({
379
+ start: (controller) => {
380
+ if (!signal) return
381
+
382
+ onAbort = () => {
383
+ controller.error(signal.reason)
384
+ reader.cancel(signal.reason).catch(noopFn)
385
+ void rpcStreams.abort(currentCallId).catch(noopFn)
386
+ }
387
+
388
+ if (signal.aborted) {
389
+ onAbort()
390
+ } else {
391
+ signal.addEventListener('abort', onAbort, { once: true })
392
+ }
393
+ },
394
+ transform: (chunk) => {
395
+ return transformer.decode(call.procedure, core.format.decode(chunk))
396
+ },
397
+ readableStrategy: { highWaterMark: 0 },
398
+ })
399
+
400
+ rpcStreams.add(currentCallId, stream)
401
+ call.resolve(stream)
402
+
403
+ void (async () => {
404
+ try {
405
+ while (true) {
406
+ const { done, value } = await reader.read()
407
+ if (done) break
408
+ await rpcStreams.push(currentCallId, value)
409
+ }
410
+ await rpcStreams.end(currentCallId)
411
+ } catch {
412
+ await rpcStreams.abort(currentCallId).catch(noopFn)
413
+ } finally {
414
+ reader.releaseLock()
415
+ if (signal && onAbort) {
416
+ signal.removeEventListener('abort', onAbort)
417
+ }
418
+ }
419
+ })()
420
+
421
+ return
422
+ }
423
+
424
+ if (response.type === 'blob') {
425
+ if (!call) {
426
+ response.source.cancel().catch(noopFn)
427
+ return
428
+ }
429
+
430
+ core.emitClientEvent({
431
+ kind: 'rpc_response',
432
+ timestamp: Date.now(),
433
+ callId: currentCallId,
434
+ procedure: call.procedure,
435
+ stream: true,
436
+ })
437
+
438
+ const { stream } = streams.addServerBlobStream(response.metadata)
439
+ let started = false
440
+ call.resolve(
441
+ createServerBlobConsumer(response.metadata, ({ signal } = {}) => {
442
+ if (!started) {
443
+ started = true
444
+ response.source.pipeTo(stream.writable, { signal }).catch(noopFn)
445
+ }
446
+
447
+ return stream
448
+ }),
449
+ )
450
+ return
451
+ }
452
+
453
+ if (!call) return
454
+
455
+ try {
456
+ const decodedPayload =
457
+ response.result.byteLength === 0
458
+ ? undefined
459
+ : core.format.decode(response.result)
460
+
461
+ const transformed = transformer.decode(call.procedure, decodedPayload)
462
+ core.emitClientEvent({
463
+ kind: 'rpc_response',
464
+ timestamp: Date.now(),
465
+ callId: currentCallId,
466
+ procedure: call.procedure,
467
+ body: transformed,
468
+ })
469
+ call.resolve(transformed)
470
+ } catch (error) {
471
+ core.emitClientEvent({
472
+ kind: 'rpc_error',
473
+ timestamp: Date.now(),
474
+ callId: currentCallId,
475
+ procedure: call.procedure,
476
+ error,
477
+ })
478
+ call.reject(
479
+ new ProtocolError(
480
+ ErrorCode.ClientRequestError,
481
+ 'Unable to decode response',
482
+ error,
483
+ ),
484
+ )
485
+ }
486
+ }
487
+
488
+ core.on('message', (message: any) => {
489
+ switch (message.type) {
490
+ case ServerMessageType.RpcResponse:
491
+ handleRPCResponseMessage(message)
492
+ break
493
+ case ServerMessageType.RpcStreamResponse:
494
+ handleRPCStreamResponseMessage(message)
495
+ break
496
+ case ServerMessageType.RpcStreamChunk:
497
+ core.emitStreamEvent({
498
+ direction: 'incoming',
499
+ streamType: 'rpc',
500
+ action: 'push',
501
+ callId: message.callId,
502
+ byteLength: message.chunk.byteLength,
503
+ })
504
+ void rpcStreams.push(message.callId, message.chunk)
505
+ break
506
+ case ServerMessageType.RpcStreamEnd:
507
+ calls.get(message.callId)?.cleanup?.()
508
+ core.emitStreamEvent({
509
+ direction: 'incoming',
510
+ streamType: 'rpc',
511
+ action: 'end',
512
+ callId: message.callId,
513
+ })
514
+ void rpcStreams.end(message.callId)
515
+ calls.delete(message.callId)
516
+ break
517
+ case ServerMessageType.RpcStreamAbort:
518
+ calls.get(message.callId)?.cleanup?.()
519
+ core.emitStreamEvent({
520
+ direction: 'incoming',
521
+ streamType: 'rpc',
522
+ action: 'abort',
523
+ callId: message.callId,
524
+ reason: message.reason,
525
+ })
526
+ void rpcStreams.abort(message.callId)
527
+ calls.delete(message.callId)
528
+ break
529
+ }
530
+ })
531
+
532
+ core.on('disconnected', (reason) => {
533
+ const error = new ProtocolError(ErrorCode.ConnectionError, 'Disconnected', {
534
+ reason,
535
+ })
536
+
537
+ for (const call of calls.values()) {
538
+ call.cleanup?.()
539
+ call.reject(error)
540
+ }
541
+ calls.clear()
542
+ void rpcStreams.clear(error).catch(noopFn)
543
+ })
544
+
545
+ const callInternal = async (
546
+ procedure: string,
547
+ payload: any,
548
+ callOptions: ClientCallOptions = {},
549
+ ) => {
550
+ const timeout = callOptions.timeout ?? options.timeout
551
+ const controller = new AbortController()
552
+
553
+ const signals: AbortSignal[] = [controller.signal]
554
+
555
+ if (timeout) signals.push(AbortSignal.timeout(timeout))
556
+ if (callOptions.signal) signals.push(callOptions.signal)
557
+ if (core.connectionSignal) signals.push(core.connectionSignal)
558
+
559
+ const signal = signals.length ? anyAbortSignal(...signals) : undefined
560
+ const currentCallId = nextCallId()
561
+ const call = createFuture() as ProtocolClientCall
562
+ call.procedure = procedure
563
+ call.signal = signal
564
+
565
+ calls.set(currentCallId, call)
566
+ core.emitClientEvent({
567
+ kind: 'rpc_request',
568
+ timestamp: Date.now(),
569
+ callId: currentCallId,
570
+ procedure,
571
+ body: payload,
572
+ })
573
+
574
+ if (signal?.aborted) {
575
+ call.reject(
576
+ new ProtocolError(ErrorCode.ClientRequestError, String(signal.reason)),
577
+ )
578
+ } else {
579
+ signal?.addEventListener(
580
+ 'abort',
581
+ () => {
582
+ call.reject(
583
+ new ProtocolError(
584
+ ErrorCode.ClientRequestError,
585
+ String(signal.reason),
586
+ ),
587
+ )
588
+
589
+ if (
590
+ core.transportType === ConnectionType.Bidirectional &&
591
+ core.messageContext
592
+ ) {
593
+ const buffer = core.protocol.encodeMessage(
594
+ core.messageContext,
595
+ ClientMessageType.RpcAbort,
596
+ { callId: currentCallId, reason: toReasonString(signal.reason) },
597
+ )
598
+ core.send(buffer).catch(noopFn)
599
+ }
600
+ },
601
+ { once: true },
602
+ )
603
+
604
+ try {
605
+ const transformedPayload = transformer.encode(procedure, payload)
606
+
607
+ if (core.transportType === ConnectionType.Bidirectional) {
608
+ if (!core.messageContext) {
609
+ throw new ProtocolError(
610
+ ErrorCode.ConnectionError,
611
+ 'Client is not connected',
612
+ )
613
+ }
614
+
615
+ const buffer = core.protocol.encodeMessage(
616
+ core.messageContext,
617
+ ClientMessageType.Rpc,
618
+ { callId: currentCallId, procedure, payload: transformedPayload },
619
+ )
620
+
621
+ await core.send(buffer, signal)
622
+ } else {
623
+ const blob =
624
+ transformedPayload instanceof ProtocolBlob
625
+ ? {
626
+ source: transformedPayload.source,
627
+ metadata: transformedPayload.metadata,
628
+ }
629
+ : undefined
630
+
631
+ const encodedPayload = blob
632
+ ? new Uint8Array(0)
633
+ : transformedPayload === undefined
634
+ ? new Uint8Array(0)
635
+ : core.format.encode(transformedPayload)
636
+
637
+ const response = await core.transportCall(
638
+ {
639
+ application: core.application,
640
+ auth: core.auth,
641
+ contentType: core.format.contentType,
642
+ },
643
+ { callId: currentCallId, procedure, payload: encodedPayload, blob },
644
+ { signal, streamResponse: callOptions._stream_response },
645
+ )
646
+
647
+ handleCallResponse(currentCallId, response)
648
+ }
649
+ } catch (error) {
650
+ core.emitClientEvent({
651
+ kind: 'rpc_error',
652
+ timestamp: Date.now(),
653
+ callId: currentCallId,
654
+ procedure,
655
+ error,
656
+ })
657
+ call.reject(error)
658
+ }
659
+ }
660
+
661
+ return call.promise
662
+ .then((value) => {
663
+ if (value instanceof ProtocolServerRPCStream) {
664
+ const stream = createManagedAsyncIterable(value, {
665
+ onDone: () => {
666
+ call.cleanup?.()
667
+ },
668
+ onReturn: (reason) => {
669
+ controller.abort(reason)
670
+ },
671
+ onThrow: (error) => {
672
+ controller.abort(error)
673
+ },
674
+ })
675
+
676
+ if (callOptions.autoReconnect) {
677
+ return reconnectingAsyncIterable(
678
+ core,
679
+ stream,
680
+ () =>
681
+ callInternal(procedure, payload, {
682
+ ...callOptions,
683
+ autoReconnect: false,
684
+ }),
685
+ callOptions.signal,
686
+ )
687
+ }
688
+
689
+ return stream
690
+ }
691
+
692
+ if (typeof value === 'function') {
693
+ return value
694
+ }
695
+
696
+ controller.abort()
697
+ return value
698
+ })
699
+ .catch((error) => {
700
+ controller.abort()
701
+ throw error
702
+ })
703
+ .finally(() => {
704
+ calls.delete(currentCallId)
705
+ })
706
+ }
707
+
708
+ return {
709
+ async call(procedure, payload, callOptions = {}) {
710
+ if (!options.safe) {
711
+ return callInternal(procedure, payload, callOptions)
712
+ }
713
+
714
+ return callInternal(procedure, payload, callOptions)
715
+ .then((result) => ({ result }))
716
+ .catch((error) => ({ error }))
717
+ },
718
+ get pendingCallCount() {
719
+ return calls.size
720
+ },
721
+ get activeStreamCount() {
722
+ return rpcStreams.size
723
+ },
724
+ }
725
+ }