@nmtjs/client 0.15.0-beta.8 → 0.15.0
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/README.md +31 -1
- 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/dist/core.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { anyAbortSignal, createFuture, MAX_UINT32, noopFn } from '@nmtjs/common';
|
|
1
|
+
import { anyAbortSignal, createFuture, MAX_UINT32, noopFn, withTimeout, } from '@nmtjs/common';
|
|
2
2
|
import { ClientMessageType, ConnectionType, ErrorCode, ProtocolBlob, ServerMessageType, } from '@nmtjs/protocol';
|
|
3
3
|
import { ProtocolError, ProtocolServerBlobStream, ProtocolServerRPCStream, ProtocolServerStream, versions, } from '@nmtjs/protocol/client';
|
|
4
|
-
import { EventEmitter } from
|
|
5
|
-
import { ClientStreams, ServerStreams } from
|
|
4
|
+
import { EventEmitter } from './events.js';
|
|
5
|
+
import { ClientStreams, ServerStreams } from './streams.js';
|
|
6
6
|
export { ErrorCode, ProtocolBlob, } from '@nmtjs/protocol';
|
|
7
|
-
export * from
|
|
7
|
+
export * from './types.js';
|
|
8
8
|
export class ClientError extends ProtocolError {
|
|
9
9
|
}
|
|
10
|
-
const
|
|
11
|
-
const DEFAULT_MAX_RECONNECT_TIMEOUT = 60000;
|
|
10
|
+
const DEFAULT_RECONNECT_REASON = 'connect_error';
|
|
12
11
|
/**
|
|
13
12
|
* @todo Add error logging in ClientStreamPull rejection handler for easier debugging
|
|
14
13
|
* @todo Consider edge case where callId/streamId overflow at MAX_UINT32 with existing entries
|
|
@@ -28,10 +27,16 @@ export class BaseClient extends EventEmitter {
|
|
|
28
27
|
callId = 0;
|
|
29
28
|
streamId = 0;
|
|
30
29
|
cab = null;
|
|
31
|
-
reconnectTimeout = DEFAULT_RECONNECT_TIMEOUT;
|
|
32
30
|
connecting = null;
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
_state = 'disconnected';
|
|
32
|
+
_lastDisconnectReason = 'server';
|
|
33
|
+
_disposed = false;
|
|
34
|
+
pingNonce = 0;
|
|
35
|
+
pendingPings = new Map();
|
|
36
|
+
plugins = [];
|
|
37
|
+
clientDisconnectAsReconnect = false;
|
|
38
|
+
clientDisconnectOverrideReason = null;
|
|
39
|
+
authValue;
|
|
35
40
|
constructor(options, transportFactory, transportOptions) {
|
|
36
41
|
super();
|
|
37
42
|
this.options = options;
|
|
@@ -40,46 +45,55 @@ export class BaseClient extends EventEmitter {
|
|
|
40
45
|
this.protocol = versions[options.protocol];
|
|
41
46
|
const { format, protocol } = this.options;
|
|
42
47
|
this.transport = this.transportFactory({ protocol, format }, this.transportOptions);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
while (this.state === 'disconnected') {
|
|
47
|
-
const timeout = new Promise((resolve) => setTimeout(resolve, this.reconnectTimeout));
|
|
48
|
-
this.reconnectTimeout = Math.min(this.reconnectTimeout * 2, DEFAULT_MAX_RECONNECT_TIMEOUT);
|
|
49
|
-
await timeout;
|
|
50
|
-
await this.connect().catch(noopFn);
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
this.on('connected', () => {
|
|
54
|
-
this.reconnectTimeout = DEFAULT_RECONNECT_TIMEOUT;
|
|
55
|
-
});
|
|
56
|
-
if (globalThis.window) {
|
|
57
|
-
globalThis.window.addEventListener('pageshow', () => {
|
|
58
|
-
if (this.state === 'disconnected')
|
|
59
|
-
this.connect();
|
|
60
|
-
});
|
|
61
|
-
}
|
|
48
|
+
this.plugins = this.options.plugins?.map((plugin) => plugin(this)) ?? [];
|
|
49
|
+
for (const plugin of this.plugins) {
|
|
50
|
+
plugin.onInit?.();
|
|
62
51
|
}
|
|
63
52
|
}
|
|
53
|
+
dispose() {
|
|
54
|
+
this._disposed = true;
|
|
55
|
+
this.stopAllPendingPings('dispose');
|
|
56
|
+
for (let i = this.plugins.length - 1; i >= 0; i--) {
|
|
57
|
+
this.plugins[i].dispose?.();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
get state() {
|
|
61
|
+
return this._state;
|
|
62
|
+
}
|
|
63
|
+
get lastDisconnectReason() {
|
|
64
|
+
return this._lastDisconnectReason;
|
|
65
|
+
}
|
|
66
|
+
get transportType() {
|
|
67
|
+
return this.transport.type;
|
|
68
|
+
}
|
|
69
|
+
isDisposed() {
|
|
70
|
+
return this._disposed;
|
|
71
|
+
}
|
|
72
|
+
requestReconnect(reason) {
|
|
73
|
+
return this.disconnect({ reconnect: true, reason });
|
|
74
|
+
}
|
|
64
75
|
get auth() {
|
|
65
|
-
return this
|
|
76
|
+
return this.authValue;
|
|
66
77
|
}
|
|
67
78
|
set auth(value) {
|
|
68
|
-
this
|
|
79
|
+
this.authValue = value;
|
|
69
80
|
}
|
|
70
81
|
connect() {
|
|
71
|
-
if (this.
|
|
82
|
+
if (this._state === 'connected')
|
|
72
83
|
return Promise.resolve();
|
|
73
84
|
if (this.connecting)
|
|
74
85
|
return this.connecting;
|
|
86
|
+
if (this._disposed)
|
|
87
|
+
return Promise.reject(new Error('Client is disposed'));
|
|
75
88
|
const _connect = async () => {
|
|
76
89
|
if (this.transport.type === ConnectionType.Bidirectional) {
|
|
90
|
+
const client = this;
|
|
77
91
|
this.cab = new AbortController();
|
|
78
92
|
const protocol = this.protocol;
|
|
79
93
|
const serverStreams = this.serverStreams;
|
|
80
94
|
const transport = {
|
|
81
95
|
send: (buffer) => {
|
|
82
|
-
this
|
|
96
|
+
this.send(buffer).catch(noopFn);
|
|
83
97
|
},
|
|
84
98
|
};
|
|
85
99
|
this.messageContext = {
|
|
@@ -87,12 +101,19 @@ export class BaseClient extends EventEmitter {
|
|
|
87
101
|
encoder: this.options.format,
|
|
88
102
|
decoder: this.options.format,
|
|
89
103
|
addClientStream: (blob) => {
|
|
90
|
-
const streamId = this
|
|
104
|
+
const streamId = this.getStreamId();
|
|
91
105
|
return this.clientStreams.add(blob.source, streamId, blob.metadata);
|
|
92
106
|
},
|
|
93
107
|
addServerStream(streamId, metadata) {
|
|
94
108
|
const stream = new ProtocolServerBlobStream(metadata, {
|
|
95
109
|
pull: (controller) => {
|
|
110
|
+
client.emitStreamEvent({
|
|
111
|
+
direction: 'outgoing',
|
|
112
|
+
streamType: 'server_blob',
|
|
113
|
+
action: 'pull',
|
|
114
|
+
streamId,
|
|
115
|
+
byteLength: 65535,
|
|
116
|
+
});
|
|
96
117
|
transport.send(protocol.encodeMessage(this, ClientMessageType.ServerStreamPull, { streamId, size: 65535 /* 64kb by default */ }));
|
|
97
118
|
},
|
|
98
119
|
close: () => {
|
|
@@ -104,13 +125,19 @@ export class BaseClient extends EventEmitter {
|
|
|
104
125
|
return ({ signal } = {}) => {
|
|
105
126
|
if (signal)
|
|
106
127
|
signal.addEventListener('abort', () => {
|
|
128
|
+
client.emitStreamEvent({
|
|
129
|
+
direction: 'outgoing',
|
|
130
|
+
streamType: 'server_blob',
|
|
131
|
+
action: 'abort',
|
|
132
|
+
streamId,
|
|
133
|
+
});
|
|
107
134
|
transport.send(protocol.encodeMessage(this, ClientMessageType.ServerStreamAbort, { streamId }));
|
|
108
135
|
serverStreams.abort(streamId);
|
|
109
136
|
}, { once: true });
|
|
110
137
|
return stream;
|
|
111
138
|
};
|
|
112
139
|
},
|
|
113
|
-
streamId: this
|
|
140
|
+
streamId: this.getStreamId.bind(this),
|
|
114
141
|
};
|
|
115
142
|
return this.transport.connect({
|
|
116
143
|
auth: this.auth,
|
|
@@ -121,17 +148,40 @@ export class BaseClient extends EventEmitter {
|
|
|
121
148
|
});
|
|
122
149
|
}
|
|
123
150
|
};
|
|
151
|
+
let emitDisconnectOnFailure = null;
|
|
124
152
|
this.connecting = _connect()
|
|
125
153
|
.then(() => {
|
|
126
|
-
this.
|
|
154
|
+
this._state = 'connected';
|
|
155
|
+
})
|
|
156
|
+
.catch((error) => {
|
|
157
|
+
if (this.transport.type === ConnectionType.Bidirectional) {
|
|
158
|
+
emitDisconnectOnFailure = DEFAULT_RECONNECT_REASON;
|
|
159
|
+
}
|
|
160
|
+
throw error;
|
|
127
161
|
})
|
|
128
162
|
.finally(() => {
|
|
129
163
|
this.connecting = null;
|
|
164
|
+
if (emitDisconnectOnFailure && !this._disposed) {
|
|
165
|
+
this._state = 'disconnected';
|
|
166
|
+
this._lastDisconnectReason = emitDisconnectOnFailure;
|
|
167
|
+
void this.onDisconnect(emitDisconnectOnFailure);
|
|
168
|
+
}
|
|
130
169
|
});
|
|
131
170
|
return this.connecting;
|
|
132
171
|
}
|
|
133
|
-
async disconnect() {
|
|
172
|
+
async disconnect(options = {}) {
|
|
134
173
|
if (this.transport.type === ConnectionType.Bidirectional) {
|
|
174
|
+
// Ensure connect() won't short-circuit while the transport is closing.
|
|
175
|
+
this._state = 'disconnected';
|
|
176
|
+
this._lastDisconnectReason = 'client';
|
|
177
|
+
if (options.reconnect) {
|
|
178
|
+
this.clientDisconnectAsReconnect = true;
|
|
179
|
+
this.clientDisconnectOverrideReason = options.reason ?? 'server';
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
this.clientDisconnectAsReconnect = false;
|
|
183
|
+
this.clientDisconnectOverrideReason = null;
|
|
184
|
+
}
|
|
135
185
|
this.cab.abort();
|
|
136
186
|
await this.transport.disconnect();
|
|
137
187
|
this.messageContext = null;
|
|
@@ -153,11 +203,18 @@ export class BaseClient extends EventEmitter {
|
|
|
153
203
|
if (this.cab?.signal)
|
|
154
204
|
signals.push(this.cab.signal);
|
|
155
205
|
const signal = signals.length ? anyAbortSignal(...signals) : undefined;
|
|
156
|
-
const callId = this
|
|
206
|
+
const callId = this.getCallId();
|
|
157
207
|
const call = createFuture();
|
|
158
208
|
call.procedure = procedure;
|
|
159
209
|
call.signal = signal;
|
|
160
210
|
this.calls.set(callId, call);
|
|
211
|
+
this.emitClientEvent({
|
|
212
|
+
kind: 'rpc_request',
|
|
213
|
+
timestamp: Date.now(),
|
|
214
|
+
callId,
|
|
215
|
+
procedure,
|
|
216
|
+
body: payload,
|
|
217
|
+
});
|
|
161
218
|
// Check if signal is already aborted before proceeding
|
|
162
219
|
if (signal?.aborted) {
|
|
163
220
|
this.calls.delete(callId);
|
|
@@ -171,7 +228,7 @@ export class BaseClient extends EventEmitter {
|
|
|
171
228
|
if (this.transport.type === ConnectionType.Bidirectional &&
|
|
172
229
|
this.messageContext) {
|
|
173
230
|
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.RpcAbort, { callId });
|
|
174
|
-
this
|
|
231
|
+
this.send(buffer).catch(noopFn);
|
|
175
232
|
}
|
|
176
233
|
}, { once: true });
|
|
177
234
|
}
|
|
@@ -179,7 +236,7 @@ export class BaseClient extends EventEmitter {
|
|
|
179
236
|
const transformedPayload = this.transformer.encode(procedure, payload);
|
|
180
237
|
if (this.transport.type === ConnectionType.Bidirectional) {
|
|
181
238
|
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.Rpc, { callId, procedure, payload: transformedPayload });
|
|
182
|
-
await this
|
|
239
|
+
await this.send(buffer, signal);
|
|
183
240
|
}
|
|
184
241
|
else {
|
|
185
242
|
const response = await this.transport.call({
|
|
@@ -187,10 +244,17 @@ export class BaseClient extends EventEmitter {
|
|
|
187
244
|
format: this.options.format,
|
|
188
245
|
auth: this.auth,
|
|
189
246
|
}, { callId, procedure, payload: transformedPayload }, { signal, _stream_response: options._stream_response });
|
|
190
|
-
this
|
|
247
|
+
this.handleCallResponse(callId, response);
|
|
191
248
|
}
|
|
192
249
|
}
|
|
193
250
|
catch (error) {
|
|
251
|
+
this.emitClientEvent({
|
|
252
|
+
kind: 'rpc_error',
|
|
253
|
+
timestamp: Date.now(),
|
|
254
|
+
callId,
|
|
255
|
+
procedure,
|
|
256
|
+
error,
|
|
257
|
+
});
|
|
194
258
|
call.reject(error);
|
|
195
259
|
}
|
|
196
260
|
}
|
|
@@ -200,6 +264,9 @@ export class BaseClient extends EventEmitter {
|
|
|
200
264
|
controller.abort();
|
|
201
265
|
});
|
|
202
266
|
}
|
|
267
|
+
if (options._stream_response && typeof value === 'function') {
|
|
268
|
+
return value;
|
|
269
|
+
}
|
|
203
270
|
controller.abort();
|
|
204
271
|
return value;
|
|
205
272
|
}, (err) => {
|
|
@@ -221,97 +288,304 @@ export class BaseClient extends EventEmitter {
|
|
|
221
288
|
}
|
|
222
289
|
}
|
|
223
290
|
async onConnect() {
|
|
224
|
-
this.
|
|
291
|
+
this._state = 'connected';
|
|
292
|
+
this._lastDisconnectReason = 'server';
|
|
293
|
+
this.emitClientEvent({
|
|
294
|
+
kind: 'connected',
|
|
295
|
+
timestamp: Date.now(),
|
|
296
|
+
transportType: this.transport.type === ConnectionType.Bidirectional
|
|
297
|
+
? 'bidirectional'
|
|
298
|
+
: 'unidirectional',
|
|
299
|
+
});
|
|
300
|
+
for (const plugin of this.plugins) {
|
|
301
|
+
await plugin.onConnect?.();
|
|
302
|
+
}
|
|
225
303
|
this.emit('connected');
|
|
226
304
|
}
|
|
227
305
|
async onDisconnect(reason) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
this.
|
|
232
|
-
this.
|
|
306
|
+
const effectiveReason = reason === 'client' && this.clientDisconnectAsReconnect
|
|
307
|
+
? (this.clientDisconnectOverrideReason ?? 'server')
|
|
308
|
+
: reason;
|
|
309
|
+
this.clientDisconnectAsReconnect = false;
|
|
310
|
+
this.clientDisconnectOverrideReason = null;
|
|
311
|
+
this._state = 'disconnected';
|
|
312
|
+
this._lastDisconnectReason = effectiveReason;
|
|
313
|
+
this.emitClientEvent({
|
|
314
|
+
kind: 'disconnected',
|
|
315
|
+
timestamp: Date.now(),
|
|
316
|
+
reason: effectiveReason,
|
|
317
|
+
});
|
|
318
|
+
// Connection is gone, never keep old message context around.
|
|
319
|
+
this.messageContext = null;
|
|
320
|
+
this.stopAllPendingPings(effectiveReason);
|
|
321
|
+
// Fail-fast: do not keep pending calls around across disconnects.
|
|
322
|
+
if (this.calls.size) {
|
|
323
|
+
const error = new ProtocolError(ErrorCode.ConnectionError, 'Disconnected', { reason: effectiveReason });
|
|
324
|
+
for (const call of this.calls.values()) {
|
|
325
|
+
call.reject(error);
|
|
326
|
+
}
|
|
327
|
+
this.calls.clear();
|
|
328
|
+
}
|
|
329
|
+
if (this.cab) {
|
|
330
|
+
try {
|
|
331
|
+
this.cab.abort(reason);
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
this.cab.abort();
|
|
335
|
+
}
|
|
336
|
+
this.cab = null;
|
|
337
|
+
}
|
|
338
|
+
this.emit('disconnected', effectiveReason);
|
|
339
|
+
for (let i = this.plugins.length - 1; i >= 0; i--) {
|
|
340
|
+
await this.plugins[i].onDisconnect?.(effectiveReason);
|
|
341
|
+
}
|
|
342
|
+
void this.clientStreams.clear(effectiveReason);
|
|
343
|
+
void this.serverStreams.clear(effectiveReason);
|
|
344
|
+
void this.rpcStreams.clear(effectiveReason);
|
|
345
|
+
}
|
|
346
|
+
nextPingNonce() {
|
|
347
|
+
if (this.pingNonce >= MAX_UINT32)
|
|
348
|
+
this.pingNonce = 0;
|
|
349
|
+
return this.pingNonce++;
|
|
350
|
+
}
|
|
351
|
+
ping(timeout, signal) {
|
|
352
|
+
if (!this.messageContext ||
|
|
353
|
+
this.transport.type !== ConnectionType.Bidirectional) {
|
|
354
|
+
return Promise.reject(new Error('Client is not connected'));
|
|
355
|
+
}
|
|
356
|
+
const nonce = this.nextPingNonce();
|
|
357
|
+
const future = createFuture();
|
|
358
|
+
this.pendingPings.set(nonce, future);
|
|
359
|
+
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.Ping, { nonce });
|
|
360
|
+
return this.send(buffer, signal)
|
|
361
|
+
.then(() => withTimeout(future.promise, timeout, new Error('Heartbeat timeout')))
|
|
362
|
+
.finally(() => {
|
|
363
|
+
this.pendingPings.delete(nonce);
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
stopAllPendingPings(reason) {
|
|
367
|
+
if (!this.pendingPings.size)
|
|
368
|
+
return;
|
|
369
|
+
const error = new Error('Heartbeat stopped', { cause: reason });
|
|
370
|
+
for (const pending of this.pendingPings.values())
|
|
371
|
+
pending.reject(error);
|
|
372
|
+
this.pendingPings.clear();
|
|
233
373
|
}
|
|
234
374
|
async onMessage(buffer) {
|
|
235
375
|
if (!this.messageContext)
|
|
236
376
|
return;
|
|
237
377
|
const message = this.protocol.decodeMessage(this.messageContext, buffer);
|
|
378
|
+
for (const plugin of this.plugins) {
|
|
379
|
+
plugin.onServerMessage?.(message, buffer);
|
|
380
|
+
}
|
|
381
|
+
this.emitClientEvent({
|
|
382
|
+
kind: 'server_message',
|
|
383
|
+
timestamp: Date.now(),
|
|
384
|
+
messageType: message.type,
|
|
385
|
+
rawByteLength: buffer.byteLength,
|
|
386
|
+
body: message,
|
|
387
|
+
});
|
|
238
388
|
switch (message.type) {
|
|
239
389
|
case ServerMessageType.RpcResponse:
|
|
240
|
-
this
|
|
390
|
+
this.handleRPCResponseMessage(message);
|
|
241
391
|
break;
|
|
242
392
|
case ServerMessageType.RpcStreamResponse:
|
|
243
|
-
this
|
|
393
|
+
this.handleRPCStreamResponseMessage(message);
|
|
394
|
+
break;
|
|
395
|
+
case ServerMessageType.Pong: {
|
|
396
|
+
const pending = this.pendingPings.get(message.nonce);
|
|
397
|
+
if (pending) {
|
|
398
|
+
this.pendingPings.delete(message.nonce);
|
|
399
|
+
pending.resolve();
|
|
400
|
+
}
|
|
401
|
+
this.emit('pong', message.nonce);
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
case ServerMessageType.Ping: {
|
|
405
|
+
if (this.messageContext) {
|
|
406
|
+
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.Pong, { nonce: message.nonce });
|
|
407
|
+
this.send(buffer).catch(noopFn);
|
|
408
|
+
}
|
|
244
409
|
break;
|
|
410
|
+
}
|
|
245
411
|
case ServerMessageType.RpcStreamChunk:
|
|
412
|
+
this.emitStreamEvent({
|
|
413
|
+
direction: 'incoming',
|
|
414
|
+
streamType: 'rpc',
|
|
415
|
+
action: 'push',
|
|
416
|
+
callId: message.callId,
|
|
417
|
+
byteLength: message.chunk.byteLength,
|
|
418
|
+
});
|
|
246
419
|
this.rpcStreams.push(message.callId, message.chunk);
|
|
247
420
|
break;
|
|
248
421
|
case ServerMessageType.RpcStreamEnd:
|
|
422
|
+
this.emitStreamEvent({
|
|
423
|
+
direction: 'incoming',
|
|
424
|
+
streamType: 'rpc',
|
|
425
|
+
action: 'end',
|
|
426
|
+
callId: message.callId,
|
|
427
|
+
});
|
|
249
428
|
this.rpcStreams.end(message.callId);
|
|
250
429
|
this.calls.delete(message.callId);
|
|
251
430
|
break;
|
|
252
431
|
case ServerMessageType.RpcStreamAbort:
|
|
432
|
+
this.emitStreamEvent({
|
|
433
|
+
direction: 'incoming',
|
|
434
|
+
streamType: 'rpc',
|
|
435
|
+
action: 'abort',
|
|
436
|
+
callId: message.callId,
|
|
437
|
+
reason: message.reason,
|
|
438
|
+
});
|
|
253
439
|
this.rpcStreams.abort(message.callId);
|
|
254
440
|
this.calls.delete(message.callId);
|
|
255
441
|
break;
|
|
256
442
|
case ServerMessageType.ServerStreamPush:
|
|
443
|
+
this.emitStreamEvent({
|
|
444
|
+
direction: 'incoming',
|
|
445
|
+
streamType: 'server_blob',
|
|
446
|
+
action: 'push',
|
|
447
|
+
streamId: message.streamId,
|
|
448
|
+
byteLength: message.chunk.byteLength,
|
|
449
|
+
});
|
|
257
450
|
this.serverStreams.push(message.streamId, message.chunk);
|
|
258
451
|
break;
|
|
259
452
|
case ServerMessageType.ServerStreamEnd:
|
|
453
|
+
this.emitStreamEvent({
|
|
454
|
+
direction: 'incoming',
|
|
455
|
+
streamType: 'server_blob',
|
|
456
|
+
action: 'end',
|
|
457
|
+
streamId: message.streamId,
|
|
458
|
+
});
|
|
260
459
|
this.serverStreams.end(message.streamId);
|
|
261
460
|
break;
|
|
262
461
|
case ServerMessageType.ServerStreamAbort:
|
|
462
|
+
this.emitStreamEvent({
|
|
463
|
+
direction: 'incoming',
|
|
464
|
+
streamType: 'server_blob',
|
|
465
|
+
action: 'abort',
|
|
466
|
+
streamId: message.streamId,
|
|
467
|
+
reason: message.reason,
|
|
468
|
+
});
|
|
263
469
|
this.serverStreams.abort(message.streamId);
|
|
264
470
|
break;
|
|
265
471
|
case ServerMessageType.ClientStreamPull:
|
|
472
|
+
this.emitStreamEvent({
|
|
473
|
+
direction: 'incoming',
|
|
474
|
+
streamType: 'client_blob',
|
|
475
|
+
action: 'pull',
|
|
476
|
+
streamId: message.streamId,
|
|
477
|
+
byteLength: message.size,
|
|
478
|
+
});
|
|
266
479
|
this.clientStreams.pull(message.streamId, message.size).then((chunk) => {
|
|
267
480
|
if (chunk) {
|
|
481
|
+
this.emitStreamEvent({
|
|
482
|
+
direction: 'outgoing',
|
|
483
|
+
streamType: 'client_blob',
|
|
484
|
+
action: 'push',
|
|
485
|
+
streamId: message.streamId,
|
|
486
|
+
byteLength: chunk.byteLength,
|
|
487
|
+
});
|
|
268
488
|
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.ClientStreamPush, { streamId: message.streamId, chunk });
|
|
269
|
-
this
|
|
489
|
+
this.send(buffer).catch(noopFn);
|
|
270
490
|
}
|
|
271
491
|
else {
|
|
492
|
+
this.emitStreamEvent({
|
|
493
|
+
direction: 'outgoing',
|
|
494
|
+
streamType: 'client_blob',
|
|
495
|
+
action: 'end',
|
|
496
|
+
streamId: message.streamId,
|
|
497
|
+
});
|
|
272
498
|
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.ClientStreamEnd, { streamId: message.streamId });
|
|
273
|
-
this
|
|
499
|
+
this.send(buffer).catch(noopFn);
|
|
274
500
|
this.clientStreams.end(message.streamId);
|
|
275
501
|
}
|
|
276
502
|
}, () => {
|
|
503
|
+
this.emitStreamEvent({
|
|
504
|
+
direction: 'outgoing',
|
|
505
|
+
streamType: 'client_blob',
|
|
506
|
+
action: 'abort',
|
|
507
|
+
streamId: message.streamId,
|
|
508
|
+
});
|
|
277
509
|
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.ClientStreamAbort, { streamId: message.streamId });
|
|
278
|
-
this
|
|
510
|
+
this.send(buffer).catch(noopFn);
|
|
279
511
|
this.clientStreams.remove(message.streamId);
|
|
280
512
|
});
|
|
281
513
|
break;
|
|
282
514
|
case ServerMessageType.ClientStreamAbort:
|
|
515
|
+
this.emitStreamEvent({
|
|
516
|
+
direction: 'incoming',
|
|
517
|
+
streamType: 'client_blob',
|
|
518
|
+
action: 'abort',
|
|
519
|
+
streamId: message.streamId,
|
|
520
|
+
reason: message.reason,
|
|
521
|
+
});
|
|
283
522
|
this.clientStreams.abort(message.streamId);
|
|
284
523
|
break;
|
|
285
524
|
}
|
|
286
525
|
}
|
|
287
|
-
|
|
526
|
+
handleRPCResponseMessage(message) {
|
|
288
527
|
const { callId, result, error } = message;
|
|
289
528
|
const call = this.calls.get(callId);
|
|
290
529
|
if (!call)
|
|
291
530
|
return;
|
|
292
531
|
if (error) {
|
|
532
|
+
this.emitClientEvent({
|
|
533
|
+
kind: 'rpc_error',
|
|
534
|
+
timestamp: Date.now(),
|
|
535
|
+
callId,
|
|
536
|
+
procedure: call.procedure,
|
|
537
|
+
error,
|
|
538
|
+
});
|
|
293
539
|
call.reject(new ProtocolError(error.code, error.message, error.data));
|
|
294
540
|
}
|
|
295
541
|
else {
|
|
296
542
|
try {
|
|
297
543
|
const transformed = this.transformer.decode(call.procedure, result);
|
|
544
|
+
this.emitClientEvent({
|
|
545
|
+
kind: 'rpc_response',
|
|
546
|
+
timestamp: Date.now(),
|
|
547
|
+
callId,
|
|
548
|
+
procedure: call.procedure,
|
|
549
|
+
body: transformed,
|
|
550
|
+
});
|
|
298
551
|
call.resolve(transformed);
|
|
299
552
|
}
|
|
300
553
|
catch (error) {
|
|
554
|
+
this.emitClientEvent({
|
|
555
|
+
kind: 'rpc_error',
|
|
556
|
+
timestamp: Date.now(),
|
|
557
|
+
callId,
|
|
558
|
+
procedure: call.procedure,
|
|
559
|
+
error,
|
|
560
|
+
});
|
|
301
561
|
call.reject(new ProtocolError(ErrorCode.ClientRequestError, 'Unable to decode response', error));
|
|
302
562
|
}
|
|
303
563
|
}
|
|
304
564
|
}
|
|
305
|
-
|
|
565
|
+
handleRPCStreamResponseMessage(message) {
|
|
306
566
|
const call = this.calls.get(message.callId);
|
|
307
567
|
if (message.error) {
|
|
308
568
|
if (!call)
|
|
309
569
|
return;
|
|
570
|
+
this.emitClientEvent({
|
|
571
|
+
kind: 'rpc_error',
|
|
572
|
+
timestamp: Date.now(),
|
|
573
|
+
callId: message.callId,
|
|
574
|
+
procedure: call.procedure,
|
|
575
|
+
error: message.error,
|
|
576
|
+
});
|
|
310
577
|
call.reject(new ProtocolError(message.error.code, message.error.message, message.error.data));
|
|
311
578
|
}
|
|
312
579
|
else {
|
|
313
580
|
if (call) {
|
|
314
581
|
const { procedure, signal } = call;
|
|
582
|
+
this.emitClientEvent({
|
|
583
|
+
kind: 'rpc_response',
|
|
584
|
+
timestamp: Date.now(),
|
|
585
|
+
callId: message.callId,
|
|
586
|
+
procedure,
|
|
587
|
+
stream: true,
|
|
588
|
+
});
|
|
315
589
|
const stream = new ProtocolServerRPCStream({
|
|
316
590
|
start: (controller) => {
|
|
317
591
|
if (signal) {
|
|
@@ -325,7 +599,7 @@ export class BaseClient extends EventEmitter {
|
|
|
325
599
|
this.calls.delete(message.callId);
|
|
326
600
|
if (this.messageContext) {
|
|
327
601
|
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.RpcAbort, { callId: message.callId, reason: signal.reason });
|
|
328
|
-
this
|
|
602
|
+
this.send(buffer).catch(noopFn);
|
|
329
603
|
}
|
|
330
604
|
}
|
|
331
605
|
}, { once: true });
|
|
@@ -334,10 +608,6 @@ export class BaseClient extends EventEmitter {
|
|
|
334
608
|
transform: (chunk) => {
|
|
335
609
|
return this.transformer.decode(procedure, this.options.format.decode(chunk));
|
|
336
610
|
},
|
|
337
|
-
pull: () => {
|
|
338
|
-
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.RpcPull, { callId: message.callId });
|
|
339
|
-
this.#send(buffer).catch(noopFn);
|
|
340
|
-
},
|
|
341
611
|
readableStrategy: { highWaterMark: 0 },
|
|
342
612
|
});
|
|
343
613
|
this.rpcStreams.add(message.callId, stream);
|
|
@@ -349,15 +619,22 @@ export class BaseClient extends EventEmitter {
|
|
|
349
619
|
// Need to send an abort for the stream to avoid resource leaks from server side
|
|
350
620
|
if (this.messageContext) {
|
|
351
621
|
const buffer = this.protocol.encodeMessage(this.messageContext, ClientMessageType.RpcAbort, { callId: message.callId });
|
|
352
|
-
this
|
|
622
|
+
this.send(buffer).catch(noopFn);
|
|
353
623
|
}
|
|
354
624
|
}
|
|
355
625
|
}
|
|
356
626
|
}
|
|
357
|
-
|
|
627
|
+
handleCallResponse(callId, response) {
|
|
358
628
|
const call = this.calls.get(callId);
|
|
359
629
|
if (response.type === 'rpc_stream') {
|
|
360
630
|
if (call) {
|
|
631
|
+
this.emitClientEvent({
|
|
632
|
+
kind: 'rpc_response',
|
|
633
|
+
timestamp: Date.now(),
|
|
634
|
+
callId,
|
|
635
|
+
procedure: call.procedure,
|
|
636
|
+
stream: true,
|
|
637
|
+
});
|
|
361
638
|
const stream = new ProtocolServerStream({
|
|
362
639
|
transform: (chunk) => {
|
|
363
640
|
return this.transformer.decode(call.procedure, this.options.format.decode(chunk));
|
|
@@ -365,7 +642,38 @@ export class BaseClient extends EventEmitter {
|
|
|
365
642
|
});
|
|
366
643
|
this.rpcStreams.add(callId, stream);
|
|
367
644
|
call.resolve(({ signal }) => {
|
|
368
|
-
response.stream.
|
|
645
|
+
const reader = response.stream.getReader();
|
|
646
|
+
let onAbort;
|
|
647
|
+
if (signal) {
|
|
648
|
+
onAbort = () => {
|
|
649
|
+
reader.cancel(signal.reason).catch(noopFn);
|
|
650
|
+
this.rpcStreams.abort(callId).catch(noopFn);
|
|
651
|
+
};
|
|
652
|
+
if (signal.aborted)
|
|
653
|
+
onAbort();
|
|
654
|
+
else
|
|
655
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
656
|
+
}
|
|
657
|
+
void (async () => {
|
|
658
|
+
try {
|
|
659
|
+
while (true) {
|
|
660
|
+
const { done, value } = await reader.read();
|
|
661
|
+
if (done)
|
|
662
|
+
break;
|
|
663
|
+
await this.rpcStreams.push(callId, value);
|
|
664
|
+
}
|
|
665
|
+
await this.rpcStreams.end(callId);
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
await this.rpcStreams.abort(callId).catch(noopFn);
|
|
669
|
+
}
|
|
670
|
+
finally {
|
|
671
|
+
reader.releaseLock();
|
|
672
|
+
if (signal && onAbort) {
|
|
673
|
+
signal.removeEventListener('abort', onAbort);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
})();
|
|
369
677
|
return stream;
|
|
370
678
|
});
|
|
371
679
|
}
|
|
@@ -378,9 +686,16 @@ export class BaseClient extends EventEmitter {
|
|
|
378
686
|
}
|
|
379
687
|
else if (response.type === 'blob') {
|
|
380
688
|
if (call) {
|
|
689
|
+
this.emitClientEvent({
|
|
690
|
+
kind: 'rpc_response',
|
|
691
|
+
timestamp: Date.now(),
|
|
692
|
+
callId,
|
|
693
|
+
procedure: call.procedure,
|
|
694
|
+
stream: true,
|
|
695
|
+
});
|
|
381
696
|
const { metadata, source } = response;
|
|
382
697
|
const stream = new ProtocolServerBlobStream(metadata);
|
|
383
|
-
this.serverStreams.add(this
|
|
698
|
+
this.serverStreams.add(this.getStreamId(), stream);
|
|
384
699
|
call.resolve(({ signal }) => {
|
|
385
700
|
source.pipeTo(stream.writable, { signal }).catch(noopFn);
|
|
386
701
|
return stream;
|
|
@@ -397,30 +712,63 @@ export class BaseClient extends EventEmitter {
|
|
|
397
712
|
if (!call)
|
|
398
713
|
return;
|
|
399
714
|
try {
|
|
400
|
-
const
|
|
715
|
+
const decodedPayload = response.result.byteLength === 0
|
|
716
|
+
? undefined
|
|
717
|
+
: this.options.format.decode(response.result);
|
|
718
|
+
const transformed = this.transformer.decode(call.procedure, decodedPayload);
|
|
719
|
+
this.emitClientEvent({
|
|
720
|
+
kind: 'rpc_response',
|
|
721
|
+
timestamp: Date.now(),
|
|
722
|
+
callId,
|
|
723
|
+
procedure: call.procedure,
|
|
724
|
+
body: transformed,
|
|
725
|
+
});
|
|
401
726
|
call.resolve(transformed);
|
|
402
727
|
}
|
|
403
728
|
catch (error) {
|
|
729
|
+
this.emitClientEvent({
|
|
730
|
+
kind: 'rpc_error',
|
|
731
|
+
timestamp: Date.now(),
|
|
732
|
+
callId,
|
|
733
|
+
procedure: call.procedure,
|
|
734
|
+
error,
|
|
735
|
+
});
|
|
404
736
|
call.reject(new ProtocolError(ErrorCode.ClientRequestError, 'Unable to decode response', error));
|
|
405
737
|
}
|
|
406
738
|
}
|
|
407
739
|
}
|
|
408
|
-
|
|
740
|
+
send(buffer, signal) {
|
|
409
741
|
if (this.transport.type === ConnectionType.Unidirectional)
|
|
410
742
|
throw new Error('Invalid transport type for send');
|
|
411
743
|
return this.transport.send(buffer, { signal });
|
|
412
744
|
}
|
|
413
|
-
|
|
745
|
+
emitStreamEvent(event) {
|
|
746
|
+
this.emitClientEvent({
|
|
747
|
+
kind: 'stream_event',
|
|
748
|
+
timestamp: Date.now(),
|
|
749
|
+
...event,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
getStreamId() {
|
|
414
753
|
if (this.streamId >= MAX_UINT32) {
|
|
415
754
|
this.streamId = 0;
|
|
416
755
|
}
|
|
417
756
|
return this.streamId++;
|
|
418
757
|
}
|
|
419
|
-
|
|
758
|
+
getCallId() {
|
|
420
759
|
if (this.callId >= MAX_UINT32) {
|
|
421
760
|
this.callId = 0;
|
|
422
761
|
}
|
|
423
762
|
return this.callId++;
|
|
424
763
|
}
|
|
764
|
+
emitClientEvent(event) {
|
|
765
|
+
for (const plugin of this.plugins) {
|
|
766
|
+
try {
|
|
767
|
+
const result = plugin.onClientEvent?.(event);
|
|
768
|
+
Promise.resolve(result).catch(noopFn);
|
|
769
|
+
}
|
|
770
|
+
catch { }
|
|
771
|
+
}
|
|
772
|
+
}
|
|
425
773
|
}
|
|
426
774
|
//# sourceMappingURL=core.js.map
|