tci-client-node 0.1.0
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/LICENSE +21 -0
- package/README.md +133 -0
- package/dist/audio/index.cjs +343 -0
- package/dist/audio/index.cjs.map +1 -0
- package/dist/audio/index.d.cts +60 -0
- package/dist/audio/index.d.ts +60 -0
- package/dist/audio/index.js +301 -0
- package/dist/audio/index.js.map +1 -0
- package/dist/index-CK3XdXP3.d.cts +42 -0
- package/dist/index-Dfmrk2MR.d.ts +42 -0
- package/dist/index.cjs +1182 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +130 -0
- package/dist/index.d.ts +130 -0
- package/dist/index.js +1117 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol/index.cjs +309 -0
- package/dist/protocol/index.cjs.map +1 -0
- package/dist/protocol/index.d.cts +2 -0
- package/dist/protocol/index.d.ts +2 -0
- package/dist/protocol/index.js +274 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/testing/index.cjs +572 -0
- package/dist/testing/index.cjs.map +1 -0
- package/dist/testing/index.d.cts +78 -0
- package/dist/testing/index.d.ts +78 -0
- package/dist/testing/index.js +533 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/text-BwCWY1k1.d.cts +21 -0
- package/dist/text-BwCWY1k1.d.ts +21 -0
- package/package.json +93 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// src/protocol/text.ts
|
|
2
|
+
var ESCAPE_TO_CHAR = {
|
|
3
|
+
"^": ":",
|
|
4
|
+
"~": ",",
|
|
5
|
+
"*": ";"
|
|
6
|
+
};
|
|
7
|
+
var CHAR_TO_ESCAPE = {
|
|
8
|
+
":": "^",
|
|
9
|
+
",": "~",
|
|
10
|
+
";": "*"
|
|
11
|
+
};
|
|
12
|
+
function escapeTciText(value) {
|
|
13
|
+
return String(value).replace(/[:;,]/g, (char) => CHAR_TO_ESCAPE[char] ?? char);
|
|
14
|
+
}
|
|
15
|
+
function unescapeTciText(value) {
|
|
16
|
+
return value.replace(/[\^~*]/g, (char) => ESCAPE_TO_CHAR[char] ?? char);
|
|
17
|
+
}
|
|
18
|
+
function parseTciText(text) {
|
|
19
|
+
const source = normalizeTextInput(text);
|
|
20
|
+
const commands = [];
|
|
21
|
+
for (const fragment of source.split(";")) {
|
|
22
|
+
const raw = fragment.trim();
|
|
23
|
+
if (!raw) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const colonIndex = raw.indexOf(":");
|
|
27
|
+
const originalName = (colonIndex >= 0 ? raw.slice(0, colonIndex) : raw).trim();
|
|
28
|
+
if (!originalName) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const argsText = colonIndex >= 0 ? raw.slice(colonIndex + 1) : void 0;
|
|
32
|
+
commands.push({
|
|
33
|
+
name: originalName.toLowerCase(),
|
|
34
|
+
originalName,
|
|
35
|
+
args: argsText === void 0 ? [] : splitArgs(argsText),
|
|
36
|
+
raw
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return commands;
|
|
40
|
+
}
|
|
41
|
+
function parseTciCommand(input) {
|
|
42
|
+
if (typeof input !== "string") {
|
|
43
|
+
return input;
|
|
44
|
+
}
|
|
45
|
+
const [command] = parseTciText(input);
|
|
46
|
+
if (!command) {
|
|
47
|
+
throw new Error(`Invalid TCI command: ${input}`);
|
|
48
|
+
}
|
|
49
|
+
return command;
|
|
50
|
+
}
|
|
51
|
+
function formatTciCommand(name, args = []) {
|
|
52
|
+
const commandName = name.trim().toUpperCase();
|
|
53
|
+
if (!commandName) {
|
|
54
|
+
throw new Error("TCI command name cannot be empty");
|
|
55
|
+
}
|
|
56
|
+
if (args.length === 0) {
|
|
57
|
+
return `${commandName};`;
|
|
58
|
+
}
|
|
59
|
+
return `${commandName}:${args.map(escapeTciText).join(",")};`;
|
|
60
|
+
}
|
|
61
|
+
function normalizeCommandName(name) {
|
|
62
|
+
return name.trim().toLowerCase();
|
|
63
|
+
}
|
|
64
|
+
function isCommandReplyTo(replyInput, requestInput) {
|
|
65
|
+
const reply = parseTciCommand(replyInput);
|
|
66
|
+
const request = parseTciCommand(requestInput);
|
|
67
|
+
if (reply.name !== request.name) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if (request.args.length === 0) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
if (argsHavePrefix(reply.args, request.args)) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
return isKnownVariantReply(reply, request);
|
|
77
|
+
}
|
|
78
|
+
function commandKey(command) {
|
|
79
|
+
const parsed = parseTciCommand(command);
|
|
80
|
+
return `${parsed.name}:${parsed.args.join(",")}`;
|
|
81
|
+
}
|
|
82
|
+
function normalizeTextInput(text) {
|
|
83
|
+
if (typeof text === "string") {
|
|
84
|
+
return text;
|
|
85
|
+
}
|
|
86
|
+
if (Buffer.isBuffer(text)) {
|
|
87
|
+
return text.toString("utf8");
|
|
88
|
+
}
|
|
89
|
+
if (text instanceof ArrayBuffer) {
|
|
90
|
+
return Buffer.from(text).toString("utf8");
|
|
91
|
+
}
|
|
92
|
+
return Buffer.from(text.buffer, text.byteOffset, text.byteLength).toString("utf8");
|
|
93
|
+
}
|
|
94
|
+
function splitArgs(argsText) {
|
|
95
|
+
return argsText.split(",").map((arg) => unescapeTciText(arg.trim()));
|
|
96
|
+
}
|
|
97
|
+
function argsHavePrefix(args, prefix) {
|
|
98
|
+
if (args.length < prefix.length) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return prefix.every((arg, index) => args[index]?.toLowerCase() === arg.toLowerCase());
|
|
102
|
+
}
|
|
103
|
+
function isKnownVariantReply(reply, request) {
|
|
104
|
+
if (reply.name === "modulation") {
|
|
105
|
+
if (request.args.length === 2 && reply.args.length >= 3) {
|
|
106
|
+
return reply.args[0] === request.args[0] && reply.args[2]?.toLowerCase() === request.args[1]?.toLowerCase();
|
|
107
|
+
}
|
|
108
|
+
if (request.args.length === 3 && reply.args.length === 2) {
|
|
109
|
+
return reply.args[0] === request.args[0] && reply.args[1]?.toLowerCase() === request.args[2]?.toLowerCase();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (reply.name === "protocol") {
|
|
113
|
+
return request.args.length <= 1;
|
|
114
|
+
}
|
|
115
|
+
if (reply.name === "trx" && request.args.length >= 3 && reply.args.length >= 2) {
|
|
116
|
+
return reply.args[0] === request.args[0] && reply.args[1]?.toLowerCase() === request.args[1]?.toLowerCase();
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/errors.ts
|
|
122
|
+
var TciError = class extends Error {
|
|
123
|
+
code;
|
|
124
|
+
details;
|
|
125
|
+
constructor(code, message, details) {
|
|
126
|
+
super(message);
|
|
127
|
+
this.name = "TciError";
|
|
128
|
+
this.code = code;
|
|
129
|
+
this.details = details;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// src/protocol/commandQueue.ts
|
|
134
|
+
var TciCommandQueue = class {
|
|
135
|
+
send;
|
|
136
|
+
defaultTimeoutMs;
|
|
137
|
+
queue = [];
|
|
138
|
+
active;
|
|
139
|
+
connected = true;
|
|
140
|
+
constructor(options) {
|
|
141
|
+
this.send = options.send;
|
|
142
|
+
this.defaultTimeoutMs = options.timeoutMs ?? 1e3;
|
|
143
|
+
}
|
|
144
|
+
setConnected(connected) {
|
|
145
|
+
this.connected = connected;
|
|
146
|
+
if (!connected) {
|
|
147
|
+
this.cancelAll(new TciError("disconnected", "TCI connection closed"));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
enqueue(command, options = {}) {
|
|
151
|
+
const request = parseTciCommand(command);
|
|
152
|
+
const raw = typeof command === "string" ? ensureSemicolon(command) : formatTciCommand(command.originalName, command.args);
|
|
153
|
+
if (!this.connected) {
|
|
154
|
+
return Promise.reject(new TciError("not-connected", "TCI socket is not connected"));
|
|
155
|
+
}
|
|
156
|
+
return new Promise((resolve, reject) => {
|
|
157
|
+
const pending = {
|
|
158
|
+
raw,
|
|
159
|
+
request,
|
|
160
|
+
timeoutMs: options.timeoutMs ?? this.defaultTimeoutMs,
|
|
161
|
+
matcher: options.matcher ?? ((reply, req) => isCommandReplyTo(reply, req)),
|
|
162
|
+
resolve,
|
|
163
|
+
reject
|
|
164
|
+
};
|
|
165
|
+
if (options.signal) {
|
|
166
|
+
if (options.signal.aborted) {
|
|
167
|
+
reject(new TciError("cancelled", "TCI command was cancelled"));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const onAbort = () => this.rejectPending(pending, new TciError("cancelled", "TCI command was cancelled"));
|
|
171
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
172
|
+
pending.abortCleanup = () => options.signal?.removeEventListener("abort", onAbort);
|
|
173
|
+
}
|
|
174
|
+
this.queue.push(pending);
|
|
175
|
+
void this.pump();
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
handleCommand(commandInput) {
|
|
179
|
+
const active = this.active;
|
|
180
|
+
if (!active) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
const reply = parseTciCommand(commandInput);
|
|
184
|
+
if (!active.matcher(reply, active.request)) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
this.finishActive(reply);
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
cancelAll(error = new TciError("cancelled", "TCI command queue cancelled")) {
|
|
191
|
+
const pending = [...this.queue];
|
|
192
|
+
this.queue = [];
|
|
193
|
+
if (this.active) {
|
|
194
|
+
pending.unshift(this.active);
|
|
195
|
+
this.active = void 0;
|
|
196
|
+
}
|
|
197
|
+
for (const item of pending) {
|
|
198
|
+
this.rejectPending(item, error);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
get size() {
|
|
202
|
+
return this.queue.length + (this.active ? 1 : 0);
|
|
203
|
+
}
|
|
204
|
+
async pump() {
|
|
205
|
+
if (this.active || !this.connected) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const next = this.queue.shift();
|
|
209
|
+
if (!next) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
this.active = next;
|
|
213
|
+
next.timer = setTimeout(() => {
|
|
214
|
+
this.rejectPending(next, new TciError("command-timeout", `Timed out waiting for TCI reply to ${next.raw}`));
|
|
215
|
+
if (this.active === next) {
|
|
216
|
+
this.active = void 0;
|
|
217
|
+
}
|
|
218
|
+
void this.pump();
|
|
219
|
+
}, next.timeoutMs);
|
|
220
|
+
try {
|
|
221
|
+
await this.send(next.raw);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
this.rejectPending(next, new TciError("disconnected", error instanceof Error ? error.message : String(error), error));
|
|
224
|
+
if (this.active === next) {
|
|
225
|
+
this.active = void 0;
|
|
226
|
+
}
|
|
227
|
+
void this.pump();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
finishActive(reply) {
|
|
231
|
+
const active = this.active;
|
|
232
|
+
if (!active) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
this.active = void 0;
|
|
236
|
+
if (active.timer) {
|
|
237
|
+
clearTimeout(active.timer);
|
|
238
|
+
}
|
|
239
|
+
active.abortCleanup?.();
|
|
240
|
+
active.resolve({ request: active.request, reply });
|
|
241
|
+
void this.pump();
|
|
242
|
+
}
|
|
243
|
+
rejectPending(pending, error) {
|
|
244
|
+
if (pending.timer) {
|
|
245
|
+
clearTimeout(pending.timer);
|
|
246
|
+
}
|
|
247
|
+
pending.abortCleanup?.();
|
|
248
|
+
const wasActive = this.active === pending;
|
|
249
|
+
if (wasActive) {
|
|
250
|
+
this.active = void 0;
|
|
251
|
+
} else {
|
|
252
|
+
this.queue = this.queue.filter((item) => item !== pending);
|
|
253
|
+
}
|
|
254
|
+
pending.reject(error);
|
|
255
|
+
if (wasActive && this.connected) {
|
|
256
|
+
void this.pump();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
function ensureSemicolon(command) {
|
|
261
|
+
return command.trim().endsWith(";") ? command.trim() : `${command.trim()};`;
|
|
262
|
+
}
|
|
263
|
+
export {
|
|
264
|
+
TciCommandQueue,
|
|
265
|
+
commandKey,
|
|
266
|
+
escapeTciText,
|
|
267
|
+
formatTciCommand,
|
|
268
|
+
isCommandReplyTo,
|
|
269
|
+
normalizeCommandName,
|
|
270
|
+
parseTciCommand,
|
|
271
|
+
parseTciText,
|
|
272
|
+
unescapeTciText
|
|
273
|
+
};
|
|
274
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/protocol/text.ts","../../src/errors.ts","../../src/protocol/commandQueue.ts"],"sourcesContent":["export interface TciCommand {\n /** Lower-case command name for case-insensitive matching. */\n name: string;\n /** Original command name as received, without surrounding whitespace. */\n originalName: string;\n /** Unescaped argument list. Empty commands have an empty array. */\n args: string[];\n /** Raw command fragment without the trailing semicolon. */\n raw: string;\n}\n\nexport type TciCommandInput = string | TciCommand;\n\nconst ESCAPE_TO_CHAR: Record<string, string> = {\n '^': ':',\n '~': ',',\n '*': ';',\n};\n\nconst CHAR_TO_ESCAPE: Record<string, string> = {\n ':': '^',\n ',': '~',\n ';': '*',\n};\n\nexport function escapeTciText(value: unknown): string {\n return String(value).replace(/[:;,]/g, (char) => CHAR_TO_ESCAPE[char] ?? char);\n}\n\nexport function unescapeTciText(value: string): string {\n return value.replace(/[\\^~*]/g, (char) => ESCAPE_TO_CHAR[char] ?? char);\n}\n\nexport function parseTciText(text: string | Buffer | ArrayBuffer | ArrayBufferView): TciCommand[] {\n const source = normalizeTextInput(text);\n const commands: TciCommand[] = [];\n\n for (const fragment of source.split(';')) {\n const raw = fragment.trim();\n if (!raw) {\n continue;\n }\n\n const colonIndex = raw.indexOf(':');\n const originalName = (colonIndex >= 0 ? raw.slice(0, colonIndex) : raw).trim();\n if (!originalName) {\n continue;\n }\n\n const argsText = colonIndex >= 0 ? raw.slice(colonIndex + 1) : undefined;\n commands.push({\n name: originalName.toLowerCase(),\n originalName,\n args: argsText === undefined ? [] : splitArgs(argsText),\n raw,\n });\n }\n\n return commands;\n}\n\nexport function parseTciCommand(input: TciCommandInput): TciCommand {\n if (typeof input !== 'string') {\n return input;\n }\n const [command] = parseTciText(input);\n if (!command) {\n throw new Error(`Invalid TCI command: ${input}`);\n }\n return command;\n}\n\nexport function formatTciCommand(name: string, args: readonly unknown[] = []): string {\n const commandName = name.trim().toUpperCase();\n if (!commandName) {\n throw new Error('TCI command name cannot be empty');\n }\n if (args.length === 0) {\n return `${commandName};`;\n }\n return `${commandName}:${args.map(escapeTciText).join(',')};`;\n}\n\nexport function normalizeCommandName(name: string): string {\n return name.trim().toLowerCase();\n}\n\nexport function isCommandReplyTo(replyInput: TciCommandInput, requestInput: TciCommandInput): boolean {\n const reply = parseTciCommand(replyInput);\n const request = parseTciCommand(requestInput);\n if (reply.name !== request.name) {\n return false;\n }\n\n if (request.args.length === 0) {\n return true;\n }\n\n if (argsHavePrefix(reply.args, request.args)) {\n return true;\n }\n\n return isKnownVariantReply(reply, request);\n}\n\nexport function commandKey(command: TciCommandInput): string {\n const parsed = parseTciCommand(command);\n return `${parsed.name}:${parsed.args.join(',')}`;\n}\n\nfunction normalizeTextInput(text: string | Buffer | ArrayBuffer | ArrayBufferView): string {\n if (typeof text === 'string') {\n return text;\n }\n if (Buffer.isBuffer(text)) {\n return text.toString('utf8');\n }\n if (text instanceof ArrayBuffer) {\n return Buffer.from(text).toString('utf8');\n }\n return Buffer.from(text.buffer, text.byteOffset, text.byteLength).toString('utf8');\n}\n\nfunction splitArgs(argsText: string): string[] {\n return argsText.split(',').map((arg) => unescapeTciText(arg.trim()));\n}\n\nfunction argsHavePrefix(args: readonly string[], prefix: readonly string[]): boolean {\n if (args.length < prefix.length) {\n return false;\n }\n return prefix.every((arg, index) => args[index]?.toLowerCase() === arg.toLowerCase());\n}\n\nfunction isKnownVariantReply(reply: TciCommand, request: TciCommand): boolean {\n if (reply.name === 'modulation') {\n // ExpertSDR/WSJT-X variants can use MODULATION:rx,mode and MODULATION:rx,vfo,mode.\n if (request.args.length === 2 && reply.args.length >= 3) {\n return reply.args[0] === request.args[0] && reply.args[2]?.toLowerCase() === request.args[1]?.toLowerCase();\n }\n if (request.args.length === 3 && reply.args.length === 2) {\n return reply.args[0] === request.args[0] && reply.args[1]?.toLowerCase() === request.args[2]?.toLowerCase();\n }\n }\n\n if (reply.name === 'protocol') {\n return request.args.length <= 1;\n }\n\n if (reply.name === 'trx' && request.args.length >= 3 && reply.args.length >= 2) {\n // Official TRX writes may include an audio source as arg3, while replies only echo trx+state.\n return reply.args[0] === request.args[0] && reply.args[1]?.toLowerCase() === request.args[1]?.toLowerCase();\n }\n\n return false;\n}\n","export type TciErrorCode =\n | 'connect-timeout'\n | 'command-timeout'\n | 'not-connected'\n | 'disconnected'\n | 'protocol-error'\n | 'invalid-frame'\n | 'cancelled';\n\nexport class TciError extends Error {\n readonly code: TciErrorCode;\n readonly details?: unknown;\n\n constructor(code: TciErrorCode, message: string, details?: unknown) {\n super(message);\n this.name = 'TciError';\n this.code = code;\n this.details = details;\n }\n}\n\nexport function toTciError(error: unknown, fallbackCode: TciErrorCode = 'protocol-error'): TciError {\n if (error instanceof TciError) {\n return error;\n }\n if (error instanceof Error) {\n return new TciError(fallbackCode, error.message, error);\n }\n return new TciError(fallbackCode, String(error), error);\n}\n","import { TciError } from '../errors.js';\nimport {\n formatTciCommand,\n isCommandReplyTo,\n parseTciCommand,\n type TciCommand,\n type TciCommandInput,\n} from './text.js';\n\nexport type TciCommandMatcher = (reply: TciCommand, request: TciCommand) => boolean;\n\nexport interface QueueCommandOptions {\n timeoutMs?: number;\n matcher?: TciCommandMatcher;\n signal?: AbortSignal;\n}\n\nexport interface QueuedCommandResult {\n request: TciCommand;\n reply: TciCommand;\n}\n\ninterface PendingCommand {\n raw: string;\n request: TciCommand;\n timeoutMs: number;\n matcher: TciCommandMatcher;\n resolve: (result: QueuedCommandResult) => void;\n reject: (error: TciError) => void;\n timer?: NodeJS.Timeout;\n abortCleanup?: () => void;\n}\n\nexport interface TciCommandQueueOptions {\n send: (raw: string) => void | Promise<void>;\n timeoutMs?: number;\n}\n\nexport class TciCommandQueue {\n private readonly send: (raw: string) => void | Promise<void>;\n private readonly defaultTimeoutMs: number;\n private queue: PendingCommand[] = [];\n private active?: PendingCommand;\n private connected = true;\n\n constructor(options: TciCommandQueueOptions) {\n this.send = options.send;\n this.defaultTimeoutMs = options.timeoutMs ?? 1_000;\n }\n\n setConnected(connected: boolean): void {\n this.connected = connected;\n if (!connected) {\n this.cancelAll(new TciError('disconnected', 'TCI connection closed'));\n }\n }\n\n enqueue(command: TciCommandInput, options: QueueCommandOptions = {}): Promise<QueuedCommandResult> {\n const request = parseTciCommand(command);\n const raw = typeof command === 'string' ? ensureSemicolon(command) : formatTciCommand(command.originalName, command.args);\n\n if (!this.connected) {\n return Promise.reject(new TciError('not-connected', 'TCI socket is not connected'));\n }\n\n return new Promise<QueuedCommandResult>((resolve, reject) => {\n const pending: PendingCommand = {\n raw,\n request,\n timeoutMs: options.timeoutMs ?? this.defaultTimeoutMs,\n matcher: options.matcher ?? ((reply, req) => isCommandReplyTo(reply, req)),\n resolve,\n reject,\n };\n\n if (options.signal) {\n if (options.signal.aborted) {\n reject(new TciError('cancelled', 'TCI command was cancelled'));\n return;\n }\n const onAbort = () => this.rejectPending(pending, new TciError('cancelled', 'TCI command was cancelled'));\n options.signal.addEventListener('abort', onAbort, { once: true });\n pending.abortCleanup = () => options.signal?.removeEventListener('abort', onAbort);\n }\n\n this.queue.push(pending);\n void this.pump();\n });\n }\n\n handleCommand(commandInput: TciCommandInput): boolean {\n const active = this.active;\n if (!active) {\n return false;\n }\n const reply = parseTciCommand(commandInput);\n if (!active.matcher(reply, active.request)) {\n return false;\n }\n this.finishActive(reply);\n return true;\n }\n\n cancelAll(error = new TciError('cancelled', 'TCI command queue cancelled')): void {\n const pending = [...this.queue];\n this.queue = [];\n if (this.active) {\n pending.unshift(this.active);\n this.active = undefined;\n }\n for (const item of pending) {\n this.rejectPending(item, error);\n }\n }\n\n get size(): number {\n return this.queue.length + (this.active ? 1 : 0);\n }\n\n private async pump(): Promise<void> {\n if (this.active || !this.connected) {\n return;\n }\n const next = this.queue.shift();\n if (!next) {\n return;\n }\n\n this.active = next;\n next.timer = setTimeout(() => {\n this.rejectPending(next, new TciError('command-timeout', `Timed out waiting for TCI reply to ${next.raw}`));\n if (this.active === next) {\n this.active = undefined;\n }\n void this.pump();\n }, next.timeoutMs);\n\n try {\n await this.send(next.raw);\n } catch (error) {\n this.rejectPending(next, new TciError('disconnected', error instanceof Error ? error.message : String(error), error));\n if (this.active === next) {\n this.active = undefined;\n }\n void this.pump();\n }\n }\n\n private finishActive(reply: TciCommand): void {\n const active = this.active;\n if (!active) {\n return;\n }\n this.active = undefined;\n if (active.timer) {\n clearTimeout(active.timer);\n }\n active.abortCleanup?.();\n active.resolve({ request: active.request, reply });\n void this.pump();\n }\n\n private rejectPending(pending: PendingCommand, error: TciError): void {\n if (pending.timer) {\n clearTimeout(pending.timer);\n }\n pending.abortCleanup?.();\n const wasActive = this.active === pending;\n if (wasActive) {\n this.active = undefined;\n } else {\n this.queue = this.queue.filter((item) => item !== pending);\n }\n pending.reject(error);\n if (wasActive && this.connected) {\n void this.pump();\n }\n }\n}\n\nfunction ensureSemicolon(command: string): string {\n return command.trim().endsWith(';') ? command.trim() : `${command.trim()};`;\n}\n"],"mappings":"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","names":[]}
|