@honeybbq/teamspeak-client 0.3.0 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/README.md +322 -60
  2. package/dist/abort.d.ts +5 -0
  3. package/dist/abort.d.ts.map +1 -0
  4. package/dist/address.d.ts +8 -0
  5. package/dist/address.d.ts.map +1 -1
  6. package/dist/api/voice.d.ts +24 -0
  7. package/dist/api/voice.d.ts.map +1 -0
  8. package/dist/capabilities.d.ts +62 -0
  9. package/dist/capabilities.d.ts.map +1 -0
  10. package/dist/client.d.ts +172 -39
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/commands.d.ts +45 -27
  13. package/dist/commands.d.ts.map +1 -1
  14. package/dist/crypto/crypt.d.ts +19 -4
  15. package/dist/crypto/crypt.d.ts.map +1 -1
  16. package/dist/crypto/eax.d.ts.map +1 -1
  17. package/dist/crypto/identity.d.ts +87 -8
  18. package/dist/crypto/identity.d.ts.map +1 -1
  19. package/dist/crypto/index.d.ts +2 -1
  20. package/dist/crypto/index.d.ts.map +1 -1
  21. package/dist/diagnostics.d.ts +45 -0
  22. package/dist/diagnostics.d.ts.map +1 -0
  23. package/dist/discovery/resolver.d.ts +21 -3
  24. package/dist/discovery/resolver.d.ts.map +1 -1
  25. package/dist/errors.d.ts +70 -16
  26. package/dist/errors.d.ts.map +1 -1
  27. package/dist/events.d.ts +4 -5
  28. package/dist/events.d.ts.map +1 -1
  29. package/dist/handshake/crypt-handshake.d.ts +1 -0
  30. package/dist/handshake/crypt-handshake.d.ts.map +1 -1
  31. package/dist/handshake/crypt-init2.d.ts.map +1 -1
  32. package/dist/handshake/license.d.ts +7 -0
  33. package/dist/handshake/license.d.ts.map +1 -1
  34. package/dist/handshake.d.ts +14 -5
  35. package/dist/handshake.d.ts.map +1 -1
  36. package/dist/helpers.d.ts +0 -8
  37. package/dist/helpers.d.ts.map +1 -1
  38. package/dist/index.d.ts +20 -6
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +3967 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/notifications.d.ts +7 -15
  43. package/dist/notifications.d.ts.map +1 -1
  44. package/dist/reconnect.d.ts +69 -0
  45. package/dist/reconnect.d.ts.map +1 -0
  46. package/dist/throttle.d.ts +9 -0
  47. package/dist/throttle.d.ts.map +1 -1
  48. package/dist/transfer.d.ts +27 -25
  49. package/dist/transfer.d.ts.map +1 -1
  50. package/dist/transport/generation-window.d.ts.map +1 -1
  51. package/dist/transport/handler.d.ts +37 -5
  52. package/dist/transport/handler.d.ts.map +1 -1
  53. package/dist/transport/quicklz.d.ts +19 -0
  54. package/dist/transport/quicklz.d.ts.map +1 -1
  55. package/dist/types.d.ts +277 -97
  56. package/dist/types.d.ts.map +1 -1
  57. package/package.json +21 -57
  58. package/src/abort.ts +24 -0
  59. package/src/address.ts +74 -0
  60. package/src/api/voice.ts +61 -0
  61. package/src/capabilities.ts +92 -0
  62. package/src/client.ts +1392 -0
  63. package/src/commands.ts +193 -0
  64. package/src/crypto/crypt-ops.ts +6 -0
  65. package/src/crypto/crypt.ts +274 -0
  66. package/src/crypto/eax.ts +152 -0
  67. package/src/crypto/identity.ts +484 -0
  68. package/src/crypto/index.ts +21 -0
  69. package/src/crypto/primitives.ts +141 -0
  70. package/src/diagnostics.ts +58 -0
  71. package/src/discovery/index.ts +1 -0
  72. package/src/discovery/resolver.ts +272 -0
  73. package/src/errors.ts +156 -0
  74. package/src/events.ts +28 -0
  75. package/src/handshake/crypt-handshake.ts +135 -0
  76. package/src/handshake/crypt-init2.ts +48 -0
  77. package/src/handshake/index.ts +3 -0
  78. package/src/handshake/license.test.ts +49 -0
  79. package/src/handshake/license.ts +208 -0
  80. package/src/handshake.ts +136 -0
  81. package/src/helpers.ts +10 -0
  82. package/src/index.ts +74 -0
  83. package/src/notifications.ts +136 -0
  84. package/src/reconnect.ts +141 -0
  85. package/src/throttle.ts +92 -0
  86. package/src/transfer.ts +127 -0
  87. package/src/transport/generation-window.ts +77 -0
  88. package/src/transport/handler.ts +914 -0
  89. package/src/transport/index.ts +14 -0
  90. package/src/transport/packet.ts +69 -0
  91. package/src/transport/quicklz.ts +209 -0
  92. package/src/types.ts +349 -0
  93. package/dist/address-BR-5unVo.cjs +0 -2
  94. package/dist/address-BR-5unVo.cjs.map +0 -1
  95. package/dist/address-BfmovVpp.js +0 -65
  96. package/dist/address-BfmovVpp.js.map +0 -1
  97. package/dist/address.test.d.ts +0 -2
  98. package/dist/address.test.d.ts.map +0 -1
  99. package/dist/api.d.ts +0 -17
  100. package/dist/api.d.ts.map +0 -1
  101. package/dist/api.test.d.ts +0 -2
  102. package/dist/api.test.d.ts.map +0 -1
  103. package/dist/command/command.d.ts +0 -12
  104. package/dist/command/command.d.ts.map +0 -1
  105. package/dist/command/command.test.d.ts +0 -2
  106. package/dist/command/command.test.d.ts.map +0 -1
  107. package/dist/command/index.cjs +0 -1
  108. package/dist/command/index.d.ts +0 -4
  109. package/dist/command/index.d.ts.map +0 -1
  110. package/dist/command/index.mjs +0 -3
  111. package/dist/command/parser.d.ts +0 -3
  112. package/dist/command/parser.d.ts.map +0 -1
  113. package/dist/command-BLe-C2L2.js +0 -74
  114. package/dist/command-BLe-C2L2.js.map +0 -1
  115. package/dist/command-DAK4pUBP.cjs +0 -4
  116. package/dist/command-DAK4pUBP.cjs.map +0 -1
  117. package/dist/crypt-handshake-CQkQreLx.cjs +0 -2
  118. package/dist/crypt-handshake-CQkQreLx.cjs.map +0 -1
  119. package/dist/crypt-handshake-DziOY4Lq.js +0 -55
  120. package/dist/crypt-handshake-DziOY4Lq.js.map +0 -1
  121. package/dist/crypt-init2-D70ha4LB.js +0 -158
  122. package/dist/crypt-init2-D70ha4LB.js.map +0 -1
  123. package/dist/crypt-init2-DparLekm.cjs +0 -2
  124. package/dist/crypt-init2-DparLekm.cjs.map +0 -1
  125. package/dist/crypto/eax.test.d.ts +0 -2
  126. package/dist/crypto/eax.test.d.ts.map +0 -1
  127. package/dist/crypto/identity.test.d.ts +0 -2
  128. package/dist/crypto/identity.test.d.ts.map +0 -1
  129. package/dist/crypto/index.cjs +0 -1
  130. package/dist/crypto/index.mjs +0 -3
  131. package/dist/crypto-Bg02m4ZE.js +0 -165
  132. package/dist/crypto-Bg02m4ZE.js.map +0 -1
  133. package/dist/crypto-CDlweUL3.cjs +0 -2
  134. package/dist/crypto-CDlweUL3.cjs.map +0 -1
  135. package/dist/discovery/index.cjs +0 -1
  136. package/dist/discovery/index.mjs +0 -2
  137. package/dist/handler-DI_abe9c.js +0 -483
  138. package/dist/handler-DI_abe9c.js.map +0 -1
  139. package/dist/handler-DuwoO5RC.cjs +0 -2
  140. package/dist/handler-DuwoO5RC.cjs.map +0 -1
  141. package/dist/handshake/index.cjs +0 -1
  142. package/dist/handshake/index.mjs +0 -3
  143. package/dist/handshake.test.d.ts +0 -2
  144. package/dist/handshake.test.d.ts.map +0 -1
  145. package/dist/helpers.test.d.ts +0 -2
  146. package/dist/helpers.test.d.ts.map +0 -1
  147. package/dist/index.cjs +0 -4
  148. package/dist/index.cjs.map +0 -1
  149. package/dist/index.mjs +0 -796
  150. package/dist/index.mjs.map +0 -1
  151. package/dist/integration.test.d.ts +0 -12
  152. package/dist/integration.test.d.ts.map +0 -1
  153. package/dist/notifications.test.d.ts +0 -2
  154. package/dist/notifications.test.d.ts.map +0 -1
  155. package/dist/parser-CL33R9tr.cjs +0 -2
  156. package/dist/parser-CL33R9tr.cjs.map +0 -1
  157. package/dist/parser-DZMv3Jm-.js +0 -33
  158. package/dist/parser-DZMv3Jm-.js.map +0 -1
  159. package/dist/primitives-cNNAeFCo.cjs +0 -2
  160. package/dist/primitives-cNNAeFCo.cjs.map +0 -1
  161. package/dist/primitives-xIr6Tjk4.js +0 -1457
  162. package/dist/primitives-xIr6Tjk4.js.map +0 -1
  163. package/dist/resolver-6B8dI9gr.js +0 -141
  164. package/dist/resolver-6B8dI9gr.js.map +0 -1
  165. package/dist/resolver-DCcmyWSK.cjs +0 -4
  166. package/dist/resolver-DCcmyWSK.cjs.map +0 -1
  167. package/dist/throttle.test.d.ts +0 -2
  168. package/dist/throttle.test.d.ts.map +0 -1
  169. package/dist/transport/generation-window.test.d.ts +0 -2
  170. package/dist/transport/generation-window.test.d.ts.map +0 -1
  171. package/dist/transport/index.cjs +0 -1
  172. package/dist/transport/index.mjs +0 -2
  173. package/dist/transport/packet.test.d.ts +0 -2
  174. package/dist/transport/packet.test.d.ts.map +0 -1
  175. package/dist/transport/quicklz.test.d.ts +0 -2
  176. package/dist/transport/quicklz.test.d.ts.map +0 -1
  177. package/dist/transport/whisper.test.d.ts +0 -2
  178. package/dist/transport/whisper.test.d.ts.map +0 -1
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Reconnection policy.
3
+ *
4
+ * A dropped connection is normal on a UDP voice session, so a bot that cannot
5
+ * recover is not usable unattended. Reconnecting is opt-in rather than
6
+ * automatic: a silent reconnect makes a dropped session look like a healthy
7
+ * one, and a caller that wanted to know about the drop never finds out.
8
+ *
9
+ * Backoff is not a nicety here. TeamSpeak bans an IP that opens several
10
+ * connections in quick succession — measured at roughly three, after which the
11
+ * server answers `error id=3329` and refuses everything. A reconnect loop
12
+ * without a delay therefore bans itself, and the ban outlasts the outage it was
13
+ * recovering from.
14
+ */
15
+
16
+ import { ErrorCode, Reason, ServerError } from "@honeybbq/teamspeak-core";
17
+ import { AlreadyConnectedError, InvalidAddressError, RemovedError } from "./errors.js";
18
+
19
+ /** How long to wait before each attempt. */
20
+ export interface BackoffPolicy {
21
+ /** Delay before the first retry, in milliseconds. */
22
+ readonly initialDelayMs: number;
23
+ /** Ceiling for the delay; it never grows past this. */
24
+ readonly maxDelayMs: number;
25
+ /** Multiplier applied after each failed attempt. */
26
+ readonly factor: number;
27
+ /**
28
+ * Fraction of the delay to randomise, 0 to 1.
29
+ *
30
+ * A fleet of bots that drop together would otherwise retry in lockstep and
31
+ * hit the server in a burst. Jitter spreads them out.
32
+ */
33
+ readonly jitter: number;
34
+ /** Give up after this many consecutive failures. 0 means never give up. */
35
+ readonly maxAttempts: number;
36
+ }
37
+
38
+ /** One second, doubling, capped at a minute, with 20% jitter, never giving up. */
39
+ export const defaultBackoffPolicy: Readonly<BackoffPolicy> = Object.freeze({
40
+ initialDelayMs: 1_000,
41
+ maxDelayMs: 60_000,
42
+ factor: 2,
43
+ jitter: 0.2,
44
+ maxAttempts: 0,
45
+ });
46
+
47
+ /**
48
+ * How a client reconnects after its connection drops. Supplying it, even as
49
+ * `{}`, turns reconnecting on; a deliberate `disconnect()` never reconnects.
50
+ */
51
+ export interface ReconnectOptions {
52
+ /** Tuning for the retry delays; unset fields keep {@link defaultBackoffPolicy}'s. */
53
+ readonly backoff?:
54
+ | { readonly [K in keyof BackoffPolicy]?: BackoffPolicy[K] | undefined }
55
+ | undefined;
56
+ /**
57
+ * Decide whether a given failure is worth retrying. Returning false stops the
58
+ * loop. Defaults to {@link defaultShouldRetry}.
59
+ */
60
+ readonly shouldRetry?: ((error: Error | undefined, attempt: number) => boolean) | undefined;
61
+ }
62
+
63
+ /**
64
+ * Compute the delay before an attempt.
65
+ *
66
+ * `attempt` is 1-based: the delay before the first retry is `initialDelayMs`,
67
+ * give or take the jitter, and the delay never grows past `maxDelayMs`.
68
+ */
69
+ export function backoffDelay(policy: Readonly<BackoffPolicy>, attempt: number): number {
70
+ return jitteredDelay(policy, attempt, Math.random);
71
+ }
72
+
73
+ /** @internal The delay, with `random` supplying the jitter. */
74
+ export function jitteredDelay(
75
+ policy: Readonly<BackoffPolicy>,
76
+ attempt: number,
77
+ random: () => number,
78
+ ): number {
79
+ const exponent = Math.max(0, attempt - 1);
80
+ const raw = policy.initialDelayMs * policy.factor ** exponent;
81
+ const capped = Math.min(raw, policy.maxDelayMs);
82
+
83
+ if (policy.jitter <= 0) return Math.round(capped);
84
+
85
+ // Full jitter would ignore the computed delay; this keeps the trend while
86
+ // spreading peers, so a whole fleet does not retry on the same millisecond.
87
+ // Kept under the ceiling, which a caller reasonably reads as a promise.
88
+ const spread = capped * Math.min(policy.jitter, 1);
89
+ const delay = capped + (random() * 2 - 1) * spread;
90
+ return Math.round(Math.max(0, Math.min(delay, policy.maxDelayMs)));
91
+ }
92
+
93
+ /**
94
+ * Server errors where retrying cannot help.
95
+ *
96
+ * Deliberately a short list of codes known to be refusals, rather than "any
97
+ * ServerError". The server also answers transiently: after a dropped
98
+ * connection it still holds the old session, and a reconnect with the same
99
+ * identity is refused with 521 "too many clones already connected" until it
100
+ * reaps the zombie — a condition that clears itself and is worth retrying.
101
+ *
102
+ * The asymmetry is deliberate. Retrying a permanent failure costs a few bounded
103
+ * attempts; not retrying a transient one means never recovering.
104
+ */
105
+ const PERMANENT_SERVER_ERRORS: ReadonlySet<number> = new Set([
106
+ ErrorCode.ConnectFailedBanned,
107
+ ErrorCode.BanFlooding,
108
+ ErrorCode.ServerInvalidPassword,
109
+ ErrorCode.ChannelInvalidPassword,
110
+ ErrorCode.PermissionsClientInsufficient,
111
+ ]);
112
+
113
+ /**
114
+ * Whether a failure is worth another attempt.
115
+ *
116
+ * Transport failures are retried; so is a server that stopped, and a server
117
+ * error other than the known refusals — a ban, flooding, a wrong server or
118
+ * channel password, missing permissions — since some, such as 521 "too many
119
+ * clones" right after a drop, clear by themselves. A kick or a ban is not:
120
+ * someone removed the client on purpose. Nor is an AlreadyConnectedError: the
121
+ * client was connected by other means; nor an InvalidAddressError.
122
+ */
123
+ export function defaultShouldRetry(error: Error | undefined): boolean {
124
+ // The client is connected, or connecting, already: there is nothing to retry.
125
+ if (error instanceof AlreadyConnectedError) return false;
126
+ // An address that cannot be one stays that way.
127
+ if (error instanceof InvalidAddressError) return false;
128
+ if (error instanceof RemovedError) {
129
+ return error.reason !== Reason.KickServer && error.reason !== Reason.KickServerBan;
130
+ }
131
+ return !(error instanceof ServerError && PERMANENT_SERVER_ERRORS.has(error.code));
132
+ }
133
+
134
+ export function resolveBackoffPolicy(options: ReconnectOptions): BackoffPolicy {
135
+ const policy: Record<string, number> = { ...defaultBackoffPolicy };
136
+ // An explicit undefined keeps the default, as an unset field does.
137
+ for (const [key, value] of Object.entries(options.backoff ?? {})) {
138
+ if (value !== undefined) policy[key] = value;
139
+ }
140
+ return policy as unknown as BackoffPolicy;
141
+ }
@@ -0,0 +1,92 @@
1
+ interface Waiter {
2
+ readonly resolve: () => void;
3
+ readonly signal: AbortSignal | undefined;
4
+ readonly onAbort: () => void;
5
+ }
6
+
7
+ /**
8
+ * Token-bucket limiter for outbound TS3 commands.
9
+ * Mirrors Go's commandThrottle.
10
+ *
11
+ * Waiters are released in the order they arrived: each used to poll for a
12
+ * token on its own timer, so a command that came later could take the turn of
13
+ * one that had waited longer, and the commands went out of order.
14
+ */
15
+ export class CommandThrottle {
16
+ static readonly TOKEN_RATE = 4.0; // tokens per second
17
+ static readonly TOKEN_MAX = 8.0; // bucket capacity
18
+
19
+ #tokens = 5.0;
20
+ #lastUpdate = Date.now();
21
+ readonly #waiters: Waiter[] = [];
22
+ #timer: ReturnType<typeof setTimeout> | undefined;
23
+
24
+ /**
25
+ * Wait for a token, behind every earlier waiter. Aborting `signal` rejects
26
+ * with its reason and gives the place in line up; the abort listener is
27
+ * removed however the wait ends.
28
+ */
29
+ wait(signal?: AbortSignal): Promise<void> {
30
+ if (signal?.aborted) return Promise.reject(signal.reason);
31
+ this.#refill();
32
+ if (this.#waiters.length === 0 && this.#tokens >= 1.0) {
33
+ this.#tokens -= 1.0;
34
+ return Promise.resolve();
35
+ }
36
+ return new Promise<void>((resolve, reject) => {
37
+ const waiter: Waiter = {
38
+ resolve,
39
+ signal,
40
+ onAbort: () => {
41
+ const index = this.#waiters.indexOf(waiter);
42
+ if (index >= 0) this.#waiters.splice(index, 1);
43
+ if (this.#waiters.length === 0) this.#stopTimer();
44
+ reject(signal!.reason);
45
+ },
46
+ };
47
+ signal?.addEventListener("abort", waiter.onAbort, { once: true });
48
+ this.#waiters.push(waiter);
49
+ this.#schedule();
50
+ });
51
+ }
52
+
53
+ #refill(): void {
54
+ const now = Date.now();
55
+ const elapsed = (now - this.#lastUpdate) / 1000;
56
+ this.#tokens = Math.min(
57
+ this.#tokens + elapsed * CommandThrottle.TOKEN_RATE,
58
+ CommandThrottle.TOKEN_MAX,
59
+ );
60
+ this.#lastUpdate = now;
61
+ }
62
+
63
+ /** Arm the timer for when the next token is due, unless it already is. */
64
+ #schedule(): void {
65
+ if (this.#timer !== undefined || this.#waiters.length === 0) return;
66
+ const waitMs =
67
+ this.#tokens >= 1.0
68
+ ? 0
69
+ : Math.ceil(((1.0 - this.#tokens) / CommandThrottle.TOKEN_RATE) * 1000) + 10;
70
+ this.#timer = setTimeout(() => {
71
+ this.#timer = undefined;
72
+ this.#release();
73
+ }, waitMs);
74
+ }
75
+
76
+ #stopTimer(): void {
77
+ clearTimeout(this.#timer);
78
+ this.#timer = undefined;
79
+ }
80
+
81
+ /** Hand the tokens there are to the waiters at the head of the line. */
82
+ #release(): void {
83
+ this.#refill();
84
+ while (this.#waiters.length > 0 && this.#tokens >= 1.0) {
85
+ this.#tokens -= 1.0;
86
+ const waiter = this.#waiters.shift()!;
87
+ waiter.signal?.removeEventListener("abort", waiter.onAbort);
88
+ waiter.resolve();
89
+ }
90
+ this.#schedule();
91
+ }
92
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * File transfer data.
3
+ *
4
+ * Negotiating a transfer is a command (`initFileUpload`, `initFileDownload`);
5
+ * the bytes then go over a separate TCP connection to the server's file
6
+ * transfer port, opened with the key the negotiation returned.
7
+ */
8
+
9
+ import { connect } from "node:net";
10
+ import { pipeline } from "node:stream/promises";
11
+ import type { Duplex, Readable, Writable } from "node:stream";
12
+ import type { FileUploadInfo, FileDownloadInfo } from "@honeybbq/teamspeak-core";
13
+ import type { FileTransferSocketFactory } from "./types.js";
14
+ import { FileTransferError } from "./errors.js";
15
+
16
+ /** How long dialing may take before it fails, unless a signal says otherwise. */
17
+ const DIAL_TIMEOUT_MS = 10_000;
18
+
19
+ /** Options for a file transfer. */
20
+ export interface FileTransferOptions {
21
+ /** Abandons the transfer; it then rejects with the signal's reason. */
22
+ readonly signal?: AbortSignal | undefined;
23
+ /**
24
+ * Opens the connection instead of a direct TCP dial, to route the transfer
25
+ * through a proxy or tunnel.
26
+ */
27
+ readonly socketFactory?: FileTransferSocketFactory | undefined;
28
+ }
29
+
30
+ /**
31
+ * Open a connection to the file transfer port and send the transfer key.
32
+ * The caller owns the returned stream and must end or destroy it.
33
+ */
34
+ export async function dialFileTransfer(
35
+ host: string,
36
+ port: number,
37
+ key: string,
38
+ options: FileTransferOptions = {},
39
+ ): Promise<Duplex> {
40
+ const { signal, socketFactory } = options;
41
+ signal?.throwIfAborted();
42
+ const socket: Duplex = socketFactory ? socketFactory(host, port) : connect({ host, port });
43
+
44
+ // Until the caller takes the stream over, its errors are this function's to
45
+ // report: a stream's unhandled `error` event takes the process down.
46
+ const swallow = (): void => {};
47
+ socket.on("error", swallow);
48
+ try {
49
+ await connected(socket, signal);
50
+ await new Promise<void>((resolve, reject) => {
51
+ socket.write(key, (err) =>
52
+ err
53
+ ? reject(
54
+ new FileTransferError(`failed to send transfer key: ${err.message}`, { cause: err }),
55
+ )
56
+ : resolve(),
57
+ );
58
+ });
59
+ } catch (error) {
60
+ socket.destroy();
61
+ throw error;
62
+ } finally {
63
+ socket.off("error", swallow);
64
+ }
65
+ return socket;
66
+ }
67
+
68
+ /**
69
+ * Wait for a stream to be connected. A `node:net` socket says so with
70
+ * `connect`; a stream from elsewhere that is already writable is taken as
71
+ * connected.
72
+ */
73
+ function connected(socket: Duplex, signal: AbortSignal | undefined): Promise<void> {
74
+ const pending = (socket as { pending?: boolean }).pending;
75
+ if (pending === false || (pending === undefined && socket.writable)) return Promise.resolve();
76
+
77
+ return new Promise<void>((resolve, reject) => {
78
+ const timeout = AbortSignal.timeout(DIAL_TIMEOUT_MS);
79
+ const done = (error?: unknown): void => {
80
+ socket.off("connect", onConnect);
81
+ socket.off("ready", onConnect);
82
+ socket.off("error", onError);
83
+ signal?.removeEventListener("abort", onAbort);
84
+ timeout.removeEventListener("abort", onTimeout);
85
+ if (error === undefined) resolve();
86
+ else reject(error);
87
+ };
88
+ const onConnect = (): void => done();
89
+ const onError = (error: Error): void =>
90
+ done(
91
+ new FileTransferError(`file transfer connection failed: ${error.message}`, {
92
+ cause: error,
93
+ }),
94
+ );
95
+ const onAbort = (): void => done(signal!.reason);
96
+ const onTimeout = (): void => done(new FileTransferError("file transfer connection timed out"));
97
+ socket.once("connect", onConnect);
98
+ socket.once("ready", onConnect);
99
+ socket.once("error", onError);
100
+ signal?.addEventListener("abort", onAbort, { once: true });
101
+ timeout.addEventListener("abort", onTimeout, { once: true });
102
+ });
103
+ }
104
+
105
+ /** Upload `data` over a negotiated transfer. Resolves once every byte is sent. */
106
+ export async function uploadFileData(
107
+ host: string,
108
+ info: FileUploadInfo,
109
+ data: Readable,
110
+ options: FileTransferOptions = {},
111
+ ): Promise<void> {
112
+ const socket = await dialFileTransfer(host, info.port, info.fileTransferKey, options);
113
+ await pipeline(data, socket, options.signal ? { signal: options.signal } : {});
114
+ }
115
+
116
+ /** Download a negotiated transfer into `dest`. Resolves once the file is written. */
117
+ export async function downloadFileData(
118
+ host: string,
119
+ info: FileDownloadInfo,
120
+ dest: Writable,
121
+ options: FileTransferOptions = {},
122
+ ): Promise<void> {
123
+ const socket = await dialFileTransfer(host, info.port, info.fileTransferKey, options);
124
+ await pipeline(socket, dest, options.signal ? { signal: options.signal } : {});
125
+ }
126
+
127
+ export { FileTransferError };
@@ -0,0 +1,77 @@
1
+ export class GenerationWindow {
2
+ #mappedBaseOffset = 0;
3
+ #generation = 0;
4
+ readonly #mod: number;
5
+ readonly #receiveWindow: number;
6
+
7
+ constructor(mod: number, windowSize: number) {
8
+ this.#mod = mod;
9
+ this.#receiveWindow = windowSize;
10
+ }
11
+
12
+ get generation(): number {
13
+ return this.#generation;
14
+ }
15
+
16
+ advance(amount: number): void {
17
+ if (amount <= 0) return;
18
+ const newBaseOffset = this.#mappedBaseOffset + amount;
19
+ const genStep = Math.floor(newBaseOffset / this.#mod);
20
+ if (genStep > 0) {
21
+ this.#generation = Math.min(this.#generation + genStep, 0xffff_ffff);
22
+ }
23
+ this.#mappedBaseOffset = newBaseOffset % this.#mod;
24
+ }
25
+
26
+ advanceToExcluded(mappedValue: number): void {
27
+ let moveDist = mappedValue - this.#mappedBaseOffset;
28
+ if (moveDist < 0) moveDist += this.#mod;
29
+ this.advance(moveDist + 1);
30
+ }
31
+
32
+ syncTo(mappedValue: number): void {
33
+ let moveDist = mappedValue - this.#mappedBaseOffset;
34
+ if (moveDist < 0) moveDist += this.#mod;
35
+ this.advance(moveDist);
36
+ }
37
+
38
+ isInWindow(mappedValue: number): boolean {
39
+ const maxOffset = this.#mappedBaseOffset + this.#receiveWindow;
40
+ if (maxOffset < this.#mod) {
41
+ return mappedValue >= this.#mappedBaseOffset && mappedValue < maxOffset;
42
+ }
43
+ return mappedValue >= this.#mappedBaseOffset || mappedValue < maxOffset - this.#mod;
44
+ }
45
+
46
+ mappedToIndex(mappedValue: number): number {
47
+ if (this.#isNextGen(mappedValue)) {
48
+ return mappedValue + this.#mod - this.#mappedBaseOffset;
49
+ }
50
+ return mappedValue - this.#mappedBaseOffset;
51
+ }
52
+
53
+ isOldPacket(mappedValue: number): boolean {
54
+ return this.mappedToIndex(mappedValue) < 0;
55
+ }
56
+
57
+ isFuturePacket(mappedValue: number): boolean {
58
+ return this.mappedToIndex(mappedValue) >= this.#receiveWindow;
59
+ }
60
+
61
+ #isNextGen(mappedValue: number): boolean {
62
+ return (
63
+ this.#mappedBaseOffset > this.#mod - this.#receiveWindow &&
64
+ mappedValue < this.#mappedBaseOffset + this.#receiveWindow - this.#mod
65
+ );
66
+ }
67
+
68
+ getGeneration(mappedValue: number): number {
69
+ if (this.#isNextGen(mappedValue)) return this.#generation + 1;
70
+ return this.#generation;
71
+ }
72
+
73
+ reset(): void {
74
+ this.#mappedBaseOffset = 0;
75
+ this.#generation = 0;
76
+ }
77
+ }