@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.
- package/externals/nats.js/core/src/authenticator.ts +159 -0
- package/externals/nats.js/core/src/bench.ts +426 -0
- package/externals/nats.js/core/src/codec.ts +28 -0
- package/externals/nats.js/core/src/core.ts +1219 -0
- package/externals/nats.js/core/src/databuffer.ts +129 -0
- package/externals/nats.js/core/src/denobuffer.ts +248 -0
- package/externals/nats.js/core/src/encoders.ts +53 -0
- package/externals/nats.js/core/src/errors.ts +300 -0
- package/externals/nats.js/core/src/headers.ts +315 -0
- package/externals/nats.js/core/src/heartbeats.ts +114 -0
- package/externals/nats.js/core/src/idleheartbeat_monitor.ts +140 -0
- package/externals/nats.js/core/src/internal_mod.ts +167 -0
- package/externals/nats.js/core/src/ipparser.ts +215 -0
- package/externals/nats.js/core/src/mod.ts +113 -0
- package/externals/nats.js/core/src/msg.ts +120 -0
- package/externals/nats.js/core/src/muxsubscription.ts +111 -0
- package/externals/nats.js/core/src/nats.ts +650 -0
- package/externals/nats.js/core/src/nkeys.ts +1 -0
- package/externals/nats.js/core/src/nuid.ts +16 -0
- package/externals/nats.js/core/src/options.ts +202 -0
- package/externals/nats.js/core/src/parser.ts +756 -0
- package/externals/nats.js/core/src/protocol.ts +1304 -0
- package/externals/nats.js/core/src/queued_iterator.ts +171 -0
- package/externals/nats.js/core/src/request.ts +177 -0
- package/externals/nats.js/core/src/semver.ts +165 -0
- package/externals/nats.js/core/src/servers.ts +424 -0
- package/externals/nats.js/core/src/transport.ts +117 -0
- package/externals/nats.js/core/src/types.ts +17 -0
- package/externals/nats.js/core/src/util.ts +367 -0
- package/externals/nats.js/core/src/version.ts +2 -0
- package/externals/nats.js/core/src/ws_transport.ts +391 -0
- 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
|
+
}
|