@nmtjs/client 0.15.0-beta.9 → 0.15.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.
- package/dist/clients/runtime.d.ts +2 -2
- package/dist/clients/runtime.js +1 -1
- package/dist/clients/runtime.js.map +1 -1
- package/dist/clients/static.d.ts +2 -2
- package/dist/clients/static.js +6 -3
- package/dist/clients/static.js.map +1 -1
- package/dist/core.d.ts +38 -8
- package/dist/core.js +414 -66
- package/dist/core.js.map +1 -1
- package/dist/events.d.ts +1 -1
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -5
- 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 +63 -0
- package/dist/plugins/types.js +2 -0
- package/dist/plugins/types.js.map +1 -0
- package/dist/streams.d.ts +3 -3
- package/dist/streams.js.map +1 -1
- package/dist/transformers.js.map +1 -1
- package/dist/types.d.ts +1 -4
- package/dist/types.js.map +1 -1
- package/package.json +27 -17
- package/src/clients/runtime.ts +4 -4
- package/src/clients/static.ts +9 -13
- package/src/core.ts +476 -77
- 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 +72 -0
- package/src/streams.ts +3 -3
- package/src/types.ts +1 -17
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,56 +159,63 @@ 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) {
|
|
212
|
+
const client = this
|
|
185
213
|
this.cab = new AbortController()
|
|
186
214
|
const protocol = this.protocol
|
|
187
215
|
const serverStreams = this.serverStreams
|
|
188
216
|
const transport = {
|
|
189
217
|
send: (buffer) => {
|
|
190
|
-
this
|
|
218
|
+
this.send(buffer).catch(noopFn)
|
|
191
219
|
},
|
|
192
220
|
}
|
|
193
221
|
this.messageContext = {
|
|
@@ -195,12 +223,19 @@ export abstract class BaseClient<
|
|
|
195
223
|
encoder: this.options.format,
|
|
196
224
|
decoder: this.options.format,
|
|
197
225
|
addClientStream: (blob) => {
|
|
198
|
-
const streamId = this
|
|
226
|
+
const streamId = this.getStreamId()
|
|
199
227
|
return this.clientStreams.add(blob.source, streamId, blob.metadata)
|
|
200
228
|
},
|
|
201
229
|
addServerStream(streamId, metadata) {
|
|
202
230
|
const stream = new ProtocolServerBlobStream(metadata, {
|
|
203
231
|
pull: (controller) => {
|
|
232
|
+
client.emitStreamEvent({
|
|
233
|
+
direction: 'outgoing',
|
|
234
|
+
streamType: 'server_blob',
|
|
235
|
+
action: 'pull',
|
|
236
|
+
streamId,
|
|
237
|
+
byteLength: 65535,
|
|
238
|
+
})
|
|
204
239
|
transport.send(
|
|
205
240
|
protocol.encodeMessage(
|
|
206
241
|
this,
|
|
@@ -220,6 +255,12 @@ export abstract class BaseClient<
|
|
|
220
255
|
signal.addEventListener(
|
|
221
256
|
'abort',
|
|
222
257
|
() => {
|
|
258
|
+
client.emitStreamEvent({
|
|
259
|
+
direction: 'outgoing',
|
|
260
|
+
streamType: 'server_blob',
|
|
261
|
+
action: 'abort',
|
|
262
|
+
streamId,
|
|
263
|
+
})
|
|
223
264
|
transport.send(
|
|
224
265
|
protocol.encodeMessage(
|
|
225
266
|
this,
|
|
@@ -234,7 +275,7 @@ export abstract class BaseClient<
|
|
|
234
275
|
return stream
|
|
235
276
|
}
|
|
236
277
|
},
|
|
237
|
-
streamId: this
|
|
278
|
+
streamId: this.getStreamId.bind(this),
|
|
238
279
|
}
|
|
239
280
|
return this.transport.connect({
|
|
240
281
|
auth: this.auth,
|
|
@@ -246,19 +287,46 @@ export abstract class BaseClient<
|
|
|
246
287
|
}
|
|
247
288
|
}
|
|
248
289
|
|
|
290
|
+
let emitDisconnectOnFailure: 'server' | 'client' | (string & {}) | null =
|
|
291
|
+
null
|
|
292
|
+
|
|
249
293
|
this.connecting = _connect()
|
|
250
294
|
.then(() => {
|
|
251
|
-
this.
|
|
295
|
+
this._state = 'connected'
|
|
296
|
+
})
|
|
297
|
+
.catch((error) => {
|
|
298
|
+
if (this.transport.type === ConnectionType.Bidirectional) {
|
|
299
|
+
emitDisconnectOnFailure = DEFAULT_RECONNECT_REASON
|
|
300
|
+
}
|
|
301
|
+
throw error
|
|
252
302
|
})
|
|
253
303
|
.finally(() => {
|
|
254
304
|
this.connecting = null
|
|
305
|
+
|
|
306
|
+
if (emitDisconnectOnFailure && !this._disposed) {
|
|
307
|
+
this._state = 'disconnected'
|
|
308
|
+
this._lastDisconnectReason = emitDisconnectOnFailure
|
|
309
|
+
void this.onDisconnect(emitDisconnectOnFailure)
|
|
310
|
+
}
|
|
255
311
|
})
|
|
256
312
|
|
|
257
313
|
return this.connecting
|
|
258
314
|
}
|
|
259
315
|
|
|
260
|
-
async disconnect() {
|
|
316
|
+
async disconnect(options: { reconnect?: boolean; reason?: string } = {}) {
|
|
261
317
|
if (this.transport.type === ConnectionType.Bidirectional) {
|
|
318
|
+
// Ensure connect() won't short-circuit while the transport is closing.
|
|
319
|
+
this._state = 'disconnected'
|
|
320
|
+
this._lastDisconnectReason = 'client'
|
|
321
|
+
|
|
322
|
+
if (options.reconnect) {
|
|
323
|
+
this.clientDisconnectAsReconnect = true
|
|
324
|
+
this.clientDisconnectOverrideReason = options.reason ?? 'server'
|
|
325
|
+
} else {
|
|
326
|
+
this.clientDisconnectAsReconnect = false
|
|
327
|
+
this.clientDisconnectOverrideReason = null
|
|
328
|
+
}
|
|
329
|
+
|
|
262
330
|
this.cab!.abort()
|
|
263
331
|
await this.transport.disconnect()
|
|
264
332
|
this.messageContext = null
|
|
@@ -290,12 +358,19 @@ export abstract class BaseClient<
|
|
|
290
358
|
|
|
291
359
|
const signal = signals.length ? anyAbortSignal(...signals) : undefined
|
|
292
360
|
|
|
293
|
-
const callId = this
|
|
361
|
+
const callId = this.getCallId()
|
|
294
362
|
const call = createFuture() as ProtocolClientCall
|
|
295
363
|
call.procedure = procedure
|
|
296
364
|
call.signal = signal
|
|
297
365
|
|
|
298
366
|
this.calls.set(callId, call)
|
|
367
|
+
this.emitClientEvent({
|
|
368
|
+
kind: 'rpc_request',
|
|
369
|
+
timestamp: Date.now(),
|
|
370
|
+
callId,
|
|
371
|
+
procedure,
|
|
372
|
+
body: payload,
|
|
373
|
+
})
|
|
299
374
|
|
|
300
375
|
// Check if signal is already aborted before proceeding
|
|
301
376
|
if (signal?.aborted) {
|
|
@@ -322,7 +397,7 @@ export abstract class BaseClient<
|
|
|
322
397
|
ClientMessageType.RpcAbort,
|
|
323
398
|
{ callId },
|
|
324
399
|
)
|
|
325
|
-
this
|
|
400
|
+
this.send(buffer).catch(noopFn)
|
|
326
401
|
}
|
|
327
402
|
},
|
|
328
403
|
{ once: true },
|
|
@@ -337,7 +412,7 @@ export abstract class BaseClient<
|
|
|
337
412
|
ClientMessageType.Rpc,
|
|
338
413
|
{ callId, procedure, payload: transformedPayload },
|
|
339
414
|
)
|
|
340
|
-
await this
|
|
415
|
+
await this.send(buffer, signal)
|
|
341
416
|
} else {
|
|
342
417
|
const response = await this.transport.call(
|
|
343
418
|
{
|
|
@@ -348,9 +423,16 @@ export abstract class BaseClient<
|
|
|
348
423
|
{ callId, procedure, payload: transformedPayload },
|
|
349
424
|
{ signal, _stream_response: options._stream_response },
|
|
350
425
|
)
|
|
351
|
-
this
|
|
426
|
+
this.handleCallResponse(callId, response)
|
|
352
427
|
}
|
|
353
428
|
} catch (error) {
|
|
429
|
+
this.emitClientEvent({
|
|
430
|
+
kind: 'rpc_error',
|
|
431
|
+
timestamp: Date.now(),
|
|
432
|
+
callId,
|
|
433
|
+
procedure,
|
|
434
|
+
error,
|
|
435
|
+
})
|
|
354
436
|
call.reject(error)
|
|
355
437
|
}
|
|
356
438
|
}
|
|
@@ -362,6 +444,11 @@ export abstract class BaseClient<
|
|
|
362
444
|
controller.abort()
|
|
363
445
|
})
|
|
364
446
|
}
|
|
447
|
+
|
|
448
|
+
if (options._stream_response && typeof value === 'function') {
|
|
449
|
+
return value
|
|
450
|
+
}
|
|
451
|
+
|
|
365
452
|
controller.abort()
|
|
366
453
|
return value
|
|
367
454
|
},
|
|
@@ -386,100 +473,322 @@ export abstract class BaseClient<
|
|
|
386
473
|
}
|
|
387
474
|
|
|
388
475
|
protected async onConnect() {
|
|
389
|
-
this.
|
|
476
|
+
this._state = 'connected'
|
|
477
|
+
this._lastDisconnectReason = 'server'
|
|
478
|
+
this.emitClientEvent({
|
|
479
|
+
kind: 'connected',
|
|
480
|
+
timestamp: Date.now(),
|
|
481
|
+
transportType:
|
|
482
|
+
this.transport.type === ConnectionType.Bidirectional
|
|
483
|
+
? 'bidirectional'
|
|
484
|
+
: 'unidirectional',
|
|
485
|
+
})
|
|
486
|
+
for (const plugin of this.plugins) {
|
|
487
|
+
await plugin.onConnect?.()
|
|
488
|
+
}
|
|
390
489
|
this.emit('connected')
|
|
391
490
|
}
|
|
392
491
|
|
|
393
|
-
protected async onDisconnect(reason:
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
492
|
+
protected async onDisconnect(reason: ClientDisconnectReason) {
|
|
493
|
+
const effectiveReason =
|
|
494
|
+
reason === 'client' && this.clientDisconnectAsReconnect
|
|
495
|
+
? (this.clientDisconnectOverrideReason ?? 'server')
|
|
496
|
+
: reason
|
|
497
|
+
|
|
498
|
+
this.clientDisconnectAsReconnect = false
|
|
499
|
+
this.clientDisconnectOverrideReason = null
|
|
500
|
+
|
|
501
|
+
this._state = 'disconnected'
|
|
502
|
+
this._lastDisconnectReason = effectiveReason
|
|
503
|
+
this.emitClientEvent({
|
|
504
|
+
kind: 'disconnected',
|
|
505
|
+
timestamp: Date.now(),
|
|
506
|
+
reason: effectiveReason,
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
// Connection is gone, never keep old message context around.
|
|
510
|
+
this.messageContext = null
|
|
511
|
+
|
|
512
|
+
this.stopAllPendingPings(effectiveReason)
|
|
513
|
+
|
|
514
|
+
// Fail-fast: do not keep pending calls around across disconnects.
|
|
515
|
+
if (this.calls.size) {
|
|
516
|
+
const error = new ProtocolError(
|
|
517
|
+
ErrorCode.ConnectionError,
|
|
518
|
+
'Disconnected',
|
|
519
|
+
{ reason: effectiveReason },
|
|
520
|
+
)
|
|
521
|
+
for (const call of this.calls.values()) {
|
|
522
|
+
call.reject(error)
|
|
523
|
+
}
|
|
524
|
+
this.calls.clear()
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (this.cab) {
|
|
528
|
+
try {
|
|
529
|
+
this.cab.abort(reason)
|
|
530
|
+
} catch {
|
|
531
|
+
this.cab.abort()
|
|
532
|
+
}
|
|
533
|
+
this.cab = null
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
this.emit('disconnected', effectiveReason)
|
|
537
|
+
|
|
538
|
+
for (let i = this.plugins.length - 1; i >= 0; i--) {
|
|
539
|
+
await this.plugins[i].onDisconnect?.(effectiveReason)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
void this.clientStreams.clear(effectiveReason)
|
|
543
|
+
void this.serverStreams.clear(effectiveReason)
|
|
544
|
+
void this.rpcStreams.clear(effectiveReason)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
protected nextPingNonce() {
|
|
548
|
+
if (this.pingNonce >= MAX_UINT32) this.pingNonce = 0
|
|
549
|
+
return this.pingNonce++
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
ping(timeout: number, signal?: AbortSignal) {
|
|
553
|
+
if (
|
|
554
|
+
!this.messageContext ||
|
|
555
|
+
this.transport.type !== ConnectionType.Bidirectional
|
|
556
|
+
) {
|
|
557
|
+
return Promise.reject(new Error('Client is not connected'))
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const nonce = this.nextPingNonce()
|
|
561
|
+
const future = createFuture<void>()
|
|
562
|
+
this.pendingPings.set(nonce, future)
|
|
563
|
+
|
|
564
|
+
const buffer = this.protocol.encodeMessage(
|
|
565
|
+
this.messageContext,
|
|
566
|
+
ClientMessageType.Ping,
|
|
567
|
+
{ nonce },
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
return this.send(buffer, signal)
|
|
571
|
+
.then(() =>
|
|
572
|
+
withTimeout(future.promise, timeout, new Error('Heartbeat timeout')),
|
|
573
|
+
)
|
|
574
|
+
.finally(() => {
|
|
575
|
+
this.pendingPings.delete(nonce)
|
|
576
|
+
})
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
protected stopAllPendingPings(reason?: any) {
|
|
580
|
+
if (!this.pendingPings.size) return
|
|
581
|
+
const error = new Error('Heartbeat stopped', { cause: reason })
|
|
582
|
+
for (const pending of this.pendingPings.values()) pending.reject(error)
|
|
583
|
+
this.pendingPings.clear()
|
|
399
584
|
}
|
|
400
585
|
|
|
401
586
|
protected async onMessage(buffer: ArrayBufferView) {
|
|
402
587
|
if (!this.messageContext) return
|
|
403
588
|
|
|
404
589
|
const message = this.protocol.decodeMessage(this.messageContext, buffer)
|
|
590
|
+
for (const plugin of this.plugins) {
|
|
591
|
+
plugin.onServerMessage?.(message, buffer)
|
|
592
|
+
}
|
|
593
|
+
this.emitClientEvent({
|
|
594
|
+
kind: 'server_message',
|
|
595
|
+
timestamp: Date.now(),
|
|
596
|
+
messageType: message.type,
|
|
597
|
+
rawByteLength: buffer.byteLength,
|
|
598
|
+
body: message,
|
|
599
|
+
})
|
|
405
600
|
|
|
406
601
|
switch (message.type) {
|
|
407
602
|
case ServerMessageType.RpcResponse:
|
|
408
|
-
this
|
|
603
|
+
this.handleRPCResponseMessage(message)
|
|
409
604
|
break
|
|
410
605
|
case ServerMessageType.RpcStreamResponse:
|
|
411
|
-
this
|
|
606
|
+
this.handleRPCStreamResponseMessage(message)
|
|
607
|
+
break
|
|
608
|
+
case ServerMessageType.Pong: {
|
|
609
|
+
const pending = this.pendingPings.get(message.nonce)
|
|
610
|
+
if (pending) {
|
|
611
|
+
this.pendingPings.delete(message.nonce)
|
|
612
|
+
pending.resolve()
|
|
613
|
+
}
|
|
614
|
+
this.emit('pong', message.nonce)
|
|
412
615
|
break
|
|
616
|
+
}
|
|
617
|
+
case ServerMessageType.Ping: {
|
|
618
|
+
if (this.messageContext) {
|
|
619
|
+
const buffer = this.protocol.encodeMessage(
|
|
620
|
+
this.messageContext,
|
|
621
|
+
ClientMessageType.Pong,
|
|
622
|
+
{ nonce: message.nonce },
|
|
623
|
+
)
|
|
624
|
+
this.send(buffer).catch(noopFn)
|
|
625
|
+
}
|
|
626
|
+
break
|
|
627
|
+
}
|
|
413
628
|
case ServerMessageType.RpcStreamChunk:
|
|
629
|
+
this.emitStreamEvent({
|
|
630
|
+
direction: 'incoming',
|
|
631
|
+
streamType: 'rpc',
|
|
632
|
+
action: 'push',
|
|
633
|
+
callId: message.callId,
|
|
634
|
+
byteLength: message.chunk.byteLength,
|
|
635
|
+
})
|
|
414
636
|
this.rpcStreams.push(message.callId, message.chunk)
|
|
415
637
|
break
|
|
416
638
|
case ServerMessageType.RpcStreamEnd:
|
|
639
|
+
this.emitStreamEvent({
|
|
640
|
+
direction: 'incoming',
|
|
641
|
+
streamType: 'rpc',
|
|
642
|
+
action: 'end',
|
|
643
|
+
callId: message.callId,
|
|
644
|
+
})
|
|
417
645
|
this.rpcStreams.end(message.callId)
|
|
418
646
|
this.calls.delete(message.callId)
|
|
419
647
|
break
|
|
420
648
|
case ServerMessageType.RpcStreamAbort:
|
|
649
|
+
this.emitStreamEvent({
|
|
650
|
+
direction: 'incoming',
|
|
651
|
+
streamType: 'rpc',
|
|
652
|
+
action: 'abort',
|
|
653
|
+
callId: message.callId,
|
|
654
|
+
reason: message.reason,
|
|
655
|
+
})
|
|
421
656
|
this.rpcStreams.abort(message.callId)
|
|
422
657
|
this.calls.delete(message.callId)
|
|
423
658
|
break
|
|
424
659
|
case ServerMessageType.ServerStreamPush:
|
|
660
|
+
this.emitStreamEvent({
|
|
661
|
+
direction: 'incoming',
|
|
662
|
+
streamType: 'server_blob',
|
|
663
|
+
action: 'push',
|
|
664
|
+
streamId: message.streamId,
|
|
665
|
+
byteLength: message.chunk.byteLength,
|
|
666
|
+
})
|
|
425
667
|
this.serverStreams.push(message.streamId, message.chunk)
|
|
426
668
|
break
|
|
427
669
|
case ServerMessageType.ServerStreamEnd:
|
|
670
|
+
this.emitStreamEvent({
|
|
671
|
+
direction: 'incoming',
|
|
672
|
+
streamType: 'server_blob',
|
|
673
|
+
action: 'end',
|
|
674
|
+
streamId: message.streamId,
|
|
675
|
+
})
|
|
428
676
|
this.serverStreams.end(message.streamId)
|
|
429
677
|
break
|
|
430
678
|
case ServerMessageType.ServerStreamAbort:
|
|
679
|
+
this.emitStreamEvent({
|
|
680
|
+
direction: 'incoming',
|
|
681
|
+
streamType: 'server_blob',
|
|
682
|
+
action: 'abort',
|
|
683
|
+
streamId: message.streamId,
|
|
684
|
+
reason: message.reason,
|
|
685
|
+
})
|
|
431
686
|
this.serverStreams.abort(message.streamId)
|
|
432
687
|
break
|
|
433
688
|
case ServerMessageType.ClientStreamPull:
|
|
689
|
+
this.emitStreamEvent({
|
|
690
|
+
direction: 'incoming',
|
|
691
|
+
streamType: 'client_blob',
|
|
692
|
+
action: 'pull',
|
|
693
|
+
streamId: message.streamId,
|
|
694
|
+
byteLength: message.size,
|
|
695
|
+
})
|
|
434
696
|
this.clientStreams.pull(message.streamId, message.size).then(
|
|
435
697
|
(chunk) => {
|
|
436
698
|
if (chunk) {
|
|
699
|
+
this.emitStreamEvent({
|
|
700
|
+
direction: 'outgoing',
|
|
701
|
+
streamType: 'client_blob',
|
|
702
|
+
action: 'push',
|
|
703
|
+
streamId: message.streamId,
|
|
704
|
+
byteLength: chunk.byteLength,
|
|
705
|
+
})
|
|
437
706
|
const buffer = this.protocol.encodeMessage(
|
|
438
707
|
this.messageContext!,
|
|
439
708
|
ClientMessageType.ClientStreamPush,
|
|
440
709
|
{ streamId: message.streamId, chunk },
|
|
441
710
|
)
|
|
442
|
-
this
|
|
711
|
+
this.send(buffer).catch(noopFn)
|
|
443
712
|
} else {
|
|
713
|
+
this.emitStreamEvent({
|
|
714
|
+
direction: 'outgoing',
|
|
715
|
+
streamType: 'client_blob',
|
|
716
|
+
action: 'end',
|
|
717
|
+
streamId: message.streamId,
|
|
718
|
+
})
|
|
444
719
|
const buffer = this.protocol.encodeMessage(
|
|
445
720
|
this.messageContext!,
|
|
446
721
|
ClientMessageType.ClientStreamEnd,
|
|
447
722
|
{ streamId: message.streamId },
|
|
448
723
|
)
|
|
449
|
-
this
|
|
724
|
+
this.send(buffer).catch(noopFn)
|
|
450
725
|
this.clientStreams.end(message.streamId)
|
|
451
726
|
}
|
|
452
727
|
},
|
|
453
728
|
() => {
|
|
729
|
+
this.emitStreamEvent({
|
|
730
|
+
direction: 'outgoing',
|
|
731
|
+
streamType: 'client_blob',
|
|
732
|
+
action: 'abort',
|
|
733
|
+
streamId: message.streamId,
|
|
734
|
+
})
|
|
454
735
|
const buffer = this.protocol.encodeMessage(
|
|
455
736
|
this.messageContext!,
|
|
456
737
|
ClientMessageType.ClientStreamAbort,
|
|
457
738
|
{ streamId: message.streamId },
|
|
458
739
|
)
|
|
459
|
-
this
|
|
740
|
+
this.send(buffer).catch(noopFn)
|
|
460
741
|
this.clientStreams.remove(message.streamId)
|
|
461
742
|
},
|
|
462
743
|
)
|
|
463
744
|
break
|
|
464
745
|
case ServerMessageType.ClientStreamAbort:
|
|
746
|
+
this.emitStreamEvent({
|
|
747
|
+
direction: 'incoming',
|
|
748
|
+
streamType: 'client_blob',
|
|
749
|
+
action: 'abort',
|
|
750
|
+
streamId: message.streamId,
|
|
751
|
+
reason: message.reason,
|
|
752
|
+
})
|
|
465
753
|
this.clientStreams.abort(message.streamId)
|
|
466
754
|
break
|
|
467
755
|
}
|
|
468
756
|
}
|
|
469
757
|
|
|
470
|
-
|
|
758
|
+
private handleRPCResponseMessage(
|
|
471
759
|
message: ServerMessageTypePayload[ServerMessageType.RpcResponse],
|
|
472
760
|
) {
|
|
473
761
|
const { callId, result, error } = message
|
|
474
762
|
const call = this.calls.get(callId)
|
|
475
763
|
if (!call) return
|
|
476
764
|
if (error) {
|
|
765
|
+
this.emitClientEvent({
|
|
766
|
+
kind: 'rpc_error',
|
|
767
|
+
timestamp: Date.now(),
|
|
768
|
+
callId,
|
|
769
|
+
procedure: call.procedure,
|
|
770
|
+
error,
|
|
771
|
+
})
|
|
477
772
|
call.reject(new ProtocolError(error.code, error.message, error.data))
|
|
478
773
|
} else {
|
|
479
774
|
try {
|
|
480
775
|
const transformed = this.transformer.decode(call.procedure, result)
|
|
776
|
+
this.emitClientEvent({
|
|
777
|
+
kind: 'rpc_response',
|
|
778
|
+
timestamp: Date.now(),
|
|
779
|
+
callId,
|
|
780
|
+
procedure: call.procedure,
|
|
781
|
+
body: transformed,
|
|
782
|
+
})
|
|
481
783
|
call.resolve(transformed)
|
|
482
784
|
} catch (error) {
|
|
785
|
+
this.emitClientEvent({
|
|
786
|
+
kind: 'rpc_error',
|
|
787
|
+
timestamp: Date.now(),
|
|
788
|
+
callId,
|
|
789
|
+
procedure: call.procedure,
|
|
790
|
+
error,
|
|
791
|
+
})
|
|
483
792
|
call.reject(
|
|
484
793
|
new ProtocolError(
|
|
485
794
|
ErrorCode.ClientRequestError,
|
|
@@ -491,12 +800,19 @@ export abstract class BaseClient<
|
|
|
491
800
|
}
|
|
492
801
|
}
|
|
493
802
|
|
|
494
|
-
|
|
803
|
+
private handleRPCStreamResponseMessage(
|
|
495
804
|
message: ServerMessageTypePayload[ServerMessageType.RpcStreamResponse],
|
|
496
805
|
) {
|
|
497
806
|
const call = this.calls.get(message.callId)
|
|
498
807
|
if (message.error) {
|
|
499
808
|
if (!call) return
|
|
809
|
+
this.emitClientEvent({
|
|
810
|
+
kind: 'rpc_error',
|
|
811
|
+
timestamp: Date.now(),
|
|
812
|
+
callId: message.callId,
|
|
813
|
+
procedure: call.procedure,
|
|
814
|
+
error: message.error,
|
|
815
|
+
})
|
|
500
816
|
call.reject(
|
|
501
817
|
new ProtocolError(
|
|
502
818
|
message.error.code,
|
|
@@ -507,6 +823,13 @@ export abstract class BaseClient<
|
|
|
507
823
|
} else {
|
|
508
824
|
if (call) {
|
|
509
825
|
const { procedure, signal } = call
|
|
826
|
+
this.emitClientEvent({
|
|
827
|
+
kind: 'rpc_response',
|
|
828
|
+
timestamp: Date.now(),
|
|
829
|
+
callId: message.callId,
|
|
830
|
+
procedure,
|
|
831
|
+
stream: true,
|
|
832
|
+
})
|
|
510
833
|
const stream = new ProtocolServerRPCStream({
|
|
511
834
|
start: (controller) => {
|
|
512
835
|
if (signal) {
|
|
@@ -525,7 +848,7 @@ export abstract class BaseClient<
|
|
|
525
848
|
ClientMessageType.RpcAbort,
|
|
526
849
|
{ callId: message.callId, reason: signal.reason },
|
|
527
850
|
)
|
|
528
|
-
this
|
|
851
|
+
this.send(buffer).catch(noopFn)
|
|
529
852
|
}
|
|
530
853
|
}
|
|
531
854
|
},
|
|
@@ -539,14 +862,6 @@ export abstract class BaseClient<
|
|
|
539
862
|
this.options.format.decode(chunk),
|
|
540
863
|
)
|
|
541
864
|
},
|
|
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
865
|
readableStrategy: { highWaterMark: 0 },
|
|
551
866
|
})
|
|
552
867
|
this.rpcStreams.add(message.callId, stream)
|
|
@@ -561,17 +876,24 @@ export abstract class BaseClient<
|
|
|
561
876
|
ClientMessageType.RpcAbort,
|
|
562
877
|
{ callId: message.callId },
|
|
563
878
|
)
|
|
564
|
-
this
|
|
879
|
+
this.send(buffer).catch(noopFn)
|
|
565
880
|
}
|
|
566
881
|
}
|
|
567
882
|
}
|
|
568
883
|
}
|
|
569
884
|
|
|
570
|
-
|
|
885
|
+
private handleCallResponse(callId: number, response: ClientCallResponse) {
|
|
571
886
|
const call = this.calls.get(callId)
|
|
572
887
|
|
|
573
888
|
if (response.type === 'rpc_stream') {
|
|
574
889
|
if (call) {
|
|
890
|
+
this.emitClientEvent({
|
|
891
|
+
kind: 'rpc_response',
|
|
892
|
+
timestamp: Date.now(),
|
|
893
|
+
callId,
|
|
894
|
+
procedure: call.procedure,
|
|
895
|
+
stream: true,
|
|
896
|
+
})
|
|
575
897
|
const stream = new ProtocolServerStream({
|
|
576
898
|
transform: (chunk) => {
|
|
577
899
|
return this.transformer.decode(
|
|
@@ -582,7 +904,36 @@ export abstract class BaseClient<
|
|
|
582
904
|
})
|
|
583
905
|
this.rpcStreams.add(callId, stream)
|
|
584
906
|
call.resolve(({ signal }: { signal?: AbortSignal }) => {
|
|
585
|
-
response.stream.
|
|
907
|
+
const reader = response.stream.getReader()
|
|
908
|
+
|
|
909
|
+
let onAbort: (() => void) | undefined
|
|
910
|
+
if (signal) {
|
|
911
|
+
onAbort = () => {
|
|
912
|
+
reader.cancel(signal.reason).catch(noopFn)
|
|
913
|
+
this.rpcStreams.abort(callId).catch(noopFn)
|
|
914
|
+
}
|
|
915
|
+
if (signal.aborted) onAbort()
|
|
916
|
+
else signal.addEventListener('abort', onAbort, { once: true })
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
void (async () => {
|
|
920
|
+
try {
|
|
921
|
+
while (true) {
|
|
922
|
+
const { done, value } = await reader.read()
|
|
923
|
+
if (done) break
|
|
924
|
+
await this.rpcStreams.push(callId, value)
|
|
925
|
+
}
|
|
926
|
+
await this.rpcStreams.end(callId)
|
|
927
|
+
} catch {
|
|
928
|
+
await this.rpcStreams.abort(callId).catch(noopFn)
|
|
929
|
+
} finally {
|
|
930
|
+
reader.releaseLock()
|
|
931
|
+
if (signal && onAbort) {
|
|
932
|
+
signal.removeEventListener('abort', onAbort)
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
})()
|
|
936
|
+
|
|
586
937
|
return stream
|
|
587
938
|
})
|
|
588
939
|
} else {
|
|
@@ -593,9 +944,16 @@ export abstract class BaseClient<
|
|
|
593
944
|
}
|
|
594
945
|
} else if (response.type === 'blob') {
|
|
595
946
|
if (call) {
|
|
947
|
+
this.emitClientEvent({
|
|
948
|
+
kind: 'rpc_response',
|
|
949
|
+
timestamp: Date.now(),
|
|
950
|
+
callId,
|
|
951
|
+
procedure: call.procedure,
|
|
952
|
+
stream: true,
|
|
953
|
+
})
|
|
596
954
|
const { metadata, source } = response
|
|
597
955
|
const stream = new ProtocolServerBlobStream(metadata)
|
|
598
|
-
this.serverStreams.add(this
|
|
956
|
+
this.serverStreams.add(this.getStreamId(), stream)
|
|
599
957
|
call.resolve(({ signal }: { signal?: AbortSignal }) => {
|
|
600
958
|
source.pipeTo(stream.writable, { signal }).catch(noopFn)
|
|
601
959
|
return stream
|
|
@@ -609,12 +967,31 @@ export abstract class BaseClient<
|
|
|
609
967
|
} else if (response.type === 'rpc') {
|
|
610
968
|
if (!call) return
|
|
611
969
|
try {
|
|
970
|
+
const decodedPayload =
|
|
971
|
+
response.result.byteLength === 0
|
|
972
|
+
? undefined
|
|
973
|
+
: this.options.format.decode(response.result)
|
|
974
|
+
|
|
612
975
|
const transformed = this.transformer.decode(
|
|
613
976
|
call.procedure,
|
|
614
|
-
|
|
977
|
+
decodedPayload,
|
|
615
978
|
)
|
|
979
|
+
this.emitClientEvent({
|
|
980
|
+
kind: 'rpc_response',
|
|
981
|
+
timestamp: Date.now(),
|
|
982
|
+
callId,
|
|
983
|
+
procedure: call.procedure,
|
|
984
|
+
body: transformed,
|
|
985
|
+
})
|
|
616
986
|
call.resolve(transformed)
|
|
617
987
|
} catch (error) {
|
|
988
|
+
this.emitClientEvent({
|
|
989
|
+
kind: 'rpc_error',
|
|
990
|
+
timestamp: Date.now(),
|
|
991
|
+
callId,
|
|
992
|
+
procedure: call.procedure,
|
|
993
|
+
error,
|
|
994
|
+
})
|
|
618
995
|
call.reject(
|
|
619
996
|
new ProtocolError(
|
|
620
997
|
ErrorCode.ClientRequestError,
|
|
@@ -626,23 +1003,45 @@ export abstract class BaseClient<
|
|
|
626
1003
|
}
|
|
627
1004
|
}
|
|
628
1005
|
|
|
629
|
-
|
|
1006
|
+
protected send(buffer: ArrayBufferView, signal?: AbortSignal) {
|
|
630
1007
|
if (this.transport.type === ConnectionType.Unidirectional)
|
|
631
1008
|
throw new Error('Invalid transport type for send')
|
|
632
1009
|
return this.transport.send(buffer, { signal })
|
|
633
1010
|
}
|
|
634
1011
|
|
|
635
|
-
|
|
1012
|
+
protected emitStreamEvent(
|
|
1013
|
+
event: Omit<
|
|
1014
|
+
Extract<ClientPluginEvent, { kind: 'stream_event' }>,
|
|
1015
|
+
'kind' | 'timestamp'
|
|
1016
|
+
>,
|
|
1017
|
+
) {
|
|
1018
|
+
this.emitClientEvent({
|
|
1019
|
+
kind: 'stream_event',
|
|
1020
|
+
timestamp: Date.now(),
|
|
1021
|
+
...event,
|
|
1022
|
+
})
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
protected getStreamId() {
|
|
636
1026
|
if (this.streamId >= MAX_UINT32) {
|
|
637
1027
|
this.streamId = 0
|
|
638
1028
|
}
|
|
639
1029
|
return this.streamId++
|
|
640
1030
|
}
|
|
641
1031
|
|
|
642
|
-
|
|
1032
|
+
protected getCallId() {
|
|
643
1033
|
if (this.callId >= MAX_UINT32) {
|
|
644
1034
|
this.callId = 0
|
|
645
1035
|
}
|
|
646
1036
|
return this.callId++
|
|
647
1037
|
}
|
|
1038
|
+
|
|
1039
|
+
protected emitClientEvent(event: ClientPluginEvent) {
|
|
1040
|
+
for (const plugin of this.plugins) {
|
|
1041
|
+
try {
|
|
1042
|
+
const result = plugin.onClientEvent?.(event)
|
|
1043
|
+
Promise.resolve(result).catch(noopFn)
|
|
1044
|
+
} catch {}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
648
1047
|
}
|