@nmtjs/client 0.15.0-beta.43 → 0.15.0-beta.45
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/dist/clients/static.js +4 -1
- package/dist/clients/static.js.map +1 -1
- package/dist/core.d.ts +35 -8
- package/dist/core.js +314 -59
- package/dist/core.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/plugins/browser.d.ts +2 -0
- package/dist/plugins/browser.js +41 -0
- package/dist/plugins/browser.js.map +1 -0
- package/dist/plugins/heartbeat.d.ts +6 -0
- package/dist/plugins/heartbeat.js +86 -0
- package/dist/plugins/heartbeat.js.map +1 -0
- package/dist/plugins/index.d.ts +5 -0
- package/dist/plugins/index.js +6 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/plugins/logging.d.ts +9 -0
- package/dist/plugins/logging.js +30 -0
- package/dist/plugins/logging.js.map +1 -0
- package/dist/plugins/reconnect.d.ts +6 -0
- package/dist/plugins/reconnect.js +98 -0
- package/dist/plugins/reconnect.js.map +1 -0
- package/dist/plugins/types.d.ts +53 -0
- package/dist/plugins/types.js +2 -0
- package/dist/plugins/types.js.map +1 -0
- package/package.json +25 -13
- package/src/clients/static.ts +5 -1
- package/src/core.ts +371 -69
- package/src/index.ts +1 -0
- package/src/plugins/browser.ts +61 -0
- package/src/plugins/heartbeat.ts +111 -0
- package/src/plugins/index.ts +5 -0
- package/src/plugins/logging.ts +42 -0
- package/src/plugins/reconnect.ts +130 -0
- package/src/plugins/types.ts +61 -0
package/src/core.ts
CHANGED
|
@@ -7,7 +7,13 @@ import type {
|
|
|
7
7
|
ProtocolVersionInterface,
|
|
8
8
|
ServerMessageTypePayload,
|
|
9
9
|
} from '@nmtjs/protocol/client'
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
anyAbortSignal,
|
|
12
|
+
createFuture,
|
|
13
|
+
MAX_UINT32,
|
|
14
|
+
noopFn,
|
|
15
|
+
withTimeout,
|
|
16
|
+
} from '@nmtjs/common'
|
|
11
17
|
import {
|
|
12
18
|
ClientMessageType,
|
|
13
19
|
ConnectionType,
|
|
@@ -23,6 +29,12 @@ import {
|
|
|
23
29
|
versions,
|
|
24
30
|
} from '@nmtjs/protocol/client'
|
|
25
31
|
|
|
32
|
+
import type {
|
|
33
|
+
ClientDisconnectReason,
|
|
34
|
+
ClientPlugin,
|
|
35
|
+
ClientPluginEvent,
|
|
36
|
+
ClientPluginInstance,
|
|
37
|
+
} from './plugins/types.ts'
|
|
26
38
|
import type { BaseClientTransformer } from './transformers.ts'
|
|
27
39
|
import type { ClientCallResponse, ClientTransportFactory } from './transport.ts'
|
|
28
40
|
import type {
|
|
@@ -48,8 +60,7 @@ export type ProtocolClientCall = Future<any> & {
|
|
|
48
60
|
signal?: AbortSignal
|
|
49
61
|
}
|
|
50
62
|
|
|
51
|
-
const
|
|
52
|
-
const DEFAULT_MAX_RECONNECT_TIMEOUT = 60000
|
|
63
|
+
const DEFAULT_RECONNECT_REASON = 'connect_error'
|
|
53
64
|
|
|
54
65
|
export interface BaseClientOptions<
|
|
55
66
|
RouterContract extends TAnyRouterContract = TAnyRouterContract,
|
|
@@ -60,7 +71,7 @@ export interface BaseClientOptions<
|
|
|
60
71
|
format: BaseClientFormat
|
|
61
72
|
application?: string
|
|
62
73
|
timeout?: number
|
|
63
|
-
|
|
74
|
+
plugins?: ClientPlugin[]
|
|
64
75
|
safe?: SafeCall
|
|
65
76
|
}
|
|
66
77
|
|
|
@@ -79,7 +90,8 @@ export abstract class BaseClient<
|
|
|
79
90
|
OutputTypeProvider extends TypeProvider = TypeProvider,
|
|
80
91
|
> extends EventEmitter<{
|
|
81
92
|
connected: []
|
|
82
|
-
disconnected: [reason:
|
|
93
|
+
disconnected: [reason: ClientDisconnectReason]
|
|
94
|
+
pong: [nonce: number]
|
|
83
95
|
}> {
|
|
84
96
|
_!: {
|
|
85
97
|
routes: ResolveAPIRouterRoutes<
|
|
@@ -111,11 +123,20 @@ export abstract class BaseClient<
|
|
|
111
123
|
protected callId = 0
|
|
112
124
|
protected streamId = 0
|
|
113
125
|
protected cab: AbortController | null = null
|
|
114
|
-
protected reconnectTimeout = DEFAULT_RECONNECT_TIMEOUT
|
|
115
126
|
protected connecting: Promise<void> | null = null
|
|
116
|
-
protected state: 'connected' | 'disconnected' = 'disconnected'
|
|
117
127
|
|
|
118
|
-
|
|
128
|
+
protected _state: 'connected' | 'disconnected' = 'disconnected'
|
|
129
|
+
protected _lastDisconnectReason: ClientDisconnectReason = 'server'
|
|
130
|
+
protected _disposed = false
|
|
131
|
+
|
|
132
|
+
protected pingNonce = 0
|
|
133
|
+
protected pendingPings = new Map<number, Future<void>>()
|
|
134
|
+
protected plugins: ClientPluginInstance[] = []
|
|
135
|
+
|
|
136
|
+
private clientDisconnectAsReconnect = false
|
|
137
|
+
private clientDisconnectOverrideReason: string | null = null
|
|
138
|
+
|
|
139
|
+
private authValue: any
|
|
119
140
|
|
|
120
141
|
constructor(
|
|
121
142
|
readonly options: BaseClientOptions<RouterContract, SafeCall>,
|
|
@@ -138,48 +159,54 @@ export abstract class BaseClient<
|
|
|
138
159
|
this.transportOptions,
|
|
139
160
|
) as any
|
|
140
161
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
+
this.plugins = this.options.plugins?.map((plugin) => plugin(this)) ?? []
|
|
163
|
+
for (const plugin of this.plugins) {
|
|
164
|
+
plugin.onInit?.()
|
|
165
|
+
}
|
|
166
|
+
}
|
|
162
167
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
+
dispose() {
|
|
169
|
+
this._disposed = true
|
|
170
|
+
this.stopAllPendingPings('dispose')
|
|
171
|
+
for (let i = this.plugins.length - 1; i >= 0; i--) {
|
|
172
|
+
this.plugins[i].dispose?.()
|
|
168
173
|
}
|
|
169
174
|
}
|
|
170
175
|
|
|
176
|
+
get state() {
|
|
177
|
+
return this._state
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
get lastDisconnectReason() {
|
|
181
|
+
return this._lastDisconnectReason
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
get transportType() {
|
|
185
|
+
return this.transport.type
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
isDisposed() {
|
|
189
|
+
return this._disposed
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
requestReconnect(reason?: string) {
|
|
193
|
+
return this.disconnect({ reconnect: true, reason })
|
|
194
|
+
}
|
|
195
|
+
|
|
171
196
|
get auth() {
|
|
172
|
-
return this
|
|
197
|
+
return this.authValue
|
|
173
198
|
}
|
|
174
199
|
|
|
175
200
|
set auth(value) {
|
|
176
|
-
this
|
|
201
|
+
this.authValue = value
|
|
177
202
|
}
|
|
178
203
|
|
|
179
204
|
connect() {
|
|
180
|
-
if (this.
|
|
205
|
+
if (this._state === 'connected') return Promise.resolve()
|
|
181
206
|
if (this.connecting) return this.connecting
|
|
182
207
|
|
|
208
|
+
if (this._disposed) return Promise.reject(new Error('Client is disposed'))
|
|
209
|
+
|
|
183
210
|
const _connect = async () => {
|
|
184
211
|
if (this.transport.type === ConnectionType.Bidirectional) {
|
|
185
212
|
this.cab = new AbortController()
|
|
@@ -187,7 +214,7 @@ export abstract class BaseClient<
|
|
|
187
214
|
const serverStreams = this.serverStreams
|
|
188
215
|
const transport = {
|
|
189
216
|
send: (buffer) => {
|
|
190
|
-
this
|
|
217
|
+
this.send(buffer).catch(noopFn)
|
|
191
218
|
},
|
|
192
219
|
}
|
|
193
220
|
this.messageContext = {
|
|
@@ -195,7 +222,7 @@ export abstract class BaseClient<
|
|
|
195
222
|
encoder: this.options.format,
|
|
196
223
|
decoder: this.options.format,
|
|
197
224
|
addClientStream: (blob) => {
|
|
198
|
-
const streamId = this
|
|
225
|
+
const streamId = this.getStreamId()
|
|
199
226
|
return this.clientStreams.add(blob.source, streamId, blob.metadata)
|
|
200
227
|
},
|
|
201
228
|
addServerStream(streamId, metadata) {
|
|
@@ -234,7 +261,7 @@ export abstract class BaseClient<
|
|
|
234
261
|
return stream
|
|
235
262
|
}
|
|
236
263
|
},
|
|
237
|
-
streamId: this
|
|
264
|
+
streamId: this.getStreamId.bind(this),
|
|
238
265
|
}
|
|
239
266
|
return this.transport.connect({
|
|
240
267
|
auth: this.auth,
|
|
@@ -246,19 +273,46 @@ export abstract class BaseClient<
|
|
|
246
273
|
}
|
|
247
274
|
}
|
|
248
275
|
|
|
276
|
+
let emitDisconnectOnFailure: 'server' | 'client' | (string & {}) | null =
|
|
277
|
+
null
|
|
278
|
+
|
|
249
279
|
this.connecting = _connect()
|
|
250
280
|
.then(() => {
|
|
251
|
-
this.
|
|
281
|
+
this._state = 'connected'
|
|
282
|
+
})
|
|
283
|
+
.catch((error) => {
|
|
284
|
+
if (this.transport.type === ConnectionType.Bidirectional) {
|
|
285
|
+
emitDisconnectOnFailure = DEFAULT_RECONNECT_REASON
|
|
286
|
+
}
|
|
287
|
+
throw error
|
|
252
288
|
})
|
|
253
289
|
.finally(() => {
|
|
254
290
|
this.connecting = null
|
|
291
|
+
|
|
292
|
+
if (emitDisconnectOnFailure && !this._disposed) {
|
|
293
|
+
this._state = 'disconnected'
|
|
294
|
+
this._lastDisconnectReason = emitDisconnectOnFailure
|
|
295
|
+
void this.onDisconnect(emitDisconnectOnFailure)
|
|
296
|
+
}
|
|
255
297
|
})
|
|
256
298
|
|
|
257
299
|
return this.connecting
|
|
258
300
|
}
|
|
259
301
|
|
|
260
|
-
async disconnect() {
|
|
302
|
+
async disconnect(options: { reconnect?: boolean; reason?: string } = {}) {
|
|
261
303
|
if (this.transport.type === ConnectionType.Bidirectional) {
|
|
304
|
+
// Ensure connect() won't short-circuit while the transport is closing.
|
|
305
|
+
this._state = 'disconnected'
|
|
306
|
+
this._lastDisconnectReason = 'client'
|
|
307
|
+
|
|
308
|
+
if (options.reconnect) {
|
|
309
|
+
this.clientDisconnectAsReconnect = true
|
|
310
|
+
this.clientDisconnectOverrideReason = options.reason ?? 'server'
|
|
311
|
+
} else {
|
|
312
|
+
this.clientDisconnectAsReconnect = false
|
|
313
|
+
this.clientDisconnectOverrideReason = null
|
|
314
|
+
}
|
|
315
|
+
|
|
262
316
|
this.cab!.abort()
|
|
263
317
|
await this.transport.disconnect()
|
|
264
318
|
this.messageContext = null
|
|
@@ -290,12 +344,19 @@ export abstract class BaseClient<
|
|
|
290
344
|
|
|
291
345
|
const signal = signals.length ? anyAbortSignal(...signals) : undefined
|
|
292
346
|
|
|
293
|
-
const callId = this
|
|
347
|
+
const callId = this.getCallId()
|
|
294
348
|
const call = createFuture() as ProtocolClientCall
|
|
295
349
|
call.procedure = procedure
|
|
296
350
|
call.signal = signal
|
|
297
351
|
|
|
298
352
|
this.calls.set(callId, call)
|
|
353
|
+
this.emitClientEvent({
|
|
354
|
+
kind: 'rpc_request',
|
|
355
|
+
timestamp: Date.now(),
|
|
356
|
+
callId,
|
|
357
|
+
procedure,
|
|
358
|
+
body: payload,
|
|
359
|
+
})
|
|
299
360
|
|
|
300
361
|
// Check if signal is already aborted before proceeding
|
|
301
362
|
if (signal?.aborted) {
|
|
@@ -322,7 +383,7 @@ export abstract class BaseClient<
|
|
|
322
383
|
ClientMessageType.RpcAbort,
|
|
323
384
|
{ callId },
|
|
324
385
|
)
|
|
325
|
-
this
|
|
386
|
+
this.send(buffer).catch(noopFn)
|
|
326
387
|
}
|
|
327
388
|
},
|
|
328
389
|
{ once: true },
|
|
@@ -337,7 +398,7 @@ export abstract class BaseClient<
|
|
|
337
398
|
ClientMessageType.Rpc,
|
|
338
399
|
{ callId, procedure, payload: transformedPayload },
|
|
339
400
|
)
|
|
340
|
-
await this
|
|
401
|
+
await this.send(buffer, signal)
|
|
341
402
|
} else {
|
|
342
403
|
const response = await this.transport.call(
|
|
343
404
|
{
|
|
@@ -348,9 +409,16 @@ export abstract class BaseClient<
|
|
|
348
409
|
{ callId, procedure, payload: transformedPayload },
|
|
349
410
|
{ signal, _stream_response: options._stream_response },
|
|
350
411
|
)
|
|
351
|
-
this
|
|
412
|
+
this.handleCallResponse(callId, response)
|
|
352
413
|
}
|
|
353
414
|
} catch (error) {
|
|
415
|
+
this.emitClientEvent({
|
|
416
|
+
kind: 'rpc_error',
|
|
417
|
+
timestamp: Date.now(),
|
|
418
|
+
callId,
|
|
419
|
+
procedure,
|
|
420
|
+
error,
|
|
421
|
+
})
|
|
354
422
|
call.reject(error)
|
|
355
423
|
}
|
|
356
424
|
}
|
|
@@ -362,6 +430,11 @@ export abstract class BaseClient<
|
|
|
362
430
|
controller.abort()
|
|
363
431
|
})
|
|
364
432
|
}
|
|
433
|
+
|
|
434
|
+
if (options._stream_response && typeof value === 'function') {
|
|
435
|
+
return value
|
|
436
|
+
}
|
|
437
|
+
|
|
365
438
|
controller.abort()
|
|
366
439
|
return value
|
|
367
440
|
},
|
|
@@ -386,30 +459,158 @@ export abstract class BaseClient<
|
|
|
386
459
|
}
|
|
387
460
|
|
|
388
461
|
protected async onConnect() {
|
|
389
|
-
this.
|
|
462
|
+
this._state = 'connected'
|
|
463
|
+
this._lastDisconnectReason = 'server'
|
|
464
|
+
this.emitClientEvent({
|
|
465
|
+
kind: 'connected',
|
|
466
|
+
timestamp: Date.now(),
|
|
467
|
+
transportType:
|
|
468
|
+
this.transport.type === ConnectionType.Bidirectional
|
|
469
|
+
? 'bidirectional'
|
|
470
|
+
: 'unidirectional',
|
|
471
|
+
})
|
|
472
|
+
for (const plugin of this.plugins) {
|
|
473
|
+
await plugin.onConnect?.()
|
|
474
|
+
}
|
|
390
475
|
this.emit('connected')
|
|
391
476
|
}
|
|
392
477
|
|
|
393
|
-
protected async onDisconnect(reason:
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
478
|
+
protected async onDisconnect(reason: ClientDisconnectReason) {
|
|
479
|
+
const effectiveReason =
|
|
480
|
+
reason === 'client' && this.clientDisconnectAsReconnect
|
|
481
|
+
? (this.clientDisconnectOverrideReason ?? 'server')
|
|
482
|
+
: reason
|
|
483
|
+
|
|
484
|
+
this.clientDisconnectAsReconnect = false
|
|
485
|
+
this.clientDisconnectOverrideReason = null
|
|
486
|
+
|
|
487
|
+
this._state = 'disconnected'
|
|
488
|
+
this._lastDisconnectReason = effectiveReason
|
|
489
|
+
this.emitClientEvent({
|
|
490
|
+
kind: 'disconnected',
|
|
491
|
+
timestamp: Date.now(),
|
|
492
|
+
reason: effectiveReason,
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
// Connection is gone, never keep old message context around.
|
|
496
|
+
this.messageContext = null
|
|
497
|
+
|
|
498
|
+
this.stopAllPendingPings(effectiveReason)
|
|
499
|
+
|
|
500
|
+
// Fail-fast: do not keep pending calls around across disconnects.
|
|
501
|
+
if (this.calls.size) {
|
|
502
|
+
const error = new ProtocolError(
|
|
503
|
+
ErrorCode.ConnectionError,
|
|
504
|
+
'Disconnected',
|
|
505
|
+
{ reason: effectiveReason },
|
|
506
|
+
)
|
|
507
|
+
for (const call of this.calls.values()) {
|
|
508
|
+
call.reject(error)
|
|
509
|
+
}
|
|
510
|
+
this.calls.clear()
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (this.cab) {
|
|
514
|
+
try {
|
|
515
|
+
this.cab.abort(reason)
|
|
516
|
+
} catch {
|
|
517
|
+
this.cab.abort()
|
|
518
|
+
}
|
|
519
|
+
this.cab = null
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
this.emit('disconnected', effectiveReason)
|
|
523
|
+
|
|
524
|
+
for (let i = this.plugins.length - 1; i >= 0; i--) {
|
|
525
|
+
await this.plugins[i].onDisconnect?.(effectiveReason)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
void this.clientStreams.clear(effectiveReason)
|
|
529
|
+
void this.serverStreams.clear(effectiveReason)
|
|
530
|
+
void this.rpcStreams.clear(effectiveReason)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
protected nextPingNonce() {
|
|
534
|
+
if (this.pingNonce >= MAX_UINT32) this.pingNonce = 0
|
|
535
|
+
return this.pingNonce++
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
ping(timeout: number, signal?: AbortSignal) {
|
|
539
|
+
if (
|
|
540
|
+
!this.messageContext ||
|
|
541
|
+
this.transport.type !== ConnectionType.Bidirectional
|
|
542
|
+
) {
|
|
543
|
+
return Promise.reject(new Error('Client is not connected'))
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const nonce = this.nextPingNonce()
|
|
547
|
+
const future = createFuture<void>()
|
|
548
|
+
this.pendingPings.set(nonce, future)
|
|
549
|
+
|
|
550
|
+
const buffer = this.protocol.encodeMessage(
|
|
551
|
+
this.messageContext,
|
|
552
|
+
ClientMessageType.Ping,
|
|
553
|
+
{ nonce },
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
return this.send(buffer, signal)
|
|
557
|
+
.then(() =>
|
|
558
|
+
withTimeout(future.promise, timeout, new Error('Heartbeat timeout')),
|
|
559
|
+
)
|
|
560
|
+
.finally(() => {
|
|
561
|
+
this.pendingPings.delete(nonce)
|
|
562
|
+
})
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
protected stopAllPendingPings(reason?: any) {
|
|
566
|
+
if (!this.pendingPings.size) return
|
|
567
|
+
const error = new Error('Heartbeat stopped', { cause: reason })
|
|
568
|
+
for (const pending of this.pendingPings.values()) pending.reject(error)
|
|
569
|
+
this.pendingPings.clear()
|
|
399
570
|
}
|
|
400
571
|
|
|
401
572
|
protected async onMessage(buffer: ArrayBufferView) {
|
|
402
573
|
if (!this.messageContext) return
|
|
403
574
|
|
|
404
575
|
const message = this.protocol.decodeMessage(this.messageContext, buffer)
|
|
576
|
+
for (const plugin of this.plugins) {
|
|
577
|
+
plugin.onServerMessage?.(message, buffer)
|
|
578
|
+
}
|
|
579
|
+
this.emitClientEvent({
|
|
580
|
+
kind: 'server_message',
|
|
581
|
+
timestamp: Date.now(),
|
|
582
|
+
messageType: message.type,
|
|
583
|
+
rawByteLength: buffer.byteLength,
|
|
584
|
+
body: message,
|
|
585
|
+
})
|
|
405
586
|
|
|
406
587
|
switch (message.type) {
|
|
407
588
|
case ServerMessageType.RpcResponse:
|
|
408
|
-
this
|
|
589
|
+
this.handleRPCResponseMessage(message)
|
|
409
590
|
break
|
|
410
591
|
case ServerMessageType.RpcStreamResponse:
|
|
411
|
-
this
|
|
592
|
+
this.handleRPCStreamResponseMessage(message)
|
|
412
593
|
break
|
|
594
|
+
case ServerMessageType.Pong: {
|
|
595
|
+
const pending = this.pendingPings.get(message.nonce)
|
|
596
|
+
if (pending) {
|
|
597
|
+
this.pendingPings.delete(message.nonce)
|
|
598
|
+
pending.resolve()
|
|
599
|
+
}
|
|
600
|
+
this.emit('pong', message.nonce)
|
|
601
|
+
break
|
|
602
|
+
}
|
|
603
|
+
case ServerMessageType.Ping: {
|
|
604
|
+
if (this.messageContext) {
|
|
605
|
+
const buffer = this.protocol.encodeMessage(
|
|
606
|
+
this.messageContext,
|
|
607
|
+
ClientMessageType.Pong,
|
|
608
|
+
{ nonce: message.nonce },
|
|
609
|
+
)
|
|
610
|
+
this.send(buffer).catch(noopFn)
|
|
611
|
+
}
|
|
612
|
+
break
|
|
613
|
+
}
|
|
413
614
|
case ServerMessageType.RpcStreamChunk:
|
|
414
615
|
this.rpcStreams.push(message.callId, message.chunk)
|
|
415
616
|
break
|
|
@@ -439,14 +640,14 @@ export abstract class BaseClient<
|
|
|
439
640
|
ClientMessageType.ClientStreamPush,
|
|
440
641
|
{ streamId: message.streamId, chunk },
|
|
441
642
|
)
|
|
442
|
-
this
|
|
643
|
+
this.send(buffer).catch(noopFn)
|
|
443
644
|
} else {
|
|
444
645
|
const buffer = this.protocol.encodeMessage(
|
|
445
646
|
this.messageContext!,
|
|
446
647
|
ClientMessageType.ClientStreamEnd,
|
|
447
648
|
{ streamId: message.streamId },
|
|
448
649
|
)
|
|
449
|
-
this
|
|
650
|
+
this.send(buffer).catch(noopFn)
|
|
450
651
|
this.clientStreams.end(message.streamId)
|
|
451
652
|
}
|
|
452
653
|
},
|
|
@@ -456,7 +657,7 @@ export abstract class BaseClient<
|
|
|
456
657
|
ClientMessageType.ClientStreamAbort,
|
|
457
658
|
{ streamId: message.streamId },
|
|
458
659
|
)
|
|
459
|
-
this
|
|
660
|
+
this.send(buffer).catch(noopFn)
|
|
460
661
|
this.clientStreams.remove(message.streamId)
|
|
461
662
|
},
|
|
462
663
|
)
|
|
@@ -467,19 +668,40 @@ export abstract class BaseClient<
|
|
|
467
668
|
}
|
|
468
669
|
}
|
|
469
670
|
|
|
470
|
-
|
|
671
|
+
private handleRPCResponseMessage(
|
|
471
672
|
message: ServerMessageTypePayload[ServerMessageType.RpcResponse],
|
|
472
673
|
) {
|
|
473
674
|
const { callId, result, error } = message
|
|
474
675
|
const call = this.calls.get(callId)
|
|
475
676
|
if (!call) return
|
|
476
677
|
if (error) {
|
|
678
|
+
this.emitClientEvent({
|
|
679
|
+
kind: 'rpc_error',
|
|
680
|
+
timestamp: Date.now(),
|
|
681
|
+
callId,
|
|
682
|
+
procedure: call.procedure,
|
|
683
|
+
error,
|
|
684
|
+
})
|
|
477
685
|
call.reject(new ProtocolError(error.code, error.message, error.data))
|
|
478
686
|
} else {
|
|
479
687
|
try {
|
|
480
688
|
const transformed = this.transformer.decode(call.procedure, result)
|
|
689
|
+
this.emitClientEvent({
|
|
690
|
+
kind: 'rpc_response',
|
|
691
|
+
timestamp: Date.now(),
|
|
692
|
+
callId,
|
|
693
|
+
procedure: call.procedure,
|
|
694
|
+
body: transformed,
|
|
695
|
+
})
|
|
481
696
|
call.resolve(transformed)
|
|
482
697
|
} catch (error) {
|
|
698
|
+
this.emitClientEvent({
|
|
699
|
+
kind: 'rpc_error',
|
|
700
|
+
timestamp: Date.now(),
|
|
701
|
+
callId,
|
|
702
|
+
procedure: call.procedure,
|
|
703
|
+
error,
|
|
704
|
+
})
|
|
483
705
|
call.reject(
|
|
484
706
|
new ProtocolError(
|
|
485
707
|
ErrorCode.ClientRequestError,
|
|
@@ -491,12 +713,19 @@ export abstract class BaseClient<
|
|
|
491
713
|
}
|
|
492
714
|
}
|
|
493
715
|
|
|
494
|
-
|
|
716
|
+
private handleRPCStreamResponseMessage(
|
|
495
717
|
message: ServerMessageTypePayload[ServerMessageType.RpcStreamResponse],
|
|
496
718
|
) {
|
|
497
719
|
const call = this.calls.get(message.callId)
|
|
498
720
|
if (message.error) {
|
|
499
721
|
if (!call) return
|
|
722
|
+
this.emitClientEvent({
|
|
723
|
+
kind: 'rpc_error',
|
|
724
|
+
timestamp: Date.now(),
|
|
725
|
+
callId: message.callId,
|
|
726
|
+
procedure: call.procedure,
|
|
727
|
+
error: message.error,
|
|
728
|
+
})
|
|
500
729
|
call.reject(
|
|
501
730
|
new ProtocolError(
|
|
502
731
|
message.error.code,
|
|
@@ -507,6 +736,13 @@ export abstract class BaseClient<
|
|
|
507
736
|
} else {
|
|
508
737
|
if (call) {
|
|
509
738
|
const { procedure, signal } = call
|
|
739
|
+
this.emitClientEvent({
|
|
740
|
+
kind: 'rpc_response',
|
|
741
|
+
timestamp: Date.now(),
|
|
742
|
+
callId: message.callId,
|
|
743
|
+
procedure,
|
|
744
|
+
stream: true,
|
|
745
|
+
})
|
|
510
746
|
const stream = new ProtocolServerRPCStream({
|
|
511
747
|
start: (controller) => {
|
|
512
748
|
if (signal) {
|
|
@@ -525,7 +761,7 @@ export abstract class BaseClient<
|
|
|
525
761
|
ClientMessageType.RpcAbort,
|
|
526
762
|
{ callId: message.callId, reason: signal.reason },
|
|
527
763
|
)
|
|
528
|
-
this
|
|
764
|
+
this.send(buffer).catch(noopFn)
|
|
529
765
|
}
|
|
530
766
|
}
|
|
531
767
|
},
|
|
@@ -545,7 +781,7 @@ export abstract class BaseClient<
|
|
|
545
781
|
ClientMessageType.RpcPull,
|
|
546
782
|
{ callId: message.callId },
|
|
547
783
|
)
|
|
548
|
-
this
|
|
784
|
+
this.send(buffer).catch(noopFn)
|
|
549
785
|
},
|
|
550
786
|
readableStrategy: { highWaterMark: 0 },
|
|
551
787
|
})
|
|
@@ -561,17 +797,24 @@ export abstract class BaseClient<
|
|
|
561
797
|
ClientMessageType.RpcAbort,
|
|
562
798
|
{ callId: message.callId },
|
|
563
799
|
)
|
|
564
|
-
this
|
|
800
|
+
this.send(buffer).catch(noopFn)
|
|
565
801
|
}
|
|
566
802
|
}
|
|
567
803
|
}
|
|
568
804
|
}
|
|
569
805
|
|
|
570
|
-
|
|
806
|
+
private handleCallResponse(callId: number, response: ClientCallResponse) {
|
|
571
807
|
const call = this.calls.get(callId)
|
|
572
808
|
|
|
573
809
|
if (response.type === 'rpc_stream') {
|
|
574
810
|
if (call) {
|
|
811
|
+
this.emitClientEvent({
|
|
812
|
+
kind: 'rpc_response',
|
|
813
|
+
timestamp: Date.now(),
|
|
814
|
+
callId,
|
|
815
|
+
procedure: call.procedure,
|
|
816
|
+
stream: true,
|
|
817
|
+
})
|
|
575
818
|
const stream = new ProtocolServerStream({
|
|
576
819
|
transform: (chunk) => {
|
|
577
820
|
return this.transformer.decode(
|
|
@@ -582,7 +825,36 @@ export abstract class BaseClient<
|
|
|
582
825
|
})
|
|
583
826
|
this.rpcStreams.add(callId, stream)
|
|
584
827
|
call.resolve(({ signal }: { signal?: AbortSignal }) => {
|
|
585
|
-
response.stream.
|
|
828
|
+
const reader = response.stream.getReader()
|
|
829
|
+
|
|
830
|
+
let onAbort: (() => void) | undefined
|
|
831
|
+
if (signal) {
|
|
832
|
+
onAbort = () => {
|
|
833
|
+
reader.cancel(signal.reason).catch(noopFn)
|
|
834
|
+
this.rpcStreams.abort(callId).catch(noopFn)
|
|
835
|
+
}
|
|
836
|
+
if (signal.aborted) onAbort()
|
|
837
|
+
else signal.addEventListener('abort', onAbort, { once: true })
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
void (async () => {
|
|
841
|
+
try {
|
|
842
|
+
while (true) {
|
|
843
|
+
const { done, value } = await reader.read()
|
|
844
|
+
if (done) break
|
|
845
|
+
await this.rpcStreams.push(callId, value)
|
|
846
|
+
}
|
|
847
|
+
await this.rpcStreams.end(callId)
|
|
848
|
+
} catch {
|
|
849
|
+
await this.rpcStreams.abort(callId).catch(noopFn)
|
|
850
|
+
} finally {
|
|
851
|
+
reader.releaseLock()
|
|
852
|
+
if (signal && onAbort) {
|
|
853
|
+
signal.removeEventListener('abort', onAbort)
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
})()
|
|
857
|
+
|
|
586
858
|
return stream
|
|
587
859
|
})
|
|
588
860
|
} else {
|
|
@@ -593,9 +865,16 @@ export abstract class BaseClient<
|
|
|
593
865
|
}
|
|
594
866
|
} else if (response.type === 'blob') {
|
|
595
867
|
if (call) {
|
|
868
|
+
this.emitClientEvent({
|
|
869
|
+
kind: 'rpc_response',
|
|
870
|
+
timestamp: Date.now(),
|
|
871
|
+
callId,
|
|
872
|
+
procedure: call.procedure,
|
|
873
|
+
stream: true,
|
|
874
|
+
})
|
|
596
875
|
const { metadata, source } = response
|
|
597
876
|
const stream = new ProtocolServerBlobStream(metadata)
|
|
598
|
-
this.serverStreams.add(this
|
|
877
|
+
this.serverStreams.add(this.getStreamId(), stream)
|
|
599
878
|
call.resolve(({ signal }: { signal?: AbortSignal }) => {
|
|
600
879
|
source.pipeTo(stream.writable, { signal }).catch(noopFn)
|
|
601
880
|
return stream
|
|
@@ -613,8 +892,22 @@ export abstract class BaseClient<
|
|
|
613
892
|
call.procedure,
|
|
614
893
|
response.result,
|
|
615
894
|
)
|
|
895
|
+
this.emitClientEvent({
|
|
896
|
+
kind: 'rpc_response',
|
|
897
|
+
timestamp: Date.now(),
|
|
898
|
+
callId,
|
|
899
|
+
procedure: call.procedure,
|
|
900
|
+
body: transformed,
|
|
901
|
+
})
|
|
616
902
|
call.resolve(transformed)
|
|
617
903
|
} catch (error) {
|
|
904
|
+
this.emitClientEvent({
|
|
905
|
+
kind: 'rpc_error',
|
|
906
|
+
timestamp: Date.now(),
|
|
907
|
+
callId,
|
|
908
|
+
procedure: call.procedure,
|
|
909
|
+
error,
|
|
910
|
+
})
|
|
618
911
|
call.reject(
|
|
619
912
|
new ProtocolError(
|
|
620
913
|
ErrorCode.ClientRequestError,
|
|
@@ -626,23 +919,32 @@ export abstract class BaseClient<
|
|
|
626
919
|
}
|
|
627
920
|
}
|
|
628
921
|
|
|
629
|
-
|
|
922
|
+
protected send(buffer: ArrayBufferView, signal?: AbortSignal) {
|
|
630
923
|
if (this.transport.type === ConnectionType.Unidirectional)
|
|
631
924
|
throw new Error('Invalid transport type for send')
|
|
632
925
|
return this.transport.send(buffer, { signal })
|
|
633
926
|
}
|
|
634
927
|
|
|
635
|
-
|
|
928
|
+
protected getStreamId() {
|
|
636
929
|
if (this.streamId >= MAX_UINT32) {
|
|
637
930
|
this.streamId = 0
|
|
638
931
|
}
|
|
639
932
|
return this.streamId++
|
|
640
933
|
}
|
|
641
934
|
|
|
642
|
-
|
|
935
|
+
protected getCallId() {
|
|
643
936
|
if (this.callId >= MAX_UINT32) {
|
|
644
937
|
this.callId = 0
|
|
645
938
|
}
|
|
646
939
|
return this.callId++
|
|
647
940
|
}
|
|
941
|
+
|
|
942
|
+
protected emitClientEvent(event: ClientPluginEvent) {
|
|
943
|
+
for (const plugin of this.plugins) {
|
|
944
|
+
try {
|
|
945
|
+
const result = plugin.onClientEvent?.(event)
|
|
946
|
+
Promise.resolve(result).catch(noopFn)
|
|
947
|
+
} catch {}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
648
950
|
}
|