@camera.ui/rpc 1.0.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/README.md +3 -0
- package/dist/channel.d.ts +106 -0
- package/dist/channel.js +347 -0
- package/dist/channel.js.map +1 -0
- package/dist/chunking.d.ts +77 -0
- package/dist/chunking.js +151 -0
- package/dist/chunking.js.map +1 -0
- package/dist/client.d.ts +245 -0
- package/dist/client.js +1863 -0
- package/dist/client.js.map +1 -0
- package/dist/codec.d.ts +12 -0
- package/dist/codec.js +31 -0
- package/dist/codec.js.map +1 -0
- package/dist/decorators.d.ts +7 -0
- package/dist/decorators.js +192 -0
- package/dist/decorators.js.map +1 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.js +54 -0
- package/dist/errors.js.map +1 -0
- package/dist/handler.d.ts +35 -0
- package/dist/handler.js +399 -0
- package/dist/handler.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/service.d.ts +60 -0
- package/dist/service.js +286 -0
- package/dist/service.js.map +1 -0
- package/dist/types.d.ts +300 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +83 -0
- package/dist/utils.js +273 -0
- package/dist/utils.js.map +1 -0
- package/dist/wrapper.d.ts +1 -0
- package/dist/wrapper.js +2 -0
- package/dist/wrapper.js.map +1 -0
- package/package.json +49 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,1863 @@
|
|
|
1
|
+
import { connect, createInbox, errors, headers } from '@nats-io/transport-node';
|
|
2
|
+
import { Channel, PrivateChannel } from './channel.js';
|
|
3
|
+
import { ChunkingManager, createChunks } from './chunking.js';
|
|
4
|
+
import { decode, encode } from './codec.js';
|
|
5
|
+
import { extractNestedMethodsWithDecorators, extractNestedMethodsWithoutDecorators } from './decorators.js';
|
|
6
|
+
import { RPCException, createError } from './errors.js';
|
|
7
|
+
import { formatErrorObject, handleCallbackRequest, handleNormalRPC, handlePullCallbackRequest, handlePullIteratorRequest, handleStreamRequest } from './handler.js';
|
|
8
|
+
import { RPCService } from './service.js';
|
|
9
|
+
import { ERROR_CODES } from './types.js';
|
|
10
|
+
import { createProxy, createServiceProxy, generateId, isPromise, sleep } from './utils.js';
|
|
11
|
+
export function createRPCClient(options) {
|
|
12
|
+
return new RPCClient(options);
|
|
13
|
+
}
|
|
14
|
+
export class RPCClient {
|
|
15
|
+
options;
|
|
16
|
+
service = new RPCService(this);
|
|
17
|
+
chunkingManager = new ChunkingManager();
|
|
18
|
+
pullIteratorCleanups = new Map();
|
|
19
|
+
callbackCleanups = new Map();
|
|
20
|
+
nc;
|
|
21
|
+
subscriptions = new Map();
|
|
22
|
+
_subscriptionMeta = new Map();
|
|
23
|
+
_maxPayloadSize = 1024 * 1024; // Default 1MB
|
|
24
|
+
connectionPromise;
|
|
25
|
+
closed = false;
|
|
26
|
+
isolatedClients = [];
|
|
27
|
+
pendingRequests = new Map();
|
|
28
|
+
streamHandlers = new Map();
|
|
29
|
+
/**
|
|
30
|
+
* Check connection status
|
|
31
|
+
*/
|
|
32
|
+
get isConnected() {
|
|
33
|
+
return this.nc?.isClosed() === false;
|
|
34
|
+
}
|
|
35
|
+
get isClosed() {
|
|
36
|
+
return this.closed;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Get the maximum payload size
|
|
40
|
+
*/
|
|
41
|
+
get maxPayloadSize() {
|
|
42
|
+
return this._maxPayloadSize;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Access the underlying NATS connection status events.
|
|
46
|
+
* Yields events like 'reconnect', 'disconnect', 'reconnecting'.
|
|
47
|
+
*/
|
|
48
|
+
status() {
|
|
49
|
+
return this.nc?.status();
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Active liveness probe via NATS PING/PONG round-trip. Resolves when a PONG
|
|
53
|
+
* arrives, rejects on timeout. Caller treats timeout as "connection is dead"
|
|
54
|
+
* — the underlying socket may still report OPEN in that case (silent-dead
|
|
55
|
+
* TCP). Use a tight timeout (a few seconds) so a stale connection is
|
|
56
|
+
* detected promptly.
|
|
57
|
+
*/
|
|
58
|
+
async flush(timeoutMs = 5000) {
|
|
59
|
+
if (!this.nc) {
|
|
60
|
+
throw createError(ERROR_CODES.CONNECTION_CLOSED, 'Not connected');
|
|
61
|
+
}
|
|
62
|
+
let timeoutHandle;
|
|
63
|
+
try {
|
|
64
|
+
await Promise.race([
|
|
65
|
+
this.nc.flush(),
|
|
66
|
+
new Promise((_, reject) => {
|
|
67
|
+
timeoutHandle = setTimeout(() => reject(createError(ERROR_CODES.TIMEOUT, `flush() timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
68
|
+
}),
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
if (timeoutHandle)
|
|
73
|
+
clearTimeout(timeoutHandle);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
constructor(options) {
|
|
77
|
+
this.options = options;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Create a new isolated RPC client
|
|
81
|
+
* @param options - Options for the isolated client
|
|
82
|
+
*/
|
|
83
|
+
createIsolatedClient(options) {
|
|
84
|
+
return new RPCClient(options);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Connect to NATS server. Accepts an AbortSignal so callers can cancel
|
|
88
|
+
* an in-flight handshake when another candidate has won.
|
|
89
|
+
* The underlying nats-js connect() does not itself accept a signal,
|
|
90
|
+
* so we wrap it: on abort we synchronously close any late-arriving connection
|
|
91
|
+
* via the fork's abortClose() and reject with AbortError.
|
|
92
|
+
*/
|
|
93
|
+
async connect(options) {
|
|
94
|
+
if (options?.signal?.aborted)
|
|
95
|
+
throw new DOMException('Aborted', 'AbortError');
|
|
96
|
+
if (this.nc && !this.nc.isClosed()) {
|
|
97
|
+
// Already connected
|
|
98
|
+
return this.nc;
|
|
99
|
+
}
|
|
100
|
+
const natsOptions = {
|
|
101
|
+
servers: this.options.servers,
|
|
102
|
+
user: this.options.auth?.user,
|
|
103
|
+
pass: this.options.auth?.password,
|
|
104
|
+
name: this.options.name,
|
|
105
|
+
reconnect: this.options.reconnect ?? true,
|
|
106
|
+
maxPingOut: this.options.maxPingOut ?? 2,
|
|
107
|
+
maxReconnectAttempts: this.options.maxReconnectAttempts ?? -1,
|
|
108
|
+
reconnectTimeWait: this.options.reconnectTimeWait ?? 2000,
|
|
109
|
+
reconnectJitter: this.options.reconnectJitter ?? 100,
|
|
110
|
+
reconnectJitterTLS: this.options.reconnectJitterTLS ?? 1000,
|
|
111
|
+
ignoreAuthErrorAbort: this.options.ignoreAuthErrorAbort ?? false,
|
|
112
|
+
pingInterval: this.options.pingInterval ?? 120000,
|
|
113
|
+
pingTimeout: this.options.pingTimeout ?? 0,
|
|
114
|
+
reconnectionDelayMax: this.options.reconnectionDelayMax ?? 0,
|
|
115
|
+
reconnectionRandomizationFactor: this.options.reconnectionRandomizationFactor ?? 0,
|
|
116
|
+
tls: this.options.tls,
|
|
117
|
+
debug: this.options.debug ?? false,
|
|
118
|
+
// noEcho: true,
|
|
119
|
+
noAsyncTraces: true,
|
|
120
|
+
waitOnFirstConnect: this.options.waitOnFirstConnect ?? true,
|
|
121
|
+
signal: options?.signal,
|
|
122
|
+
};
|
|
123
|
+
this.connectionPromise ??= connect(natsOptions);
|
|
124
|
+
const innerPromise = this.connectionPromise;
|
|
125
|
+
try {
|
|
126
|
+
this.nc = await makeAbortableConnect(innerPromise, options?.signal);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
this.connectionPromise = undefined;
|
|
130
|
+
}
|
|
131
|
+
this.service.init(this.nc);
|
|
132
|
+
// Get max_payload from server info
|
|
133
|
+
try {
|
|
134
|
+
const serverInfo = this.nc.info;
|
|
135
|
+
this._maxPayloadSize = serverInfo?.max_payload ?? this.options.maxPayloadSize ?? 1024 * 1024;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
this._maxPayloadSize = this.options.maxPayloadSize ?? 1024 * 1024;
|
|
139
|
+
}
|
|
140
|
+
// Reserve 8KB for NATS protocol overhead and MsgPack envelope per message
|
|
141
|
+
this._maxPayloadSize = this._maxPayloadSize - 8192;
|
|
142
|
+
// Restore subscriptions after reconnect (from suspend)
|
|
143
|
+
if (this._subscriptionMeta.size > 0) {
|
|
144
|
+
const metas = [...this._subscriptionMeta.values()];
|
|
145
|
+
this._subscriptionMeta.clear();
|
|
146
|
+
for (const meta of metas) {
|
|
147
|
+
await this.subscribe(meta.pattern, meta.handler, meta.options);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return this.nc;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Disconnect from NATS server
|
|
154
|
+
* Cleans up resources without rejecting pending operations
|
|
155
|
+
*/
|
|
156
|
+
async disconnect() {
|
|
157
|
+
this.closed = true;
|
|
158
|
+
// Cleanup pending requests
|
|
159
|
+
for (const [, pending] of this.pendingRequests) {
|
|
160
|
+
if (pending.timeout) {
|
|
161
|
+
clearTimeout(pending.timeout);
|
|
162
|
+
}
|
|
163
|
+
pending.reject(createError(ERROR_CODES.CONNECTION_CLOSED, 'Connection closed'));
|
|
164
|
+
}
|
|
165
|
+
this.pendingRequests.clear();
|
|
166
|
+
// Cleanup stream handlers
|
|
167
|
+
for (const [, handler] of this.streamHandlers) {
|
|
168
|
+
try {
|
|
169
|
+
handler.end();
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// Ignore errors during cleanup
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
this.streamHandlers.clear();
|
|
176
|
+
await Promise.allSettled(Array.from(this.pullIteratorCleanups.values()).map((cleanup) => cleanup()));
|
|
177
|
+
this.pullIteratorCleanups.clear();
|
|
178
|
+
// Cleanup callbacks
|
|
179
|
+
await Promise.allSettled(Array.from(this.callbackCleanups.values()).map((cleanup) => cleanup()));
|
|
180
|
+
this.callbackCleanups.clear();
|
|
181
|
+
// Unsubscribe all subscriptions
|
|
182
|
+
for (const subs of this.subscriptions.values()) {
|
|
183
|
+
for (const sub of subs) {
|
|
184
|
+
try {
|
|
185
|
+
sub.unsubscribe();
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// Ignore errors during cleanup
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
this.subscriptions.clear();
|
|
193
|
+
this._subscriptionMeta.clear();
|
|
194
|
+
// Clear chunking manager
|
|
195
|
+
this.chunkingManager = new ChunkingManager();
|
|
196
|
+
// Disconnect isolated clients
|
|
197
|
+
await Promise.allSettled(this.isolatedClients.map((client) => client.disconnect()));
|
|
198
|
+
// Drain and close connection
|
|
199
|
+
if (this.nc) {
|
|
200
|
+
try {
|
|
201
|
+
await this.nc.close();
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// Ignore errors
|
|
205
|
+
}
|
|
206
|
+
// Ensure the connection is truly closed. With waitOnFirstConnect: false the
|
|
207
|
+
// underlying WebSocket transport may still be mid-handshake when close() is
|
|
208
|
+
// called. nc.closed() resolves once the NATS protocol is fully shut down,
|
|
209
|
+
// preventing zombie connections that survive a disconnect/reconnect cycle.
|
|
210
|
+
try {
|
|
211
|
+
const timeout = this.options.disconnectTimeout ?? 2_000;
|
|
212
|
+
await Promise.race([this.nc.closed(), new Promise((r) => setTimeout(r, timeout))]);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// Ignore errors
|
|
216
|
+
}
|
|
217
|
+
this.nc = undefined;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Update connection options between suspend() and connect(). Used for token
|
|
222
|
+
* rotation / endpoint switching: subscription metadata is preserved by
|
|
223
|
+
* suspend(), reconfigure() points the next connect() at the new server, and
|
|
224
|
+
* connect() re-subscribes everything on the fresh transport.
|
|
225
|
+
*/
|
|
226
|
+
reconfigure(overrides) {
|
|
227
|
+
if (this.nc && !this.nc.isClosed()) {
|
|
228
|
+
throw new Error('Cannot reconfigure while connected. Call suspend() first.');
|
|
229
|
+
}
|
|
230
|
+
if (overrides.servers !== undefined) {
|
|
231
|
+
this.options.servers = overrides.servers;
|
|
232
|
+
}
|
|
233
|
+
if (overrides.auth !== undefined) {
|
|
234
|
+
this.options.auth = overrides.auth;
|
|
235
|
+
}
|
|
236
|
+
for (const child of this.isolatedClients) {
|
|
237
|
+
child.reconfigure(overrides);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Hot-swap the server pool without dropping the live connection.
|
|
242
|
+
* Use this when only the URL needs to change but the existing connection
|
|
243
|
+
* is still authenticated and serving traffic. The new pool kicks in on the next auto-reconnect.
|
|
244
|
+
*
|
|
245
|
+
* For a forced switch (e.g. endpoint host changed), use suspend() +
|
|
246
|
+
* reconfigure() + connect() instead.
|
|
247
|
+
*/
|
|
248
|
+
setServers(servers) {
|
|
249
|
+
this.options.servers = servers;
|
|
250
|
+
if (this.nc && !this.nc.isClosed()) {
|
|
251
|
+
this.nc.setServers(servers);
|
|
252
|
+
}
|
|
253
|
+
for (const child of this.isolatedClients) {
|
|
254
|
+
child.setServers(servers);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Force the current dial loop (if any) to immediately retry with the latest
|
|
259
|
+
* server pool — including from a stuck mid-handshake or sleeping-on-delay
|
|
260
|
+
* state. Falls back to standard `reconnect()` on transports that don't ship
|
|
261
|
+
* the fork's `forceReconnect()` (e.g. server-side TCP via npm `@nats-io/nats-core`).
|
|
262
|
+
*/
|
|
263
|
+
async forceReconnect() {
|
|
264
|
+
const nc = this.nc;
|
|
265
|
+
if (!nc || nc.isClosed())
|
|
266
|
+
return;
|
|
267
|
+
try {
|
|
268
|
+
if (typeof nc.forceReconnect === 'function') {
|
|
269
|
+
await nc.forceReconnect();
|
|
270
|
+
}
|
|
271
|
+
else if (typeof nc.reconnect === 'function') {
|
|
272
|
+
await nc.reconnect();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
// Either method may reject if the connection raced into closed —
|
|
277
|
+
// not actionable from here.
|
|
278
|
+
}
|
|
279
|
+
await Promise.allSettled(this.isolatedClients.map((c) => c.forceReconnect()));
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Synchronous force-tear-down without awaiting the transport's close
|
|
283
|
+
* handshake. Use during network changes when the WS is half-open and a
|
|
284
|
+
* normal `close()` would hang for 2s+ on the dead socket. Falls back to
|
|
285
|
+
* fire-and-forget `close()` on transports without the fork patch.
|
|
286
|
+
*/
|
|
287
|
+
abortClose(err) {
|
|
288
|
+
this.closed = true;
|
|
289
|
+
for (const [, pending] of this.pendingRequests) {
|
|
290
|
+
if (pending.timeout)
|
|
291
|
+
clearTimeout(pending.timeout);
|
|
292
|
+
pending.reject(createError(ERROR_CODES.CONNECTION_CLOSED, 'Connection closed'));
|
|
293
|
+
}
|
|
294
|
+
this.pendingRequests.clear();
|
|
295
|
+
for (const [, handler] of this.streamHandlers) {
|
|
296
|
+
try {
|
|
297
|
+
handler.end();
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
// ignore
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
this.streamHandlers.clear();
|
|
304
|
+
for (const cleanup of this.pullIteratorCleanups.values()) {
|
|
305
|
+
try {
|
|
306
|
+
cleanup();
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
// ignore
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
this.pullIteratorCleanups.clear();
|
|
313
|
+
for (const cleanup of this.callbackCleanups.values()) {
|
|
314
|
+
try {
|
|
315
|
+
cleanup();
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
// ignore
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
this.callbackCleanups.clear();
|
|
322
|
+
for (const child of this.isolatedClients) {
|
|
323
|
+
try {
|
|
324
|
+
child.abortClose(err);
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
// ignore
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const nc = this.nc;
|
|
331
|
+
if (nc) {
|
|
332
|
+
try {
|
|
333
|
+
if (typeof nc.abortClose === 'function') {
|
|
334
|
+
nc.abortClose(err);
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
// Server fallback: fire-and-forget. nc.close() returns a Promise we
|
|
338
|
+
// intentionally don't await — caller wants synchronous semantics.
|
|
339
|
+
nc.close().catch(() => { });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
// ignore
|
|
344
|
+
}
|
|
345
|
+
this.nc = undefined;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Suspend the connection without clearing subscription metadata.
|
|
350
|
+
* After calling suspend(), connect() will restore previous subscriptions.
|
|
351
|
+
*/
|
|
352
|
+
async suspend() {
|
|
353
|
+
// Cleanup pending requests
|
|
354
|
+
for (const [, pending] of this.pendingRequests) {
|
|
355
|
+
if (pending.timeout) {
|
|
356
|
+
clearTimeout(pending.timeout);
|
|
357
|
+
}
|
|
358
|
+
pending.reject(createError(ERROR_CODES.CONNECTION_CLOSED, 'Connection closed'));
|
|
359
|
+
}
|
|
360
|
+
this.pendingRequests.clear();
|
|
361
|
+
// Cleanup stream handlers
|
|
362
|
+
for (const [, handler] of this.streamHandlers) {
|
|
363
|
+
try {
|
|
364
|
+
handler.end();
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
// Ignore errors during cleanup
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
this.streamHandlers.clear();
|
|
371
|
+
await Promise.allSettled(Array.from(this.pullIteratorCleanups.values()).map((cleanup) => cleanup()));
|
|
372
|
+
this.pullIteratorCleanups.clear();
|
|
373
|
+
// Cleanup callbacks
|
|
374
|
+
await Promise.allSettled(Array.from(this.callbackCleanups.values()).map((cleanup) => cleanup()));
|
|
375
|
+
this.callbackCleanups.clear();
|
|
376
|
+
// Unsubscribe all subscriptions
|
|
377
|
+
for (const subs of this.subscriptions.values()) {
|
|
378
|
+
for (const sub of subs) {
|
|
379
|
+
try {
|
|
380
|
+
sub.unsubscribe();
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
// Ignore errors during cleanup
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
this.subscriptions.clear();
|
|
388
|
+
// Clear chunking manager
|
|
389
|
+
this.chunkingManager = new ChunkingManager();
|
|
390
|
+
// Suspend isolated clients
|
|
391
|
+
await Promise.allSettled(this.isolatedClients.map((client) => client.suspend()));
|
|
392
|
+
// Drain and close connection
|
|
393
|
+
if (this.nc) {
|
|
394
|
+
try {
|
|
395
|
+
await this.nc.close();
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
// Ignore errors
|
|
399
|
+
}
|
|
400
|
+
try {
|
|
401
|
+
const timeout = this.options.disconnectTimeout ?? 2_000;
|
|
402
|
+
await Promise.race([this.nc.closed(), new Promise((r) => setTimeout(r, timeout))]);
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
// Ignore errors
|
|
406
|
+
}
|
|
407
|
+
this.nc = undefined;
|
|
408
|
+
}
|
|
409
|
+
this.connectionPromise = undefined;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Public publish method
|
|
413
|
+
*/
|
|
414
|
+
async publish(subject, data, opts) {
|
|
415
|
+
if (!this.nc) {
|
|
416
|
+
throw new Error('Not connected');
|
|
417
|
+
}
|
|
418
|
+
const encoded = encode(data);
|
|
419
|
+
// Small enough to send directly
|
|
420
|
+
if (encoded.length <= this._maxPayloadSize) {
|
|
421
|
+
this.nc.publish(subject, encoded, opts);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
// Message is too large, chunk it
|
|
425
|
+
const transferId = generateId();
|
|
426
|
+
// Calculate chunk count without materializing the generator
|
|
427
|
+
const totalChunks = Math.ceil(encoded.length / this._maxPayloadSize);
|
|
428
|
+
// Send header message first (MessagePack encoded)
|
|
429
|
+
const headerMsg = {
|
|
430
|
+
type: 'chunked',
|
|
431
|
+
transferId,
|
|
432
|
+
totalChunks,
|
|
433
|
+
totalSize: encoded.length,
|
|
434
|
+
chunkSize: this._maxPayloadSize,
|
|
435
|
+
};
|
|
436
|
+
// Header message includes original headers if any
|
|
437
|
+
const hdrs = opts?.headers ?? headers();
|
|
438
|
+
hdrs.set('x-chunked-transfer', 'header');
|
|
439
|
+
hdrs.set('x-chunk-id', transferId);
|
|
440
|
+
this.nc.publish(subject, encode(headerMsg), { headers: hdrs, reply: opts?.reply });
|
|
441
|
+
// Send chunks directly from generator (no Array.from() - saves memory)
|
|
442
|
+
let chunkIndex = 0;
|
|
443
|
+
for (const chunk of createChunks(encoded, transferId, this._maxPayloadSize)) {
|
|
444
|
+
const chunkHdrs = headers();
|
|
445
|
+
chunkHdrs.set('x-chunked-transfer', 'chunk');
|
|
446
|
+
chunkHdrs.set('x-chunk-id', transferId);
|
|
447
|
+
chunkHdrs.set('x-chunk-index', chunkIndex.toString());
|
|
448
|
+
// Send raw chunk data (not encoded)
|
|
449
|
+
this.nc.publish(subject, chunk.data, { headers: chunkHdrs, reply: opts?.reply });
|
|
450
|
+
// Yield every 50 chunks to prevent blocking
|
|
451
|
+
if (chunkIndex > 0 && chunkIndex % 50 === 0) {
|
|
452
|
+
await sleep(0);
|
|
453
|
+
}
|
|
454
|
+
chunkIndex++;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Public subscribe method
|
|
459
|
+
*/
|
|
460
|
+
async subscribe(pattern, handler, options) {
|
|
461
|
+
if (!this.nc) {
|
|
462
|
+
throw new Error('Not connected');
|
|
463
|
+
}
|
|
464
|
+
// Serialize async handlers via a per-subscription promise chain. This
|
|
465
|
+
// matches Python's behavior (client.py:434 awaits the handler) and is
|
|
466
|
+
// what backpressure-sensitive callers (e.g. pull-callback iterators)
|
|
467
|
+
// rely on: an awaiting handler blocks the next message from being
|
|
468
|
+
// dispatched, which transitively stalls the producer.
|
|
469
|
+
//
|
|
470
|
+
// Sync handlers bypass the chain and stay fire-and-forget.
|
|
471
|
+
let handlerChain = Promise.resolve();
|
|
472
|
+
const runHandler = (data) => {
|
|
473
|
+
let result;
|
|
474
|
+
try {
|
|
475
|
+
result = handler(data);
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
console.error(`Error in handler for ${pattern}:`, error);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (isPromise(result)) {
|
|
482
|
+
const pending = result;
|
|
483
|
+
handlerChain = handlerChain.then(() => pending).catch((error) => console.error(`Error in handler for ${pattern}:`, error));
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
const processMessage = (err, msg) => {
|
|
487
|
+
if (err) {
|
|
488
|
+
console.error(`Subscription error for ${pattern}:`, err);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
try {
|
|
492
|
+
const chunkType = msg.headers?.get('x-chunked-transfer');
|
|
493
|
+
if (chunkType === 'header') {
|
|
494
|
+
// Chunked transfer header
|
|
495
|
+
const data = decode(msg.data);
|
|
496
|
+
const chunkId = msg.headers?.get('x-chunk-id');
|
|
497
|
+
if (!chunkId || data.transferId !== chunkId) {
|
|
498
|
+
console.error('Invalid chunk header');
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
// Setup chunk assembly with pre-allocated buffer (optimized)
|
|
502
|
+
this.chunkingManager.startReceiving(data.transferId, data.totalChunks, (assembledData) => {
|
|
503
|
+
runHandler(assembledData);
|
|
504
|
+
}, (error) => {
|
|
505
|
+
console.error(`Error assembling chunks for ${pattern}:`, error);
|
|
506
|
+
}, data.totalSize, // Pass totalSize for pre-allocated buffer optimization
|
|
507
|
+
data.chunkSize);
|
|
508
|
+
}
|
|
509
|
+
else if (chunkType === 'chunk') {
|
|
510
|
+
// Chunk data
|
|
511
|
+
const chunkId = msg.headers?.get('x-chunk-id');
|
|
512
|
+
const chunkIndex = parseInt(msg.headers?.get('x-chunk-index') ?? '0');
|
|
513
|
+
if (!chunkId) {
|
|
514
|
+
console.error('Chunk missing chunk ID');
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
// Process raw chunk data
|
|
518
|
+
this.chunkingManager.processChunk({
|
|
519
|
+
id: chunkId,
|
|
520
|
+
chunkIndex,
|
|
521
|
+
data: msg.data,
|
|
522
|
+
isLast: false, // Determined by total chunks from header
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
// Regular message - decode MessagePack data
|
|
527
|
+
const data = decode(msg.data);
|
|
528
|
+
runHandler(data);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
console.error(`Error processing message for ${pattern}:`, error);
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
const sub = this.nc.subscribe(pattern, {
|
|
536
|
+
...(options?.queue ? { queue: options.queue } : {}),
|
|
537
|
+
callback: processMessage,
|
|
538
|
+
});
|
|
539
|
+
this.subscriptions.set(pattern, [sub]);
|
|
540
|
+
this._subscriptionMeta.set(pattern, { pattern, handler, options });
|
|
541
|
+
const unsubscribe = () => {
|
|
542
|
+
try {
|
|
543
|
+
sub.unsubscribe();
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
// Ignore unsubscribe errors
|
|
547
|
+
}
|
|
548
|
+
finally {
|
|
549
|
+
this.subscriptions.delete(pattern);
|
|
550
|
+
this._subscriptionMeta.delete(pattern);
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
// Return unsubscribe function
|
|
554
|
+
return unsubscribe;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Native NATS request/reply
|
|
558
|
+
* @param subject - The subject to send the request to
|
|
559
|
+
* @param data - The request data
|
|
560
|
+
* @param options - Request options including timeout and per-call retry override
|
|
561
|
+
*/
|
|
562
|
+
async request(subject, data, options) {
|
|
563
|
+
return this.withNoResponderRetry(() => this._requestOnce(subject, data, options), options?.noResponderRetry);
|
|
564
|
+
}
|
|
565
|
+
async _requestOnce(subject, data, options) {
|
|
566
|
+
if (!this.nc) {
|
|
567
|
+
throw new Error('Not connected');
|
|
568
|
+
}
|
|
569
|
+
const timeout = options?.timeout ?? 5000;
|
|
570
|
+
const encoded = encode(data);
|
|
571
|
+
try {
|
|
572
|
+
// Use native NATS request
|
|
573
|
+
const msg = await this.nc.request(subject, encoded, {
|
|
574
|
+
timeout,
|
|
575
|
+
headers: options?.headers,
|
|
576
|
+
noMux: false, // Allow request multiplexing
|
|
577
|
+
});
|
|
578
|
+
// Check for NATS micro service error response
|
|
579
|
+
if (msg.headers?.get('Nats-Service-Error-Code')) {
|
|
580
|
+
const errorCode = msg.headers.get('Nats-Service-Error-Code') || '500';
|
|
581
|
+
const errorMsg = msg.headers.get('Nats-Service-Error') || 'Service error';
|
|
582
|
+
let errorData = null;
|
|
583
|
+
// Try to decode error data
|
|
584
|
+
if (msg.data) {
|
|
585
|
+
try {
|
|
586
|
+
errorData = decode(msg.data);
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
// Ignore decoding errors
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
throw createError(errorCode, errorMsg, errorData);
|
|
593
|
+
}
|
|
594
|
+
const decoded = decode(msg.data);
|
|
595
|
+
// Check if response contains an error field (for request handlers)
|
|
596
|
+
if (decoded?.error) {
|
|
597
|
+
const code = decoded.code ?? ERROR_CODES.INTERNAL_ERROR;
|
|
598
|
+
const message = decoded.error ?? 'Unknown error';
|
|
599
|
+
throw createError(code, message, decoded);
|
|
600
|
+
}
|
|
601
|
+
return decoded;
|
|
602
|
+
}
|
|
603
|
+
catch (error) {
|
|
604
|
+
if (error.code === '503' || error.message?.includes('no responders')) {
|
|
605
|
+
throw createError(ERROR_CODES.NOT_FOUND, 'No responders available');
|
|
606
|
+
}
|
|
607
|
+
if (error.code === 'TIMEOUT' || error.message?.includes('timeout')) {
|
|
608
|
+
throw createError(ERROR_CODES.TIMEOUT, `Request to "${subject}" timed out after ${timeout}ms`);
|
|
609
|
+
}
|
|
610
|
+
throw error;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Retry helper for 503 / no-responder errors. The per-call `override`
|
|
615
|
+
* lets a request that targets a known-flaky responder (e.g. a child
|
|
616
|
+
* process that may be restarting) extend the wait window without
|
|
617
|
+
* affecting the client-wide default.
|
|
618
|
+
*/
|
|
619
|
+
async withNoResponderRetry(fn, override) {
|
|
620
|
+
const maxRetries = override?.maxRetries ?? this.options.noResponderRetry?.maxRetries ?? 3;
|
|
621
|
+
const delays = override?.delays ?? this.options.noResponderRetry?.delays ?? [500, 1000, 2000];
|
|
622
|
+
for (let attempt = 0;; attempt++) {
|
|
623
|
+
try {
|
|
624
|
+
return await fn();
|
|
625
|
+
}
|
|
626
|
+
catch (err) {
|
|
627
|
+
const isNoResponder = err instanceof errors.NoRespondersError || (err instanceof RPCException && err.code === ERROR_CODES.NOT_FOUND);
|
|
628
|
+
if (!isNoResponder || attempt >= maxRetries || this.closed) {
|
|
629
|
+
throw err;
|
|
630
|
+
}
|
|
631
|
+
const delay = delays[Math.min(attempt, delays.length - 1)];
|
|
632
|
+
await sleep(delay);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Make an RPC call
|
|
638
|
+
*/
|
|
639
|
+
async call(subject, ...args) {
|
|
640
|
+
return this.withNoResponderRetry(() => this._callOnce(subject, ...args));
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Make an RPC call (single attempt)
|
|
644
|
+
*/
|
|
645
|
+
async _callOnce(subject, ...args) {
|
|
646
|
+
if (!this.isConnected && !this.isClosed) {
|
|
647
|
+
await this.connect();
|
|
648
|
+
}
|
|
649
|
+
if (!this.nc) {
|
|
650
|
+
throw new Error('Not connected');
|
|
651
|
+
}
|
|
652
|
+
const id = generateId();
|
|
653
|
+
const timeout = this.options.timeout ?? 30000;
|
|
654
|
+
// Use different reply patterns for RPC vs service calls
|
|
655
|
+
const replySubject = subject.startsWith('rpc.') ? `rpc.reply.${id}` : `${subject}.reply.${id}`;
|
|
656
|
+
return new Promise(async (resolve, reject) => {
|
|
657
|
+
// Initialize variables
|
|
658
|
+
let sub;
|
|
659
|
+
let unsubscribe;
|
|
660
|
+
// Setup timeout
|
|
661
|
+
const timeoutHandle = setTimeout(() => {
|
|
662
|
+
if (this.pendingRequests.has(id)) {
|
|
663
|
+
this.pendingRequests.delete(id);
|
|
664
|
+
reject(createError(ERROR_CODES.TIMEOUT, `RPC call to "${subject}" timed out after ${timeout}ms`));
|
|
665
|
+
}
|
|
666
|
+
}, timeout);
|
|
667
|
+
// Store pending request
|
|
668
|
+
this.pendingRequests.set(id, {
|
|
669
|
+
resolve,
|
|
670
|
+
reject,
|
|
671
|
+
timeout: timeoutHandle,
|
|
672
|
+
});
|
|
673
|
+
// Unsubscribe function to clean up
|
|
674
|
+
const unsubscribeAll = async () => {
|
|
675
|
+
if (this.pendingRequests.has(id)) {
|
|
676
|
+
const pending = this.pendingRequests.get(id);
|
|
677
|
+
if (pending?.timeout) {
|
|
678
|
+
clearTimeout(pending.timeout);
|
|
679
|
+
}
|
|
680
|
+
this.pendingRequests.delete(id);
|
|
681
|
+
}
|
|
682
|
+
if (sub && !sub.isClosed()) {
|
|
683
|
+
try {
|
|
684
|
+
sub.unsubscribe();
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
// Ignore unsubscribe errors
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
unsubscribe?.();
|
|
691
|
+
};
|
|
692
|
+
// Subscribe to reply
|
|
693
|
+
const handleRpcResponse = async (data) => {
|
|
694
|
+
const response = data;
|
|
695
|
+
if (response.id === id) {
|
|
696
|
+
const pending = this.pendingRequests.get(response.id);
|
|
697
|
+
if (pending) {
|
|
698
|
+
this.pendingRequests.delete(response.id);
|
|
699
|
+
if (pending.timeout)
|
|
700
|
+
clearTimeout(pending.timeout);
|
|
701
|
+
await unsubscribeAll();
|
|
702
|
+
if (response.error) {
|
|
703
|
+
pending.reject(RPCException.fromJSON(response.error));
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
const result = response.result;
|
|
707
|
+
// Attach __methods to result for proxy method discovery
|
|
708
|
+
// Skip binary data types (Uint8Array, ArrayBuffer)
|
|
709
|
+
const isBinaryData = result instanceof Uint8Array || result instanceof ArrayBuffer || (typeof Buffer !== 'undefined' && Buffer.isBuffer(result));
|
|
710
|
+
if (response.__methods && result !== null && typeof result === 'object' && !isBinaryData) {
|
|
711
|
+
result.__methods = response.__methods;
|
|
712
|
+
}
|
|
713
|
+
pending.resolve(result);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
const requestCallback = (err, msg) => {
|
|
719
|
+
// Check for no responders status (empty message with 503 status)
|
|
720
|
+
if (msg && msg.data?.length === 0 && msg.headers?.code === 503) {
|
|
721
|
+
reject(new errors.NoRespondersError(subject));
|
|
722
|
+
unsubscribeAll();
|
|
723
|
+
}
|
|
724
|
+
else if (err) {
|
|
725
|
+
reject(err);
|
|
726
|
+
unsubscribeAll();
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
try {
|
|
730
|
+
unsubscribe = await this.subscribe(replySubject, handleRpcResponse);
|
|
731
|
+
const inbox = createInbox();
|
|
732
|
+
sub = this.nc.subscribe(inbox, {
|
|
733
|
+
max: 1,
|
|
734
|
+
callback: requestCallback,
|
|
735
|
+
});
|
|
736
|
+
// Send request
|
|
737
|
+
const message = { id, method: 'call', params: args };
|
|
738
|
+
await this.publish(subject, message, { reply: inbox });
|
|
739
|
+
}
|
|
740
|
+
catch (error) {
|
|
741
|
+
await unsubscribeAll();
|
|
742
|
+
reject(error);
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Make a streaming RPC call
|
|
748
|
+
*/
|
|
749
|
+
async *callStream(subject, ...args) {
|
|
750
|
+
const maxRetries = this.options.noResponderRetry?.maxRetries ?? 3;
|
|
751
|
+
const delays = this.options.noResponderRetry?.delays ?? [500, 1000, 2000];
|
|
752
|
+
for (let attempt = 0;; attempt++) {
|
|
753
|
+
try {
|
|
754
|
+
yield* this._callStreamOnce(subject, ...args);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
catch (err) {
|
|
758
|
+
const isNoResponder = err instanceof errors.NoRespondersError || (err instanceof RPCException && err.code === ERROR_CODES.NOT_FOUND);
|
|
759
|
+
if (!isNoResponder || attempt >= maxRetries || this.closed) {
|
|
760
|
+
throw err;
|
|
761
|
+
}
|
|
762
|
+
await sleep(delays[Math.min(attempt, delays.length - 1)]);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Make a streaming RPC call (single attempt).
|
|
768
|
+
*
|
|
769
|
+
* Manual iterator — see callPullIteratorWithCallback for rationale.
|
|
770
|
+
* The push-based stream still parks the client at `await resolver`
|
|
771
|
+
* while waiting for the next stream message; if the server stops
|
|
772
|
+
* sending without an `end` frame, the generator would hang on
|
|
773
|
+
* iter.return(). Force-settling the pending resolver lets return()
|
|
774
|
+
* run cleanup cleanly.
|
|
775
|
+
*/
|
|
776
|
+
_callStreamOnce(subject, ...args) {
|
|
777
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
778
|
+
const client = this;
|
|
779
|
+
let started = false;
|
|
780
|
+
let returned = false;
|
|
781
|
+
let cleanedUp = false;
|
|
782
|
+
let ended = false;
|
|
783
|
+
let error = null;
|
|
784
|
+
let id = '';
|
|
785
|
+
let streamSubject = '';
|
|
786
|
+
let sub;
|
|
787
|
+
let unsubscribe;
|
|
788
|
+
const queue = [];
|
|
789
|
+
let resolver = null;
|
|
790
|
+
const settlePendingAsDone = () => {
|
|
791
|
+
const r = resolver;
|
|
792
|
+
if (r) {
|
|
793
|
+
resolver = null;
|
|
794
|
+
r({ value: undefined, done: true });
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
const handler = {
|
|
798
|
+
push: (value) => {
|
|
799
|
+
if (ended)
|
|
800
|
+
return;
|
|
801
|
+
if (resolver) {
|
|
802
|
+
const r = resolver;
|
|
803
|
+
resolver = null;
|
|
804
|
+
r({ value, done: false });
|
|
805
|
+
}
|
|
806
|
+
else {
|
|
807
|
+
queue.push(value);
|
|
808
|
+
}
|
|
809
|
+
},
|
|
810
|
+
end: () => {
|
|
811
|
+
ended = true;
|
|
812
|
+
settlePendingAsDone();
|
|
813
|
+
},
|
|
814
|
+
error: (err) => {
|
|
815
|
+
error = err;
|
|
816
|
+
ended = true;
|
|
817
|
+
settlePendingAsDone();
|
|
818
|
+
},
|
|
819
|
+
};
|
|
820
|
+
const cleanupOnce = async () => {
|
|
821
|
+
if (cleanedUp)
|
|
822
|
+
return;
|
|
823
|
+
cleanedUp = true;
|
|
824
|
+
client.streamHandlers.delete(id);
|
|
825
|
+
if (sub && !sub.isClosed()) {
|
|
826
|
+
try {
|
|
827
|
+
sub.unsubscribe();
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
// ignore
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
unsubscribe?.();
|
|
834
|
+
if (!ended) {
|
|
835
|
+
ended = true;
|
|
836
|
+
try {
|
|
837
|
+
await client.publish(`${streamSubject}.cancel`, { id });
|
|
838
|
+
}
|
|
839
|
+
catch {
|
|
840
|
+
// ignore
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
const handleStreamMessage = async (msg) => {
|
|
845
|
+
if (msg.id !== id)
|
|
846
|
+
return;
|
|
847
|
+
const h = client.streamHandlers.get(msg.id);
|
|
848
|
+
if (!h)
|
|
849
|
+
return;
|
|
850
|
+
switch (msg.type) {
|
|
851
|
+
case 'data':
|
|
852
|
+
h.push(msg.data);
|
|
853
|
+
break;
|
|
854
|
+
case 'end':
|
|
855
|
+
h.end();
|
|
856
|
+
await cleanupOnce();
|
|
857
|
+
break;
|
|
858
|
+
case 'error':
|
|
859
|
+
h.error(RPCException.fromJSON(msg.error));
|
|
860
|
+
await cleanupOnce();
|
|
861
|
+
break;
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
const requestCallback = (err, msg) => {
|
|
865
|
+
if (msg && msg.data?.length === 0 && msg.headers?.code === 503) {
|
|
866
|
+
const h = client.streamHandlers.get(id);
|
|
867
|
+
h?.error(new errors.NoRespondersError(subject));
|
|
868
|
+
cleanupOnce();
|
|
869
|
+
}
|
|
870
|
+
else if (err) {
|
|
871
|
+
const h = client.streamHandlers.get(id);
|
|
872
|
+
h?.error(err);
|
|
873
|
+
cleanupOnce();
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
const setup = async () => {
|
|
877
|
+
if (!client.isConnected && !client.isClosed) {
|
|
878
|
+
await client.connect();
|
|
879
|
+
}
|
|
880
|
+
if (!client.nc)
|
|
881
|
+
throw new Error('Not connected');
|
|
882
|
+
id = generateId();
|
|
883
|
+
streamSubject = `stream.${subject}.${id}`;
|
|
884
|
+
client.streamHandlers.set(id, handler);
|
|
885
|
+
unsubscribe = await client.subscribe(streamSubject, handleStreamMessage);
|
|
886
|
+
const inbox = createInbox();
|
|
887
|
+
sub = client.nc.subscribe(inbox, { max: 1, callback: requestCallback });
|
|
888
|
+
const streamParams = { __stream: true, __streamSubject: streamSubject, args };
|
|
889
|
+
const message = { id, method: 'stream', params: streamParams };
|
|
890
|
+
await client.publish(subject, message, { reply: inbox });
|
|
891
|
+
};
|
|
892
|
+
const iter = {
|
|
893
|
+
async next() {
|
|
894
|
+
if (returned)
|
|
895
|
+
return { value: undefined, done: true };
|
|
896
|
+
if (!started) {
|
|
897
|
+
started = true;
|
|
898
|
+
try {
|
|
899
|
+
await setup();
|
|
900
|
+
}
|
|
901
|
+
catch (err) {
|
|
902
|
+
await cleanupOnce();
|
|
903
|
+
throw err;
|
|
904
|
+
}
|
|
905
|
+
if (returned) {
|
|
906
|
+
await cleanupOnce();
|
|
907
|
+
return { value: undefined, done: true };
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
if (error) {
|
|
911
|
+
await cleanupOnce();
|
|
912
|
+
throw error;
|
|
913
|
+
}
|
|
914
|
+
if (queue.length > 0) {
|
|
915
|
+
return { value: queue.shift(), done: false };
|
|
916
|
+
}
|
|
917
|
+
if (ended) {
|
|
918
|
+
await cleanupOnce();
|
|
919
|
+
return { value: undefined, done: true };
|
|
920
|
+
}
|
|
921
|
+
const result = await new Promise((resolve, reject) => {
|
|
922
|
+
if (returned) {
|
|
923
|
+
resolve({ value: undefined, done: true });
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
if (ended) {
|
|
927
|
+
resolve({ value: undefined, done: true });
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
if (error) {
|
|
931
|
+
reject(error);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
resolver = resolve;
|
|
935
|
+
});
|
|
936
|
+
if (returned) {
|
|
937
|
+
await cleanupOnce();
|
|
938
|
+
return { value: undefined, done: true };
|
|
939
|
+
}
|
|
940
|
+
if (result.done) {
|
|
941
|
+
if (error) {
|
|
942
|
+
await cleanupOnce();
|
|
943
|
+
throw error;
|
|
944
|
+
}
|
|
945
|
+
await cleanupOnce();
|
|
946
|
+
return { value: undefined, done: true };
|
|
947
|
+
}
|
|
948
|
+
return { value: result.value, done: false };
|
|
949
|
+
},
|
|
950
|
+
async return(value) {
|
|
951
|
+
if (returned)
|
|
952
|
+
return { value: value, done: true };
|
|
953
|
+
returned = true;
|
|
954
|
+
settlePendingAsDone();
|
|
955
|
+
if (started)
|
|
956
|
+
await cleanupOnce();
|
|
957
|
+
return { value: value, done: true };
|
|
958
|
+
},
|
|
959
|
+
async throw(err) {
|
|
960
|
+
if (returned)
|
|
961
|
+
throw err;
|
|
962
|
+
returned = true;
|
|
963
|
+
settlePendingAsDone();
|
|
964
|
+
if (started)
|
|
965
|
+
await cleanupOnce();
|
|
966
|
+
throw err;
|
|
967
|
+
},
|
|
968
|
+
[Symbol.asyncIterator]() {
|
|
969
|
+
return iter;
|
|
970
|
+
},
|
|
971
|
+
async [Symbol.asyncDispose]() {
|
|
972
|
+
await iter.return();
|
|
973
|
+
},
|
|
974
|
+
};
|
|
975
|
+
return iter;
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Make a pull-based iterator RPC call
|
|
979
|
+
*/
|
|
980
|
+
callPullIterator(subject, ...args) {
|
|
981
|
+
// Manual iterator — see callPullIteratorWithCallback for rationale.
|
|
982
|
+
// `iter.return()` force-settles the pending response promise so
|
|
983
|
+
// cleanup can run even when the server is parked at yield.
|
|
984
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
985
|
+
const client = this;
|
|
986
|
+
let started = false;
|
|
987
|
+
let returned = false;
|
|
988
|
+
let cleanedUp = false;
|
|
989
|
+
let ended = false;
|
|
990
|
+
let error = null;
|
|
991
|
+
let iteratorId = '';
|
|
992
|
+
let requestSubject = '';
|
|
993
|
+
let responseSubject = '';
|
|
994
|
+
let sub;
|
|
995
|
+
let inbox = '';
|
|
996
|
+
let responseUnsub;
|
|
997
|
+
const responseQueue = [];
|
|
998
|
+
let responseResolver = null;
|
|
999
|
+
const settlePendingAsDone = () => {
|
|
1000
|
+
const r = responseResolver;
|
|
1001
|
+
if (r) {
|
|
1002
|
+
responseResolver = null;
|
|
1003
|
+
r({ id: iteratorId, type: 'done' });
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
const cleanupOnce = async () => {
|
|
1007
|
+
if (cleanedUp)
|
|
1008
|
+
return;
|
|
1009
|
+
cleanedUp = true;
|
|
1010
|
+
responseUnsub?.();
|
|
1011
|
+
if (sub && !sub.isClosed()) {
|
|
1012
|
+
try {
|
|
1013
|
+
sub.unsubscribe();
|
|
1014
|
+
}
|
|
1015
|
+
catch {
|
|
1016
|
+
// ignore
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
if (!ended) {
|
|
1020
|
+
ended = true;
|
|
1021
|
+
try {
|
|
1022
|
+
const cancelRequest = { id: iteratorId, type: 'cancel' };
|
|
1023
|
+
await client.publish(requestSubject, cancelRequest);
|
|
1024
|
+
}
|
|
1025
|
+
catch {
|
|
1026
|
+
// ignore cleanup errors
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
const setup = async () => {
|
|
1031
|
+
if (!client.isConnected && !client.isClosed) {
|
|
1032
|
+
await client.connect();
|
|
1033
|
+
}
|
|
1034
|
+
if (!client.nc)
|
|
1035
|
+
throw new Error('Not connected');
|
|
1036
|
+
iteratorId = generateId();
|
|
1037
|
+
requestSubject = `_rpc.iterator.${iteratorId}.request`;
|
|
1038
|
+
responseSubject = `_rpc.iterator.${iteratorId}.response`;
|
|
1039
|
+
const initResponse = await client.call(subject, { __pullIterator: true, __iteratorId: iteratorId, args });
|
|
1040
|
+
if (initResponse?.iteratorId !== iteratorId) {
|
|
1041
|
+
throw new Error('Failed to initialize pull iterator');
|
|
1042
|
+
}
|
|
1043
|
+
responseUnsub = await client.subscribe(responseSubject, (msg) => {
|
|
1044
|
+
if (msg.type === 'error') {
|
|
1045
|
+
error = RPCException.fromJSON(msg.error);
|
|
1046
|
+
ended = true;
|
|
1047
|
+
}
|
|
1048
|
+
else if (msg.type === 'done') {
|
|
1049
|
+
ended = true;
|
|
1050
|
+
}
|
|
1051
|
+
if (responseResolver) {
|
|
1052
|
+
const r = responseResolver;
|
|
1053
|
+
responseResolver = null;
|
|
1054
|
+
r(msg);
|
|
1055
|
+
}
|
|
1056
|
+
else {
|
|
1057
|
+
responseQueue.push(msg);
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
const requestCallback = (err, msg) => {
|
|
1061
|
+
let isError = false;
|
|
1062
|
+
if (msg && msg.data?.length === 0 && msg.headers?.code === 503) {
|
|
1063
|
+
const e = new errors.NoRespondersError(subject);
|
|
1064
|
+
isError = true;
|
|
1065
|
+
ended = true;
|
|
1066
|
+
error = createError('503', e.message);
|
|
1067
|
+
}
|
|
1068
|
+
else if (err) {
|
|
1069
|
+
isError = true;
|
|
1070
|
+
ended = true;
|
|
1071
|
+
error = createError(ERROR_CODES.INTERNAL_ERROR, err.message);
|
|
1072
|
+
}
|
|
1073
|
+
if (isError) {
|
|
1074
|
+
const response = {
|
|
1075
|
+
type: 'error',
|
|
1076
|
+
id: iteratorId,
|
|
1077
|
+
error: error ? error.toJSON() : undefined,
|
|
1078
|
+
};
|
|
1079
|
+
if (responseResolver) {
|
|
1080
|
+
const r = responseResolver;
|
|
1081
|
+
responseResolver = null;
|
|
1082
|
+
r(response);
|
|
1083
|
+
}
|
|
1084
|
+
else {
|
|
1085
|
+
responseQueue.push(response);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
inbox = createInbox();
|
|
1090
|
+
sub = client.nc.subscribe(inbox, { max: 1, callback: requestCallback });
|
|
1091
|
+
};
|
|
1092
|
+
const iter = {
|
|
1093
|
+
async next() {
|
|
1094
|
+
if (returned)
|
|
1095
|
+
return { value: undefined, done: true };
|
|
1096
|
+
if (!started) {
|
|
1097
|
+
started = true;
|
|
1098
|
+
try {
|
|
1099
|
+
await setup();
|
|
1100
|
+
}
|
|
1101
|
+
catch (err) {
|
|
1102
|
+
await cleanupOnce();
|
|
1103
|
+
throw err;
|
|
1104
|
+
}
|
|
1105
|
+
if (returned) {
|
|
1106
|
+
await cleanupOnce();
|
|
1107
|
+
return { value: undefined, done: true };
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
const nextRequest = { id: iteratorId, type: 'next' };
|
|
1111
|
+
try {
|
|
1112
|
+
await client.publish(requestSubject, nextRequest, { reply: inbox });
|
|
1113
|
+
}
|
|
1114
|
+
catch (err) {
|
|
1115
|
+
await cleanupOnce();
|
|
1116
|
+
throw err;
|
|
1117
|
+
}
|
|
1118
|
+
const response = await new Promise((resolve, reject) => {
|
|
1119
|
+
if (returned) {
|
|
1120
|
+
resolve({ id: iteratorId, type: 'done' });
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
if (responseQueue.length > 0) {
|
|
1124
|
+
resolve(responseQueue.shift());
|
|
1125
|
+
}
|
|
1126
|
+
else if (ended && error) {
|
|
1127
|
+
reject(error);
|
|
1128
|
+
}
|
|
1129
|
+
else if (ended) {
|
|
1130
|
+
resolve({ id: iteratorId, type: 'done' });
|
|
1131
|
+
}
|
|
1132
|
+
else {
|
|
1133
|
+
responseResolver = resolve;
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
1136
|
+
if (returned) {
|
|
1137
|
+
await cleanupOnce();
|
|
1138
|
+
return { value: undefined, done: true };
|
|
1139
|
+
}
|
|
1140
|
+
if (response.type === 'error') {
|
|
1141
|
+
await cleanupOnce();
|
|
1142
|
+
throw RPCException.fromJSON(response.error);
|
|
1143
|
+
}
|
|
1144
|
+
if (response.type === 'done') {
|
|
1145
|
+
await cleanupOnce();
|
|
1146
|
+
return { value: undefined, done: true };
|
|
1147
|
+
}
|
|
1148
|
+
return { value: response.value, done: false };
|
|
1149
|
+
},
|
|
1150
|
+
async return(value) {
|
|
1151
|
+
if (returned)
|
|
1152
|
+
return { value: value, done: true };
|
|
1153
|
+
returned = true;
|
|
1154
|
+
settlePendingAsDone();
|
|
1155
|
+
if (started)
|
|
1156
|
+
await cleanupOnce();
|
|
1157
|
+
return { value: value, done: true };
|
|
1158
|
+
},
|
|
1159
|
+
async throw(err) {
|
|
1160
|
+
if (returned)
|
|
1161
|
+
throw err;
|
|
1162
|
+
returned = true;
|
|
1163
|
+
settlePendingAsDone();
|
|
1164
|
+
if (started)
|
|
1165
|
+
await cleanupOnce();
|
|
1166
|
+
throw err;
|
|
1167
|
+
},
|
|
1168
|
+
[Symbol.asyncIterator]() {
|
|
1169
|
+
return iter;
|
|
1170
|
+
},
|
|
1171
|
+
async [Symbol.asyncDispose]() {
|
|
1172
|
+
await iter.return();
|
|
1173
|
+
},
|
|
1174
|
+
};
|
|
1175
|
+
return iter;
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Pull-iterator-with-callbacks call.
|
|
1179
|
+
*
|
|
1180
|
+
* Combines a client-driven pull iterator (1 RTT per batch) with a oneway
|
|
1181
|
+
* callback channel (fire-and-forget server → client) for low-latency
|
|
1182
|
+
* frame-level data delivery with coarse-grained backpressure.
|
|
1183
|
+
*
|
|
1184
|
+
* The returned async generator yields `undefined` for each batch boundary
|
|
1185
|
+
* the server produces. Meaningful data is dispatched through the provided
|
|
1186
|
+
* callback object.
|
|
1187
|
+
*/
|
|
1188
|
+
callPullIteratorWithCallback(subject, callbacks, onewayMethods, ...args) {
|
|
1189
|
+
// Manual iterator implementation (not `async function*`). Rationale:
|
|
1190
|
+
// an async generator parked at an `await` cannot be woken by
|
|
1191
|
+
// `iter.return()` — per spec, return() queues behind the pending
|
|
1192
|
+
// await. When the server is parked at yield and no new response
|
|
1193
|
+
// message is in flight, that await never settles and return() hangs
|
|
1194
|
+
// forever. Implementing the iterator protocol by hand lets us
|
|
1195
|
+
// force-settle the pending response resolver from within return()/
|
|
1196
|
+
// throw() and run cleanup synchronously.
|
|
1197
|
+
//
|
|
1198
|
+
// Setup is deferred to the first next() call to preserve the lazy
|
|
1199
|
+
// semantics of the previous `async function*` implementation —
|
|
1200
|
+
// factories that are never iterated shouldn't open NATS subs.
|
|
1201
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
1202
|
+
const client = this;
|
|
1203
|
+
let started = false;
|
|
1204
|
+
let returned = false;
|
|
1205
|
+
let cleanedUp = false;
|
|
1206
|
+
let ended = false;
|
|
1207
|
+
let error = null;
|
|
1208
|
+
let iteratorId = '';
|
|
1209
|
+
let requestSubject = '';
|
|
1210
|
+
let responseSubject = '';
|
|
1211
|
+
let callbackSubject = '';
|
|
1212
|
+
let sub;
|
|
1213
|
+
let inbox = '';
|
|
1214
|
+
let callbackUnsub;
|
|
1215
|
+
let responseUnsub;
|
|
1216
|
+
const responseQueue = [];
|
|
1217
|
+
let responseResolver = null;
|
|
1218
|
+
let callbackChain = Promise.resolve();
|
|
1219
|
+
const settlePendingAsDone = () => {
|
|
1220
|
+
const r = responseResolver;
|
|
1221
|
+
if (r) {
|
|
1222
|
+
responseResolver = null;
|
|
1223
|
+
r({ id: iteratorId, type: 'done' });
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
const cleanupOnce = async () => {
|
|
1227
|
+
if (cleanedUp)
|
|
1228
|
+
return;
|
|
1229
|
+
cleanedUp = true;
|
|
1230
|
+
callbackUnsub?.();
|
|
1231
|
+
responseUnsub?.();
|
|
1232
|
+
if (sub && !sub.isClosed()) {
|
|
1233
|
+
try {
|
|
1234
|
+
sub.unsubscribe();
|
|
1235
|
+
}
|
|
1236
|
+
catch {
|
|
1237
|
+
// ignore
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
if (!ended) {
|
|
1241
|
+
ended = true;
|
|
1242
|
+
try {
|
|
1243
|
+
const cancelRequest = { id: iteratorId, type: 'cancel' };
|
|
1244
|
+
await client.publish(requestSubject, cancelRequest);
|
|
1245
|
+
}
|
|
1246
|
+
catch {
|
|
1247
|
+
// ignore cleanup errors
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
const setup = async () => {
|
|
1252
|
+
if (!client.isConnected && !client.isClosed) {
|
|
1253
|
+
await client.connect();
|
|
1254
|
+
}
|
|
1255
|
+
if (!client.nc)
|
|
1256
|
+
throw new Error('Not connected');
|
|
1257
|
+
iteratorId = generateId();
|
|
1258
|
+
requestSubject = `_rpc.iterator.${iteratorId}.request`;
|
|
1259
|
+
responseSubject = `_rpc.iterator.${iteratorId}.response`;
|
|
1260
|
+
callbackSubject = `_rpc.cb.${iteratorId}`;
|
|
1261
|
+
const callbackMethods = Object.keys(callbacks).filter((k) => typeof callbacks[k] === 'function');
|
|
1262
|
+
callbackUnsub = await client.subscribe(callbackSubject, (msg) => {
|
|
1263
|
+
const fn = callbacks[msg.method];
|
|
1264
|
+
if (!fn) {
|
|
1265
|
+
console.error(`[rpc] Pull-callback: unknown method '${msg.method}'`);
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
callbackChain = callbackChain.then(async () => {
|
|
1269
|
+
try {
|
|
1270
|
+
await fn(...(msg.args ?? []));
|
|
1271
|
+
}
|
|
1272
|
+
catch (err) {
|
|
1273
|
+
console.error(`[rpc] Pull-callback handler '${msg.method}' threw:`, err);
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
});
|
|
1277
|
+
const initParams = {
|
|
1278
|
+
__pullCallback: true,
|
|
1279
|
+
__iteratorId: iteratorId,
|
|
1280
|
+
__callbackSubject: callbackSubject,
|
|
1281
|
+
__callbackMethods: callbackMethods,
|
|
1282
|
+
__onewayMethods: onewayMethods,
|
|
1283
|
+
args,
|
|
1284
|
+
};
|
|
1285
|
+
let initResponse;
|
|
1286
|
+
try {
|
|
1287
|
+
initResponse = await client.call(subject, initParams);
|
|
1288
|
+
}
|
|
1289
|
+
catch (err) {
|
|
1290
|
+
callbackUnsub?.();
|
|
1291
|
+
callbackUnsub = undefined;
|
|
1292
|
+
throw err;
|
|
1293
|
+
}
|
|
1294
|
+
if (initResponse?.iteratorId !== iteratorId) {
|
|
1295
|
+
callbackUnsub?.();
|
|
1296
|
+
callbackUnsub = undefined;
|
|
1297
|
+
throw new Error('Failed to initialize pull-callback iterator');
|
|
1298
|
+
}
|
|
1299
|
+
responseUnsub = await client.subscribe(responseSubject, (msg) => {
|
|
1300
|
+
if (msg.type === 'error') {
|
|
1301
|
+
error = RPCException.fromJSON(msg.error);
|
|
1302
|
+
ended = true;
|
|
1303
|
+
}
|
|
1304
|
+
else if (msg.type === 'done') {
|
|
1305
|
+
ended = true;
|
|
1306
|
+
}
|
|
1307
|
+
if (responseResolver) {
|
|
1308
|
+
const r = responseResolver;
|
|
1309
|
+
responseResolver = null;
|
|
1310
|
+
r(msg);
|
|
1311
|
+
}
|
|
1312
|
+
else {
|
|
1313
|
+
responseQueue.push(msg);
|
|
1314
|
+
}
|
|
1315
|
+
});
|
|
1316
|
+
const requestCallback = (err, msg) => {
|
|
1317
|
+
let isError = false;
|
|
1318
|
+
if (msg && msg.data?.length === 0 && msg.headers?.code === 503) {
|
|
1319
|
+
const e = new errors.NoRespondersError(subject);
|
|
1320
|
+
isError = true;
|
|
1321
|
+
ended = true;
|
|
1322
|
+
error = createError('503', e.message);
|
|
1323
|
+
}
|
|
1324
|
+
else if (err) {
|
|
1325
|
+
isError = true;
|
|
1326
|
+
ended = true;
|
|
1327
|
+
error = createError(ERROR_CODES.INTERNAL_ERROR, err.message);
|
|
1328
|
+
}
|
|
1329
|
+
if (isError) {
|
|
1330
|
+
const response = {
|
|
1331
|
+
type: 'error',
|
|
1332
|
+
id: iteratorId,
|
|
1333
|
+
error: error ? error.toJSON() : undefined,
|
|
1334
|
+
};
|
|
1335
|
+
if (responseResolver) {
|
|
1336
|
+
const r = responseResolver;
|
|
1337
|
+
responseResolver = null;
|
|
1338
|
+
r(response);
|
|
1339
|
+
}
|
|
1340
|
+
else {
|
|
1341
|
+
responseQueue.push(response);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
inbox = createInbox();
|
|
1346
|
+
sub = client.nc.subscribe(inbox, { max: 1, callback: requestCallback });
|
|
1347
|
+
};
|
|
1348
|
+
const iter = {
|
|
1349
|
+
async next() {
|
|
1350
|
+
if (returned)
|
|
1351
|
+
return { value: undefined, done: true };
|
|
1352
|
+
if (!started) {
|
|
1353
|
+
started = true;
|
|
1354
|
+
try {
|
|
1355
|
+
await setup();
|
|
1356
|
+
}
|
|
1357
|
+
catch (err) {
|
|
1358
|
+
await cleanupOnce();
|
|
1359
|
+
throw err;
|
|
1360
|
+
}
|
|
1361
|
+
if (returned) {
|
|
1362
|
+
await cleanupOnce();
|
|
1363
|
+
return { value: undefined, done: true };
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
const nextRequest = { id: iteratorId, type: 'next' };
|
|
1367
|
+
try {
|
|
1368
|
+
await client.publish(requestSubject, nextRequest, { reply: inbox });
|
|
1369
|
+
}
|
|
1370
|
+
catch (err) {
|
|
1371
|
+
await cleanupOnce();
|
|
1372
|
+
throw err;
|
|
1373
|
+
}
|
|
1374
|
+
const response = await new Promise((resolve, reject) => {
|
|
1375
|
+
if (returned) {
|
|
1376
|
+
resolve({ id: iteratorId, type: 'done' });
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
if (responseQueue.length > 0) {
|
|
1380
|
+
resolve(responseQueue.shift());
|
|
1381
|
+
}
|
|
1382
|
+
else if (ended && error) {
|
|
1383
|
+
reject(error);
|
|
1384
|
+
}
|
|
1385
|
+
else if (ended) {
|
|
1386
|
+
resolve({ id: iteratorId, type: 'done' });
|
|
1387
|
+
}
|
|
1388
|
+
else {
|
|
1389
|
+
responseResolver = resolve;
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1392
|
+
if (returned) {
|
|
1393
|
+
await cleanupOnce();
|
|
1394
|
+
return { value: undefined, done: true };
|
|
1395
|
+
}
|
|
1396
|
+
if (response.type === 'error') {
|
|
1397
|
+
await cleanupOnce();
|
|
1398
|
+
throw RPCException.fromJSON(response.error);
|
|
1399
|
+
}
|
|
1400
|
+
if (response.type === 'done') {
|
|
1401
|
+
await cleanupOnce();
|
|
1402
|
+
return { value: undefined, done: true };
|
|
1403
|
+
}
|
|
1404
|
+
// 'value' — wait for all callback handlers queued for the batch
|
|
1405
|
+
// to finish. A slow handler stalls here → stalls next()
|
|
1406
|
+
// request → server parks at its own yield. End-to-end backpressure.
|
|
1407
|
+
await callbackChain;
|
|
1408
|
+
return { value: undefined, done: false };
|
|
1409
|
+
},
|
|
1410
|
+
async return(value) {
|
|
1411
|
+
if (returned)
|
|
1412
|
+
return { value: value, done: true };
|
|
1413
|
+
returned = true;
|
|
1414
|
+
settlePendingAsDone();
|
|
1415
|
+
if (started)
|
|
1416
|
+
await cleanupOnce();
|
|
1417
|
+
return { value: value, done: true };
|
|
1418
|
+
},
|
|
1419
|
+
async throw(err) {
|
|
1420
|
+
if (returned)
|
|
1421
|
+
throw err;
|
|
1422
|
+
returned = true;
|
|
1423
|
+
settlePendingAsDone();
|
|
1424
|
+
if (started)
|
|
1425
|
+
await cleanupOnce();
|
|
1426
|
+
throw err;
|
|
1427
|
+
},
|
|
1428
|
+
[Symbol.asyncIterator]() {
|
|
1429
|
+
return iter;
|
|
1430
|
+
},
|
|
1431
|
+
async [Symbol.asyncDispose]() {
|
|
1432
|
+
await iter.return();
|
|
1433
|
+
},
|
|
1434
|
+
};
|
|
1435
|
+
return iter;
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Make an RPC call with a callback subscription.
|
|
1439
|
+
* Returns an unsubscribe function.
|
|
1440
|
+
*/
|
|
1441
|
+
async callWithCallback(subject, args, callback) {
|
|
1442
|
+
if (!this.isConnected && !this.isClosed) {
|
|
1443
|
+
await this.connect();
|
|
1444
|
+
}
|
|
1445
|
+
if (!this.nc) {
|
|
1446
|
+
throw new Error('Not connected');
|
|
1447
|
+
}
|
|
1448
|
+
const id = generateId();
|
|
1449
|
+
const callbackSubject = `rpc.cb.${id}`;
|
|
1450
|
+
// Subscribe to callback messages
|
|
1451
|
+
const unsub = await this.subscribe(callbackSubject, async (msg) => {
|
|
1452
|
+
if (msg.type === 'data') {
|
|
1453
|
+
try {
|
|
1454
|
+
await callback(msg.data);
|
|
1455
|
+
}
|
|
1456
|
+
catch (err) {
|
|
1457
|
+
console.error('[rpc] Callback error:', err);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
else if (msg.type === 'error') {
|
|
1461
|
+
console.error('[rpc] Callback error:', msg.error);
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
// Send RPC request with callback marker
|
|
1465
|
+
const callbackParams = {
|
|
1466
|
+
__callback: true,
|
|
1467
|
+
__callbackSubject: callbackSubject,
|
|
1468
|
+
args,
|
|
1469
|
+
};
|
|
1470
|
+
try {
|
|
1471
|
+
await this.call(subject, callbackParams);
|
|
1472
|
+
}
|
|
1473
|
+
catch (err) {
|
|
1474
|
+
unsub();
|
|
1475
|
+
throw err;
|
|
1476
|
+
}
|
|
1477
|
+
// Return unsubscribe function
|
|
1478
|
+
const unsubscribe = () => {
|
|
1479
|
+
this.publish(`${callbackSubject}.cancel`, { id }).catch(() => { });
|
|
1480
|
+
unsub();
|
|
1481
|
+
};
|
|
1482
|
+
return unsubscribe;
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Register RPC handlers
|
|
1486
|
+
*/
|
|
1487
|
+
async registerHandler(namespace, handlers, options) {
|
|
1488
|
+
if (!this.nc && !options?.isolatedConnection) {
|
|
1489
|
+
throw new Error('Not connected');
|
|
1490
|
+
}
|
|
1491
|
+
// Use isolated connection if requested
|
|
1492
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
1493
|
+
let client = this;
|
|
1494
|
+
if (options?.isolatedConnection) {
|
|
1495
|
+
// Create isolated connection for this handler namespace
|
|
1496
|
+
client = this.createIsolatedClient({
|
|
1497
|
+
...this.options,
|
|
1498
|
+
name: `${this.options.name}-handler-${namespace}`,
|
|
1499
|
+
});
|
|
1500
|
+
// Connect the isolated client
|
|
1501
|
+
await client.connect();
|
|
1502
|
+
this.isolatedClients.push(client);
|
|
1503
|
+
}
|
|
1504
|
+
const unsubscribers = [];
|
|
1505
|
+
const pullIteratorIds = [];
|
|
1506
|
+
// Extract methods based on option
|
|
1507
|
+
const handlersMap = options?.withoutDecorators ? extractNestedMethodsWithoutDecorators(handlers) : extractNestedMethodsWithDecorators(handlers);
|
|
1508
|
+
const methodNames = Object.keys(handlersMap);
|
|
1509
|
+
for (const [method, handler] of Object.entries(handlersMap)) {
|
|
1510
|
+
const subject = `rpc.${namespace}.${method}`;
|
|
1511
|
+
const unsubscribe = await client.subscribe(subject, async (msg) => {
|
|
1512
|
+
const response = { id: msg.id, __methods: methodNames };
|
|
1513
|
+
try {
|
|
1514
|
+
// Handle stream request
|
|
1515
|
+
if (msg.params?.__stream && msg.params?.__streamSubject) {
|
|
1516
|
+
const streamSubject = msg.params.__streamSubject;
|
|
1517
|
+
const args = msg.params.args ?? [];
|
|
1518
|
+
// Don't await stream requests - they run in background and send data via streamSubject
|
|
1519
|
+
// Awaiting would block the subscription handler and prevent processing of new messages
|
|
1520
|
+
handleStreamRequest(handler, args, streamSubject, msg.id, client).catch((err) => {
|
|
1521
|
+
console.error(`Stream request error for ${method}:`, err);
|
|
1522
|
+
});
|
|
1523
|
+
return; // Don't send RPC response for stream requests
|
|
1524
|
+
}
|
|
1525
|
+
else if (
|
|
1526
|
+
// Check if it's a pull iterator request
|
|
1527
|
+
// Could be direct object or wrapped in array from call()
|
|
1528
|
+
msg.params?.__pullIterator ||
|
|
1529
|
+
(Array.isArray(msg.params) && msg.params[0]?.__pullIterator)) {
|
|
1530
|
+
// Extract pull iterator params
|
|
1531
|
+
const pullParams = msg.params?.__pullIterator ? msg.params : msg.params[0];
|
|
1532
|
+
const args = pullParams.args ?? [];
|
|
1533
|
+
const iteratorId = pullParams.__iteratorId ?? msg.id;
|
|
1534
|
+
const cleanup = await handlePullIteratorRequest(handler, args, iteratorId, client);
|
|
1535
|
+
// Store cleanup function for later
|
|
1536
|
+
client.pullIteratorCleanups.set(iteratorId, cleanup);
|
|
1537
|
+
pullIteratorIds.push(iteratorId);
|
|
1538
|
+
response.result = { iteratorId };
|
|
1539
|
+
// Send response with iterator ID
|
|
1540
|
+
const replySubject = `rpc.reply.${msg.id}`;
|
|
1541
|
+
await client.publish(replySubject, response);
|
|
1542
|
+
}
|
|
1543
|
+
else if (
|
|
1544
|
+
// Check if it's a pull-iterator-with-callbacks request
|
|
1545
|
+
msg.params?.__pullCallback ||
|
|
1546
|
+
(Array.isArray(msg.params) && msg.params[0]?.__pullCallback)) {
|
|
1547
|
+
const pcParams = msg.params?.__pullCallback ? msg.params : msg.params[0];
|
|
1548
|
+
const args = pcParams.args ?? [];
|
|
1549
|
+
const iteratorId = pcParams.__iteratorId ?? msg.id;
|
|
1550
|
+
const callbackSubject = pcParams.__callbackSubject;
|
|
1551
|
+
const callbackMethods = pcParams.__callbackMethods ?? [];
|
|
1552
|
+
const onewayMethods = pcParams.__onewayMethods ?? [];
|
|
1553
|
+
const cleanup = await handlePullCallbackRequest(handler, args, iteratorId, callbackSubject, callbackMethods, onewayMethods, client);
|
|
1554
|
+
client.pullIteratorCleanups.set(iteratorId, cleanup);
|
|
1555
|
+
pullIteratorIds.push(iteratorId);
|
|
1556
|
+
response.result = { iteratorId };
|
|
1557
|
+
const replySubject = `rpc.reply.${msg.id}`;
|
|
1558
|
+
await client.publish(replySubject, response);
|
|
1559
|
+
}
|
|
1560
|
+
else if (
|
|
1561
|
+
// Check if it's a callback subscription request
|
|
1562
|
+
(msg.params && typeof msg.params === 'object' && !Array.isArray(msg.params) && msg.params.__callback && msg.params.__callbackSubject) ||
|
|
1563
|
+
(Array.isArray(msg.params) && msg.params.length > 0 && typeof msg.params[0] === 'object' && msg.params[0]?.__callback)) {
|
|
1564
|
+
// Handle callback subscription request
|
|
1565
|
+
const cbParams = Array.isArray(msg.params) && msg.params[0]?.__callback ? msg.params[0] : msg.params;
|
|
1566
|
+
const callbackSubject = cbParams.__callbackSubject;
|
|
1567
|
+
const cbArgs = cbParams.args ?? [];
|
|
1568
|
+
const cleanup = await handleCallbackRequest(handler, cbArgs, callbackSubject, msg.id, client);
|
|
1569
|
+
client.callbackCleanups.set(msg.id, cleanup);
|
|
1570
|
+
response.result = { ok: true };
|
|
1571
|
+
const replySubject = `rpc.reply.${msg.id}`;
|
|
1572
|
+
await client.publish(replySubject, response);
|
|
1573
|
+
}
|
|
1574
|
+
else {
|
|
1575
|
+
// Normal RPC call
|
|
1576
|
+
const result = await handleNormalRPC(handler, msg.params);
|
|
1577
|
+
response.result = result;
|
|
1578
|
+
// Send response
|
|
1579
|
+
const replySubject = `rpc.reply.${msg.id}`;
|
|
1580
|
+
await client.publish(replySubject, response);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
catch (error) {
|
|
1584
|
+
response.error = formatErrorObject(error);
|
|
1585
|
+
try {
|
|
1586
|
+
const replySubject = `rpc.reply.${msg.id}`;
|
|
1587
|
+
await client.publish(replySubject, response);
|
|
1588
|
+
}
|
|
1589
|
+
catch (publishError) {
|
|
1590
|
+
if (client.isClosed) {
|
|
1591
|
+
return; // Ignore publish errors if client is closed
|
|
1592
|
+
}
|
|
1593
|
+
console.error('Failed to send error response:', publishError);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
}, options?.queue ? { queue: options.queue } : undefined);
|
|
1597
|
+
unsubscribers.push(unsubscribe);
|
|
1598
|
+
}
|
|
1599
|
+
const cleanup = async () => {
|
|
1600
|
+
// Unsubscribe all handlers
|
|
1601
|
+
for (const unsub of unsubscribers) {
|
|
1602
|
+
unsub();
|
|
1603
|
+
}
|
|
1604
|
+
// Cleanup pull iterators
|
|
1605
|
+
await Promise.allSettled(Array.from(client.pullIteratorCleanups.entries()).map(([id, cleanup]) => {
|
|
1606
|
+
client.pullIteratorCleanups.delete(id);
|
|
1607
|
+
return cleanup();
|
|
1608
|
+
}));
|
|
1609
|
+
// Cleanup callbacks
|
|
1610
|
+
await Promise.allSettled(Array.from(client.callbackCleanups.entries()).map(([id, cleanup]) => {
|
|
1611
|
+
client.callbackCleanups.delete(id);
|
|
1612
|
+
return cleanup();
|
|
1613
|
+
}));
|
|
1614
|
+
// Disconnect isolated connection if used
|
|
1615
|
+
if (options?.isolatedConnection) {
|
|
1616
|
+
await client.disconnect();
|
|
1617
|
+
const index = this.isolatedClients.indexOf(client);
|
|
1618
|
+
if (index >= 0) {
|
|
1619
|
+
this.isolatedClients.splice(index, 1);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
};
|
|
1623
|
+
return cleanup;
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Setup a request handler (responder)
|
|
1627
|
+
* @param pattern - The subject pattern to listen for requests
|
|
1628
|
+
* @param handler - The handler function that receives data and subject
|
|
1629
|
+
*/
|
|
1630
|
+
async onRequest(pattern, handler) {
|
|
1631
|
+
if (!this.nc) {
|
|
1632
|
+
throw new Error('Not connected');
|
|
1633
|
+
}
|
|
1634
|
+
const sub = this.nc.subscribe(pattern, {
|
|
1635
|
+
callback: (_err, msg) => {
|
|
1636
|
+
(async () => {
|
|
1637
|
+
try {
|
|
1638
|
+
// Decode request
|
|
1639
|
+
const data = decode(msg.data);
|
|
1640
|
+
// Call handler with subject
|
|
1641
|
+
const result = await handler(data);
|
|
1642
|
+
// Send response
|
|
1643
|
+
if (msg.reply) {
|
|
1644
|
+
const response = encode(result);
|
|
1645
|
+
msg.respond(response);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
catch (error) {
|
|
1649
|
+
// Send error response
|
|
1650
|
+
if (msg.reply) {
|
|
1651
|
+
const errorResponse = encode({
|
|
1652
|
+
error: error instanceof Error ? error.message : 'Internal error',
|
|
1653
|
+
code: error instanceof RPCException ? error.code : ERROR_CODES.INTERNAL_ERROR,
|
|
1654
|
+
});
|
|
1655
|
+
msg.respond(errorResponse);
|
|
1656
|
+
}
|
|
1657
|
+
console.error(`Error in request handler for ${pattern}:`, error);
|
|
1658
|
+
}
|
|
1659
|
+
})();
|
|
1660
|
+
},
|
|
1661
|
+
});
|
|
1662
|
+
// Return unsubscribe function
|
|
1663
|
+
return () => {
|
|
1664
|
+
sub.unsubscribe();
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Create or join a bidirectional channel
|
|
1669
|
+
* @param channelId - Unique channel identifier
|
|
1670
|
+
* @param options - Optional configuration
|
|
1671
|
+
*/
|
|
1672
|
+
async channel(channelId, options) {
|
|
1673
|
+
let client;
|
|
1674
|
+
if (options?.isolatedConnection) {
|
|
1675
|
+
// Create a new isolated client for this channel
|
|
1676
|
+
client = this.createIsolatedClient({
|
|
1677
|
+
...this.options,
|
|
1678
|
+
name: `${this.options.name}-channel-${channelId}`,
|
|
1679
|
+
});
|
|
1680
|
+
this.isolatedClients.push(client);
|
|
1681
|
+
await client.connect();
|
|
1682
|
+
}
|
|
1683
|
+
else {
|
|
1684
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
1685
|
+
client = this;
|
|
1686
|
+
}
|
|
1687
|
+
const channel = new Channel(client, channelId);
|
|
1688
|
+
await channel.init();
|
|
1689
|
+
// Store reference for cleanup if isolated
|
|
1690
|
+
if (options?.isolatedConnection) {
|
|
1691
|
+
channel._isolatedClient = client;
|
|
1692
|
+
}
|
|
1693
|
+
return channel;
|
|
1694
|
+
}
|
|
1695
|
+
/**
|
|
1696
|
+
* Create a private 1:1 channel
|
|
1697
|
+
* @param channelId - Unique channel identifier
|
|
1698
|
+
* @param targetClientId - Target client to connect to
|
|
1699
|
+
* @param options - Optional configuration
|
|
1700
|
+
*/
|
|
1701
|
+
async privateChannel(channelId, targetClientId, options) {
|
|
1702
|
+
let client;
|
|
1703
|
+
if (options?.isolatedConnection) {
|
|
1704
|
+
// Create a new isolated client for this channel
|
|
1705
|
+
client = this.createIsolatedClient({
|
|
1706
|
+
...this.options,
|
|
1707
|
+
name: `${this.options.name}-private-${channelId}`,
|
|
1708
|
+
});
|
|
1709
|
+
this.isolatedClients.push(client);
|
|
1710
|
+
await client.connect();
|
|
1711
|
+
}
|
|
1712
|
+
else {
|
|
1713
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
1714
|
+
client = this;
|
|
1715
|
+
}
|
|
1716
|
+
const channel = new PrivateChannel(client, channelId, targetClientId);
|
|
1717
|
+
await channel.init();
|
|
1718
|
+
// Store reference for cleanup if isolated
|
|
1719
|
+
if (options?.isolatedConnection) {
|
|
1720
|
+
channel._isolatedClient = client;
|
|
1721
|
+
}
|
|
1722
|
+
return channel;
|
|
1723
|
+
}
|
|
1724
|
+
// prettier-ignore
|
|
1725
|
+
createProxy(namespace, options) {
|
|
1726
|
+
let client;
|
|
1727
|
+
if (options?.isolatedConnection) {
|
|
1728
|
+
// Create an isolated proxy with its own connection
|
|
1729
|
+
client = this.createIsolatedClient({
|
|
1730
|
+
...this.options,
|
|
1731
|
+
name: `${this.options.name}-proxy-${namespace}`,
|
|
1732
|
+
});
|
|
1733
|
+
this.isolatedClients.push(client);
|
|
1734
|
+
}
|
|
1735
|
+
else {
|
|
1736
|
+
// Use the current client instance
|
|
1737
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
1738
|
+
client = this;
|
|
1739
|
+
}
|
|
1740
|
+
const proxy = createProxy(client, namespace);
|
|
1741
|
+
if (options?.isolatedConnection) {
|
|
1742
|
+
// Store reference for potential cleanup
|
|
1743
|
+
proxy._isolatedClient = client;
|
|
1744
|
+
return {
|
|
1745
|
+
proxy,
|
|
1746
|
+
close: async () => {
|
|
1747
|
+
await client.disconnect();
|
|
1748
|
+
const index = this.isolatedClients.indexOf(client);
|
|
1749
|
+
if (index >= 0)
|
|
1750
|
+
this.isolatedClients.splice(index, 1);
|
|
1751
|
+
},
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
else {
|
|
1755
|
+
return proxy;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
async createServiceProxy(serviceName, options) {
|
|
1759
|
+
let client;
|
|
1760
|
+
if (options?.isolatedConnection) {
|
|
1761
|
+
// Create a new isolated client for this service proxy
|
|
1762
|
+
client = this.createIsolatedClient({
|
|
1763
|
+
...this.options,
|
|
1764
|
+
name: `${this.options.name}-service-${serviceName}`,
|
|
1765
|
+
});
|
|
1766
|
+
this.isolatedClients.push(client);
|
|
1767
|
+
await client.connect();
|
|
1768
|
+
}
|
|
1769
|
+
else {
|
|
1770
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
1771
|
+
client = this;
|
|
1772
|
+
}
|
|
1773
|
+
// Discover available services
|
|
1774
|
+
const monitor = client.service.monitor();
|
|
1775
|
+
const services = [];
|
|
1776
|
+
for await (const info of await monitor.info(serviceName)) {
|
|
1777
|
+
services.push(info);
|
|
1778
|
+
}
|
|
1779
|
+
if (services.length === 0) {
|
|
1780
|
+
if (options?.isolatedConnection) {
|
|
1781
|
+
await client.disconnect();
|
|
1782
|
+
const index = this.isolatedClients.indexOf(client);
|
|
1783
|
+
if (index >= 0)
|
|
1784
|
+
this.isolatedClients.splice(index, 1);
|
|
1785
|
+
}
|
|
1786
|
+
throw new Error(`No services found with name: ${serviceName}`);
|
|
1787
|
+
}
|
|
1788
|
+
// Select service (prefer specific ID if provided)
|
|
1789
|
+
const selected = options?.preferredId ? (services.find((s) => s.id === options.preferredId) ?? services[0]) : services[0];
|
|
1790
|
+
// Create the proxy
|
|
1791
|
+
const proxy = createServiceProxy(client, selected, options?.timeout);
|
|
1792
|
+
// If isolated, store reference for potential cleanup
|
|
1793
|
+
if (options?.isolatedConnection) {
|
|
1794
|
+
proxy._isolatedClient = client;
|
|
1795
|
+
return {
|
|
1796
|
+
proxy,
|
|
1797
|
+
close: async () => {
|
|
1798
|
+
await client.disconnect();
|
|
1799
|
+
const index = this.isolatedClients.indexOf(client);
|
|
1800
|
+
if (index >= 0)
|
|
1801
|
+
this.isolatedClients.splice(index, 1);
|
|
1802
|
+
},
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
else {
|
|
1806
|
+
return proxy;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
// Wraps a connect() promise so that an external AbortSignal can synchronously
|
|
1811
|
+
// reject the wait. If the underlying connect resolves AFTER abort, the
|
|
1812
|
+
// resulting NatsConnection is force-closed via the fork's abortClose() (or
|
|
1813
|
+
// the standard close() as a fallback) — otherwise we'd leak a live WS that
|
|
1814
|
+
// nobody owns.
|
|
1815
|
+
function makeAbortableConnect(p, signal) {
|
|
1816
|
+
if (!signal)
|
|
1817
|
+
return p;
|
|
1818
|
+
if (signal.aborted) {
|
|
1819
|
+
p.then((nc) => closeLeaked(nc)).catch(() => { });
|
|
1820
|
+
return Promise.reject(new DOMException('Aborted', 'AbortError'));
|
|
1821
|
+
}
|
|
1822
|
+
return new Promise((resolve, reject) => {
|
|
1823
|
+
let settled = false;
|
|
1824
|
+
const onAbort = () => {
|
|
1825
|
+
if (settled)
|
|
1826
|
+
return;
|
|
1827
|
+
settled = true;
|
|
1828
|
+
p.then((nc) => closeLeaked(nc)).catch(() => { });
|
|
1829
|
+
reject(new DOMException('Aborted', 'AbortError'));
|
|
1830
|
+
};
|
|
1831
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
1832
|
+
p.then((val) => {
|
|
1833
|
+
if (settled) {
|
|
1834
|
+
closeLeaked(val);
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
settled = true;
|
|
1838
|
+
signal.removeEventListener('abort', onAbort);
|
|
1839
|
+
resolve(val);
|
|
1840
|
+
}, (err) => {
|
|
1841
|
+
if (settled)
|
|
1842
|
+
return;
|
|
1843
|
+
settled = true;
|
|
1844
|
+
signal.removeEventListener('abort', onAbort);
|
|
1845
|
+
reject(err);
|
|
1846
|
+
});
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
function closeLeaked(nc) {
|
|
1850
|
+
const withAbort = nc;
|
|
1851
|
+
try {
|
|
1852
|
+
if (typeof withAbort.abortClose === 'function') {
|
|
1853
|
+
withAbort.abortClose();
|
|
1854
|
+
}
|
|
1855
|
+
else {
|
|
1856
|
+
void nc.close().catch(() => { });
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
catch {
|
|
1860
|
+
// ignore
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
//# sourceMappingURL=client.js.map
|