@tak-ps/node-tak 11.26.2 → 12.0.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/CHANGELOG.md +4 -0
- package/dist/index.d.ts +96 -8
- package/dist/index.js +157 -44
- package/dist/index.js.map +1 -1
- package/dist/lib/api/groups.d.ts +1 -0
- package/dist/lib/api/groups.js +3 -2
- package/dist/lib/api/groups.js.map +1 -1
- package/dist/lib/utils/queue.d.ts +13 -0
- package/dist/lib/utils/queue.js +47 -0
- package/dist/lib/utils/queue.js.map +1 -0
- package/dist/test/pipeline.test.d.ts +1 -0
- package/dist/test/pipeline.test.js +223 -0
- package/dist/test/pipeline.test.js.map +1 -0
- package/dist/test/queue.test.d.ts +1 -0
- package/dist/test/queue.test.js +46 -0
- package/dist/test/queue.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/index.ts +278 -93
- package/lib/api/groups.ts +3 -2
- package/lib/utils/queue.ts +53 -0
- package/package.json +1 -1
- package/test/pipeline.test.ts +269 -0
- package/test/queue.test.ts +49 -0
package/index.ts
CHANGED
|
@@ -1,30 +1,93 @@
|
|
|
1
|
-
import EventEmitter from 'node:events';
|
|
2
1
|
import type { Static } from '@sinclair/typebox';
|
|
3
|
-
import tls from 'node:tls';
|
|
4
|
-
import CoT, { CoTParser } from '@tak-ps/node-cot';
|
|
5
2
|
import type { CoTOptions } from '@tak-ps/node-cot';
|
|
6
|
-
import
|
|
3
|
+
import CoT, { CoTParser } from '@tak-ps/node-cot';
|
|
4
|
+
import EventEmitter from 'node:events';
|
|
5
|
+
import type { TLSSocket } from 'node:tls';
|
|
6
|
+
import tls from 'node:tls';
|
|
7
7
|
|
|
8
8
|
import TAKAPI from './lib/api.js';
|
|
9
9
|
import { TAKAuth } from './lib/auth.js';
|
|
10
|
+
import { Queue } from './lib/utils/queue.js';
|
|
10
11
|
export * from './lib/auth.js';
|
|
11
12
|
|
|
12
13
|
/* eslint-disable no-control-regex */
|
|
13
14
|
export const REGEX_CONTROL = /[\u000B-\u001F\u007F-\u009F]/g;
|
|
14
15
|
|
|
15
16
|
// Match <event .../> or <event> but not <events>
|
|
16
|
-
export const REGEX_EVENT = /(<event[ >][\s\S]*?<\/event>)([\s\S]*)
|
|
17
|
+
export const REGEX_EVENT = /(<event[ >][\s\S]*?<\/event>)([\s\S]*)/;
|
|
17
18
|
|
|
18
19
|
export interface PartialCoT {
|
|
19
20
|
event: string;
|
|
20
21
|
remainder: string;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Configuration options for a TAK connection.
|
|
26
|
+
*
|
|
27
|
+
* Performance-related options control the write pipeline:
|
|
28
|
+
*
|
|
29
|
+
* ```
|
|
30
|
+
* write(cots) process()
|
|
31
|
+
* ─────────── ─────────
|
|
32
|
+
* for each CoT: while queue has items:
|
|
33
|
+
* serialize to XML ──push()──► Ring pop socketBatchSize items
|
|
34
|
+
* (returns false ◄──────── Buffer join into one string
|
|
35
|
+
* when full) (capacity = socket.write(batch)
|
|
36
|
+
* writeQueueSize) stop if backpressure
|
|
37
|
+
* when full:
|
|
38
|
+
* setImmediate() yield triggered by:
|
|
39
|
+
* (lets process() drain) - write() calling process()
|
|
40
|
+
* - socket 'drain' event
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @example High-throughput bulk ingestion
|
|
44
|
+
* ```typescript
|
|
45
|
+
* const tak = await TAK.connect(url, auth, {
|
|
46
|
+
* writeQueueSize: 50_000, // large buffer absorbs bursts
|
|
47
|
+
* socketBatchSize: 128, // 128 strings per socket.write()
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @example Low-latency real-time streams
|
|
52
|
+
* ```typescript
|
|
53
|
+
* const tak = await TAK.connect(url, auth, {
|
|
54
|
+
* writeQueueSize: 400, // small buffer keeps memory minimal
|
|
55
|
+
* socketBatchSize: 10, // flush to socket every 10 items
|
|
56
|
+
* });
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
23
59
|
export type TAKOptions = {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
60
|
+
/** Unique connection identifier. Appears in log messages for debugging.
|
|
61
|
+
* Useful when running multiple TAK connections in a single process.
|
|
62
|
+
* @default crypto.randomUUID() */
|
|
63
|
+
id?: number | string;
|
|
64
|
+
|
|
65
|
+
/** Connection type label. Informational only — helps distinguish
|
|
66
|
+
* connections in logs when multiple are active.
|
|
67
|
+
* @default 'unknown' */
|
|
68
|
+
type?: string;
|
|
69
|
+
|
|
70
|
+
/** Options passed through to `@tak-ps/node-cot` for CoT parsing
|
|
71
|
+
* (e.g., on incoming `'cot'` events). Does not affect the write pipeline. */
|
|
72
|
+
cot?: CoTOptions;
|
|
73
|
+
|
|
74
|
+
/** Capacity of the ring buffer that sits between `write()` and `process()`.
|
|
75
|
+
* When the queue is full, `write()` yields via `setImmediate()` until
|
|
76
|
+
* `process()` drains space. Larger values allow more XML strings to be
|
|
77
|
+
* buffered, increasing throughput at the cost of higher peak memory.
|
|
78
|
+
* @default 10_000 */
|
|
79
|
+
writeQueueSize?: number;
|
|
80
|
+
|
|
81
|
+
/** How many pre-serialized XML strings are popped from the ring buffer
|
|
82
|
+
* and joined into a single `socket.write()` call in `process()`. Higher
|
|
83
|
+
* values reduce syscall overhead and improve TLS frame packing, but
|
|
84
|
+
* increase per-write latency and the size of each socket write.
|
|
85
|
+
* @default 64 */
|
|
86
|
+
socketBatchSize?: number;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const DEFAULT_WRITE_QUEUE_SIZE = 10_000;
|
|
90
|
+
const DEFAULT_SOCKET_BATCH_SIZE = 64;
|
|
28
91
|
|
|
29
92
|
export default class TAK extends EventEmitter {
|
|
30
93
|
id: number | string;
|
|
@@ -33,8 +96,9 @@ export default class TAK extends EventEmitter {
|
|
|
33
96
|
auth: Static<typeof TAKAuth>;
|
|
34
97
|
open: boolean;
|
|
35
98
|
destroyed: boolean;
|
|
36
|
-
queue: string[];
|
|
37
99
|
writing: boolean;
|
|
100
|
+
writeQueueSize: number;
|
|
101
|
+
socketBatchSize: number;
|
|
38
102
|
|
|
39
103
|
cotOptions: CoTOptions;
|
|
40
104
|
|
|
@@ -42,6 +106,12 @@ export default class TAK extends EventEmitter {
|
|
|
42
106
|
client?: TLSSocket;
|
|
43
107
|
version?: string;
|
|
44
108
|
|
|
109
|
+
// Hybrid pipeline:
|
|
110
|
+
// write() serializes CoTs upfront into a bounded ring buffer of XML strings.
|
|
111
|
+
// process() drains the ring buffer to the socket, driven by drain events.
|
|
112
|
+
// Fully caller-safe: CoT objects can be mutated/GC'd after write() returns.
|
|
113
|
+
queue: Queue<string>;
|
|
114
|
+
|
|
45
115
|
/**
|
|
46
116
|
* @param url - Full URL of Streaming COT Endpoint IE: "https://ops.cotak.gov:8089"
|
|
47
117
|
* @param auth - TAK Certificate Pair
|
|
@@ -49,11 +119,7 @@ export default class TAK extends EventEmitter {
|
|
|
49
119
|
* @param opts.id - When using multiple connections in a script, allows a unique ID per connection
|
|
50
120
|
* @param opts.type - When using multiple connections in a script, allows specifying a script provided connection type
|
|
51
121
|
*/
|
|
52
|
-
constructor(
|
|
53
|
-
url: URL,
|
|
54
|
-
auth: Static<typeof TAKAuth>,
|
|
55
|
-
opts: TAKOptions = {}
|
|
56
|
-
) {
|
|
122
|
+
constructor(url: URL, auth: Static<typeof TAKAuth>, opts: TAKOptions = {}) {
|
|
57
123
|
super();
|
|
58
124
|
|
|
59
125
|
if (!opts) opts = {};
|
|
@@ -65,19 +131,21 @@ export default class TAK extends EventEmitter {
|
|
|
65
131
|
this.auth = auth;
|
|
66
132
|
|
|
67
133
|
this.writing = false;
|
|
134
|
+
this.writeQueueSize = opts.writeQueueSize || DEFAULT_WRITE_QUEUE_SIZE;
|
|
135
|
+
this.socketBatchSize =
|
|
136
|
+
opts.socketBatchSize || DEFAULT_SOCKET_BATCH_SIZE;
|
|
68
137
|
|
|
69
138
|
this.cotOptions = opts.cot || {};
|
|
70
139
|
|
|
71
140
|
this.open = false;
|
|
72
141
|
this.destroyed = false;
|
|
73
|
-
|
|
74
|
-
this.queue = [];
|
|
142
|
+
this.queue = new Queue<string>(this.writeQueueSize);
|
|
75
143
|
}
|
|
76
144
|
|
|
77
145
|
static async connect(
|
|
78
146
|
url: URL,
|
|
79
147
|
auth: Static<typeof TAKAuth>,
|
|
80
|
-
opts: TAKOptions = {}
|
|
148
|
+
opts: TAKOptions = {},
|
|
81
149
|
): Promise<TAK> {
|
|
82
150
|
const tak = new TAK(url, auth, opts);
|
|
83
151
|
|
|
@@ -107,55 +175,84 @@ export default class TAK extends EventEmitter {
|
|
|
107
175
|
this.client.setNoDelay();
|
|
108
176
|
|
|
109
177
|
this.client.on('connect', () => {
|
|
110
|
-
console.error(
|
|
178
|
+
console.error(
|
|
179
|
+
`ok - ${this.id} @ connect:${this.client ? this.client.authorized : 'NO CLIENT'} - ${this.client ? this.client.authorizationError : 'NO CLIENT'}`,
|
|
180
|
+
);
|
|
111
181
|
});
|
|
112
182
|
|
|
113
183
|
this.client.on('secureConnect', () => {
|
|
114
|
-
console.error(
|
|
115
|
-
|
|
184
|
+
console.error(
|
|
185
|
+
`ok - ${this.id} @ secure:${this.client ? this.client.authorized : 'NO CLIENT'} - ${this.client ? this.client.authorizationError : 'NO CLIENT'}`,
|
|
186
|
+
);
|
|
187
|
+
this.emit('secureConnect');
|
|
116
188
|
this.ping();
|
|
117
189
|
});
|
|
118
190
|
|
|
119
191
|
let buff = '';
|
|
120
|
-
this.client
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
192
|
+
this.client
|
|
193
|
+
.on('data', async (data: Buffer) => {
|
|
194
|
+
// Eventually Parse ProtoBuf
|
|
195
|
+
buff = buff + data.toString();
|
|
196
|
+
|
|
197
|
+
let result = TAK.findCoT(buff);
|
|
198
|
+
while (result && result.event) {
|
|
199
|
+
try {
|
|
200
|
+
const cot = CoTParser.from_xml(
|
|
201
|
+
result.event,
|
|
202
|
+
this.cotOptions,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
if (
|
|
206
|
+
cot.raw.event._attributes.type === 't-x-c-t-r'
|
|
207
|
+
) {
|
|
208
|
+
this.open = true;
|
|
209
|
+
this.emit('ping');
|
|
210
|
+
} else if (
|
|
211
|
+
cot.raw.event._attributes.type ===
|
|
212
|
+
't-x-takp-v' &&
|
|
213
|
+
cot.raw.event.detail &&
|
|
214
|
+
cot.raw.event.detail.TakControl &&
|
|
215
|
+
cot.raw.event.detail.TakControl
|
|
216
|
+
.TakServerVersionInfo &&
|
|
217
|
+
cot.raw.event.detail.TakControl
|
|
218
|
+
.TakServerVersionInfo._attributes
|
|
219
|
+
) {
|
|
220
|
+
this.version =
|
|
221
|
+
cot.raw.event.detail.TakControl.TakServerVersionInfo._attributes.serverVersion;
|
|
222
|
+
} else {
|
|
223
|
+
this.emit('cot', cot);
|
|
224
|
+
}
|
|
225
|
+
} catch (e) {
|
|
226
|
+
console.error('Error parsing', e, data.toString());
|
|
142
227
|
}
|
|
143
|
-
} catch (e) {
|
|
144
|
-
console.error('Error parsing', e, data.toString());
|
|
145
|
-
}
|
|
146
228
|
|
|
147
|
-
|
|
229
|
+
buff = result.remainder;
|
|
148
230
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
231
|
+
result = TAK.findCoT(buff);
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
.on('timeout', () => {
|
|
235
|
+
this.emit('timeout');
|
|
236
|
+
})
|
|
237
|
+
.on('error', (err: Error) => {
|
|
238
|
+
console.error(`[socket] error:`, err.message);
|
|
239
|
+
this.emit('error', err);
|
|
240
|
+
})
|
|
241
|
+
.on('end', () => {
|
|
242
|
+
this.open = false;
|
|
243
|
+
this.emit('end');
|
|
244
|
+
if (!this.destroyed) {
|
|
245
|
+
this.destroy();
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
.on('close', () => {
|
|
249
|
+
if (!this.destroyed) {
|
|
250
|
+
this.destroy();
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
.on('drain', () => {
|
|
254
|
+
this.process();
|
|
255
|
+
});
|
|
159
256
|
|
|
160
257
|
this.pingInterval = setInterval(() => {
|
|
161
258
|
this.ping();
|
|
@@ -176,75 +273,166 @@ export default class TAK extends EventEmitter {
|
|
|
176
273
|
|
|
177
274
|
destroy(): void {
|
|
178
275
|
this.destroyed = true;
|
|
276
|
+
|
|
179
277
|
if (this.client) {
|
|
180
278
|
this.client.destroy();
|
|
181
279
|
}
|
|
182
280
|
|
|
183
281
|
if (this.pingInterval) {
|
|
184
|
-
clearInterval(this.pingInterval)
|
|
282
|
+
clearInterval(this.pingInterval);
|
|
185
283
|
this.pingInterval = undefined;
|
|
186
284
|
}
|
|
285
|
+
|
|
286
|
+
// Unblock any flush() waiters
|
|
287
|
+
this.emit('_flushed');
|
|
187
288
|
}
|
|
188
289
|
|
|
189
290
|
async ping(): Promise<void> {
|
|
190
291
|
this.write([CoT.ping()]);
|
|
191
292
|
}
|
|
192
293
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
294
|
+
/**
|
|
295
|
+
* Drain the queue to the socket.
|
|
296
|
+
*
|
|
297
|
+
* Pops pre-serialized XML strings from the ring buffer, batches them
|
|
298
|
+
* (up to `socketBatchSize` per call), and writes to the socket. Runs
|
|
299
|
+
* synchronously in a single event loop tick until the socket signals
|
|
300
|
+
* backpressure or the queue is empty.
|
|
301
|
+
*
|
|
302
|
+
* Called when the socket signals readiness:
|
|
303
|
+
* - `'drain'` event (socket buffer cleared, ready for more)
|
|
304
|
+
* - After `write()` enqueues new items
|
|
305
|
+
*
|
|
306
|
+
* Emits `'_flushed'` when the queue drains to zero, waking any
|
|
307
|
+
* pending `flush()` calls.
|
|
308
|
+
*/
|
|
309
|
+
process(): void {
|
|
310
|
+
if (this.writing) return;
|
|
311
|
+
if (!this.client || this.destroyed) return;
|
|
204
312
|
this.writing = true;
|
|
205
|
-
while (this.queue.length) {
|
|
206
|
-
const body = this.queue.shift()
|
|
207
|
-
if (!body) continue;
|
|
208
|
-
await this.writer(body);
|
|
209
|
-
}
|
|
210
313
|
|
|
211
|
-
|
|
314
|
+
try {
|
|
315
|
+
while (this.queue.length > 0) {
|
|
316
|
+
if (this.destroyed || !this.client) break;
|
|
317
|
+
if (this.client.writableNeedDrain) break;
|
|
318
|
+
|
|
319
|
+
const batchCount = Math.min(
|
|
320
|
+
this.socketBatchSize,
|
|
321
|
+
this.queue.length,
|
|
322
|
+
);
|
|
323
|
+
const parts: string[] = new Array(batchCount);
|
|
324
|
+
for (let i = 0; i < batchCount; i++) {
|
|
325
|
+
const xml = this.queue.pop();
|
|
326
|
+
if (!xml) break;
|
|
327
|
+
parts[i] = xml;
|
|
328
|
+
}
|
|
212
329
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
330
|
+
const ok = this.client.write(parts.join('\n') + '\n');
|
|
331
|
+
if (!ok) break;
|
|
332
|
+
}
|
|
333
|
+
} catch (err) {
|
|
334
|
+
this.destroy();
|
|
335
|
+
this.emit('error', err);
|
|
336
|
+
} finally {
|
|
218
337
|
this.writing = false;
|
|
338
|
+
|
|
339
|
+
// Safety net: if a drain event fired while writing=true (and was
|
|
340
|
+
// therefore ignored), re-check. If the socket has capacity, reschedule
|
|
341
|
+
// on the next event loop turn so I/O callbacks can run first.
|
|
342
|
+
if (
|
|
343
|
+
this.queue.length > 0 &&
|
|
344
|
+
!this.destroyed &&
|
|
345
|
+
this.client &&
|
|
346
|
+
!this.client.writableNeedDrain
|
|
347
|
+
) {
|
|
348
|
+
setImmediate(() => this.process());
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (this.queue.length === 0) {
|
|
352
|
+
this.emit('_flushed');
|
|
353
|
+
}
|
|
219
354
|
}
|
|
220
355
|
}
|
|
221
356
|
|
|
222
357
|
/**
|
|
223
|
-
* Write
|
|
358
|
+
* Write CoTs to the TAK connection.
|
|
359
|
+
*
|
|
360
|
+
* Serializes each CoT to XML upfront and stores the string in a bounded
|
|
361
|
+
* ring buffer. Fully caller-safe: CoT objects can be mutated or GC'd
|
|
362
|
+
* immediately after this returns.
|
|
363
|
+
* Resolves when all items are queued (not when sent over the wire).
|
|
364
|
+
* Use flush() to wait for delivery.
|
|
224
365
|
*
|
|
225
|
-
* @param
|
|
366
|
+
* @param cots Array of CoT objects to send
|
|
226
367
|
*/
|
|
227
368
|
async write(cots: CoT[]): Promise<void> {
|
|
228
|
-
for (
|
|
229
|
-
this.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
369
|
+
for (let i = 0; i < cots.length; ) {
|
|
370
|
+
if (this.destroyed) return;
|
|
371
|
+
|
|
372
|
+
// Serialize upfront and push XML strings into the ring buffer
|
|
373
|
+
while (
|
|
374
|
+
i < cots.length &&
|
|
375
|
+
this.queue.push(CoTParser.to_xml(cots[i]))
|
|
376
|
+
) {
|
|
377
|
+
i++;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Kick process to start draining
|
|
233
381
|
this.process();
|
|
382
|
+
|
|
383
|
+
// Queue full — yield to let process() drain via I/O callbacks,
|
|
384
|
+
// then retry on the next event loop turn.
|
|
385
|
+
if (i < cots.length) {
|
|
386
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
387
|
+
}
|
|
234
388
|
}
|
|
235
389
|
}
|
|
236
390
|
|
|
391
|
+
/**
|
|
392
|
+
* Wait until all queued CoTs have been flushed to the socket.
|
|
393
|
+
*
|
|
394
|
+
* write() is a fast "enqueue" — it returns once items are in the queue,
|
|
395
|
+
* NOT once they've been sent over the wire.
|
|
396
|
+
*
|
|
397
|
+
* Resolves immediately if nothing is queued.
|
|
398
|
+
* Rejects if the connection is destroyed before flush completes.
|
|
399
|
+
*/
|
|
400
|
+
async flush(): Promise<void> {
|
|
401
|
+
if (this.queue.length === 0 && !this.writing) return;
|
|
402
|
+
|
|
403
|
+
return new Promise<void>((resolve, reject) => {
|
|
404
|
+
const check = () => {
|
|
405
|
+
if (this.destroyed) {
|
|
406
|
+
cleanup();
|
|
407
|
+
reject(
|
|
408
|
+
new Error(
|
|
409
|
+
'connection destroyed before flush completed',
|
|
410
|
+
),
|
|
411
|
+
);
|
|
412
|
+
} else if (this.queue.length === 0 && !this.writing) {
|
|
413
|
+
cleanup();
|
|
414
|
+
resolve();
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
const cleanup = () => {
|
|
418
|
+
this.removeListener('_flushed', check);
|
|
419
|
+
};
|
|
420
|
+
this.on('_flushed', check);
|
|
421
|
+
check();
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
237
425
|
write_xml(body: string): void {
|
|
238
426
|
this.queue.push(body);
|
|
239
427
|
|
|
240
|
-
if (this.queue.length && !this.writing) {
|
|
428
|
+
if (this.queue.length > 0 && !this.writing) {
|
|
241
429
|
this.process();
|
|
242
430
|
}
|
|
243
431
|
}
|
|
244
432
|
|
|
245
433
|
// https://github.com/vidterra/multitak/blob/main/app/lib/helper.js#L4
|
|
246
434
|
static findCoT(str: string): null | PartialCoT {
|
|
247
|
-
str = str.replace(REGEX_CONTROL,
|
|
435
|
+
str = str.replace(REGEX_CONTROL, '');
|
|
248
436
|
|
|
249
437
|
const match = str.match(REGEX_EVENT); // find first CoT
|
|
250
438
|
if (!match) return null;
|
|
@@ -258,7 +446,4 @@ export default class TAK extends EventEmitter {
|
|
|
258
446
|
|
|
259
447
|
export * from './lib/api.js';
|
|
260
448
|
export { CommandOutputFormat } from './lib/commands.js';
|
|
261
|
-
export {
|
|
262
|
-
TAKAPI,
|
|
263
|
-
CoT,
|
|
264
|
-
}
|
|
449
|
+
export { CoT, TAKAPI };
|
package/lib/api/groups.ts
CHANGED
|
@@ -14,7 +14,8 @@ export const Group = Type.Object({
|
|
|
14
14
|
})
|
|
15
15
|
|
|
16
16
|
export const GroupListInput = Type.Object({
|
|
17
|
-
useCache: Type.Optional(Type.Boolean())
|
|
17
|
+
useCache: Type.Optional(Type.Boolean()),
|
|
18
|
+
sendLatestSA: Type.Optional(Type.Boolean())
|
|
18
19
|
})
|
|
19
20
|
|
|
20
21
|
export const TAKList_Group = TAKList(Group);
|
|
@@ -22,7 +23,7 @@ export const TAKList_Group = TAKList(Group);
|
|
|
22
23
|
export default class GroupCommands extends Commands {
|
|
23
24
|
schema = {
|
|
24
25
|
list: {
|
|
25
|
-
description: 'List
|
|
26
|
+
description: 'List Groups',
|
|
26
27
|
params: Type.Object({}),
|
|
27
28
|
query: Type.Object({}),
|
|
28
29
|
formats: [ CommandOutputFormat.JSON ]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// A fixed-size Ring Buffer (Circular Queue) for zero-allocation operations.
|
|
2
|
+
export class Queue<T> {
|
|
3
|
+
private buffer: (T | undefined)[];
|
|
4
|
+
private capacity: number;
|
|
5
|
+
private head: number;
|
|
6
|
+
private tail: number;
|
|
7
|
+
private _length: number;
|
|
8
|
+
|
|
9
|
+
constructor(capacity: number = 10000) {
|
|
10
|
+
this.capacity = capacity;
|
|
11
|
+
this.buffer = new Array(capacity);
|
|
12
|
+
this.head = 0;
|
|
13
|
+
this.tail = 0;
|
|
14
|
+
this._length = 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Add item to the queue. Returns false if full.
|
|
18
|
+
push(item: T): boolean {
|
|
19
|
+
if (this._length >= this.capacity) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this.buffer[this.tail] = item;
|
|
24
|
+
this.tail = (this.tail + 1) % this.capacity;
|
|
25
|
+
this._length++;
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Peek at the next item
|
|
30
|
+
peek(): T | undefined {
|
|
31
|
+
if (this._length === 0) return undefined;
|
|
32
|
+
return this.buffer[this.head];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
pop(): T | undefined {
|
|
36
|
+
if (this._length === 0) return undefined;
|
|
37
|
+
|
|
38
|
+
const item = this.buffer[this.head];
|
|
39
|
+
this.buffer[this.head] = undefined; // Clear reference for GC
|
|
40
|
+
this.head = (this.head + 1) % this.capacity;
|
|
41
|
+
this._length--;
|
|
42
|
+
|
|
43
|
+
return item;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get length(): number {
|
|
47
|
+
return this._length;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get isFull(): boolean {
|
|
51
|
+
return this._length === this.capacity;
|
|
52
|
+
}
|
|
53
|
+
}
|
package/package.json
CHANGED