@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.
- package/dist/client.d.ts +64 -0
- package/dist/client.js +97 -0
- package/dist/client.js.map +1 -0
- package/dist/clients/runtime.d.ts +6 -12
- package/dist/clients/runtime.js +58 -57
- package/dist/clients/runtime.js.map +1 -1
- package/dist/clients/static.d.ts +4 -9
- package/dist/clients/static.js +20 -20
- package/dist/clients/static.js.map +1 -1
- package/dist/core.d.ts +33 -83
- package/dist/core.js +305 -690
- package/dist/core.js.map +1 -1
- package/dist/events.d.ts +0 -1
- package/dist/events.js +74 -11
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/layers/ping.d.ts +6 -0
- package/dist/layers/ping.js +65 -0
- package/dist/layers/ping.js.map +1 -0
- package/dist/layers/rpc.d.ts +19 -0
- package/dist/layers/rpc.js +521 -0
- package/dist/layers/rpc.js.map +1 -0
- package/dist/layers/streams.d.ts +20 -0
- package/dist/layers/streams.js +194 -0
- package/dist/layers/streams.js.map +1 -0
- package/dist/plugins/browser.js +28 -9
- package/dist/plugins/browser.js.map +1 -1
- package/dist/plugins/heartbeat.js +10 -10
- package/dist/plugins/heartbeat.js.map +1 -1
- package/dist/plugins/index.d.ts +1 -1
- package/dist/plugins/index.js +0 -1
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/reconnect.js +11 -94
- package/dist/plugins/reconnect.js.map +1 -1
- package/dist/plugins/types.d.ts +27 -11
- package/dist/transport.d.ts +49 -31
- package/dist/types.d.ts +21 -5
- package/package.json +10 -10
- package/src/client.ts +216 -0
- package/src/clients/runtime.ts +93 -79
- package/src/clients/static.ts +46 -38
- package/src/core.ts +394 -901
- package/src/events.ts +113 -14
- package/src/index.ts +4 -0
- package/src/layers/ping.ts +99 -0
- package/src/layers/rpc.ts +725 -0
- package/src/layers/streams.ts +277 -0
- package/src/plugins/browser.ts +39 -9
- package/src/plugins/heartbeat.ts +10 -10
- package/src/plugins/index.ts +8 -1
- package/src/plugins/reconnect.ts +12 -119
- package/src/plugins/types.ts +30 -13
- package/src/transport.ts +75 -46
- 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
|
+
}
|