@nmtjs/client 0.15.0-beta.2 → 0.15.0-beta.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/core.ts ADDED
@@ -0,0 +1,648 @@
1
+ import type { Future, TypeProvider } from '@nmtjs/common'
2
+ import type { TAnyRouterContract } from '@nmtjs/contract'
3
+ import type { ProtocolBlobMetadata, ProtocolVersion } from '@nmtjs/protocol'
4
+ import type {
5
+ BaseClientFormat,
6
+ MessageContext,
7
+ ProtocolVersionInterface,
8
+ ServerMessageTypePayload,
9
+ } from '@nmtjs/protocol/client'
10
+ import { anyAbortSignal, createFuture, MAX_UINT32, noopFn } from '@nmtjs/common'
11
+ import {
12
+ ClientMessageType,
13
+ ConnectionType,
14
+ ErrorCode,
15
+ ProtocolBlob,
16
+ ServerMessageType,
17
+ } from '@nmtjs/protocol'
18
+ import {
19
+ ProtocolError,
20
+ ProtocolServerBlobStream,
21
+ ProtocolServerRPCStream,
22
+ ProtocolServerStream,
23
+ versions,
24
+ } from '@nmtjs/protocol/client'
25
+
26
+ import type { BaseClientTransformer } from './transformers.ts'
27
+ import type { ClientCallResponse, ClientTransportFactory } from './transport.ts'
28
+ import type {
29
+ ClientCallers,
30
+ ClientCallOptions,
31
+ ResolveAPIRouterRoutes,
32
+ } from './types.ts'
33
+ import { EventEmitter } from './events.ts'
34
+ import { ClientStreams, ServerStreams } from './streams.ts'
35
+
36
+ export {
37
+ ErrorCode,
38
+ ProtocolBlob,
39
+ type ProtocolBlobMetadata,
40
+ } from '@nmtjs/protocol'
41
+
42
+ export * from './types.ts'
43
+
44
+ export class ClientError extends ProtocolError {}
45
+
46
+ export type ProtocolClientCall = Future<any> & {
47
+ procedure: string
48
+ signal?: AbortSignal
49
+ }
50
+
51
+ const DEFAULT_RECONNECT_TIMEOUT = 1000
52
+ const DEFAULT_MAX_RECONNECT_TIMEOUT = 60000
53
+
54
+ export interface BaseClientOptions<
55
+ RouterContract extends TAnyRouterContract = TAnyRouterContract,
56
+ SafeCall extends boolean = false,
57
+ > {
58
+ contract: RouterContract
59
+ protocol: ProtocolVersion
60
+ format: BaseClientFormat
61
+ application?: string
62
+ timeout?: number
63
+ autoreconnect?: boolean
64
+ safe?: SafeCall
65
+ }
66
+
67
+ /**
68
+ * @todo Add error logging in ClientStreamPull rejection handler for easier debugging
69
+ * @todo Consider edge case where callId/streamId overflow at MAX_UINT32 with existing entries
70
+ */
71
+ export abstract class BaseClient<
72
+ TransportFactory extends ClientTransportFactory<
73
+ any,
74
+ any
75
+ > = ClientTransportFactory<any, any>,
76
+ RouterContract extends TAnyRouterContract = TAnyRouterContract,
77
+ SafeCall extends boolean = false,
78
+ InputTypeProvider extends TypeProvider = TypeProvider,
79
+ OutputTypeProvider extends TypeProvider = TypeProvider,
80
+ > extends EventEmitter<{
81
+ connected: []
82
+ disconnected: [reason: 'server' | 'client' | (string & {})]
83
+ }> {
84
+ _!: {
85
+ routes: ResolveAPIRouterRoutes<
86
+ RouterContract,
87
+ InputTypeProvider,
88
+ OutputTypeProvider
89
+ >
90
+ safe: SafeCall
91
+ }
92
+
93
+ protected abstract readonly transformer: BaseClientTransformer
94
+
95
+ abstract call: ClientCallers<this['_']['routes'], SafeCall, false>
96
+ abstract stream: ClientCallers<this['_']['routes'], SafeCall, true>
97
+
98
+ protected calls = new Map<number, ProtocolClientCall>()
99
+ protected transport: TransportFactory extends ClientTransportFactory<
100
+ any,
101
+ any,
102
+ infer T
103
+ >
104
+ ? T
105
+ : never
106
+ protected protocol: ProtocolVersionInterface
107
+ protected messageContext!: MessageContext | null
108
+ protected clientStreams = new ClientStreams()
109
+ protected serverStreams = new ServerStreams()
110
+ protected rpcStreams = new ServerStreams()
111
+ protected callId = 0
112
+ protected streamId = 0
113
+ protected cab: AbortController | null = null
114
+ protected reconnectTimeout = DEFAULT_RECONNECT_TIMEOUT
115
+ protected connecting: Promise<void> | null = null
116
+ protected state: 'connected' | 'disconnected' = 'disconnected'
117
+
118
+ #auth: any
119
+
120
+ constructor(
121
+ readonly options: BaseClientOptions<RouterContract, SafeCall>,
122
+ readonly transportFactory: TransportFactory,
123
+ readonly transportOptions: TransportFactory extends ClientTransportFactory<
124
+ any,
125
+ infer U
126
+ >
127
+ ? U
128
+ : never,
129
+ ) {
130
+ super()
131
+
132
+ this.protocol = versions[options.protocol]
133
+
134
+ const { format, protocol } = this.options
135
+
136
+ this.transport = this.transportFactory(
137
+ { protocol, format },
138
+ this.transportOptions,
139
+ ) as any
140
+
141
+ if (
142
+ this.transport.type === ConnectionType.Bidirectional &&
143
+ this.options.autoreconnect
144
+ ) {
145
+ this.on('disconnected', async (reason) => {
146
+ while (this.state === 'disconnected') {
147
+ const timeout = new Promise((resolve) =>
148
+ setTimeout(resolve, this.reconnectTimeout),
149
+ )
150
+ this.reconnectTimeout = Math.min(
151
+ this.reconnectTimeout * 2,
152
+ DEFAULT_MAX_RECONNECT_TIMEOUT,
153
+ )
154
+ await timeout
155
+ await this.connect().catch(noopFn)
156
+ }
157
+ })
158
+
159
+ this.on('connected', () => {
160
+ this.reconnectTimeout = DEFAULT_RECONNECT_TIMEOUT
161
+ })
162
+
163
+ if (globalThis.window) {
164
+ globalThis.window.addEventListener('pageshow', () => {
165
+ if (this.state === 'disconnected') this.connect()
166
+ })
167
+ }
168
+ }
169
+ }
170
+
171
+ get auth() {
172
+ return this.#auth
173
+ }
174
+
175
+ set auth(value) {
176
+ this.#auth = value
177
+ }
178
+
179
+ connect() {
180
+ if (this.state === 'connected') return Promise.resolve()
181
+ if (this.connecting) return this.connecting
182
+
183
+ const _connect = async () => {
184
+ if (this.transport.type === ConnectionType.Bidirectional) {
185
+ this.cab = new AbortController()
186
+ const protocol = this.protocol
187
+ const serverStreams = this.serverStreams
188
+ const transport = {
189
+ send: (buffer) => {
190
+ this.#send(buffer).catch(noopFn)
191
+ },
192
+ }
193
+ this.messageContext = {
194
+ transport,
195
+ encoder: this.options.format,
196
+ decoder: this.options.format,
197
+ addClientStream: (blob) => {
198
+ const streamId = this.#getStreamId()
199
+ return this.clientStreams.add(blob.source, streamId, blob.metadata)
200
+ },
201
+ addServerStream(streamId, metadata) {
202
+ const stream = new ProtocolServerBlobStream(metadata, {
203
+ pull: (controller) => {
204
+ transport.send(
205
+ protocol.encodeMessage(
206
+ this,
207
+ ClientMessageType.ServerStreamPull,
208
+ { streamId, size: 65535 /* 64kb by default */ },
209
+ ),
210
+ )
211
+ },
212
+ close: () => {
213
+ serverStreams.remove(streamId)
214
+ },
215
+ readableStrategy: { highWaterMark: 0 },
216
+ })
217
+ serverStreams.add(streamId, stream)
218
+ return ({ signal }: { signal?: AbortSignal } = {}) => {
219
+ if (signal)
220
+ signal.addEventListener(
221
+ 'abort',
222
+ () => {
223
+ transport.send(
224
+ protocol.encodeMessage(
225
+ this,
226
+ ClientMessageType.ServerStreamAbort,
227
+ { streamId },
228
+ ),
229
+ )
230
+ serverStreams.abort(streamId)
231
+ },
232
+ { once: true },
233
+ )
234
+ return stream
235
+ }
236
+ },
237
+ streamId: this.#getStreamId.bind(this),
238
+ }
239
+ return this.transport.connect({
240
+ auth: this.auth,
241
+ application: this.options.application,
242
+ onMessage: this.onMessage.bind(this),
243
+ onConnect: this.onConnect.bind(this),
244
+ onDisconnect: this.onDisconnect.bind(this),
245
+ })
246
+ }
247
+ }
248
+
249
+ this.connecting = _connect()
250
+ .then(() => {
251
+ this.state = 'connected'
252
+ })
253
+ .finally(() => {
254
+ this.connecting = null
255
+ })
256
+
257
+ return this.connecting
258
+ }
259
+
260
+ async disconnect() {
261
+ if (this.transport.type === ConnectionType.Bidirectional) {
262
+ this.cab!.abort()
263
+ await this.transport.disconnect()
264
+ this.messageContext = null
265
+ this.cab = null
266
+ }
267
+ }
268
+
269
+ blob(
270
+ source: Blob | ReadableStream | string | AsyncIterable<Uint8Array>,
271
+ metadata?: ProtocolBlobMetadata,
272
+ ) {
273
+ return ProtocolBlob.from(source, metadata)
274
+ }
275
+
276
+ protected async _call(
277
+ procedure: string,
278
+ payload: any,
279
+ options: ClientCallOptions = {},
280
+ ) {
281
+ const timeout = options.timeout ?? this.options.timeout
282
+ const controller = new AbortController()
283
+
284
+ // attach all abort signals
285
+ const signals: AbortSignal[] = [controller.signal]
286
+
287
+ if (timeout) signals.push(AbortSignal.timeout(timeout))
288
+ if (options.signal) signals.push(options.signal)
289
+ if (this.cab?.signal) signals.push(this.cab.signal)
290
+
291
+ const signal = signals.length ? anyAbortSignal(...signals) : undefined
292
+
293
+ const callId = this.#getCallId()
294
+ const call = createFuture() as ProtocolClientCall
295
+ call.procedure = procedure
296
+ call.signal = signal
297
+
298
+ this.calls.set(callId, call)
299
+
300
+ // Check if signal is already aborted before proceeding
301
+ if (signal?.aborted) {
302
+ this.calls.delete(callId)
303
+ const error = new ProtocolError(
304
+ ErrorCode.ClientRequestError,
305
+ signal.reason,
306
+ )
307
+ call.reject(error)
308
+ } else {
309
+ if (signal) {
310
+ signal.addEventListener(
311
+ 'abort',
312
+ () => {
313
+ call.reject(
314
+ new ProtocolError(ErrorCode.ClientRequestError, signal!.reason),
315
+ )
316
+ if (
317
+ this.transport.type === ConnectionType.Bidirectional &&
318
+ this.messageContext
319
+ ) {
320
+ const buffer = this.protocol.encodeMessage(
321
+ this.messageContext,
322
+ ClientMessageType.RpcAbort,
323
+ { callId },
324
+ )
325
+ this.#send(buffer).catch(noopFn)
326
+ }
327
+ },
328
+ { once: true },
329
+ )
330
+ }
331
+
332
+ try {
333
+ const transformedPayload = this.transformer.encode(procedure, payload)
334
+ if (this.transport.type === ConnectionType.Bidirectional) {
335
+ const buffer = this.protocol.encodeMessage(
336
+ this.messageContext!,
337
+ ClientMessageType.Rpc,
338
+ { callId, procedure, payload: transformedPayload },
339
+ )
340
+ await this.#send(buffer, signal)
341
+ } else {
342
+ const response = await this.transport.call(
343
+ {
344
+ application: this.options.application,
345
+ format: this.options.format,
346
+ auth: this.auth,
347
+ },
348
+ { callId, procedure, payload: transformedPayload },
349
+ { signal, _stream_response: options._stream_response },
350
+ )
351
+ this.#handleCallResponse(callId, response)
352
+ }
353
+ } catch (error) {
354
+ call.reject(error)
355
+ }
356
+ }
357
+
358
+ const result = call.promise.then(
359
+ (value) => {
360
+ if (value instanceof ProtocolServerRPCStream) {
361
+ return value.createAsyncIterable(() => {
362
+ controller.abort()
363
+ })
364
+ }
365
+ controller.abort()
366
+ return value
367
+ },
368
+ (err) => {
369
+ controller.abort()
370
+ throw err
371
+ },
372
+ )
373
+
374
+ if (this.options.safe) {
375
+ return await result
376
+ .then((result) => ({ result }))
377
+ .catch((error) => ({ error }))
378
+ .finally(() => {
379
+ this.calls.delete(callId)
380
+ })
381
+ } else {
382
+ return await result.finally(() => {
383
+ this.calls.delete(callId)
384
+ })
385
+ }
386
+ }
387
+
388
+ protected async onConnect() {
389
+ this.state = 'connected'
390
+ this.emit('connected')
391
+ }
392
+
393
+ protected async onDisconnect(reason: 'client' | 'server' | (string & {})) {
394
+ this.state = 'disconnected'
395
+ this.emit('disconnected', reason)
396
+ this.clientStreams.clear(reason)
397
+ this.serverStreams.clear(reason)
398
+ this.rpcStreams.clear(reason)
399
+ }
400
+
401
+ protected async onMessage(buffer: ArrayBufferView) {
402
+ if (!this.messageContext) return
403
+
404
+ const message = this.protocol.decodeMessage(this.messageContext, buffer)
405
+
406
+ switch (message.type) {
407
+ case ServerMessageType.RpcResponse:
408
+ this.#handleRPCResponseMessage(message)
409
+ break
410
+ case ServerMessageType.RpcStreamResponse:
411
+ this.#handleRPCStreamResponseMessage(message)
412
+ break
413
+ case ServerMessageType.RpcStreamChunk:
414
+ this.rpcStreams.push(message.callId, message.chunk)
415
+ break
416
+ case ServerMessageType.RpcStreamEnd:
417
+ this.rpcStreams.end(message.callId)
418
+ this.calls.delete(message.callId)
419
+ break
420
+ case ServerMessageType.RpcStreamAbort:
421
+ this.rpcStreams.abort(message.callId)
422
+ this.calls.delete(message.callId)
423
+ break
424
+ case ServerMessageType.ServerStreamPush:
425
+ this.serverStreams.push(message.streamId, message.chunk)
426
+ break
427
+ case ServerMessageType.ServerStreamEnd:
428
+ this.serverStreams.end(message.streamId)
429
+ break
430
+ case ServerMessageType.ServerStreamAbort:
431
+ this.serverStreams.abort(message.streamId)
432
+ break
433
+ case ServerMessageType.ClientStreamPull:
434
+ this.clientStreams.pull(message.streamId, message.size).then(
435
+ (chunk) => {
436
+ if (chunk) {
437
+ const buffer = this.protocol.encodeMessage(
438
+ this.messageContext!,
439
+ ClientMessageType.ClientStreamPush,
440
+ { streamId: message.streamId, chunk },
441
+ )
442
+ this.#send(buffer).catch(noopFn)
443
+ } else {
444
+ const buffer = this.protocol.encodeMessage(
445
+ this.messageContext!,
446
+ ClientMessageType.ClientStreamEnd,
447
+ { streamId: message.streamId },
448
+ )
449
+ this.#send(buffer).catch(noopFn)
450
+ this.clientStreams.end(message.streamId)
451
+ }
452
+ },
453
+ () => {
454
+ const buffer = this.protocol.encodeMessage(
455
+ this.messageContext!,
456
+ ClientMessageType.ClientStreamAbort,
457
+ { streamId: message.streamId },
458
+ )
459
+ this.#send(buffer).catch(noopFn)
460
+ this.clientStreams.remove(message.streamId)
461
+ },
462
+ )
463
+ break
464
+ case ServerMessageType.ClientStreamAbort:
465
+ this.clientStreams.abort(message.streamId)
466
+ break
467
+ }
468
+ }
469
+
470
+ #handleRPCResponseMessage(
471
+ message: ServerMessageTypePayload[ServerMessageType.RpcResponse],
472
+ ) {
473
+ const { callId, result, error } = message
474
+ const call = this.calls.get(callId)
475
+ if (!call) return
476
+ if (error) {
477
+ call.reject(new ProtocolError(error.code, error.message, error.data))
478
+ } else {
479
+ try {
480
+ const transformed = this.transformer.decode(call.procedure, result)
481
+ call.resolve(transformed)
482
+ } catch (error) {
483
+ call.reject(
484
+ new ProtocolError(
485
+ ErrorCode.ClientRequestError,
486
+ 'Unable to decode response',
487
+ error,
488
+ ),
489
+ )
490
+ }
491
+ }
492
+ }
493
+
494
+ #handleRPCStreamResponseMessage(
495
+ message: ServerMessageTypePayload[ServerMessageType.RpcStreamResponse],
496
+ ) {
497
+ const call = this.calls.get(message.callId)
498
+ if (message.error) {
499
+ if (!call) return
500
+ call.reject(
501
+ new ProtocolError(
502
+ message.error.code,
503
+ message.error.message,
504
+ message.error.data,
505
+ ),
506
+ )
507
+ } else {
508
+ if (call) {
509
+ const { procedure, signal } = call
510
+ const stream = new ProtocolServerRPCStream({
511
+ start: (controller) => {
512
+ if (signal) {
513
+ if (signal.aborted) controller.error(signal.reason)
514
+ else
515
+ signal.addEventListener(
516
+ 'abort',
517
+ () => {
518
+ controller.error(signal.reason)
519
+ if (this.rpcStreams.has(message.callId)) {
520
+ this.rpcStreams.remove(message.callId)
521
+ this.calls.delete(message.callId)
522
+ if (this.messageContext) {
523
+ const buffer = this.protocol.encodeMessage(
524
+ this.messageContext,
525
+ ClientMessageType.RpcAbort,
526
+ { callId: message.callId, reason: signal.reason },
527
+ )
528
+ this.#send(buffer).catch(noopFn)
529
+ }
530
+ }
531
+ },
532
+ { once: true },
533
+ )
534
+ }
535
+ },
536
+ transform: (chunk) => {
537
+ return this.transformer.decode(
538
+ procedure,
539
+ this.options.format.decode(chunk),
540
+ )
541
+ },
542
+ pull: () => {
543
+ const buffer = this.protocol.encodeMessage(
544
+ this.messageContext!,
545
+ ClientMessageType.RpcPull,
546
+ { callId: message.callId },
547
+ )
548
+ this.#send(buffer).catch(noopFn)
549
+ },
550
+ readableStrategy: { highWaterMark: 0 },
551
+ })
552
+ this.rpcStreams.add(message.callId, stream)
553
+ call.resolve(stream)
554
+ } else {
555
+ // Call not found, but stream response received
556
+ // This can happen if the call was aborted or timed out
557
+ // Need to send an abort for the stream to avoid resource leaks from server side
558
+ if (this.messageContext) {
559
+ const buffer = this.protocol.encodeMessage(
560
+ this.messageContext,
561
+ ClientMessageType.RpcAbort,
562
+ { callId: message.callId },
563
+ )
564
+ this.#send(buffer).catch(noopFn)
565
+ }
566
+ }
567
+ }
568
+ }
569
+
570
+ #handleCallResponse(callId: number, response: ClientCallResponse) {
571
+ const call = this.calls.get(callId)
572
+
573
+ if (response.type === 'rpc_stream') {
574
+ if (call) {
575
+ const stream = new ProtocolServerStream({
576
+ transform: (chunk) => {
577
+ return this.transformer.decode(
578
+ call.procedure,
579
+ this.options.format.decode(chunk),
580
+ )
581
+ },
582
+ })
583
+ this.rpcStreams.add(callId, stream)
584
+ call.resolve(({ signal }: { signal?: AbortSignal }) => {
585
+ response.stream.pipeTo(stream.writable, { signal }).catch(noopFn)
586
+ return stream
587
+ })
588
+ } else {
589
+ // Call not found, but stream response received
590
+ // This can happen if the call was aborted or timed out
591
+ // Need to cancel the stream to avoid resource leaks from server side
592
+ response.stream.cancel().catch(noopFn)
593
+ }
594
+ } else if (response.type === 'blob') {
595
+ if (call) {
596
+ const { metadata, source } = response
597
+ const stream = new ProtocolServerBlobStream(metadata)
598
+ this.serverStreams.add(this.#getStreamId(), stream)
599
+ call.resolve(({ signal }: { signal?: AbortSignal }) => {
600
+ source.pipeTo(stream.writable, { signal }).catch(noopFn)
601
+ return stream
602
+ })
603
+ } else {
604
+ // Call not found, but blob response received
605
+ // This can happen if the call was aborted or timed out
606
+ // Need to cancel the stream to avoid resource leaks from server side
607
+ response.source.cancel().catch(noopFn)
608
+ }
609
+ } else if (response.type === 'rpc') {
610
+ if (!call) return
611
+ try {
612
+ const transformed = this.transformer.decode(
613
+ call.procedure,
614
+ response.result,
615
+ )
616
+ call.resolve(transformed)
617
+ } catch (error) {
618
+ call.reject(
619
+ new ProtocolError(
620
+ ErrorCode.ClientRequestError,
621
+ 'Unable to decode response',
622
+ error,
623
+ ),
624
+ )
625
+ }
626
+ }
627
+ }
628
+
629
+ #send(buffer: ArrayBufferView, signal?: AbortSignal) {
630
+ if (this.transport.type === ConnectionType.Unidirectional)
631
+ throw new Error('Invalid transport type for send')
632
+ return this.transport.send(buffer, { signal })
633
+ }
634
+
635
+ #getStreamId() {
636
+ if (this.streamId >= MAX_UINT32) {
637
+ this.streamId = 0
638
+ }
639
+ return this.streamId++
640
+ }
641
+
642
+ #getCallId() {
643
+ if (this.callId >= MAX_UINT32) {
644
+ this.callId = 0
645
+ }
646
+ return this.callId++
647
+ }
648
+ }
package/src/events.ts ADDED
@@ -0,0 +1,70 @@
1
+ import type { Callback } from '@nmtjs/common'
2
+
3
+ export type EventMap = { [K: string]: any[] }
4
+
5
+ // TODO: add errors and promise rejections handling
6
+ /**
7
+ * Thin node-like event emitter wrapper around EventTarget
8
+ */
9
+ export class EventEmitter<
10
+ Events extends EventMap = EventMap,
11
+ EventName extends Extract<keyof Events, string> = Extract<
12
+ keyof Events,
13
+ string
14
+ >,
15
+ > {
16
+ static once<
17
+ T extends EventEmitter,
18
+ E extends T extends EventEmitter<any, infer Event> ? Event : never,
19
+ >(ee: T, event: E) {
20
+ return new Promise((resolve) => ee.once(event, resolve))
21
+ }
22
+
23
+ #target = new EventTarget()
24
+ #listeners = new Map<Callback, Callback>()
25
+
26
+ on<E extends EventName>(
27
+ event: E | (Object & string),
28
+ listener: (...args: Events[E]) => void,
29
+ options?: AddEventListenerOptions,
30
+ ) {
31
+ const wrapper = (event) => listener(...event.detail)
32
+ this.#listeners.set(listener, wrapper)
33
+ this.#target.addEventListener(event, wrapper, { ...options, once: false })
34
+ return () => this.#target.removeEventListener(event, wrapper)
35
+ }
36
+
37
+ once<E extends EventName>(
38
+ event: E | (Object & string),
39
+ listener: (...args: Events[E]) => void,
40
+ options?: AddEventListenerOptions,
41
+ ) {
42
+ return this.on(event, listener, { ...options, once: true })
43
+ }
44
+
45
+ off(event: EventName | (Object & string), listener: Callback) {
46
+ const wrapper = this.#listeners.get(listener)
47
+ if (wrapper) this.#target.removeEventListener(event, wrapper)
48
+ }
49
+
50
+ emit<E extends EventName | (Object & string)>(
51
+ event: E,
52
+ ...args: E extends EventName ? Events[E] : any[]
53
+ ) {
54
+ return this.#target.dispatchEvent(new CustomEvent(event, { detail: args }))
55
+ }
56
+ }
57
+
58
+ export const once = <
59
+ T extends EventEmitter,
60
+ EventMap extends T extends EventEmitter<infer E, any> ? E : never,
61
+ EventName extends T extends EventEmitter<any, infer N> ? N : never,
62
+ >(
63
+ ee: T,
64
+ event: EventName,
65
+ signal?: AbortSignal,
66
+ ) => {
67
+ return new Promise<EventMap[EventName]>((resolve) => {
68
+ ee.once(event, resolve, { signal })
69
+ })
70
+ }