@camera.ui/rpc 1.0.3 → 1.0.4

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.
Files changed (32) hide show
  1. package/externals/nats.js/core/src/authenticator.ts +159 -0
  2. package/externals/nats.js/core/src/bench.ts +426 -0
  3. package/externals/nats.js/core/src/codec.ts +28 -0
  4. package/externals/nats.js/core/src/core.ts +1219 -0
  5. package/externals/nats.js/core/src/databuffer.ts +129 -0
  6. package/externals/nats.js/core/src/denobuffer.ts +248 -0
  7. package/externals/nats.js/core/src/encoders.ts +53 -0
  8. package/externals/nats.js/core/src/errors.ts +300 -0
  9. package/externals/nats.js/core/src/headers.ts +315 -0
  10. package/externals/nats.js/core/src/heartbeats.ts +114 -0
  11. package/externals/nats.js/core/src/idleheartbeat_monitor.ts +140 -0
  12. package/externals/nats.js/core/src/internal_mod.ts +167 -0
  13. package/externals/nats.js/core/src/ipparser.ts +215 -0
  14. package/externals/nats.js/core/src/mod.ts +113 -0
  15. package/externals/nats.js/core/src/msg.ts +120 -0
  16. package/externals/nats.js/core/src/muxsubscription.ts +111 -0
  17. package/externals/nats.js/core/src/nats.ts +650 -0
  18. package/externals/nats.js/core/src/nkeys.ts +1 -0
  19. package/externals/nats.js/core/src/nuid.ts +16 -0
  20. package/externals/nats.js/core/src/options.ts +202 -0
  21. package/externals/nats.js/core/src/parser.ts +756 -0
  22. package/externals/nats.js/core/src/protocol.ts +1304 -0
  23. package/externals/nats.js/core/src/queued_iterator.ts +171 -0
  24. package/externals/nats.js/core/src/request.ts +177 -0
  25. package/externals/nats.js/core/src/semver.ts +165 -0
  26. package/externals/nats.js/core/src/servers.ts +424 -0
  27. package/externals/nats.js/core/src/transport.ts +117 -0
  28. package/externals/nats.js/core/src/types.ts +17 -0
  29. package/externals/nats.js/core/src/util.ts +367 -0
  30. package/externals/nats.js/core/src/version.ts +2 -0
  31. package/externals/nats.js/core/src/ws_transport.ts +391 -0
  32. package/package.json +2 -1
@@ -0,0 +1,1304 @@
1
+ /*
2
+ * Copyright 2018-2024 The NATS Authors
3
+ * Licensed under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License.
5
+ * You may obtain a copy of the License at
6
+ *
7
+ * http://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software
10
+ * distributed under the License is distributed on an "AS IS" BASIS,
11
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ * See the License for the specific language governing permissions and
13
+ * limitations under the License.
14
+ */
15
+ import { decode, Empty, encode, TE } from "./encoders.ts";
16
+ import type { Transport } from "./transport.ts";
17
+ import { CR_LF, CRLF, getResolveFn, newTransport } from "./transport.ts";
18
+ import type { Deferred, Delay, Timeout } from "./util.ts";
19
+ import { deferred, delay, extend, timeout } from "./util.ts";
20
+ import { DataBuffer } from "./databuffer.ts";
21
+ import type { ServerImpl } from "./servers.ts";
22
+ import { Servers } from "./servers.ts";
23
+ import { QueuedIteratorImpl } from "./queued_iterator.ts";
24
+ import type { MsgHdrsImpl } from "./headers.ts";
25
+ import { MuxSubscription } from "./muxsubscription.ts";
26
+ import type { PH } from "./heartbeats.ts";
27
+ import { Heartbeat } from "./heartbeats.ts";
28
+ import type { MsgArg, ParserEvent } from "./parser.ts";
29
+ import { Kind, Parser } from "./parser.ts";
30
+ import { MsgImpl } from "./msg.ts";
31
+ import { Features, parseSemVer } from "./semver.ts";
32
+ import type {
33
+ ConnectionOptions,
34
+ Dispatcher,
35
+ Msg,
36
+ Payload,
37
+ Publisher,
38
+ PublishOptions,
39
+ Request,
40
+ Server,
41
+ ServerInfo,
42
+ Status,
43
+ Subscription,
44
+ SubscriptionOptions,
45
+ } from "./core.ts";
46
+
47
+ import {
48
+ DEFAULT_MAX_PING_OUT,
49
+ DEFAULT_PING_INTERVAL,
50
+ DEFAULT_PING_TIMEOUT,
51
+ DEFAULT_RECONNECT_TIME_WAIT,
52
+ } from "./options.ts";
53
+ import { errors, InvalidArgumentError } from "./errors.ts";
54
+
55
+ import type {
56
+ AuthorizationError,
57
+ PermissionViolationError,
58
+ UserAuthenticationExpiredError,
59
+ } from "./errors.ts";
60
+
61
+ const FLUSH_THRESHOLD = 1024 * 32;
62
+
63
+ export const INFO = /^INFO\s+([^\r\n]+)\r\n/i;
64
+
65
+ const PONG_CMD = encode("PONG\r\n");
66
+ const PING_CMD = encode("PING\r\n");
67
+
68
+ const ERR_RECONNECT_HANDLER_FAILED =
69
+ "client option reconnectToServer handler failed";
70
+ const ERR_RECONNECT_HANDLER_NOT_IN_POOL = "returned server is not in the pool";
71
+
72
+ function isDelayedServer(
73
+ x: unknown,
74
+ ): x is { server: Server; delay: number } {
75
+ return (
76
+ x !== null && typeof x === "object" &&
77
+ "server" in x && "delay" in x
78
+ );
79
+ }
80
+
81
+ // camera.ui fork patch — coerce an aborted-signal reason into a thrown value.
82
+ // Browser AbortControllers populate `signal.reason` (DOMException or the
83
+ // arg passed to `abort()`); we wrap plain reasons as Error to keep callers
84
+ // from having to handle non-Error throws.
85
+ function abortReason(signal: AbortSignal): Error {
86
+ const reason = (signal as { reason?: unknown }).reason;
87
+ if (reason instanceof Error) return reason;
88
+ if (typeof reason === "string") return new Error(reason);
89
+ if (reason !== undefined && reason !== null) {
90
+ try {
91
+ return new Error(String(reason));
92
+ } catch {
93
+ // ignore — fall through to default
94
+ }
95
+ }
96
+ return new Error("aborted");
97
+ }
98
+
99
+ export class Connect {
100
+ echo?: boolean;
101
+ no_responders?: boolean;
102
+ protocol: number;
103
+ verbose?: boolean;
104
+ pedantic?: boolean;
105
+ jwt?: string;
106
+ nkey?: string;
107
+ sig?: string;
108
+ user?: string;
109
+ pass?: string;
110
+ auth_token?: string;
111
+ tls_required?: boolean;
112
+ name?: string;
113
+ lang: string;
114
+ version: string;
115
+ headers?: boolean;
116
+
117
+ constructor(
118
+ transport: { version: string; lang: string },
119
+ opts: ConnectionOptions,
120
+ nonce?: string,
121
+ ) {
122
+ this.protocol = 1;
123
+ this.version = transport.version;
124
+ this.lang = transport.lang;
125
+ this.echo = opts.noEcho ? false : undefined;
126
+ this.verbose = opts.verbose;
127
+ this.pedantic = opts.pedantic;
128
+ this.tls_required = opts.tls ? true : undefined;
129
+ this.name = opts.name;
130
+
131
+ const creds =
132
+ (opts && typeof opts.authenticator === "function"
133
+ ? opts.authenticator(nonce)
134
+ : {}) || {};
135
+ extend(this, creds);
136
+ }
137
+ }
138
+
139
+ class SlowNotifier {
140
+ slow: number;
141
+ cb: (pending: number) => void;
142
+ notified: boolean;
143
+
144
+ constructor(slow: number, cb: (pending: number) => void) {
145
+ this.slow = slow;
146
+ this.cb = cb;
147
+ this.notified = false;
148
+ }
149
+
150
+ maybeNotify(pending: number): void {
151
+ // if we are below the threshold reset the ability to notify
152
+ if (pending <= this.slow) {
153
+ this.notified = false;
154
+ } else {
155
+ if (!this.notified) {
156
+ // crossed the threshold, notify and silence.
157
+ this.cb(pending);
158
+ this.notified = true;
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ export class SubscriptionImpl extends QueuedIteratorImpl<Msg>
165
+ implements Subscription {
166
+ sid!: number;
167
+ queue?: string;
168
+ draining: boolean;
169
+ max?: number;
170
+ subject: string;
171
+ drained?: Promise<void>;
172
+ protocol: ProtocolHandler;
173
+ timer?: Timeout<void>;
174
+ info?: unknown;
175
+ cleanupFn?: (sub: Subscription, info?: unknown) => void;
176
+ closed: Deferred<void | Error>;
177
+ requestSubject?: string;
178
+ slow?: SlowNotifier;
179
+
180
+ constructor(
181
+ protocol: ProtocolHandler,
182
+ subject: string,
183
+ opts: SubscriptionOptions = {},
184
+ ) {
185
+ super();
186
+ extend(this, opts);
187
+ this.protocol = protocol;
188
+ this.subject = subject;
189
+ this.draining = false;
190
+ this.noIterator = typeof opts.callback === "function";
191
+ this.closed = deferred<void | Error>();
192
+
193
+ const asyncTraces = !(protocol.options?.noAsyncTraces || false);
194
+
195
+ if (opts.timeout) {
196
+ this.timer = timeout<void>(opts.timeout, asyncTraces);
197
+ this.timer
198
+ .then(() => {
199
+ // timer was cancelled
200
+ this.timer = undefined;
201
+ })
202
+ .catch((err) => {
203
+ // timer fired
204
+ this.stop(err);
205
+ if (this.noIterator) {
206
+ this.callback(err, {} as Msg);
207
+ }
208
+ });
209
+ }
210
+ if (!this.noIterator) {
211
+ // cleanup - they used break or return from the iterator
212
+ // make sure we clean up, if they didn't call unsub
213
+ this.iterClosed.then((err: void | Error) => {
214
+ this.closed.resolve(err);
215
+ this.unsubscribe();
216
+ });
217
+ }
218
+ }
219
+
220
+ setSlowNotificationFn(slow: number, fn?: (pending: number) => void): void {
221
+ this.slow = undefined;
222
+ if (fn) {
223
+ if (this.noIterator) {
224
+ throw new Error("callbacks don't support slow notifications");
225
+ }
226
+ this.slow = new SlowNotifier(slow, fn);
227
+ }
228
+ }
229
+
230
+ callback(err: Error | null, msg: Msg) {
231
+ this.cancelTimeout();
232
+ err ? this.stop(err) : this.push(msg);
233
+ if (!err && this.slow) {
234
+ this.slow.maybeNotify(this.getPending());
235
+ }
236
+ }
237
+
238
+ close(err?: Error): void {
239
+ if (!this.isClosed()) {
240
+ this.cancelTimeout();
241
+ const fn = () => {
242
+ this.stop();
243
+ if (this.cleanupFn) {
244
+ try {
245
+ this.cleanupFn(this, this.info);
246
+ } catch (_err) {
247
+ // ignoring
248
+ }
249
+ }
250
+ this.closed.resolve(err);
251
+ };
252
+
253
+ if (this.noIterator) {
254
+ fn();
255
+ } else {
256
+ this.push(fn);
257
+ }
258
+ }
259
+ }
260
+
261
+ unsubscribe(max?: number): void {
262
+ this.protocol.unsubscribe(this, max);
263
+ }
264
+
265
+ cancelTimeout(): void {
266
+ if (this.timer) {
267
+ this.timer.cancel();
268
+ this.timer = undefined;
269
+ }
270
+ }
271
+
272
+ drain(): Promise<void> {
273
+ if (this.protocol.isClosed()) {
274
+ return Promise.reject(new errors.ClosedConnectionError());
275
+ }
276
+ if (this.isClosed()) {
277
+ return Promise.reject(
278
+ new errors.InvalidOperationError("subscription is already closed"),
279
+ );
280
+ }
281
+ if (!this.drained) {
282
+ this.draining = true;
283
+ this.protocol.unsub(this);
284
+ this.drained = this.protocol.flush(deferred<void>())
285
+ .then(() => {
286
+ this.protocol.subscriptions.cancel(this);
287
+ })
288
+ .catch(() => {
289
+ this.protocol.subscriptions.cancel(this);
290
+ });
291
+ }
292
+ return this.drained;
293
+ }
294
+
295
+ async [Symbol.asyncDispose](): Promise<void> {
296
+ if (this.protocol.isClosed() || this.isClosed()) {
297
+ return;
298
+ }
299
+ if (this.drained) {
300
+ await this.drained;
301
+ return;
302
+ }
303
+ await this.drain();
304
+ }
305
+
306
+ isDraining(): boolean {
307
+ return this.draining;
308
+ }
309
+
310
+ isClosed(): boolean {
311
+ return this.done;
312
+ }
313
+
314
+ getSubject(): string {
315
+ return this.subject;
316
+ }
317
+
318
+ getMax(): number | undefined {
319
+ return this.max;
320
+ }
321
+
322
+ getID(): number {
323
+ return this.sid;
324
+ }
325
+ }
326
+
327
+ export class Subscriptions {
328
+ mux: SubscriptionImpl | null;
329
+ subs: Map<number, SubscriptionImpl>;
330
+ sidCounter: number;
331
+
332
+ constructor() {
333
+ this.sidCounter = 0;
334
+ this.mux = null;
335
+ this.subs = new Map<number, SubscriptionImpl>();
336
+ }
337
+
338
+ size(): number {
339
+ return this.subs.size;
340
+ }
341
+
342
+ add(s: SubscriptionImpl): SubscriptionImpl {
343
+ this.sidCounter++;
344
+ s.sid = this.sidCounter;
345
+ this.subs.set(s.sid, s);
346
+ return s;
347
+ }
348
+
349
+ setMux(s: SubscriptionImpl | null): SubscriptionImpl | null {
350
+ this.mux = s;
351
+ return s;
352
+ }
353
+
354
+ getMux(): SubscriptionImpl | null {
355
+ return this.mux;
356
+ }
357
+
358
+ get(sid: number): SubscriptionImpl | undefined {
359
+ return this.subs.get(sid);
360
+ }
361
+
362
+ resub(s: SubscriptionImpl): SubscriptionImpl {
363
+ this.sidCounter++;
364
+ this.subs.delete(s.sid);
365
+ s.sid = this.sidCounter;
366
+ this.subs.set(s.sid, s);
367
+ return s;
368
+ }
369
+
370
+ all(): (SubscriptionImpl)[] {
371
+ return Array.from(this.subs.values());
372
+ }
373
+
374
+ cancel(s: SubscriptionImpl): void {
375
+ if (s) {
376
+ s.close();
377
+ this.subs.delete(s.sid);
378
+ }
379
+ }
380
+
381
+ handleError(err: PermissionViolationError): boolean {
382
+ const subs = this.all();
383
+ let sub;
384
+ if (err.operation === "subscription") {
385
+ sub = subs.find((s) => {
386
+ return s.subject === err.subject && s.queue === err.queue;
387
+ });
388
+ } else if (err.operation === "publish") {
389
+ // we have a no mux subscription
390
+ sub = subs.find((s) => {
391
+ return s.requestSubject === err.subject;
392
+ });
393
+ }
394
+ if (sub) {
395
+ sub.callback(err, {} as Msg);
396
+ sub.close(err);
397
+ this.subs.delete(sub.sid);
398
+ return sub !== this.mux;
399
+ }
400
+
401
+ return false;
402
+ }
403
+
404
+ close() {
405
+ this.subs.forEach((sub) => {
406
+ sub.close();
407
+ });
408
+ }
409
+ }
410
+
411
+ export class ProtocolHandler implements Dispatcher<ParserEvent> {
412
+ connected: boolean;
413
+ connectedOnce: boolean;
414
+ infoReceived: boolean;
415
+ info?: ServerInfo;
416
+ muxSubscriptions: MuxSubscription;
417
+ options: ConnectionOptions;
418
+ outbound: DataBuffer;
419
+ pongs: Array<Deferred<void>>;
420
+ subscriptions: Subscriptions;
421
+ transport!: Transport;
422
+ noMorePublishing: boolean;
423
+ connectError?: (err?: Error) => void;
424
+ publisher: Publisher;
425
+ _closed: boolean;
426
+ closed: Deferred<Error | void>;
427
+ listeners: QueuedIteratorImpl<Status>[];
428
+ heartbeats: Heartbeat;
429
+ parser: Parser;
430
+ outMsgs: number;
431
+ inMsgs: number;
432
+ outBytes: number;
433
+ inBytes: number;
434
+ pendingLimit: number;
435
+ lastError?: Error;
436
+ abortReconnect: boolean;
437
+ whyClosed: string;
438
+
439
+ servers: Servers;
440
+ server!: ServerImpl;
441
+ features: Features;
442
+ connectPromise: Promise<void> | null;
443
+ dialDelay: Delay | null;
444
+ raceTimer?: Timeout<void>;
445
+
446
+ constructor(options: ConnectionOptions, publisher: Publisher) {
447
+ this._closed = false;
448
+ this.connected = false;
449
+ this.connectedOnce = false;
450
+ this.infoReceived = false;
451
+ this.noMorePublishing = false;
452
+ this.abortReconnect = false;
453
+ this.listeners = [];
454
+ this.pendingLimit = FLUSH_THRESHOLD;
455
+ this.outMsgs = 0;
456
+ this.inMsgs = 0;
457
+ this.outBytes = 0;
458
+ this.inBytes = 0;
459
+ this.options = options;
460
+ this.publisher = publisher;
461
+ this.subscriptions = new Subscriptions();
462
+ this.muxSubscriptions = new MuxSubscription();
463
+ this.outbound = new DataBuffer();
464
+ this.pongs = [];
465
+ this.whyClosed = "";
466
+ //@ts-ignore: options.pendingLimit is hidden
467
+ this.pendingLimit = options.pendingLimit || this.pendingLimit;
468
+ this.features = new Features({ major: 0, minor: 0, micro: 0 });
469
+ this.connectPromise = null;
470
+ this.dialDelay = null;
471
+
472
+ const servers = typeof options.servers === "string"
473
+ ? [options.servers]
474
+ : options.servers;
475
+
476
+ this.servers = new Servers({
477
+ randomize: !options.noRandomize,
478
+ });
479
+ this.servers.setServers(servers as string[]);
480
+ this.closed = deferred<Error | void>();
481
+ this.parser = new Parser(this);
482
+
483
+ this.heartbeats = new Heartbeat(
484
+ this as PH,
485
+ this.options.pingInterval || DEFAULT_PING_INTERVAL,
486
+ this.options.maxPingOut || DEFAULT_MAX_PING_OUT,
487
+ this.options.pingTimeout ?? DEFAULT_PING_TIMEOUT,
488
+ );
489
+ }
490
+
491
+ resetOutbound(): void {
492
+ this.outbound.reset();
493
+ const pongs = this.pongs;
494
+ this.pongs = [];
495
+ // reject the pongs - the disconnect from here shouldn't have a trace
496
+ // because that confuses API consumers
497
+ const err = new errors.RequestError("connection disconnected");
498
+ err.stack = "";
499
+ pongs.forEach((p) => {
500
+ p.reject(err);
501
+ });
502
+ this.parser = new Parser(this);
503
+ this.infoReceived = false;
504
+ }
505
+
506
+ dispatchStatus(status: Status): void {
507
+ this.listeners.forEach((q) => {
508
+ q.push(status);
509
+ });
510
+ }
511
+
512
+ private prepare(): Deferred<void> {
513
+ if (this.transport) {
514
+ this.transport.discard();
515
+ }
516
+ this.info = undefined;
517
+ this.resetOutbound();
518
+
519
+ const pong = deferred<void>();
520
+ pong.catch(() => {
521
+ // provide at least one catch - as pong rejection can happen before it is expected
522
+ });
523
+ this.pongs.unshift(pong);
524
+
525
+ this.connectError = (err?: Error) => {
526
+ pong.reject(err);
527
+ };
528
+
529
+ this.transport = newTransport();
530
+ this.transport.closed()
531
+ .then(async (_err?) => {
532
+ this.connected = false;
533
+ if (!this.isClosed()) {
534
+ // if the transport gave an error use that, otherwise
535
+ // we may have received a protocol error
536
+ await this.disconnected(this.transport.closeError || this.lastError);
537
+ return;
538
+ }
539
+ });
540
+
541
+ return pong;
542
+ }
543
+
544
+ public disconnect(): void {
545
+ this.dispatchStatus({ type: "staleConnection" });
546
+ this.transport.disconnect();
547
+ }
548
+
549
+ public reconnect(): Promise<void> {
550
+ if (this.connected) {
551
+ this.dispatchStatus({
552
+ type: "forceReconnect",
553
+ });
554
+ this.transport.disconnect();
555
+ }
556
+ return Promise.resolve();
557
+ }
558
+
559
+ // camera.ui fork patch.
560
+ // Unlike reconnect(), this works also when the protocol is NOT in the
561
+ // connected state — e.g. dial loop is sleeping on dialDelay or stuck on
562
+ // raceTimer mid-handshake against a now-unreachable host. Cancelling those
563
+ // timers makes the loop's current iteration resolve immediately so it
564
+ // picks up the current (possibly just-updated) servers list. If neither
565
+ // a live connection nor a running dial loop exists, kicks off a fresh one.
566
+ public forceReconnect(): Promise<void> {
567
+ this.dispatchStatus({ type: "forceReconnect" });
568
+ this.dialDelay?.cancel();
569
+ this.raceTimer?.cancel();
570
+
571
+ if (this.connected) {
572
+ // disconnected() handler runs dialLoop() with current servers.
573
+ this.transport.disconnect();
574
+ return Promise.resolve();
575
+ }
576
+
577
+ if (this.connectPromise === null && !this._closed) {
578
+ // No live dial loop — start one. close()-style finally clears
579
+ // connectPromise so subsequent calls are idempotent.
580
+ return this.dialLoop()
581
+ .then(() => {
582
+ this.dispatchStatus({
583
+ type: "reconnect",
584
+ server: this.servers.getCurrentServer().toString(),
585
+ });
586
+ })
587
+ .catch((err) => {
588
+ // dialLoop exhausted — fall back to existing close path.
589
+ this.close(err).catch(() => {});
590
+ });
591
+ }
592
+
593
+ return Promise.resolve();
594
+ }
595
+
596
+ async disconnected(err?: Error): Promise<void> {
597
+ this.dispatchStatus(
598
+ {
599
+ type: "disconnect",
600
+ server: this.servers.getCurrentServer().toString(),
601
+ },
602
+ );
603
+ if (this.options.reconnect) {
604
+ await this.dialLoop()
605
+ .then(() => {
606
+ this.dispatchStatus(
607
+ {
608
+ type: "reconnect",
609
+ server: this.servers.getCurrentServer().toString(),
610
+ },
611
+ );
612
+ // if we are here we reconnected, but we have an authentication
613
+ // that expired, we need to clean it up, otherwise we'll queue up
614
+ // two of these, and the default for the client will be to
615
+ // close, rather than attempt again - possibly they have an
616
+ // authenticator that dynamically updates
617
+ if (this.lastError instanceof errors.UserAuthenticationExpiredError) {
618
+ this.lastError = undefined;
619
+ }
620
+ })
621
+ .catch((err) => {
622
+ this.close(err).catch();
623
+ });
624
+ } else {
625
+ await this.close(err).catch();
626
+ }
627
+ }
628
+
629
+ async dial(srv: Server): Promise<void> {
630
+ const pong = this.prepare();
631
+ try {
632
+ this.raceTimer = timeout(this.options.timeout || 20000);
633
+ const cp = this.transport.connect(srv, this.options);
634
+ await Promise.race([cp, this.raceTimer]);
635
+ (async () => {
636
+ try {
637
+ for await (const b of this.transport) {
638
+ this.parser.parse(b);
639
+ }
640
+ } catch (err) {
641
+ console.log("reader closed", err);
642
+ }
643
+ })().then();
644
+ } catch (err) {
645
+ pong.reject(err);
646
+ }
647
+
648
+ try {
649
+ await Promise.race([this.raceTimer, pong]);
650
+ this.raceTimer?.cancel();
651
+ this.connected = true;
652
+ this.connectError = undefined;
653
+ this.sendSubscriptions();
654
+ this.connectedOnce = true;
655
+ this.server.didConnect = true;
656
+ this.server.reconnects = 0;
657
+ this.flushPending();
658
+ this.heartbeats.start();
659
+ } catch (err) {
660
+ this.raceTimer?.cancel();
661
+ await this.transport.close(err as Error);
662
+ throw err;
663
+ }
664
+ }
665
+
666
+ async _doDial(srv: ServerImpl): Promise<void> {
667
+ const { resolve } = this.options;
668
+ const alts = await srv.resolve({
669
+ fn: getResolveFn(),
670
+ debug: this.options.debug,
671
+ randomize: !this.options.noRandomize,
672
+ resolve,
673
+ });
674
+
675
+ let lastErr: Error | null = null;
676
+ for (const a of alts) {
677
+ try {
678
+ lastErr = null;
679
+ this.dispatchStatus(
680
+ { type: "reconnecting" },
681
+ );
682
+ await this.dial(a);
683
+ // if here we connected
684
+ return;
685
+ } catch (err) {
686
+ lastErr = err as Error;
687
+ }
688
+ }
689
+ // if we are here, we failed, and we have no additional
690
+ // alternatives for this server
691
+ throw lastErr;
692
+ }
693
+
694
+ dialLoop(): Promise<void> {
695
+ if (this.connectPromise === null) {
696
+ this.connectPromise = this.dodialLoop();
697
+ this.connectPromise
698
+ .then(() => {})
699
+ .catch(() => {})
700
+ .finally(() => {
701
+ this.connectPromise = null;
702
+ });
703
+ }
704
+ return this.connectPromise;
705
+ }
706
+
707
+ async dodialLoop(): Promise<void> {
708
+ let lastError: Error | undefined;
709
+ while (true) {
710
+ if (this._closed) {
711
+ // if we are disconnected, and close is called, the client
712
+ // still tries to reconnect - to match the reconnect policy
713
+ // in the case of close, want to stop.
714
+ this.servers.clear();
715
+ }
716
+ const srv = this.selectServer();
717
+ const wait = this.options.reconnectDelayHandler
718
+ ? this.options.reconnectDelayHandler(srv?.reconnects ?? 0)
719
+ : DEFAULT_RECONNECT_TIME_WAIT;
720
+ let maxWait = wait;
721
+ if (!srv || this.abortReconnect) {
722
+ if (lastError) {
723
+ throw lastError;
724
+ } else if (this.lastError) {
725
+ throw this.lastError;
726
+ } else {
727
+ throw new errors.ConnectionError("connection refused");
728
+ }
729
+ }
730
+ const now = Date.now();
731
+ if (srv.lastConnect === 0 || srv.lastConnect + wait <= now) {
732
+ let target = srv;
733
+ let extraDelay = 0;
734
+ if (this.options.reconnectToServer) {
735
+ try {
736
+ const snap = this.servers.snapshotForHandler();
737
+ const r = this.options.reconnectToServer(
738
+ snap,
739
+ this.info ?? null,
740
+ );
741
+ let picked: Server | null;
742
+ if (isDelayedServer(r)) {
743
+ picked = r.server;
744
+ extraDelay = Number.isFinite(r.delay) && r.delay > 0
745
+ ? Math.floor(r.delay)
746
+ : 0;
747
+ } else {
748
+ picked = r;
749
+ }
750
+ if (picked !== null) {
751
+ const found = this.servers.find(picked);
752
+ if (!found) {
753
+ throw new Error(ERR_RECONNECT_HANDLER_NOT_IN_POOL);
754
+ }
755
+ if (found !== srv) {
756
+ target = found;
757
+ this.servers.setCurrent(target);
758
+ this.server = target;
759
+ }
760
+ }
761
+ } catch (cause) {
762
+ const c = cause instanceof Error ? cause : new Error(String(cause));
763
+ throw new errors.ConnectionError(
764
+ `${ERR_RECONNECT_HANDLER_FAILED}: ${c.message}`,
765
+ { cause: c },
766
+ );
767
+ }
768
+ }
769
+ if (extraDelay > 0) {
770
+ this.dialDelay = delay(extraDelay);
771
+ await this.dialDelay;
772
+ }
773
+ target.lastConnect = Date.now();
774
+ try {
775
+ await this._doDial(target);
776
+ break;
777
+ } catch (err) {
778
+ lastError = err as Error;
779
+ if (!this.connectedOnce) {
780
+ if (this.options.waitOnFirstConnect) {
781
+ continue;
782
+ }
783
+ this.servers.removeCurrentServer();
784
+ }
785
+ target.reconnects++;
786
+ const mra = this.options.maxReconnectAttempts || 0;
787
+ if (mra !== -1 && target.reconnects >= mra) {
788
+ this.servers.removeCurrentServer();
789
+ }
790
+ }
791
+ } else {
792
+ maxWait = Math.min(maxWait, srv.lastConnect + wait - now);
793
+ this.dialDelay = delay(maxWait);
794
+ await this.dialDelay;
795
+ }
796
+ }
797
+ }
798
+
799
+ public static async connect(
800
+ options: ConnectionOptions,
801
+ publisher: Publisher,
802
+ ): Promise<ProtocolHandler> {
803
+ const h = new ProtocolHandler(options, publisher);
804
+
805
+ // camera.ui fork patch — see ConnectionOptions.signal docstring.
806
+ // The handler is constructed BEFORE dialing starts, so we wire the
807
+ // abort listener now: an abort cancels dialDelay/raceTimer and flips
808
+ // abortReconnect → the dial loop throws on its next iteration.
809
+ const signal = options.signal;
810
+ let onAbort: (() => void) | undefined;
811
+ if (signal) {
812
+ if (signal.aborted) {
813
+ const reason = abortReason(signal);
814
+ h.abortClose(reason);
815
+ throw reason;
816
+ }
817
+ onAbort = () => {
818
+ try {
819
+ h.abortClose(abortReason(signal));
820
+ } catch (_e) {
821
+ // ignore — best-effort cleanup
822
+ }
823
+ };
824
+ signal.addEventListener("abort", onAbort, { once: true });
825
+ }
826
+
827
+ try {
828
+ await h.dialLoop();
829
+ return h;
830
+ } finally {
831
+ if (signal && onAbort) {
832
+ signal.removeEventListener("abort", onAbort);
833
+ }
834
+ }
835
+ }
836
+
837
+ static toError(s: string): Error {
838
+ let err: Error | null = errors.PermissionViolationError.parse(s);
839
+ if (err) {
840
+ return err;
841
+ }
842
+ err = errors.UserAuthenticationExpiredError.parse(s);
843
+ if (err) {
844
+ return err;
845
+ }
846
+ err = errors.AuthorizationError.parse(s);
847
+ if (err) {
848
+ return err;
849
+ }
850
+ return new errors.ProtocolError(s);
851
+ }
852
+
853
+ processMsg(msg: MsgArg, data: Uint8Array) {
854
+ this.inMsgs++;
855
+ this.inBytes += data.length;
856
+ if (!this.subscriptions.sidCounter) {
857
+ return;
858
+ }
859
+
860
+ const sub = this.subscriptions.get(msg.sid);
861
+ if (!sub) {
862
+ return;
863
+ }
864
+ sub.received += 1;
865
+
866
+ if (sub.callback) {
867
+ sub.callback(null, new MsgImpl(msg, data, this));
868
+ }
869
+
870
+ if (sub.max !== undefined && sub.received >= sub.max) {
871
+ sub.unsubscribe();
872
+ }
873
+ }
874
+
875
+ processError(m: Uint8Array) {
876
+ let s = decode(m);
877
+ if (s.startsWith("'") && s.endsWith("'")) {
878
+ s = s.slice(1, s.length - 1);
879
+ }
880
+ const err = ProtocolHandler.toError(s);
881
+
882
+ switch (err.constructor) {
883
+ case errors.PermissionViolationError: {
884
+ const pe = err as PermissionViolationError;
885
+ const mux = this.subscriptions.getMux();
886
+ const isMuxPermission = mux ? pe.subject === mux.subject : false;
887
+ this.subscriptions.handleError(pe);
888
+ this.muxSubscriptions.handleError(isMuxPermission, pe);
889
+ if (isMuxPermission) {
890
+ // remove the permission - enable it to be recreated
891
+ this.subscriptions.setMux(null);
892
+ }
893
+ }
894
+ }
895
+
896
+ this.dispatchStatus({ type: "error", error: err });
897
+ this.handleError(err);
898
+ }
899
+
900
+ handleError(err: Error) {
901
+ if (
902
+ err instanceof errors.UserAuthenticationExpiredError ||
903
+ err instanceof errors.AuthorizationError
904
+ ) {
905
+ this.handleAuthError(err);
906
+ }
907
+
908
+ if (!(err instanceof errors.PermissionViolationError)) {
909
+ this.lastError = err;
910
+ }
911
+ }
912
+
913
+ handleAuthError(err: UserAuthenticationExpiredError | AuthorizationError) {
914
+ if (
915
+ (this.lastError instanceof errors.UserAuthenticationExpiredError ||
916
+ this.lastError instanceof errors.AuthorizationError) &&
917
+ this.options.ignoreAuthErrorAbort === false
918
+ ) {
919
+ this.abortReconnect = true;
920
+ }
921
+ if (this.connectError) {
922
+ this.connectError(err);
923
+ } else {
924
+ this.disconnect();
925
+ }
926
+ }
927
+
928
+ processPing() {
929
+ this.transport.send(PONG_CMD);
930
+ }
931
+
932
+ processPong() {
933
+ const cb = this.pongs.shift();
934
+ if (cb) {
935
+ cb.resolve();
936
+ }
937
+ }
938
+
939
+ processInfo(m: Uint8Array) {
940
+ const info = JSON.parse(decode(m));
941
+ this.info = info;
942
+ const updates = this.options && this.options.ignoreClusterUpdates
943
+ ? undefined
944
+ : this.servers.update(info, this.transport.isEncrypted());
945
+ if (!this.infoReceived) {
946
+ this.features.update(parseSemVer(info.version));
947
+ this.infoReceived = true;
948
+ if (this.transport.isEncrypted()) {
949
+ this.servers.updateTLSName();
950
+ }
951
+ // send connect
952
+ const { version, lang } = this.transport;
953
+ try {
954
+ const c = new Connect(
955
+ { version, lang },
956
+ this.options,
957
+ info.nonce,
958
+ );
959
+
960
+ if (info.headers) {
961
+ c.headers = true;
962
+ c.no_responders = true;
963
+ }
964
+ const cs = JSON.stringify(c);
965
+ this.transport.send(
966
+ encode(`CONNECT ${cs}${CR_LF}`),
967
+ );
968
+ this.transport.send(PING_CMD);
969
+ } catch (err) {
970
+ // if we are dying here, this is likely some an authenticator blowing up
971
+ this.close(err as Error).catch();
972
+ }
973
+ }
974
+ if (updates) {
975
+ const { added, deleted } = updates;
976
+
977
+ this.dispatchStatus({ type: "update", added, deleted });
978
+ }
979
+ const ldm = info.ldm !== undefined ? info.ldm : false;
980
+ if (ldm) {
981
+ this.dispatchStatus(
982
+ {
983
+ type: "ldm",
984
+ server: this.servers.getCurrentServer().toString(),
985
+ },
986
+ );
987
+ }
988
+ }
989
+
990
+ push(e: ParserEvent): void {
991
+ switch (e.kind) {
992
+ case Kind.MSG: {
993
+ const { msg, data } = e;
994
+ this.processMsg(msg!, data!);
995
+ break;
996
+ }
997
+ case Kind.OK:
998
+ break;
999
+ case Kind.ERR:
1000
+ this.processError(e.data!);
1001
+ break;
1002
+ case Kind.PING:
1003
+ this.processPing();
1004
+ break;
1005
+ case Kind.PONG:
1006
+ this.processPong();
1007
+ break;
1008
+ case Kind.INFO:
1009
+ this.processInfo(e.data!);
1010
+ break;
1011
+ }
1012
+ }
1013
+
1014
+ sendCommand(cmd: string | Uint8Array, ...payloads: Uint8Array[]) {
1015
+ const len = this.outbound.length();
1016
+ let buf: Uint8Array;
1017
+ if (typeof cmd === "string") {
1018
+ buf = encode(cmd);
1019
+ } else {
1020
+ buf = cmd as Uint8Array;
1021
+ }
1022
+ this.outbound.fill(buf, ...payloads);
1023
+
1024
+ if (len === 0) {
1025
+ queueMicrotask(() => {
1026
+ this.flushPending();
1027
+ });
1028
+ } else if (this.outbound.size() >= this.pendingLimit) {
1029
+ // flush inline
1030
+ this.flushPending();
1031
+ }
1032
+ }
1033
+
1034
+ publish(
1035
+ subject: string,
1036
+ payload: Payload = Empty,
1037
+ options?: PublishOptions,
1038
+ ): void {
1039
+ let data;
1040
+ if (payload instanceof Uint8Array) {
1041
+ data = payload;
1042
+ } else if (typeof payload === "string") {
1043
+ data = TE.encode(payload);
1044
+ } else {
1045
+ throw new TypeError(
1046
+ "payload types can be strings or Uint8Array",
1047
+ );
1048
+ }
1049
+
1050
+ let len = data.length;
1051
+ options = options || {};
1052
+ options.reply = options.reply || "";
1053
+
1054
+ let headers = Empty;
1055
+ let hlen = 0;
1056
+ if (options.headers) {
1057
+ if (this.info && !this.info.headers) {
1058
+ InvalidArgumentError.format(
1059
+ "headers",
1060
+ "are not available on this server",
1061
+ );
1062
+ }
1063
+ const hdrs = options.headers as MsgHdrsImpl;
1064
+ headers = hdrs.encode();
1065
+ hlen = headers.length;
1066
+ len = data.length + hlen;
1067
+ }
1068
+
1069
+ if (this.info && len > this.info.max_payload) {
1070
+ throw InvalidArgumentError.format("payload", "max_payload size exceeded");
1071
+ }
1072
+ this.outBytes += len;
1073
+ this.outMsgs++;
1074
+
1075
+ let proto: string;
1076
+ if (options.headers) {
1077
+ if (options.reply) {
1078
+ proto = `HPUB ${subject} ${options.reply} ${hlen} ${len}\r\n`;
1079
+ } else {
1080
+ proto = `HPUB ${subject} ${hlen} ${len}\r\n`;
1081
+ }
1082
+ this.sendCommand(proto, headers, data, CRLF);
1083
+ } else {
1084
+ if (options.reply) {
1085
+ proto = `PUB ${subject} ${options.reply} ${len}\r\n`;
1086
+ } else {
1087
+ proto = `PUB ${subject} ${len}\r\n`;
1088
+ }
1089
+ this.sendCommand(proto, data, CRLF);
1090
+ }
1091
+ }
1092
+
1093
+ request(r: Request): Request {
1094
+ this.initMux();
1095
+ this.muxSubscriptions.add(r);
1096
+ return r;
1097
+ }
1098
+
1099
+ subscribe(s: SubscriptionImpl): Subscription {
1100
+ this.subscriptions.add(s);
1101
+ this._subunsub(s);
1102
+ return s;
1103
+ }
1104
+
1105
+ _sub(s: SubscriptionImpl): void {
1106
+ if (s.queue) {
1107
+ this.sendCommand(`SUB ${s.subject} ${s.queue} ${s.sid}\r\n`);
1108
+ } else {
1109
+ this.sendCommand(`SUB ${s.subject} ${s.sid}\r\n`);
1110
+ }
1111
+ }
1112
+
1113
+ _subunsub(s: SubscriptionImpl): SubscriptionImpl {
1114
+ this._sub(s);
1115
+ if (s.max) {
1116
+ this.unsubscribe(s, s.max);
1117
+ }
1118
+ return s;
1119
+ }
1120
+
1121
+ unsubscribe(s: SubscriptionImpl, max?: number): void {
1122
+ this.unsub(s, max);
1123
+ if (s.max === undefined || s.received >= s.max) {
1124
+ this.subscriptions.cancel(s);
1125
+ }
1126
+ }
1127
+
1128
+ unsub(s: SubscriptionImpl, max?: number): void {
1129
+ if (!s || this.isClosed()) {
1130
+ return;
1131
+ }
1132
+ if (max) {
1133
+ this.sendCommand(`UNSUB ${s.sid} ${max}\r\n`);
1134
+ } else {
1135
+ this.sendCommand(`UNSUB ${s.sid}\r\n`);
1136
+ }
1137
+ s.max = max;
1138
+ }
1139
+
1140
+ resub(s: SubscriptionImpl, subject: string): void {
1141
+ if (!s || this.isClosed()) {
1142
+ return;
1143
+ }
1144
+ this.unsub(s);
1145
+ s.subject = subject;
1146
+ this.subscriptions.resub(s);
1147
+ // we don't auto-unsub here because we don't
1148
+ // really know "processed"
1149
+ this._sub(s);
1150
+ }
1151
+
1152
+ flush(p?: Deferred<void>): Promise<void> {
1153
+ if (!p) {
1154
+ p = deferred<void>();
1155
+ }
1156
+ this.pongs.push(p);
1157
+ this.outbound.fill(PING_CMD);
1158
+ this.flushPending();
1159
+ return p;
1160
+ }
1161
+
1162
+ sendSubscriptions(): void {
1163
+ const cmds: string[] = [];
1164
+ this.subscriptions.all().forEach((s) => {
1165
+ const sub = s;
1166
+ if (sub.queue) {
1167
+ cmds.push(`SUB ${sub.subject} ${sub.queue} ${sub.sid}${CR_LF}`);
1168
+ } else {
1169
+ cmds.push(`SUB ${sub.subject} ${sub.sid}${CR_LF}`);
1170
+ }
1171
+ });
1172
+ if (cmds.length) {
1173
+ this.transport.send(encode(cmds.join("")));
1174
+ }
1175
+ }
1176
+
1177
+ async close(err?: Error): Promise<void> {
1178
+ if (this._closed) {
1179
+ return;
1180
+ }
1181
+ this.whyClosed = new Error("close trace").stack || "";
1182
+ this.heartbeats.cancel();
1183
+ if (this.connectError) {
1184
+ this.connectError(err);
1185
+ this.connectError = undefined;
1186
+ }
1187
+ this.muxSubscriptions.close();
1188
+ this.subscriptions.close();
1189
+ const proms = [];
1190
+ for (let i = 0; i < this.listeners.length; i++) {
1191
+ const qi = this.listeners[i];
1192
+ if (qi) {
1193
+ qi.push({ type: "close" });
1194
+ qi.stop();
1195
+ proms.push(qi.iterClosed);
1196
+ }
1197
+ }
1198
+ if (proms.length) {
1199
+ await Promise.all(proms);
1200
+ }
1201
+ this._closed = true;
1202
+ await this.transport.close(err);
1203
+ this.raceTimer?.cancel();
1204
+ this.dialDelay?.cancel();
1205
+ this.closed.resolve(err);
1206
+ }
1207
+
1208
+ // camera.ui fork patch.
1209
+ // Synchronous force-close: tears down state without awaiting transport
1210
+ // close or listener iterators. Use when the network is known-dead and
1211
+ // graceful shutdown would hang on the close handshake. Caller MUST treat
1212
+ // this handler as unusable after this call (same as close()).
1213
+ abortClose(err?: Error): void {
1214
+ if (this._closed) {
1215
+ return;
1216
+ }
1217
+ this.whyClosed = new Error("abortClose trace").stack || "";
1218
+ this._closed = true;
1219
+ this.abortReconnect = true;
1220
+ this.heartbeats.cancel();
1221
+ if (this.connectError) {
1222
+ this.connectError(err);
1223
+ this.connectError = undefined;
1224
+ }
1225
+ this.muxSubscriptions.close();
1226
+ this.subscriptions.close();
1227
+ for (let i = 0; i < this.listeners.length; i++) {
1228
+ const qi = this.listeners[i];
1229
+ if (qi) {
1230
+ qi.push({ type: "close" });
1231
+ qi.stop();
1232
+ }
1233
+ }
1234
+ this.raceTimer?.cancel();
1235
+ this.dialDelay?.cancel();
1236
+ // Fire-and-forget — transport.close on a dead WS may never resolve.
1237
+ try {
1238
+ this.transport?.close(err);
1239
+ } catch (_e) {
1240
+ // ignore
1241
+ }
1242
+ this.closed.resolve(err);
1243
+ }
1244
+
1245
+ isClosed(): boolean {
1246
+ return this._closed;
1247
+ }
1248
+
1249
+ async drain(): Promise<void> {
1250
+ const subs = this.subscriptions.all();
1251
+ const promises: Promise<void>[] = [];
1252
+ subs.forEach((sub: Subscription) => {
1253
+ promises.push(sub.drain());
1254
+ });
1255
+ try {
1256
+ await Promise.allSettled(promises);
1257
+ } catch {
1258
+ // nothing we can do here
1259
+ } finally {
1260
+ this.noMorePublishing = true;
1261
+ await this.flush();
1262
+ }
1263
+ return this.close();
1264
+ }
1265
+
1266
+ private flushPending() {
1267
+ if (!this.infoReceived || !this.connected) {
1268
+ return;
1269
+ }
1270
+
1271
+ if (this.outbound.size()) {
1272
+ const d = this.outbound.drain();
1273
+ this.transport.send(d);
1274
+ }
1275
+ }
1276
+
1277
+ private initMux(): void {
1278
+ const mux = this.subscriptions.getMux();
1279
+ if (!mux) {
1280
+ const inbox = this.muxSubscriptions.init(
1281
+ this.options.inboxPrefix,
1282
+ );
1283
+ // dot is already part of mux
1284
+ const sub = new SubscriptionImpl(this, `${inbox}*`);
1285
+ sub.callback = this.muxSubscriptions.dispatcher();
1286
+ this.subscriptions.setMux(sub);
1287
+ this.subscribe(sub);
1288
+ }
1289
+ }
1290
+
1291
+ private selectServer(): ServerImpl | undefined {
1292
+ const server = this.servers.selectServer();
1293
+ if (server === undefined) {
1294
+ return undefined;
1295
+ }
1296
+ // Place in client context.
1297
+ this.server = server;
1298
+ return this.server;
1299
+ }
1300
+
1301
+ getServer(): ServerImpl | undefined {
1302
+ return this.server;
1303
+ }
1304
+ }