@honeybbq/teamspeak 1.0.0-beta.26 → 1.0.0-beta.27
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 +2 -1
- package/dist/client/client.d.ts +10 -10
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +154 -114
- package/dist/client/client.js.map +1 -1
- package/dist/client/commands.d.ts +3 -3
- package/dist/client/commands.js +3 -3
- package/dist/client/handshake.d.ts.map +1 -1
- package/dist/client/handshake.js +10 -14
- package/dist/client/handshake.js.map +1 -1
- package/dist/client/types.d.ts +16 -1
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/types.js.map +1 -1
- package/dist/core/command/request.d.ts +4 -3
- package/dist/core/command/request.d.ts.map +1 -1
- package/dist/core/command/request.js.map +1 -1
- package/dist/core/command/responses.d.ts.map +1 -1
- package/dist/core/command/responses.js +10 -1
- package/dist/core/command/responses.js.map +1 -1
- package/dist/core/errors.d.ts +3 -19
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +3 -21
- package/dist/core/errors.js.map +1 -1
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/internal.d.ts +2 -2
- package/dist/core/internal.d.ts.map +1 -1
- package/dist/core/internal.js +2 -2
- package/dist/core/internal.js.map +1 -1
- package/dist/core/middleware.d.ts +3 -1
- package/dist/core/middleware.d.ts.map +1 -1
- package/dist/core/middleware.js +8 -0
- package/dist/core/middleware.js.map +1 -1
- package/dist/core/types.d.ts +60 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/query/client.d.ts +10 -3
- package/dist/query/client.d.ts.map +1 -1
- package/dist/query/client.js +80 -90
- package/dist/query/client.js.map +1 -1
- package/dist/query/options.d.ts +37 -37
- package/dist/query/options.d.ts.map +1 -1
- package/dist/query/options.js +16 -25
- package/dist/query/options.js.map +1 -1
- package/dist/query/ssh.d.ts +1 -1
- package/dist/stream/published.d.ts.map +1 -1
- package/dist/stream/published.js +2 -9
- package/dist/stream/published.js.map +1 -1
- package/dist/stream/streams.d.ts +1 -1
- package/dist/stream/streams.js +2 -2
- package/dist/stream/streams.js.map +1 -1
- package/package.json +1 -1
- package/src/client/client.ts +184 -126
- package/src/client/commands.ts +3 -3
- package/src/client/handshake.ts +10 -16
- package/src/client/types.ts +16 -0
- package/src/core/command/request.ts +4 -3
- package/src/core/command/responses.ts +10 -1
- package/src/core/errors.ts +3 -22
- package/src/core/index.ts +5 -0
- package/src/core/internal.ts +2 -8
- package/src/core/middleware.ts +13 -1
- package/src/core/types.ts +67 -1
- package/src/query/client.ts +84 -96
- package/src/query/options.ts +54 -52
- package/src/query/ssh.ts +1 -1
- package/src/query/testing/live.ts +8 -1
- package/src/stream/published.ts +2 -9
- package/src/stream/streams.ts +2 -2
- package/dist/client/helpers.d.ts +0 -6
- package/dist/client/helpers.d.ts.map +0 -1
- package/dist/client/helpers.js +0 -13
- package/dist/client/helpers.js.map +0 -1
- package/dist/client/throttle.d.ts +0 -31
- package/dist/client/throttle.d.ts.map +0 -1
- package/dist/client/throttle.js +0 -118
- package/dist/client/throttle.js.map +0 -1
- package/src/client/helpers.ts +0 -10
- package/src/client/throttle.ts +0 -130
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
- **Typed commands and events.** Every command the voice protocol answers is a typed function, and the server's notifications arrive as typed events with pluggable middleware.
|
|
36
36
|
- **File transfers.** Upload, download and delete files on the server, with avatars and icons covered.
|
|
37
37
|
- **TeamSpeak 6 screen-share signalling.** Find, publish and watch streams, with SDP and ICE as plain strings for any WebRTC stack.
|
|
38
|
-
- **Built for long-running bots.** Opt-in reconnection tuned for TeamSpeak's antiflood ban, and a token
|
|
38
|
+
- **Built for long-running bots.** Opt-in reconnection tuned for TeamSpeak's antiflood ban, and a line middleware seam for pacing commands, with recipes for a token bucket, resending a refused command and keeping a query session open.
|
|
39
39
|
- **Identities and server facts.** Generate, import, export and upgrade identities; read the server version, its uid hash (SHA-1 on TS3, SHA-256 on TS6) and its name, icon and banner.
|
|
40
40
|
- **TeamSpeak 3 and 6, ESM with types.** Pure TypeScript, zero native dependencies, full declarations, and a custom-transport hook for proxies and tests.
|
|
41
41
|
|
|
@@ -88,6 +88,7 @@ The full API reference is at [honeybbq.github.io/teamspeak-js](https://honeybbq.
|
|
|
88
88
|
- [Configuration](https://honeybbq.github.io/teamspeak-js/documents/configuration.html): options, client version, tracing, diagnostics, reconnection, custom transports and server capabilities.
|
|
89
89
|
- [Streams](https://honeybbq.github.io/teamspeak-js/documents/streams.html): TeamSpeak 6 screen-share signalling, and interop with the TeamSpeak 6 client.
|
|
90
90
|
- [ServerQuery](https://honeybbq.github.io/teamspeak-js/documents/serverquery.html): the query client and its SSH host-key policy.
|
|
91
|
+
- [Flood control](https://honeybbq.github.io/teamspeak-js/documents/flood-control.html): what the server counts, what each command costs, and line middleware recipes for pacing, resending and keeping a query session open.
|
|
91
92
|
- [Design](https://honeybbq.github.io/teamspeak-js/documents/design.html): errors, ids, absent values, events, stability, and the server behaviour the SDK works around.
|
|
92
93
|
- [Compatibility](https://honeybbq.github.io/teamspeak-js/documents/compatibility.html): supported environments and the differences from the Go SDK.
|
|
93
94
|
- [Architecture](https://honeybbq.github.io/teamspeak-js/documents/architecture.html): source layout and entry points.
|
package/dist/client/client.d.ts
CHANGED
|
@@ -93,9 +93,9 @@ export declare class Client implements CommandExecutor, AsyncDisposable {
|
|
|
93
93
|
*
|
|
94
94
|
* Says goodbye first, waiting for the server's acknowledgement for a second
|
|
95
95
|
* at most, or less if `signal` aborts sooner. The goodbye is sent at once,
|
|
96
|
-
* past the
|
|
97
|
-
* clientinit is out; commands still
|
|
98
|
-
*
|
|
96
|
+
* past the line middleware, and also by a client still connecting whose
|
|
97
|
+
* clientinit is out; commands still inside a line middleware have their
|
|
98
|
+
* signal aborted, and reject with a NotConnectedError without being sent. The connection is closed
|
|
99
99
|
* either way. A `connect()` still waiting for its handshake rejects with a
|
|
100
100
|
* NotConnectedError, and so do the commands still waiting for an answer.
|
|
101
101
|
* Resolves once `disconnected` has been delivered to the listeners, so one
|
|
@@ -125,10 +125,9 @@ export declare class Client implements CommandExecutor, AsyncDisposable {
|
|
|
125
125
|
* Rejects with a NotConnectedError unless the client is connected: while it
|
|
126
126
|
* is still connecting, the session's keys may not exist yet.
|
|
127
127
|
*
|
|
128
|
-
* A command the server refuses
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* remains rejects as it is.
|
|
128
|
+
* The line passes the `lineMiddleware`, if any. A command the server refuses
|
|
129
|
+
* as flooding (`ErrorCode.ClientIsFlooding`) rejects like any server error;
|
|
130
|
+
* `floodWaitMs` reads the wait it names.
|
|
132
131
|
*/
|
|
133
132
|
exec(command: string, options?: CommandOptions): Promise<Record<string, string>[]>;
|
|
134
133
|
/**
|
|
@@ -136,9 +135,10 @@ export declare class Client implements CommandExecutor, AsyncDisposable {
|
|
|
136
135
|
* commands the server answers with nothing at all. `CommandName` lists
|
|
137
136
|
* the command names.
|
|
138
137
|
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
138
|
+
* The line passes the `lineMiddleware`, if any; aborting `signal` while it
|
|
139
|
+
* waits there rejects with the signal's reason and sends nothing. Resolves
|
|
140
|
+
* once the line has been handed to the connection. Rejects with a
|
|
141
|
+
* NotConnectedError unless the client is connected.
|
|
142
142
|
*/
|
|
143
143
|
execNoWait(command: string, options?: {
|
|
144
144
|
readonly signal?: AbortSignal | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client/client.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAOnB,MAAM,YAAY,CAAC;AAIpB,OAAO,EACL,KAAK,SAAS,EAEd,KAAK,QAAQ,EACb,KAAK,KAAK,EAGV,KAAK,cAAc,EACnB,KAAK,GAAG,EAGR,KAAK,QAAQ,EAGb,KAAK,WAAW,EAGhB,YAAY,EAKb,MAAM,qBAAqB,CAAC;AAG7B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client/client.js
CHANGED
|
@@ -10,7 +10,7 @@ import { RESOLVE_BUDGET_MS, Resolver } from "./discovery/resolver.js";
|
|
|
10
10
|
import { PacketHandler } from "./transport/handler.js";
|
|
11
11
|
import { PacketType } from "./transport/packet.js";
|
|
12
12
|
import { CommandTracker, appendReturnCode } from "./commands.js";
|
|
13
|
-
import { buildCommand, buildCommandChain,
|
|
13
|
+
import { buildCommand, buildCommandChain, buildLineChain, encodeRequest, parseServerError, resolveBackoffPolicy, } from "../core/internal.js";
|
|
14
14
|
import { uploadFileData, downloadFileData } from "./transfer.js";
|
|
15
15
|
import { buildSetConnectionInfo } from "./connection-info.js";
|
|
16
16
|
import { buildEventChain } from "./events.js";
|
|
@@ -18,9 +18,7 @@ import { processInit1 } from "./handshake/crypt-handshake.js";
|
|
|
18
18
|
import { handleHandshakeInitIV, handleHandshakeExpand2, defaultClientVersion, handleInitServer, prepareClientPassword, } from "./handshake.js";
|
|
19
19
|
import { handleNotification } from "../core/internal.js";
|
|
20
20
|
import { parseChannel, parseChannelGroup, parseCommand, parseServerGroup, parseUint16, } from "../core/internal.js";
|
|
21
|
-
import { CommandThrottle } from "./throttle.js";
|
|
22
21
|
import { splitCommandRows, sleep } from "../core/internal.js";
|
|
23
|
-
import { isAutoNicknameMatch } from "./helpers.js";
|
|
24
22
|
import { CommandTimeoutError, NotConnectedError } from "@honeybbq/teamspeak";
|
|
25
23
|
import { responseSpec } from "../core/internal.js";
|
|
26
24
|
import { unknownServerCapabilities } from "./capabilities.js";
|
|
@@ -241,7 +239,17 @@ export class Client {
|
|
|
241
239
|
/** Cuts short the reconnect loop's wait, on disconnect(). */
|
|
242
240
|
#stopReconnecting;
|
|
243
241
|
#status = ClientStatus.Disconnected;
|
|
244
|
-
|
|
242
|
+
/**
|
|
243
|
+
* Aborted when the session ends, by `disconnect()` or a drop: what a command
|
|
244
|
+
* still inside a line middleware sees as its signal. A new one for each
|
|
245
|
+
* connection attempt.
|
|
246
|
+
*/
|
|
247
|
+
#down = new AbortController();
|
|
248
|
+
/** The per-call state of the commands in the line chain, by their signal and their line. */
|
|
249
|
+
#calls = new WeakMap();
|
|
250
|
+
/** The line chain around the base that waits for the answer, and the one around the base that resolves once written. */
|
|
251
|
+
#line;
|
|
252
|
+
#lineNoWait;
|
|
245
253
|
#cmdTrack = new CommandTracker(() => this.#clid);
|
|
246
254
|
#viewStore;
|
|
247
255
|
#clients;
|
|
@@ -319,14 +327,16 @@ export class Client {
|
|
|
319
327
|
},
|
|
320
328
|
markConnected: (initServer) => this.#markConnected(initServer),
|
|
321
329
|
markHandshakeDone: (serverPublicKey) => this.#markHandshakeDone(serverPublicKey),
|
|
322
|
-
execNoWait: (command) => this.#execNoWait(command, undefined, true),
|
|
323
330
|
handlePacket: (packet) => this.#handlePacket(packet),
|
|
324
331
|
};
|
|
325
332
|
// Built once: the chains are read on every command and event, and
|
|
326
333
|
// changing them while those run would make the behaviour depend on timing.
|
|
327
334
|
const cmdMiddlewares = options.commandMiddleware ?? [];
|
|
328
335
|
this.#eventMiddlewares = options.eventMiddleware ?? [];
|
|
329
|
-
this.#send = buildCommandChain(cmdMiddlewares, (request, commandOptions = {}) => this.#exec(encodeRequest(request, prepareClientPassword), commandOptions
|
|
336
|
+
this.#send = buildCommandChain(cmdMiddlewares, (request, commandOptions = {}) => this.#exec(encodeRequest(request, prepareClientPassword), commandOptions));
|
|
337
|
+
const lineMiddlewares = options.lineMiddleware ?? [];
|
|
338
|
+
this.#line = buildLineChain(lineMiddlewares, (line, lineOptions) => this.#writeLine(line, lineOptions, false));
|
|
339
|
+
this.#lineNoWait = buildLineChain(lineMiddlewares, (line, lineOptions) => this.#writeNoWait(line, lineOptions));
|
|
330
340
|
this.#eventChain = buildEventChain([...this.#eventMiddlewares], (event) => this.#deliver(event.name, event.payload));
|
|
331
341
|
}
|
|
332
342
|
/**
|
|
@@ -504,9 +514,9 @@ export class Client {
|
|
|
504
514
|
*
|
|
505
515
|
* Says goodbye first, waiting for the server's acknowledgement for a second
|
|
506
516
|
* at most, or less if `signal` aborts sooner. The goodbye is sent at once,
|
|
507
|
-
* past the
|
|
508
|
-
* clientinit is out; commands still
|
|
509
|
-
*
|
|
517
|
+
* past the line middleware, and also by a client still connecting whose
|
|
518
|
+
* clientinit is out; commands still inside a line middleware have their
|
|
519
|
+
* signal aborted, and reject with a NotConnectedError without being sent. The connection is closed
|
|
510
520
|
* either way. A `connect()` still waiting for its handshake rejects with a
|
|
511
521
|
* NotConnectedError, and so do the commands still waiting for an answer.
|
|
512
522
|
* Resolves once `disconnected` has been delivered to the listeners, so one
|
|
@@ -538,9 +548,9 @@ export class Client {
|
|
|
538
548
|
const holdsSession = this.#status === ClientStatus.Connected ||
|
|
539
549
|
(this.#status === ClientStatus.Connecting && this.#crypt.cryptoInitComplete);
|
|
540
550
|
this.#status = ClientStatus.Disconnected;
|
|
541
|
-
// Whatever still waits
|
|
542
|
-
// goodbye does not
|
|
543
|
-
this.#
|
|
551
|
+
// Whatever still waits in a line middleware is aborted and never sent; the
|
|
552
|
+
// goodbye does not pass one.
|
|
553
|
+
this.#down.abort(new NotConnectedError());
|
|
544
554
|
let left;
|
|
545
555
|
this.#attempt.rejectAfter(new NotConnectedError(), new Promise((r) => (left = r)));
|
|
546
556
|
const leaving = this.#leave(holdsSession, options.signal, options.reasonMessage ?? "Shutdown").finally(left);
|
|
@@ -568,7 +578,7 @@ export class Client {
|
|
|
568
578
|
const answered = this.#exec(buildCommand("clientdisconnect", {
|
|
569
579
|
reasonid: String(Reason.Disconnect),
|
|
570
580
|
reasonmsg: reasonMessage,
|
|
571
|
-
}), { signal, timeoutMs: DISCONNECT_TIMEOUT_MS }, true
|
|
581
|
+
}), { signal, timeoutMs: DISCONNECT_TIMEOUT_MS }, true).then(() => true, () => false);
|
|
572
582
|
const acknowledged = await Promise.race([answered, left]);
|
|
573
583
|
this.#goodbyeAnswered = undefined;
|
|
574
584
|
this.#callHook(this.#trace?.disconnectDone, () => ({
|
|
@@ -597,44 +607,44 @@ export class Client {
|
|
|
597
607
|
* Rejects with a NotConnectedError unless the client is connected: while it
|
|
598
608
|
* is still connecting, the session's keys may not exist yet.
|
|
599
609
|
*
|
|
600
|
-
* A command the server refuses
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
* remains rejects as it is.
|
|
610
|
+
* The line passes the `lineMiddleware`, if any. A command the server refuses
|
|
611
|
+
* as flooding (`ErrorCode.ClientIsFlooding`) rejects like any server error;
|
|
612
|
+
* `floodWaitMs` reads the wait it names.
|
|
604
613
|
*/
|
|
605
614
|
// oxlint-disable-next-line typescript/array-type -- the public API report spells this `[]`; respelling it changes the report
|
|
606
615
|
exec(command, options = {}) {
|
|
607
|
-
return this.#exec(command, options
|
|
616
|
+
return this.#exec(command, options);
|
|
608
617
|
}
|
|
609
618
|
/**
|
|
610
619
|
* Send a raw command line without waiting for an answer, for the few
|
|
611
620
|
* commands the server answers with nothing at all. `CommandName` lists
|
|
612
621
|
* the command names.
|
|
613
622
|
*
|
|
614
|
-
*
|
|
615
|
-
*
|
|
616
|
-
*
|
|
623
|
+
* The line passes the `lineMiddleware`, if any; aborting `signal` while it
|
|
624
|
+
* waits there rejects with the signal's reason and sends nothing. Resolves
|
|
625
|
+
* once the line has been handed to the connection. Rejects with a
|
|
626
|
+
* NotConnectedError unless the client is connected.
|
|
617
627
|
*/
|
|
618
628
|
execNoWait(command, options = {}) {
|
|
619
|
-
return this.#execNoWait(command, options.signal
|
|
629
|
+
return this.#execNoWait(command, options.signal);
|
|
620
630
|
}
|
|
621
631
|
/**
|
|
622
|
-
* Whether a command may go out now. The
|
|
623
|
-
*
|
|
624
|
-
*
|
|
632
|
+
* Whether a command may go out now. The goodbye (`internal`) needs only an
|
|
633
|
+
* open socket; everything else needs a connected session, so nothing a
|
|
634
|
+
* caller sends can go out under the public dummy key, or after
|
|
625
635
|
* disconnect() has begun saying goodbye.
|
|
626
636
|
*/
|
|
627
637
|
#canSend(internal) {
|
|
628
638
|
return this.#handler.isOpen && (internal || this.#status === ClientStatus.Connected);
|
|
629
639
|
}
|
|
630
|
-
async #execNoWait(command, signal
|
|
640
|
+
async #execNoWait(command, signal) {
|
|
631
641
|
signal?.throwIfAborted();
|
|
632
|
-
if (!this.#canSend(
|
|
633
|
-
throw new NotConnectedError();
|
|
634
|
-
await this.#throttle.wait(signal);
|
|
635
|
-
if (!this.#canSend(internal))
|
|
642
|
+
if (!this.#canSend(false))
|
|
636
643
|
throw new NotConnectedError();
|
|
637
|
-
|
|
644
|
+
const name = parseCommand(command)?.name ?? "";
|
|
645
|
+
// No timeout, as ever for a command nobody waits an answer for: the line
|
|
646
|
+
// middleware and the caller's signal are what can hold it.
|
|
647
|
+
await this.#runLine(name, command, { signal, noWait: true });
|
|
638
648
|
}
|
|
639
649
|
/**
|
|
640
650
|
* Send a typed command; this is the CommandExecutor the typed commands run on.
|
|
@@ -654,94 +664,130 @@ export class Client {
|
|
|
654
664
|
hook(this, line);
|
|
655
665
|
this.#handler.sendPacket(PacketType.Command, Buffer.from(line), 0);
|
|
656
666
|
}
|
|
657
|
-
#exec(command, options,
|
|
667
|
+
#exec(command, options, goodbye = false) {
|
|
658
668
|
if (!commandChannel.hasSubscribers) {
|
|
659
|
-
return this.#execUntraced(command, options,
|
|
669
|
+
return this.#execUntraced(command, options, goodbye);
|
|
660
670
|
}
|
|
661
671
|
const trace = {
|
|
662
672
|
command: parseCommand(command)?.name ?? "",
|
|
663
673
|
client: this,
|
|
664
674
|
};
|
|
665
|
-
return commandChannel.tracePromise(() => this.#execUntraced(command, options,
|
|
675
|
+
return commandChannel.tracePromise(() => this.#execUntraced(command, options, goodbye), trace);
|
|
666
676
|
}
|
|
667
|
-
async #execUntraced(command, options,
|
|
677
|
+
async #execUntraced(command, options, goodbye) {
|
|
668
678
|
const { signal, timeoutMs = DEFAULT_COMMAND_TIMEOUT_MS, onSent } = options;
|
|
669
679
|
signal?.throwIfAborted();
|
|
670
680
|
// A command sent on no socket used to vanish and surface 10 seconds later
|
|
671
681
|
// as a timeout, which reads like a slow server rather than a usage error.
|
|
672
|
-
if (!this.#canSend(
|
|
682
|
+
if (!this.#canSend(goodbye))
|
|
683
|
+
throw new NotConnectedError();
|
|
684
|
+
const name = parseCommand(command)?.name ?? "";
|
|
685
|
+
return this.#runLine(name, command, { signal, timeoutMs, onSent, goodbye });
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Run one line through the line middleware, or straight to the connection
|
|
689
|
+
* for the goodbye. The timeout, the caller's signal and the end of the
|
|
690
|
+
* session each abort the signal the middleware is given, and the call
|
|
691
|
+
* rejects with the reason: the timeout bounds the whole chain, the waits of
|
|
692
|
+
* a middleware included.
|
|
693
|
+
*/
|
|
694
|
+
async #runLine(name, text, call) {
|
|
695
|
+
const { signal, timeoutMs, goodbye = false } = call;
|
|
696
|
+
const abort = new AbortController();
|
|
697
|
+
const down = this.#down.signal;
|
|
698
|
+
const stop = (reason) => abort.abort(reason);
|
|
699
|
+
const timer = timeoutMs === undefined
|
|
700
|
+
? undefined
|
|
701
|
+
: setTimeout(() => stop(new CommandTimeoutError(name)), timeoutMs);
|
|
702
|
+
const onCaller = () => stop(abortReason(signal));
|
|
703
|
+
// The goodbye leaves as the session ends, which is what aborts `down`.
|
|
704
|
+
const onDown = () => stop(down.reason);
|
|
705
|
+
signal?.addEventListener("abort", onCaller, { once: true });
|
|
706
|
+
if (!goodbye)
|
|
707
|
+
down.addEventListener("abort", onDown, { once: true });
|
|
708
|
+
if (!goodbye && down.aborted)
|
|
709
|
+
onDown();
|
|
710
|
+
// A middleware that ignores its signal must not hold the call past it.
|
|
711
|
+
const gaveUp = new Promise((_, reject) => {
|
|
712
|
+
abort.signal.addEventListener("abort", () => reject(abortReason(abort.signal)), {
|
|
713
|
+
once: true,
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
gaveUp.catch(() => { });
|
|
717
|
+
const line = { name, text };
|
|
718
|
+
const state = { onSent: call.onSent, sent: false };
|
|
719
|
+
this.#calls.set(line, state);
|
|
720
|
+
this.#calls.set(abort.signal, state);
|
|
721
|
+
try {
|
|
722
|
+
// The goodbye goes to its own base: it passes no middleware, and may go
|
|
723
|
+
// out while the status says Disconnected.
|
|
724
|
+
const send = goodbye
|
|
725
|
+
? (l, o) => this.#writeLine(l, o, true)
|
|
726
|
+
: call.noWait === true
|
|
727
|
+
? this.#lineNoWait
|
|
728
|
+
: this.#line;
|
|
729
|
+
return await Promise.race([send(line, { signal: abort.signal }), gaveUp]);
|
|
730
|
+
}
|
|
731
|
+
finally {
|
|
732
|
+
clearTimeout(timer);
|
|
733
|
+
signal?.removeEventListener("abort", onCaller);
|
|
734
|
+
down.removeEventListener("abort", onDown);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
/** Tell the call a line belongs to that its line was handed to the connection, the first time only. */
|
|
738
|
+
#sent(line, signal) {
|
|
739
|
+
const call = this.#calls.get(line) ?? this.#calls.get(signal);
|
|
740
|
+
if (call === undefined || call.sent)
|
|
741
|
+
return;
|
|
742
|
+
call.sent = true;
|
|
743
|
+
call.onSent?.();
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* The base of the line chain for a line nobody answers: write it, and
|
|
747
|
+
* resolve with no rows once it is written.
|
|
748
|
+
*/
|
|
749
|
+
async #writeNoWait(line, options) {
|
|
750
|
+
options.signal.throwIfAborted();
|
|
751
|
+
if (!this.#canSend(false))
|
|
752
|
+
throw new NotConnectedError();
|
|
753
|
+
this.#sendLine(line.text);
|
|
754
|
+
this.#sent(line, options.signal);
|
|
755
|
+
return [];
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* The base of the line chain: write the line, with a return code of its own
|
|
759
|
+
* each time it is called, and read its answer. Looks at the connection once
|
|
760
|
+
* more, as a middleware may have waited, and ends when the signal does.
|
|
761
|
+
*/
|
|
762
|
+
async #writeLine(line, options, goodbye) {
|
|
763
|
+
const { signal } = options;
|
|
764
|
+
signal.throwIfAborted();
|
|
765
|
+
if (!this.#canSend(goodbye))
|
|
673
766
|
throw new NotConnectedError();
|
|
674
767
|
// Which notifications answer this command, and — for answers that arrive
|
|
675
768
|
// after the `error` — the request field they echo back.
|
|
676
|
-
const parsed = parseCommand(
|
|
677
|
-
const spec = responseSpec(
|
|
769
|
+
const parsed = parseCommand(line.text);
|
|
770
|
+
const spec = responseSpec(line.name);
|
|
678
771
|
const key = spec?.correlated?.key;
|
|
679
772
|
const correlation = key === undefined ? undefined : parsed?.params[key];
|
|
680
773
|
const echo = spec?.echoKey === undefined ? undefined : parsed?.params[spec.echoKey];
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
let timer;
|
|
684
|
-
let onAbort;
|
|
685
|
-
// Started before the throttle: the timeout bounds the whole command, its
|
|
686
|
-
// wait for a turn included, and so does the signal.
|
|
687
|
-
const givenUp = new Promise((_, reject) => {
|
|
688
|
-
timer = setTimeout(() => reject(new CommandTimeoutError(parsed?.name ?? "")), timeoutMs);
|
|
689
|
-
onAbort = () => reject(abortReason(signal));
|
|
690
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
691
|
-
});
|
|
692
|
-
givenUp.catch(() => { });
|
|
693
|
-
const throttleAbort = new AbortController();
|
|
774
|
+
const [rc, answered] = this.#cmdTrack.register(spec, correlation, echo);
|
|
775
|
+
answered.catch(() => { });
|
|
694
776
|
try {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
if (refused === 0)
|
|
706
|
-
onSent?.();
|
|
707
|
-
const result = await Promise.race([answered, givenUp]);
|
|
708
|
-
// Refused as flooding with a time to wait, which the command can
|
|
709
|
-
// afford: nothing is sent until then, and the command, which the
|
|
710
|
-
// server did not run, goes out again.
|
|
711
|
-
const wait = result.err ? floodRetryDelayMs(result.err) : undefined;
|
|
712
|
-
const hold = wait === undefined ? undefined : floodHoldMs(wait);
|
|
713
|
-
if (hold !== undefined &&
|
|
714
|
-
!goodbye &&
|
|
715
|
-
refused < FLOOD_RETRIES &&
|
|
716
|
-
hold < deadline - performance.now()) {
|
|
717
|
-
this.#logger.debug("the server asked for a pause", {
|
|
718
|
-
command: parsed?.name,
|
|
719
|
-
waitMs: wait,
|
|
720
|
-
});
|
|
721
|
-
this.#throttle.hold(hold);
|
|
722
|
-
this.#cmdTrack.unregister(rc);
|
|
723
|
-
[rc, answered] = this.#cmdTrack.register(spec, correlation, echo);
|
|
724
|
-
continue;
|
|
725
|
-
}
|
|
726
|
-
if (result.err)
|
|
727
|
-
throw result.err;
|
|
728
|
-
// The server took the rename, so it is what this client now asks for.
|
|
729
|
-
if (!internal && parsed?.name === "clientupdate") {
|
|
730
|
-
const renamed = parsed.params["client_nickname"];
|
|
731
|
-
if (renamed !== undefined)
|
|
732
|
-
this.#requestedNickname = renamed;
|
|
733
|
-
}
|
|
734
|
-
return result.data;
|
|
777
|
+
this.#sendLine(appendReturnCode(line.text, rc));
|
|
778
|
+
this.#sent(line, signal);
|
|
779
|
+
const result = await abortable(answered, signal);
|
|
780
|
+
if (result.err)
|
|
781
|
+
throw result.err;
|
|
782
|
+
// The server took the rename, so it is what this client now asks for.
|
|
783
|
+
if (!goodbye && line.name === "clientupdate") {
|
|
784
|
+
const renamed = parsed?.params["client_nickname"];
|
|
785
|
+
if (renamed !== undefined)
|
|
786
|
+
this.#requestedNickname = renamed;
|
|
735
787
|
}
|
|
788
|
+
return result.data;
|
|
736
789
|
}
|
|
737
790
|
finally {
|
|
738
|
-
// A command given up while it waited for its turn gives the turn back.
|
|
739
|
-
throttleAbort.abort();
|
|
740
|
-
// Cleared, not left to fire: a pending timer keeps the process alive for
|
|
741
|
-
// the full timeout after the last command has long been answered.
|
|
742
|
-
clearTimeout(timer);
|
|
743
|
-
if (onAbort)
|
|
744
|
-
signal?.removeEventListener("abort", onAbort);
|
|
745
791
|
this.#cmdTrack.unregister(rc);
|
|
746
792
|
}
|
|
747
793
|
}
|
|
@@ -1175,6 +1221,8 @@ export class Client {
|
|
|
1175
1221
|
this.#handler = this.#newHandler();
|
|
1176
1222
|
this.#attempt = newConnectAttempt();
|
|
1177
1223
|
this.#cmdTrack.reset(new NotConnectedError());
|
|
1224
|
+
this.#down.abort(new NotConnectedError());
|
|
1225
|
+
this.#down = new AbortController();
|
|
1178
1226
|
this.#viewStore.reset();
|
|
1179
1227
|
this.#clid = 0;
|
|
1180
1228
|
this.#capabilities = unknownServerCapabilities();
|
|
@@ -1470,7 +1518,7 @@ export class Client {
|
|
|
1470
1518
|
*/
|
|
1471
1519
|
#answerConnectionInfoRequest() {
|
|
1472
1520
|
const line = buildSetConnectionInfo(this.#handler.trafficSnapshot(), this.#handler.roundTrip);
|
|
1473
|
-
this.#execNoWait(line, undefined
|
|
1521
|
+
this.#execNoWait(line, undefined).catch((error) => {
|
|
1474
1522
|
this.#logger.debug("could not answer a connection info request", {
|
|
1475
1523
|
error: String(error),
|
|
1476
1524
|
});
|
|
@@ -1507,12 +1555,12 @@ export class Client {
|
|
|
1507
1555
|
switch (result.kind) {
|
|
1508
1556
|
case "clientEnter": {
|
|
1509
1557
|
const info = result.info;
|
|
1510
|
-
// Identify self by the clid initserver
|
|
1511
|
-
//
|
|
1512
|
-
//
|
|
1513
|
-
//
|
|
1514
|
-
//
|
|
1515
|
-
//
|
|
1558
|
+
// Identify self by the clid initserver assigned, never by nickname: any
|
|
1559
|
+
// other client whose nickname happens to be "<our nick><digits>" — which
|
|
1560
|
+
// is how the server renames a client whose nickname was taken — would
|
|
1561
|
+
// otherwise be mistaken for us, poisoning every outgoing packet header
|
|
1562
|
+
// with a foreign clientID until the server drops the connection for
|
|
1563
|
+
// command-resend timeout.
|
|
1516
1564
|
//
|
|
1517
1565
|
// This no longer marks the end of the welcome sequence: the server
|
|
1518
1566
|
// sends more welcome data after it, and `channellistfinished` is the
|
|
@@ -1522,15 +1570,6 @@ export class Client {
|
|
|
1522
1570
|
// the nickname asked for stays as it was.
|
|
1523
1571
|
this.#nickname = info.nickname;
|
|
1524
1572
|
}
|
|
1525
|
-
else if (this.#clid === 0 &&
|
|
1526
|
-
isAutoNicknameMatch(this.#requestedNickname, info.nickname)) {
|
|
1527
|
-
// initserver carried no aclid, so the welcome enter is the only way
|
|
1528
|
-
// to learn our clid. Gated on clid still being unknown so a later
|
|
1529
|
-
// lookalike can never hijack an established session.
|
|
1530
|
-
this.#clid = info.id;
|
|
1531
|
-
this.#handler.setClientID(info.id);
|
|
1532
|
-
this.#nickname = info.nickname;
|
|
1533
|
-
}
|
|
1534
1573
|
this.#emitServerEvent({ name: "clientEnter", payload: info });
|
|
1535
1574
|
break;
|
|
1536
1575
|
}
|
|
@@ -1785,6 +1824,7 @@ export class Client {
|
|
|
1785
1824
|
// Nothing in flight can be answered any more; failing it now, with the
|
|
1786
1825
|
// reason, beats every caller timing out one by one.
|
|
1787
1826
|
this.#cmdTrack.reset(new NotConnectedError(err ? { cause: err } : undefined));
|
|
1827
|
+
this.#down.abort(new NotConnectedError(err ? { cause: err } : undefined));
|
|
1788
1828
|
if (this.#status !== ClientStatus.Connected) {
|
|
1789
1829
|
// The socket closed before the handshake finished, so this is a failed
|
|
1790
1830
|
// connection attempt rather than a disconnect of a live session.
|