vue-cot 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 +69 -0
- package/dist/index.cjs +297 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +230 -0
- package/dist/index.d.ts +230 -0
- package/dist/index.js +264 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 @belblue
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# vue-cot
|
|
2
|
+
|
|
3
|
+
A focused Vue 3 library for **Cursor on Target (CoT)** — the XML messaging protocol used
|
|
4
|
+
by NATO/US tactical systems such as ATAK, WinTAK and TAK Server.
|
|
5
|
+
|
|
6
|
+
> **Status:** feature-complete **v0.1**. A well-scoped, self-contained library — not a
|
|
7
|
+
> work in progress. Actively maintained.
|
|
8
|
+
|
|
9
|
+
Related packages (`@vue-tactical/map`, `@vue-tactical/symbols`, `@vue-tactical/coords`)
|
|
10
|
+
would be interesting to build but are **not currently planned** — open an issue if you'd
|
|
11
|
+
like to collaborate. PRs welcome.
|
|
12
|
+
|
|
13
|
+
## What it does
|
|
14
|
+
|
|
15
|
+
`vue-cot` connects to a live CoT stream over a WebSocket, parses each tactical message
|
|
16
|
+
into typed objects, and keeps a reactive store of entities (units, vehicles, sensors)
|
|
17
|
+
that updates and expires itself in real time — ready to render in any Vue 3 app.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
WebSocket stream -> connection layer -> parse CoT XML -> reactive entity store -> your Vue app
|
|
21
|
+
(CoT XML) reconnect/keepalive XML -> typed Map<uid, Entity> + TTL list / detail
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- `useWebSocket` / `useReconnectingWebSocket` — WebSocket lifecycle with exponential
|
|
27
|
+
backoff + jitter.
|
|
28
|
+
- `useTakConnection` — application-level keepalive (ping/zombie detection) and
|
|
29
|
+
newline-delimited CoT framing.
|
|
30
|
+
- `useCoTStream` — parses framed CoT into typed `CoTEvent` objects.
|
|
31
|
+
- `useEntityStore` — reactive `Map<uid, Entity>` with single-`rAF` TTL expiry and derived
|
|
32
|
+
projections.
|
|
33
|
+
- `parseCoT` / `serializeCoT` — pure, fully-typed, unit-tested CoT XML parser/serialiser.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install vue-cot vue
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`vue` (3.5+) is a peer dependency.
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { useCoTStream, useEntityStore } from 'vue-cot'
|
|
47
|
+
|
|
48
|
+
const { upsert, list, count } = useEntityStore()
|
|
49
|
+
const { status, reconnect } = useCoTStream('wss://your-tak-server:8087', {
|
|
50
|
+
onEvent: upsert,
|
|
51
|
+
})
|
|
52
|
+
// `list` is a reactive array of live entities; `status` reflects the connection.
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Not in scope for v1
|
|
56
|
+
|
|
57
|
+
mTLS client certificates, a Web Worker parser, the CoT Protobuf variant, TAK Server
|
|
58
|
+
federation, mission packages, and map/symbology rendering. These may come in later
|
|
59
|
+
releases if there's demand — file an issue.
|
|
60
|
+
|
|
61
|
+
## Links
|
|
62
|
+
|
|
63
|
+
- Repository & issues: https://github.com/belblue/vue-cot
|
|
64
|
+
- Contributing: issues and PRs welcome; safe fixes and dependency updates get merged,
|
|
65
|
+
larger changes are best discussed in an issue first.
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
[MIT](./LICENSE)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
parseCoT: () => parseCoT,
|
|
24
|
+
serializeCoT: () => serializeCoT,
|
|
25
|
+
useCoTStream: () => useCoTStream,
|
|
26
|
+
useEntityStore: () => useEntityStore,
|
|
27
|
+
useReconnectingWebSocket: () => useReconnectingWebSocket,
|
|
28
|
+
useTakConnection: () => useTakConnection,
|
|
29
|
+
useWebSocket: () => useWebSocket
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(index_exports);
|
|
32
|
+
|
|
33
|
+
// src/composables/use-cot-stream.ts
|
|
34
|
+
var import_vue3 = require("vue");
|
|
35
|
+
|
|
36
|
+
// src/protocol/parse.ts
|
|
37
|
+
var import_fast_xml_parser = require("fast-xml-parser");
|
|
38
|
+
var parser = new import_fast_xml_parser.XMLParser({
|
|
39
|
+
ignoreAttributes: false,
|
|
40
|
+
attributeNamePrefix: ""
|
|
41
|
+
});
|
|
42
|
+
function parseCoT(xml) {
|
|
43
|
+
const raw = parser.parse(xml);
|
|
44
|
+
const event = raw?.event;
|
|
45
|
+
if (!event)
|
|
46
|
+
throw new Error("invalid CoT:missing <event> root");
|
|
47
|
+
const point = event.point;
|
|
48
|
+
if (!point)
|
|
49
|
+
throw new Error("invalid CoT:<event> missing <point>");
|
|
50
|
+
return {
|
|
51
|
+
version: String(event.version),
|
|
52
|
+
uid: String(event.uid),
|
|
53
|
+
type: String(event.type),
|
|
54
|
+
time: String(event.time),
|
|
55
|
+
start: String(event.start),
|
|
56
|
+
stale: String(event.stale),
|
|
57
|
+
how: event.how ? String(event.how) : void 0,
|
|
58
|
+
point: {
|
|
59
|
+
lat: Number(point.lat),
|
|
60
|
+
lon: Number(point.lon),
|
|
61
|
+
hae: Number(point.hae),
|
|
62
|
+
ce: Number(point.ce),
|
|
63
|
+
le: Number(point.le)
|
|
64
|
+
},
|
|
65
|
+
detail: event.detail ? {
|
|
66
|
+
contact: event.detail.contact ? { callsign: event.detail.contact.callsign } : void 0
|
|
67
|
+
} : void 0
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/composables/use-tak-connection.ts
|
|
72
|
+
var import_vue2 = require("vue");
|
|
73
|
+
|
|
74
|
+
// src/composables/use-reconnecting-websocket.ts
|
|
75
|
+
var import_vue = require("vue");
|
|
76
|
+
|
|
77
|
+
// src/utils/backoff.ts
|
|
78
|
+
function backoffDelay(attempt, options = {}) {
|
|
79
|
+
const { baseMs = 1e3, maxMs = 15e3 } = options;
|
|
80
|
+
const exponential = Math.min(baseMs * 2 ** attempt, maxMs);
|
|
81
|
+
const half = exponential / 2;
|
|
82
|
+
return half + Math.random() * half;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/composables/use-reconnecting-websocket.ts
|
|
86
|
+
function useReconnectingWebSocket(url, options = {}) {
|
|
87
|
+
const {
|
|
88
|
+
baseMs = 1e3,
|
|
89
|
+
maxMs = 15e3,
|
|
90
|
+
maxAttempts = Number.POSITIVE_INFINITY
|
|
91
|
+
} = options;
|
|
92
|
+
const status = (0, import_vue.ref)("closed");
|
|
93
|
+
const data = (0, import_vue.ref)(null);
|
|
94
|
+
const attempts = (0, import_vue.ref)(0);
|
|
95
|
+
let ws = null;
|
|
96
|
+
let reconnectTimer;
|
|
97
|
+
let manualClose = false;
|
|
98
|
+
function connect() {
|
|
99
|
+
ws = new WebSocket(url);
|
|
100
|
+
status.value = "connecting";
|
|
101
|
+
ws.onopen = () => {
|
|
102
|
+
status.value = "open";
|
|
103
|
+
attempts.value = 0;
|
|
104
|
+
};
|
|
105
|
+
ws.onmessage = (event) => {
|
|
106
|
+
if (typeof event.data === "string")
|
|
107
|
+
data.value = event.data;
|
|
108
|
+
};
|
|
109
|
+
ws.onerror = () => {
|
|
110
|
+
console.error("[useReconnectingWebSocket] connection error");
|
|
111
|
+
};
|
|
112
|
+
ws.onclose = () => {
|
|
113
|
+
if (manualClose || attempts.value >= maxAttempts) {
|
|
114
|
+
status.value = "closed";
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const delay = backoffDelay(attempts.value, { baseMs, maxMs });
|
|
118
|
+
attempts.value++;
|
|
119
|
+
status.value = "reconnecting";
|
|
120
|
+
reconnectTimer = setTimeout(connect, delay);
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function reconnect() {
|
|
124
|
+
clearTimeout(reconnectTimer);
|
|
125
|
+
attempts.value = 0;
|
|
126
|
+
if (ws) {
|
|
127
|
+
ws.onclose = null;
|
|
128
|
+
ws.close();
|
|
129
|
+
}
|
|
130
|
+
manualClose = false;
|
|
131
|
+
connect();
|
|
132
|
+
}
|
|
133
|
+
function close() {
|
|
134
|
+
manualClose = true;
|
|
135
|
+
clearTimeout(reconnectTimer);
|
|
136
|
+
ws?.close();
|
|
137
|
+
}
|
|
138
|
+
function send(payload) {
|
|
139
|
+
if (ws && ws.readyState === WebSocket.OPEN)
|
|
140
|
+
ws.send(payload);
|
|
141
|
+
}
|
|
142
|
+
connect();
|
|
143
|
+
(0, import_vue.onScopeDispose)(close);
|
|
144
|
+
return { status, data, attempts, reconnect, close, send };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/composables/use-tak-connection.ts
|
|
148
|
+
function useTakConnection(url, options = {}) {
|
|
149
|
+
const connection = useReconnectingWebSocket(url);
|
|
150
|
+
const { status, data, attempts, close, reconnect, send } = connection;
|
|
151
|
+
const { pingIntervalMs = 15e3, pingTimeoutMs = 3e4 } = options;
|
|
152
|
+
let lastSeen = Date.now();
|
|
153
|
+
(0, import_vue2.watch)(status, (newStatus, oldStatus) => {
|
|
154
|
+
if (newStatus === "open") {
|
|
155
|
+
lastSeen = Date.now();
|
|
156
|
+
options.onConnect?.();
|
|
157
|
+
} else if (oldStatus === "open") {
|
|
158
|
+
options.onDisconnect?.();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
(0, import_vue2.watch)(data, () => {
|
|
162
|
+
lastSeen = Date.now();
|
|
163
|
+
});
|
|
164
|
+
let buffer = "";
|
|
165
|
+
(0, import_vue2.watch)(data, (raw) => {
|
|
166
|
+
if (raw === null)
|
|
167
|
+
return;
|
|
168
|
+
buffer += raw;
|
|
169
|
+
const frames = buffer.split("\n");
|
|
170
|
+
buffer = frames.pop() ?? "";
|
|
171
|
+
for (const frame of frames) {
|
|
172
|
+
const trimmed = frame.trim();
|
|
173
|
+
if (trimmed) {
|
|
174
|
+
options.onMessage?.(trimmed);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
const pingTimer = setInterval(() => {
|
|
179
|
+
send("ping");
|
|
180
|
+
if (Date.now() - lastSeen > pingTimeoutMs)
|
|
181
|
+
reconnect();
|
|
182
|
+
}, pingIntervalMs);
|
|
183
|
+
(0, import_vue2.onScopeDispose)(() => clearInterval(pingTimer));
|
|
184
|
+
return { status, data, attempts, reconnect, close, send };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/composables/use-cot-stream.ts
|
|
188
|
+
function useCoTStream(url, options = {}) {
|
|
189
|
+
const event = (0, import_vue3.ref)(null);
|
|
190
|
+
const error = (0, import_vue3.ref)(null);
|
|
191
|
+
const connection = useTakConnection(url, {
|
|
192
|
+
...options,
|
|
193
|
+
onMessage: (raw) => {
|
|
194
|
+
try {
|
|
195
|
+
const parsed = parseCoT(raw);
|
|
196
|
+
event.value = parsed;
|
|
197
|
+
error.value = null;
|
|
198
|
+
options.onEvent?.(parsed);
|
|
199
|
+
options.onMessage?.(raw);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
error.value = err;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
return { ...connection, event, error };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/composables/use-entity-store.ts
|
|
209
|
+
var import_vue4 = require("vue");
|
|
210
|
+
function useEntityStore() {
|
|
211
|
+
const entities = (0, import_vue4.shallowRef)(/* @__PURE__ */ new Map());
|
|
212
|
+
function upsert(event) {
|
|
213
|
+
entities.value.set(event.uid, { ...event, receivedAt: Date.now() });
|
|
214
|
+
(0, import_vue4.triggerRef)(entities);
|
|
215
|
+
}
|
|
216
|
+
let rafId = 0;
|
|
217
|
+
function sweep() {
|
|
218
|
+
const now = Date.now();
|
|
219
|
+
let remove = false;
|
|
220
|
+
for (const [uid, entity] of entities.value) {
|
|
221
|
+
if (Date.parse(entity.stale) < now) {
|
|
222
|
+
entities.value.delete(uid);
|
|
223
|
+
remove = true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (remove) {
|
|
227
|
+
(0, import_vue4.triggerRef)(entities);
|
|
228
|
+
}
|
|
229
|
+
rafId = requestAnimationFrame(sweep);
|
|
230
|
+
}
|
|
231
|
+
rafId = requestAnimationFrame(sweep);
|
|
232
|
+
(0, import_vue4.onScopeDispose)(() => cancelAnimationFrame(rafId));
|
|
233
|
+
const list = (0, import_vue4.computed)(() => Array.from(entities.value.values()));
|
|
234
|
+
const count = (0, import_vue4.computed)(() => entities.value.size);
|
|
235
|
+
const byAffiliation = (0, import_vue4.computed)(() => {
|
|
236
|
+
const groups = /* @__PURE__ */ new Map();
|
|
237
|
+
for (const entity of entities.value.values()) {
|
|
238
|
+
const affiliation = entity.type.split("-")[1] ?? "unknown";
|
|
239
|
+
const group = groups.get(affiliation) ?? [];
|
|
240
|
+
group.push(entity);
|
|
241
|
+
groups.set(affiliation, group);
|
|
242
|
+
}
|
|
243
|
+
return groups;
|
|
244
|
+
});
|
|
245
|
+
return { entities, upsert, list, count, byAffiliation };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/composables/use-websocket.ts
|
|
249
|
+
var import_vue5 = require("vue");
|
|
250
|
+
function useWebSocket(url) {
|
|
251
|
+
const status = (0, import_vue5.ref)("closed");
|
|
252
|
+
const data = (0, import_vue5.ref)(null);
|
|
253
|
+
const ws = new WebSocket(url);
|
|
254
|
+
status.value = "connecting";
|
|
255
|
+
ws.onopen = () => {
|
|
256
|
+
status.value = "open";
|
|
257
|
+
};
|
|
258
|
+
ws.onmessage = (event) => {
|
|
259
|
+
if (typeof event.data === "string") {
|
|
260
|
+
data.value = event.data;
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
ws.onclose = () => {
|
|
264
|
+
status.value = "closed";
|
|
265
|
+
};
|
|
266
|
+
ws.onerror = () => {
|
|
267
|
+
console.error("[useWebSocket] connection error");
|
|
268
|
+
};
|
|
269
|
+
function send(payload) {
|
|
270
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
271
|
+
ws.send(payload);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
(0, import_vue5.onScopeDispose)(() => {
|
|
275
|
+
ws.close();
|
|
276
|
+
});
|
|
277
|
+
return { status, data, send };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/protocol/serialize.ts
|
|
281
|
+
function serializeCoT(event) {
|
|
282
|
+
const how = event.how ? ` how="${event.how}"` : "";
|
|
283
|
+
const callsign = event.detail?.contact?.callsign;
|
|
284
|
+
const detail = callsign ? `<detail><contact callsign="${callsign}"/></detail>` : "";
|
|
285
|
+
return `<event version="${event.version}" uid="${event.uid}" type="${event.type}" time="${event.time}" start="${event.start}" stale="${event.stale}"${how}><point lat="${event.point.lat}" lon="${event.point.lon}" hae="${event.point.hae}" ce="${event.point.ce}" le="${event.point.le}"/>${detail}</event>`;
|
|
286
|
+
}
|
|
287
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
288
|
+
0 && (module.exports = {
|
|
289
|
+
parseCoT,
|
|
290
|
+
serializeCoT,
|
|
291
|
+
useCoTStream,
|
|
292
|
+
useEntityStore,
|
|
293
|
+
useReconnectingWebSocket,
|
|
294
|
+
useTakConnection,
|
|
295
|
+
useWebSocket
|
|
296
|
+
});
|
|
297
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/composables/use-cot-stream.ts","../src/protocol/parse.ts","../src/composables/use-tak-connection.ts","../src/composables/use-reconnecting-websocket.ts","../src/utils/backoff.ts","../src/composables/use-entity-store.ts","../src/composables/use-websocket.ts","../src/protocol/serialize.ts"],"sourcesContent":["export * from './composables/use-cot-stream'\nexport * from './composables/use-entity-store'\nexport * from './composables/use-reconnecting-websocket'\n\nexport * from './composables/use-tak-connection'\nexport * from './composables/use-websocket'\nexport * from './protocol/parse'\nexport * from './protocol/serialize'\nexport * from './protocol/types'\n","import type { CoTEvent } from '../protocol/types'\nimport type { TakConnectionOptions } from './use-tak-connection'\nimport { ref } from 'vue'\nimport { parseCoT } from '../protocol/parse'\nimport { useTakConnection } from './use-tak-connection'\n\nexport type CoTStreamOptions = TakConnectionOptions & {\n onEvent?: (event: CoTEvent) => void\n}\n\n/**\n * High-level CoT stream: wraps {@link useTakConnection} and parses each framed\n * message into a typed {@link CoTEvent}. Prefer the lossless `onEvent` callback\n * (fired once per parsed frame) to feed a store; the reactive `event` ref only\n * ever holds the most recent message.\n *\n * @param url - WebSocket URL of the CoT feed.\n * @param options - {@link TakConnectionOptions} plus `onEvent(event)`, called\n * for every successfully parsed CoT message.\n * @returns The underlying connection controls plus reactive `event` (latest\n * parsed `CoTEvent` or `null`) and `error` (last parse error or `null`).\n *\n * @example\n * ```ts\n * const store = useEntityStore()\n * useCoTStream(url, { onEvent: (e) => store.upsert(e) })\n * ```\n */\nexport function useCoTStream(url: string, options: CoTStreamOptions = {}) {\n const event = ref<CoTEvent | null>(null)\n const error = ref<Error | null>(null)\n\n const connection = useTakConnection(url, {\n ...options,\n onMessage: (raw) => {\n try {\n const parsed = parseCoT(raw)\n event.value = parsed\n error.value = null\n options.onEvent?.(parsed)\n options.onMessage?.(raw)\n }\n catch (err) {\n error.value = err as Error\n }\n },\n })\n\n return { ...connection, event, error }\n}\n","import type { CoTEvent } from './types'\nimport { XMLParser } from 'fast-xml-parser'\n\nconst parser = new XMLParser({\n ignoreAttributes: false,\n attributeNamePrefix: '',\n})\n\n/**\n * Parses a CoT (Cursor on Target) XML string into a typed {@link CoTEvent}.\n * Inverse of {@link serializeCoT}.\n *\n * @param xml - A single CoT XML message (an `<event>` element).\n * @returns The parsed, typed event.\n * @throws If the XML has no `<event>` root or is missing its `<point>`.\n */\nexport function parseCoT(xml: string): CoTEvent {\n const raw = parser.parse(xml)\n const event = raw?.event\n if (!event)\n throw new Error('invalid CoT:missing <event> root')\n const point = event.point\n if (!point)\n throw new Error('invalid CoT:<event> missing <point>')\n\n return {\n version: String(event.version),\n uid: String(event.uid),\n type: String(event.type),\n time: String(event.time),\n start: String(event.start),\n stale: String(event.stale),\n how: event.how ? String(event.how) : undefined,\n point: {\n lat: Number(point.lat),\n lon: Number(point.lon),\n hae: Number(point.hae),\n ce: Number(point.ce),\n le: Number(point.le),\n },\n detail: event.detail\n ? {\n contact: event.detail.contact\n ? { callsign: event.detail.contact.callsign }\n : undefined,\n }\n : undefined,\n }\n}\n","import { onScopeDispose, watch } from 'vue'\nimport { useReconnectingWebSocket } from './use-reconnecting-websocket'\n\nexport interface TakConnectionOptions {\n pingIntervalMs?: number\n pingTimeoutMs?: number\n onConnect?: () => void\n onMessage?: (cot: string) => void\n onDisconnect?: () => void\n onError?: () => void\n}\n\n/**\n * Application-level TAK connection on top of {@link useReconnectingWebSocket}.\n * Adds ping keepalive, zombie-connection detection (forces a reconnect when no\n * traffic is seen within `pingTimeoutMs`) and newline-delimited message framing,\n * so `onMessage` fires once per whole CoT message.\n *\n * @param url - WebSocket URL of the TAK/CoT feed.\n * @param options - Lifecycle hooks (`onConnect`, `onMessage`, `onDisconnect`,\n * `onError`) plus `pingIntervalMs` (default 15000) and `pingTimeoutMs` (default 30000).\n * @returns Reactive `status`, `data`, `attempts` and the `reconnect`, `close`, `send` controls.\n */\nexport function useTakConnection(\n url: string,\n options: TakConnectionOptions = {},\n) {\n const connection = useReconnectingWebSocket(url)\n const { status, data, attempts, close, reconnect, send } = connection\n const { pingIntervalMs = 15_000, pingTimeoutMs = 30_000 } = options\n let lastSeen = Date.now()\n\n watch(status, (newStatus, oldStatus) => {\n if (newStatus === 'open') {\n lastSeen = Date.now()\n options.onConnect?.()\n }\n else if (oldStatus === 'open') {\n options.onDisconnect?.()\n }\n })\n watch(data, () => {\n lastSeen = Date.now()\n })\n\n let buffer = ''\n watch(data, (raw) => {\n if (raw === null)\n return\n buffer += raw\n const frames = buffer.split('\\n')\n buffer = frames.pop() ?? ''\n for (const frame of frames) {\n const trimmed = frame.trim()\n if (trimmed) {\n options.onMessage?.(trimmed)\n }\n }\n })\n\n const pingTimer = setInterval(() => {\n send('ping')\n if (Date.now() - lastSeen > pingTimeoutMs)\n reconnect()\n }, pingIntervalMs)\n\n onScopeDispose(() => clearInterval(pingTimer))\n\n return { status, data, attempts, reconnect, close, send }\n}\n","import type { WebSocketStatus } from './use-websocket'\nimport { onScopeDispose, ref } from 'vue'\nimport { backoffDelay } from '../utils/backoff'\n\nexport type ReconnectingStatus = WebSocketStatus | 'reconnecting'\n\nexport interface ReconnectingOptions {\n baseMs?: number\n maxMs?: number\n maxAttempts?: number\n}\n\n/**\n * Like {@link useWebSocket}, but automatically reconnects with exponential\n * backoff and jitter when the connection drops unexpectedly.\n *\n * @param url - WebSocket URL to connect to.\n * @param options - Backoff tuning: `baseMs` (first delay, default 1000),\n * `maxMs` (delay cap, default 15000) and `maxAttempts` (default unlimited).\n * @returns The reactive `status`, `data` and `attempts`, plus `reconnect()`\n * (force an immediate retry), `close()` (stop for good) and `send()`.\n *\n * @example\n * ```ts\n * const { status, attempts, reconnect } = useReconnectingWebSocket(url, { maxAttempts: 10 })\n * ```\n */\nexport function useReconnectingWebSocket(\n url: string,\n options: ReconnectingOptions = {},\n) {\n const {\n baseMs = 1000,\n maxMs = 15_000,\n maxAttempts = Number.POSITIVE_INFINITY,\n } = options\n\n const status = ref<ReconnectingStatus>('closed')\n const data = ref<string | null>(null)\n const attempts = ref(0)\n\n let ws: WebSocket | null = null\n let reconnectTimer: ReturnType<typeof setTimeout> | undefined\n let manualClose = false\n\n function connect() {\n ws = new WebSocket(url)\n status.value = 'connecting'\n\n ws.onopen = () => {\n status.value = 'open'\n attempts.value = 0\n }\n ws.onmessage = (event: MessageEvent) => {\n if (typeof event.data === 'string')\n data.value = event.data\n }\n ws.onerror = () => {\n console.error('[useReconnectingWebSocket] connection error')\n }\n ws.onclose = () => {\n if (manualClose || attempts.value >= maxAttempts) {\n status.value = 'closed'\n return\n }\n const delay = backoffDelay(attempts.value, { baseMs, maxMs })\n attempts.value++\n status.value = 'reconnecting'\n reconnectTimer = setTimeout(connect, delay)\n }\n }\n\n function reconnect() {\n clearTimeout(reconnectTimer)\n attempts.value = 0\n if (ws) {\n ws.onclose = null\n ws.close()\n }\n manualClose = false\n connect()\n }\n function close() {\n manualClose = true\n clearTimeout(reconnectTimer)\n ws?.close()\n }\n\n function send(payload: string) {\n if (ws && ws.readyState === WebSocket.OPEN)\n ws.send(payload)\n }\n\n connect()\n onScopeDispose(close)\n\n return { status, data, attempts, reconnect, close, send }\n}\n","export interface BackoffOptions {\n baseMs?: number\n maxMs?: number\n}\n\n/**\n * Computes an exponential-backoff delay (in milliseconds) for a retry attempt.\n * The delay is `baseMs * 2 ** attempt`, capped at `maxMs`, with jitter added to\n * avoid the thundering-herd problem when many clients reconnect at once.\n *\n * @param attempt - Zero-based retry attempt number.\n * @param options - `baseMs` (default 1000) and `maxMs` (cap, default 15000).\n * @returns The delay to wait before the next attempt, in milliseconds.\n */\nexport function backoffDelay(\n attempt: number,\n options: BackoffOptions = {},\n): number {\n const { baseMs = 1000, maxMs = 15_000 } = options\n const exponential = Math.min(baseMs * 2 ** attempt, maxMs)\n const half = exponential / 2\n return half + Math.random() * half\n}\n","import type { CoTEvent } from '../protocol/types'\nimport { computed, onScopeDispose, shallowRef, triggerRef } from 'vue'\n\nexport type Entity = CoTEvent & { receivedAt: number }\n/**\n * Reactive store of CoT entities keyed by `uid`. Backed by a `shallowRef<Map>`\n * with a single `requestAnimationFrame` loop that sweeps out entities whose\n * `stale` time has passed, so the collection self-expires.\n *\n * @returns `entities` (the reactive `Map<uid, Entity>`), `upsert(event)`\n * (insert or replace, stamping `receivedAt`), `list` (entities as a reactive\n * array), `count` and `byAffiliation` (entities grouped by CoT affiliation).\n *\n * @example\n * ```ts\n * const store = useEntityStore()\n * store.upsert(parseCoT(xml))\n * // store.list.value -> Entity[]\n * ```\n */\nexport function useEntityStore() {\n const entities = shallowRef(new Map<string, Entity>())\n\n function upsert(event: CoTEvent) {\n entities.value.set(event.uid, { ...event, receivedAt: Date.now() })\n triggerRef(entities)\n }\n\n let rafId = 0\n function sweep() {\n const now = Date.now()\n let remove = false\n for (const [uid, entity] of entities.value) {\n if (Date.parse(entity.stale) < now) {\n entities.value.delete(uid)\n remove = true\n }\n }\n if (remove) {\n triggerRef(entities)\n }\n rafId = requestAnimationFrame(sweep)\n }\n rafId = requestAnimationFrame(sweep)\n onScopeDispose(() => cancelAnimationFrame(rafId))\n\n const list = computed(() => Array.from(entities.value.values()))\n const count = computed(() => entities.value.size)\n\n const byAffiliation = computed(() => {\n const groups = new Map<string, Entity[]>()\n for (const entity of entities.value.values()) {\n const affiliation = entity.type.split('-')[1] ?? 'unknown'\n const group = groups.get(affiliation) ?? []\n group.push(entity)\n groups.set(affiliation, group)\n }\n return groups\n })\n\n return { entities, upsert, list, count, byAffiliation }\n}\n","import { onScopeDispose, ref } from 'vue'\n\nexport type WebSocketStatus = 'connecting' | 'open' | 'closing' | 'closed'\n\n/**\n * Reactive wrapper around a single browser `WebSocket`. Opens the connection\n * immediately and exposes its live status and last received message. Does not\n * reconnect — see {@link useReconnectingWebSocket} for that.\n *\n * The socket is closed automatically when the surrounding reactive scope is disposed.\n *\n * @param url - WebSocket URL to connect to, e.g. `wss://tak.example/cot`.\n * @returns `status` (reactive connection state), `data` (latest string message\n * or `null`) and `send` (sends a payload only while the socket is open).\n *\n * @example\n * ```ts\n * const { status, data } = useWebSocket('wss://tak.example/cot')\n * ```\n */\nexport function useWebSocket(url: string) {\n const status = ref<WebSocketStatus>('closed')\n const data = ref<string | null>(null)\n\n const ws = new WebSocket(url)\n status.value = 'connecting'\n\n ws.onopen = () => {\n status.value = 'open'\n }\n ws.onmessage = (event: MessageEvent) => {\n if (typeof event.data === 'string') {\n data.value = event.data\n }\n }\n ws.onclose = () => {\n status.value = 'closed'\n }\n ws.onerror = () => {\n console.error('[useWebSocket] connection error')\n }\n\n function send(payload: string) {\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(payload)\n }\n }\n\n onScopeDispose(() => {\n ws.close()\n })\n\n return { status, data, send }\n}\n","import type { CoTEvent } from './types'\n\n/**\n * Serialises a typed {@link CoTEvent} back into a CoT XML string.\n * Inverse of {@link parseCoT}.\n *\n * @param event - The event to serialise.\n * @returns A CoT XML `<event>` string.\n */\nexport function serializeCoT(event: CoTEvent): string {\n const how = event.how ? ` how=\"${event.how}\"` : ''\n const callsign = event.detail?.contact?.callsign\n const detail = callsign\n ? `<detail><contact callsign=\"${callsign}\"/></detail>`\n : ''\n\n return (\n `<event version=\"${event.version}\" uid=\"${event.uid}\" type=\"${event.type}\" `\n + `time=\"${event.time}\" start=\"${event.start}\" stale=\"${event.stale}\"${how}>`\n + `<point lat=\"${event.point.lat}\" lon=\"${event.point.lon}\" hae=\"${event.point.hae}\" `\n + `ce=\"${event.point.ce}\" le=\"${event.point.le}\"/>${\n detail\n }</event>`\n )\n}\n"],"mappings":"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","names":["import_vue","import_vue","import_vue","import_vue"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import * as vue from 'vue';
|
|
2
|
+
|
|
3
|
+
type WebSocketStatus = 'connecting' | 'open' | 'closing' | 'closed';
|
|
4
|
+
/**
|
|
5
|
+
* Reactive wrapper around a single browser `WebSocket`. Opens the connection
|
|
6
|
+
* immediately and exposes its live status and last received message. Does not
|
|
7
|
+
* reconnect — see {@link useReconnectingWebSocket} for that.
|
|
8
|
+
*
|
|
9
|
+
* The socket is closed automatically when the surrounding reactive scope is disposed.
|
|
10
|
+
*
|
|
11
|
+
* @param url - WebSocket URL to connect to, e.g. `wss://tak.example/cot`.
|
|
12
|
+
* @returns `status` (reactive connection state), `data` (latest string message
|
|
13
|
+
* or `null`) and `send` (sends a payload only while the socket is open).
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const { status, data } = useWebSocket('wss://tak.example/cot')
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
declare function useWebSocket(url: string): {
|
|
21
|
+
status: vue.Ref<WebSocketStatus, WebSocketStatus>;
|
|
22
|
+
data: vue.Ref<string | null, string | null>;
|
|
23
|
+
send: (payload: string) => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type ReconnectingStatus = WebSocketStatus | 'reconnecting';
|
|
27
|
+
interface ReconnectingOptions {
|
|
28
|
+
baseMs?: number;
|
|
29
|
+
maxMs?: number;
|
|
30
|
+
maxAttempts?: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Like {@link useWebSocket}, but automatically reconnects with exponential
|
|
34
|
+
* backoff and jitter when the connection drops unexpectedly.
|
|
35
|
+
*
|
|
36
|
+
* @param url - WebSocket URL to connect to.
|
|
37
|
+
* @param options - Backoff tuning: `baseMs` (first delay, default 1000),
|
|
38
|
+
* `maxMs` (delay cap, default 15000) and `maxAttempts` (default unlimited).
|
|
39
|
+
* @returns The reactive `status`, `data` and `attempts`, plus `reconnect()`
|
|
40
|
+
* (force an immediate retry), `close()` (stop for good) and `send()`.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* const { status, attempts, reconnect } = useReconnectingWebSocket(url, { maxAttempts: 10 })
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
declare function useReconnectingWebSocket(url: string, options?: ReconnectingOptions): {
|
|
48
|
+
status: vue.Ref<ReconnectingStatus, ReconnectingStatus>;
|
|
49
|
+
data: vue.Ref<string | null, string | null>;
|
|
50
|
+
attempts: vue.Ref<number, number>;
|
|
51
|
+
reconnect: () => void;
|
|
52
|
+
close: () => void;
|
|
53
|
+
send: (payload: string) => void;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
interface CoTPoint {
|
|
57
|
+
lat: number;
|
|
58
|
+
lon: number;
|
|
59
|
+
hae: number;
|
|
60
|
+
ce: number;
|
|
61
|
+
le: number;
|
|
62
|
+
}
|
|
63
|
+
interface CoTContact {
|
|
64
|
+
callsign?: string;
|
|
65
|
+
endpoint?: string;
|
|
66
|
+
}
|
|
67
|
+
interface CoTDetail {
|
|
68
|
+
contact?: CoTContact;
|
|
69
|
+
}
|
|
70
|
+
interface CoTEvent {
|
|
71
|
+
version: string;
|
|
72
|
+
uid: string;
|
|
73
|
+
type: string;
|
|
74
|
+
time: string;
|
|
75
|
+
start: string;
|
|
76
|
+
stale: string;
|
|
77
|
+
how?: string;
|
|
78
|
+
point: CoTPoint;
|
|
79
|
+
detail?: CoTDetail;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface TakConnectionOptions {
|
|
83
|
+
pingIntervalMs?: number;
|
|
84
|
+
pingTimeoutMs?: number;
|
|
85
|
+
onConnect?: () => void;
|
|
86
|
+
onMessage?: (cot: string) => void;
|
|
87
|
+
onDisconnect?: () => void;
|
|
88
|
+
onError?: () => void;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Application-level TAK connection on top of {@link useReconnectingWebSocket}.
|
|
92
|
+
* Adds ping keepalive, zombie-connection detection (forces a reconnect when no
|
|
93
|
+
* traffic is seen within `pingTimeoutMs`) and newline-delimited message framing,
|
|
94
|
+
* so `onMessage` fires once per whole CoT message.
|
|
95
|
+
*
|
|
96
|
+
* @param url - WebSocket URL of the TAK/CoT feed.
|
|
97
|
+
* @param options - Lifecycle hooks (`onConnect`, `onMessage`, `onDisconnect`,
|
|
98
|
+
* `onError`) plus `pingIntervalMs` (default 15000) and `pingTimeoutMs` (default 30000).
|
|
99
|
+
* @returns Reactive `status`, `data`, `attempts` and the `reconnect`, `close`, `send` controls.
|
|
100
|
+
*/
|
|
101
|
+
declare function useTakConnection(url: string, options?: TakConnectionOptions): {
|
|
102
|
+
status: vue.Ref<ReconnectingStatus, ReconnectingStatus>;
|
|
103
|
+
data: vue.Ref<string | null, string | null>;
|
|
104
|
+
attempts: vue.Ref<number, number>;
|
|
105
|
+
reconnect: () => void;
|
|
106
|
+
close: () => void;
|
|
107
|
+
send: (payload: string) => void;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
type CoTStreamOptions = TakConnectionOptions & {
|
|
111
|
+
onEvent?: (event: CoTEvent) => void;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* High-level CoT stream: wraps {@link useTakConnection} and parses each framed
|
|
115
|
+
* message into a typed {@link CoTEvent}. Prefer the lossless `onEvent` callback
|
|
116
|
+
* (fired once per parsed frame) to feed a store; the reactive `event` ref only
|
|
117
|
+
* ever holds the most recent message.
|
|
118
|
+
*
|
|
119
|
+
* @param url - WebSocket URL of the CoT feed.
|
|
120
|
+
* @param options - {@link TakConnectionOptions} plus `onEvent(event)`, called
|
|
121
|
+
* for every successfully parsed CoT message.
|
|
122
|
+
* @returns The underlying connection controls plus reactive `event` (latest
|
|
123
|
+
* parsed `CoTEvent` or `null`) and `error` (last parse error or `null`).
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* const store = useEntityStore()
|
|
128
|
+
* useCoTStream(url, { onEvent: (e) => store.upsert(e) })
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
declare function useCoTStream(url: string, options?: CoTStreamOptions): {
|
|
132
|
+
event: vue.Ref<{
|
|
133
|
+
version: string;
|
|
134
|
+
uid: string;
|
|
135
|
+
type: string;
|
|
136
|
+
time: string;
|
|
137
|
+
start: string;
|
|
138
|
+
stale: string;
|
|
139
|
+
how?: string | undefined;
|
|
140
|
+
point: {
|
|
141
|
+
lat: number;
|
|
142
|
+
lon: number;
|
|
143
|
+
hae: number;
|
|
144
|
+
ce: number;
|
|
145
|
+
le: number;
|
|
146
|
+
};
|
|
147
|
+
detail?: {
|
|
148
|
+
contact?: {
|
|
149
|
+
callsign?: string | undefined;
|
|
150
|
+
endpoint?: string | undefined;
|
|
151
|
+
} | undefined;
|
|
152
|
+
} | undefined;
|
|
153
|
+
} | null, CoTEvent | {
|
|
154
|
+
version: string;
|
|
155
|
+
uid: string;
|
|
156
|
+
type: string;
|
|
157
|
+
time: string;
|
|
158
|
+
start: string;
|
|
159
|
+
stale: string;
|
|
160
|
+
how?: string | undefined;
|
|
161
|
+
point: {
|
|
162
|
+
lat: number;
|
|
163
|
+
lon: number;
|
|
164
|
+
hae: number;
|
|
165
|
+
ce: number;
|
|
166
|
+
le: number;
|
|
167
|
+
};
|
|
168
|
+
detail?: {
|
|
169
|
+
contact?: {
|
|
170
|
+
callsign?: string | undefined;
|
|
171
|
+
endpoint?: string | undefined;
|
|
172
|
+
} | undefined;
|
|
173
|
+
} | undefined;
|
|
174
|
+
} | null>;
|
|
175
|
+
error: vue.Ref<Error | null, Error | null>;
|
|
176
|
+
status: vue.Ref<ReconnectingStatus, ReconnectingStatus>;
|
|
177
|
+
data: vue.Ref<string | null, string | null>;
|
|
178
|
+
attempts: vue.Ref<number, number>;
|
|
179
|
+
reconnect: () => void;
|
|
180
|
+
close: () => void;
|
|
181
|
+
send: (payload: string) => void;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
type Entity = CoTEvent & {
|
|
185
|
+
receivedAt: number;
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* Reactive store of CoT entities keyed by `uid`. Backed by a `shallowRef<Map>`
|
|
189
|
+
* with a single `requestAnimationFrame` loop that sweeps out entities whose
|
|
190
|
+
* `stale` time has passed, so the collection self-expires.
|
|
191
|
+
*
|
|
192
|
+
* @returns `entities` (the reactive `Map<uid, Entity>`), `upsert(event)`
|
|
193
|
+
* (insert or replace, stamping `receivedAt`), `list` (entities as a reactive
|
|
194
|
+
* array), `count` and `byAffiliation` (entities grouped by CoT affiliation).
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```ts
|
|
198
|
+
* const store = useEntityStore()
|
|
199
|
+
* store.upsert(parseCoT(xml))
|
|
200
|
+
* // store.list.value -> Entity[]
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
declare function useEntityStore(): {
|
|
204
|
+
entities: vue.ShallowRef<Map<string, Entity>, Map<string, Entity>>;
|
|
205
|
+
upsert: (event: CoTEvent) => void;
|
|
206
|
+
list: vue.ComputedRef<Entity[]>;
|
|
207
|
+
count: vue.ComputedRef<number>;
|
|
208
|
+
byAffiliation: vue.ComputedRef<Map<string, Entity[]>>;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Parses a CoT (Cursor on Target) XML string into a typed {@link CoTEvent}.
|
|
213
|
+
* Inverse of {@link serializeCoT}.
|
|
214
|
+
*
|
|
215
|
+
* @param xml - A single CoT XML message (an `<event>` element).
|
|
216
|
+
* @returns The parsed, typed event.
|
|
217
|
+
* @throws If the XML has no `<event>` root or is missing its `<point>`.
|
|
218
|
+
*/
|
|
219
|
+
declare function parseCoT(xml: string): CoTEvent;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Serialises a typed {@link CoTEvent} back into a CoT XML string.
|
|
223
|
+
* Inverse of {@link parseCoT}.
|
|
224
|
+
*
|
|
225
|
+
* @param event - The event to serialise.
|
|
226
|
+
* @returns A CoT XML `<event>` string.
|
|
227
|
+
*/
|
|
228
|
+
declare function serializeCoT(event: CoTEvent): string;
|
|
229
|
+
|
|
230
|
+
export { type CoTContact, type CoTDetail, type CoTEvent, type CoTPoint, type CoTStreamOptions, type Entity, type ReconnectingOptions, type ReconnectingStatus, type TakConnectionOptions, type WebSocketStatus, parseCoT, serializeCoT, useCoTStream, useEntityStore, useReconnectingWebSocket, useTakConnection, useWebSocket };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import * as vue from 'vue';
|
|
2
|
+
|
|
3
|
+
type WebSocketStatus = 'connecting' | 'open' | 'closing' | 'closed';
|
|
4
|
+
/**
|
|
5
|
+
* Reactive wrapper around a single browser `WebSocket`. Opens the connection
|
|
6
|
+
* immediately and exposes its live status and last received message. Does not
|
|
7
|
+
* reconnect — see {@link useReconnectingWebSocket} for that.
|
|
8
|
+
*
|
|
9
|
+
* The socket is closed automatically when the surrounding reactive scope is disposed.
|
|
10
|
+
*
|
|
11
|
+
* @param url - WebSocket URL to connect to, e.g. `wss://tak.example/cot`.
|
|
12
|
+
* @returns `status` (reactive connection state), `data` (latest string message
|
|
13
|
+
* or `null`) and `send` (sends a payload only while the socket is open).
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const { status, data } = useWebSocket('wss://tak.example/cot')
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
declare function useWebSocket(url: string): {
|
|
21
|
+
status: vue.Ref<WebSocketStatus, WebSocketStatus>;
|
|
22
|
+
data: vue.Ref<string | null, string | null>;
|
|
23
|
+
send: (payload: string) => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type ReconnectingStatus = WebSocketStatus | 'reconnecting';
|
|
27
|
+
interface ReconnectingOptions {
|
|
28
|
+
baseMs?: number;
|
|
29
|
+
maxMs?: number;
|
|
30
|
+
maxAttempts?: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Like {@link useWebSocket}, but automatically reconnects with exponential
|
|
34
|
+
* backoff and jitter when the connection drops unexpectedly.
|
|
35
|
+
*
|
|
36
|
+
* @param url - WebSocket URL to connect to.
|
|
37
|
+
* @param options - Backoff tuning: `baseMs` (first delay, default 1000),
|
|
38
|
+
* `maxMs` (delay cap, default 15000) and `maxAttempts` (default unlimited).
|
|
39
|
+
* @returns The reactive `status`, `data` and `attempts`, plus `reconnect()`
|
|
40
|
+
* (force an immediate retry), `close()` (stop for good) and `send()`.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* const { status, attempts, reconnect } = useReconnectingWebSocket(url, { maxAttempts: 10 })
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
declare function useReconnectingWebSocket(url: string, options?: ReconnectingOptions): {
|
|
48
|
+
status: vue.Ref<ReconnectingStatus, ReconnectingStatus>;
|
|
49
|
+
data: vue.Ref<string | null, string | null>;
|
|
50
|
+
attempts: vue.Ref<number, number>;
|
|
51
|
+
reconnect: () => void;
|
|
52
|
+
close: () => void;
|
|
53
|
+
send: (payload: string) => void;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
interface CoTPoint {
|
|
57
|
+
lat: number;
|
|
58
|
+
lon: number;
|
|
59
|
+
hae: number;
|
|
60
|
+
ce: number;
|
|
61
|
+
le: number;
|
|
62
|
+
}
|
|
63
|
+
interface CoTContact {
|
|
64
|
+
callsign?: string;
|
|
65
|
+
endpoint?: string;
|
|
66
|
+
}
|
|
67
|
+
interface CoTDetail {
|
|
68
|
+
contact?: CoTContact;
|
|
69
|
+
}
|
|
70
|
+
interface CoTEvent {
|
|
71
|
+
version: string;
|
|
72
|
+
uid: string;
|
|
73
|
+
type: string;
|
|
74
|
+
time: string;
|
|
75
|
+
start: string;
|
|
76
|
+
stale: string;
|
|
77
|
+
how?: string;
|
|
78
|
+
point: CoTPoint;
|
|
79
|
+
detail?: CoTDetail;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface TakConnectionOptions {
|
|
83
|
+
pingIntervalMs?: number;
|
|
84
|
+
pingTimeoutMs?: number;
|
|
85
|
+
onConnect?: () => void;
|
|
86
|
+
onMessage?: (cot: string) => void;
|
|
87
|
+
onDisconnect?: () => void;
|
|
88
|
+
onError?: () => void;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Application-level TAK connection on top of {@link useReconnectingWebSocket}.
|
|
92
|
+
* Adds ping keepalive, zombie-connection detection (forces a reconnect when no
|
|
93
|
+
* traffic is seen within `pingTimeoutMs`) and newline-delimited message framing,
|
|
94
|
+
* so `onMessage` fires once per whole CoT message.
|
|
95
|
+
*
|
|
96
|
+
* @param url - WebSocket URL of the TAK/CoT feed.
|
|
97
|
+
* @param options - Lifecycle hooks (`onConnect`, `onMessage`, `onDisconnect`,
|
|
98
|
+
* `onError`) plus `pingIntervalMs` (default 15000) and `pingTimeoutMs` (default 30000).
|
|
99
|
+
* @returns Reactive `status`, `data`, `attempts` and the `reconnect`, `close`, `send` controls.
|
|
100
|
+
*/
|
|
101
|
+
declare function useTakConnection(url: string, options?: TakConnectionOptions): {
|
|
102
|
+
status: vue.Ref<ReconnectingStatus, ReconnectingStatus>;
|
|
103
|
+
data: vue.Ref<string | null, string | null>;
|
|
104
|
+
attempts: vue.Ref<number, number>;
|
|
105
|
+
reconnect: () => void;
|
|
106
|
+
close: () => void;
|
|
107
|
+
send: (payload: string) => void;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
type CoTStreamOptions = TakConnectionOptions & {
|
|
111
|
+
onEvent?: (event: CoTEvent) => void;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* High-level CoT stream: wraps {@link useTakConnection} and parses each framed
|
|
115
|
+
* message into a typed {@link CoTEvent}. Prefer the lossless `onEvent` callback
|
|
116
|
+
* (fired once per parsed frame) to feed a store; the reactive `event` ref only
|
|
117
|
+
* ever holds the most recent message.
|
|
118
|
+
*
|
|
119
|
+
* @param url - WebSocket URL of the CoT feed.
|
|
120
|
+
* @param options - {@link TakConnectionOptions} plus `onEvent(event)`, called
|
|
121
|
+
* for every successfully parsed CoT message.
|
|
122
|
+
* @returns The underlying connection controls plus reactive `event` (latest
|
|
123
|
+
* parsed `CoTEvent` or `null`) and `error` (last parse error or `null`).
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* const store = useEntityStore()
|
|
128
|
+
* useCoTStream(url, { onEvent: (e) => store.upsert(e) })
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
declare function useCoTStream(url: string, options?: CoTStreamOptions): {
|
|
132
|
+
event: vue.Ref<{
|
|
133
|
+
version: string;
|
|
134
|
+
uid: string;
|
|
135
|
+
type: string;
|
|
136
|
+
time: string;
|
|
137
|
+
start: string;
|
|
138
|
+
stale: string;
|
|
139
|
+
how?: string | undefined;
|
|
140
|
+
point: {
|
|
141
|
+
lat: number;
|
|
142
|
+
lon: number;
|
|
143
|
+
hae: number;
|
|
144
|
+
ce: number;
|
|
145
|
+
le: number;
|
|
146
|
+
};
|
|
147
|
+
detail?: {
|
|
148
|
+
contact?: {
|
|
149
|
+
callsign?: string | undefined;
|
|
150
|
+
endpoint?: string | undefined;
|
|
151
|
+
} | undefined;
|
|
152
|
+
} | undefined;
|
|
153
|
+
} | null, CoTEvent | {
|
|
154
|
+
version: string;
|
|
155
|
+
uid: string;
|
|
156
|
+
type: string;
|
|
157
|
+
time: string;
|
|
158
|
+
start: string;
|
|
159
|
+
stale: string;
|
|
160
|
+
how?: string | undefined;
|
|
161
|
+
point: {
|
|
162
|
+
lat: number;
|
|
163
|
+
lon: number;
|
|
164
|
+
hae: number;
|
|
165
|
+
ce: number;
|
|
166
|
+
le: number;
|
|
167
|
+
};
|
|
168
|
+
detail?: {
|
|
169
|
+
contact?: {
|
|
170
|
+
callsign?: string | undefined;
|
|
171
|
+
endpoint?: string | undefined;
|
|
172
|
+
} | undefined;
|
|
173
|
+
} | undefined;
|
|
174
|
+
} | null>;
|
|
175
|
+
error: vue.Ref<Error | null, Error | null>;
|
|
176
|
+
status: vue.Ref<ReconnectingStatus, ReconnectingStatus>;
|
|
177
|
+
data: vue.Ref<string | null, string | null>;
|
|
178
|
+
attempts: vue.Ref<number, number>;
|
|
179
|
+
reconnect: () => void;
|
|
180
|
+
close: () => void;
|
|
181
|
+
send: (payload: string) => void;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
type Entity = CoTEvent & {
|
|
185
|
+
receivedAt: number;
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* Reactive store of CoT entities keyed by `uid`. Backed by a `shallowRef<Map>`
|
|
189
|
+
* with a single `requestAnimationFrame` loop that sweeps out entities whose
|
|
190
|
+
* `stale` time has passed, so the collection self-expires.
|
|
191
|
+
*
|
|
192
|
+
* @returns `entities` (the reactive `Map<uid, Entity>`), `upsert(event)`
|
|
193
|
+
* (insert or replace, stamping `receivedAt`), `list` (entities as a reactive
|
|
194
|
+
* array), `count` and `byAffiliation` (entities grouped by CoT affiliation).
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```ts
|
|
198
|
+
* const store = useEntityStore()
|
|
199
|
+
* store.upsert(parseCoT(xml))
|
|
200
|
+
* // store.list.value -> Entity[]
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
declare function useEntityStore(): {
|
|
204
|
+
entities: vue.ShallowRef<Map<string, Entity>, Map<string, Entity>>;
|
|
205
|
+
upsert: (event: CoTEvent) => void;
|
|
206
|
+
list: vue.ComputedRef<Entity[]>;
|
|
207
|
+
count: vue.ComputedRef<number>;
|
|
208
|
+
byAffiliation: vue.ComputedRef<Map<string, Entity[]>>;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Parses a CoT (Cursor on Target) XML string into a typed {@link CoTEvent}.
|
|
213
|
+
* Inverse of {@link serializeCoT}.
|
|
214
|
+
*
|
|
215
|
+
* @param xml - A single CoT XML message (an `<event>` element).
|
|
216
|
+
* @returns The parsed, typed event.
|
|
217
|
+
* @throws If the XML has no `<event>` root or is missing its `<point>`.
|
|
218
|
+
*/
|
|
219
|
+
declare function parseCoT(xml: string): CoTEvent;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Serialises a typed {@link CoTEvent} back into a CoT XML string.
|
|
223
|
+
* Inverse of {@link parseCoT}.
|
|
224
|
+
*
|
|
225
|
+
* @param event - The event to serialise.
|
|
226
|
+
* @returns A CoT XML `<event>` string.
|
|
227
|
+
*/
|
|
228
|
+
declare function serializeCoT(event: CoTEvent): string;
|
|
229
|
+
|
|
230
|
+
export { type CoTContact, type CoTDetail, type CoTEvent, type CoTPoint, type CoTStreamOptions, type Entity, type ReconnectingOptions, type ReconnectingStatus, type TakConnectionOptions, type WebSocketStatus, parseCoT, serializeCoT, useCoTStream, useEntityStore, useReconnectingWebSocket, useTakConnection, useWebSocket };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// src/composables/use-cot-stream.ts
|
|
2
|
+
import { ref as ref2 } from "vue";
|
|
3
|
+
|
|
4
|
+
// src/protocol/parse.ts
|
|
5
|
+
import { XMLParser } from "fast-xml-parser";
|
|
6
|
+
var parser = new XMLParser({
|
|
7
|
+
ignoreAttributes: false,
|
|
8
|
+
attributeNamePrefix: ""
|
|
9
|
+
});
|
|
10
|
+
function parseCoT(xml) {
|
|
11
|
+
const raw = parser.parse(xml);
|
|
12
|
+
const event = raw?.event;
|
|
13
|
+
if (!event)
|
|
14
|
+
throw new Error("invalid CoT:missing <event> root");
|
|
15
|
+
const point = event.point;
|
|
16
|
+
if (!point)
|
|
17
|
+
throw new Error("invalid CoT:<event> missing <point>");
|
|
18
|
+
return {
|
|
19
|
+
version: String(event.version),
|
|
20
|
+
uid: String(event.uid),
|
|
21
|
+
type: String(event.type),
|
|
22
|
+
time: String(event.time),
|
|
23
|
+
start: String(event.start),
|
|
24
|
+
stale: String(event.stale),
|
|
25
|
+
how: event.how ? String(event.how) : void 0,
|
|
26
|
+
point: {
|
|
27
|
+
lat: Number(point.lat),
|
|
28
|
+
lon: Number(point.lon),
|
|
29
|
+
hae: Number(point.hae),
|
|
30
|
+
ce: Number(point.ce),
|
|
31
|
+
le: Number(point.le)
|
|
32
|
+
},
|
|
33
|
+
detail: event.detail ? {
|
|
34
|
+
contact: event.detail.contact ? { callsign: event.detail.contact.callsign } : void 0
|
|
35
|
+
} : void 0
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/composables/use-tak-connection.ts
|
|
40
|
+
import { onScopeDispose as onScopeDispose2, watch } from "vue";
|
|
41
|
+
|
|
42
|
+
// src/composables/use-reconnecting-websocket.ts
|
|
43
|
+
import { onScopeDispose, ref } from "vue";
|
|
44
|
+
|
|
45
|
+
// src/utils/backoff.ts
|
|
46
|
+
function backoffDelay(attempt, options = {}) {
|
|
47
|
+
const { baseMs = 1e3, maxMs = 15e3 } = options;
|
|
48
|
+
const exponential = Math.min(baseMs * 2 ** attempt, maxMs);
|
|
49
|
+
const half = exponential / 2;
|
|
50
|
+
return half + Math.random() * half;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/composables/use-reconnecting-websocket.ts
|
|
54
|
+
function useReconnectingWebSocket(url, options = {}) {
|
|
55
|
+
const {
|
|
56
|
+
baseMs = 1e3,
|
|
57
|
+
maxMs = 15e3,
|
|
58
|
+
maxAttempts = Number.POSITIVE_INFINITY
|
|
59
|
+
} = options;
|
|
60
|
+
const status = ref("closed");
|
|
61
|
+
const data = ref(null);
|
|
62
|
+
const attempts = ref(0);
|
|
63
|
+
let ws = null;
|
|
64
|
+
let reconnectTimer;
|
|
65
|
+
let manualClose = false;
|
|
66
|
+
function connect() {
|
|
67
|
+
ws = new WebSocket(url);
|
|
68
|
+
status.value = "connecting";
|
|
69
|
+
ws.onopen = () => {
|
|
70
|
+
status.value = "open";
|
|
71
|
+
attempts.value = 0;
|
|
72
|
+
};
|
|
73
|
+
ws.onmessage = (event) => {
|
|
74
|
+
if (typeof event.data === "string")
|
|
75
|
+
data.value = event.data;
|
|
76
|
+
};
|
|
77
|
+
ws.onerror = () => {
|
|
78
|
+
console.error("[useReconnectingWebSocket] connection error");
|
|
79
|
+
};
|
|
80
|
+
ws.onclose = () => {
|
|
81
|
+
if (manualClose || attempts.value >= maxAttempts) {
|
|
82
|
+
status.value = "closed";
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const delay = backoffDelay(attempts.value, { baseMs, maxMs });
|
|
86
|
+
attempts.value++;
|
|
87
|
+
status.value = "reconnecting";
|
|
88
|
+
reconnectTimer = setTimeout(connect, delay);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function reconnect() {
|
|
92
|
+
clearTimeout(reconnectTimer);
|
|
93
|
+
attempts.value = 0;
|
|
94
|
+
if (ws) {
|
|
95
|
+
ws.onclose = null;
|
|
96
|
+
ws.close();
|
|
97
|
+
}
|
|
98
|
+
manualClose = false;
|
|
99
|
+
connect();
|
|
100
|
+
}
|
|
101
|
+
function close() {
|
|
102
|
+
manualClose = true;
|
|
103
|
+
clearTimeout(reconnectTimer);
|
|
104
|
+
ws?.close();
|
|
105
|
+
}
|
|
106
|
+
function send(payload) {
|
|
107
|
+
if (ws && ws.readyState === WebSocket.OPEN)
|
|
108
|
+
ws.send(payload);
|
|
109
|
+
}
|
|
110
|
+
connect();
|
|
111
|
+
onScopeDispose(close);
|
|
112
|
+
return { status, data, attempts, reconnect, close, send };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/composables/use-tak-connection.ts
|
|
116
|
+
function useTakConnection(url, options = {}) {
|
|
117
|
+
const connection = useReconnectingWebSocket(url);
|
|
118
|
+
const { status, data, attempts, close, reconnect, send } = connection;
|
|
119
|
+
const { pingIntervalMs = 15e3, pingTimeoutMs = 3e4 } = options;
|
|
120
|
+
let lastSeen = Date.now();
|
|
121
|
+
watch(status, (newStatus, oldStatus) => {
|
|
122
|
+
if (newStatus === "open") {
|
|
123
|
+
lastSeen = Date.now();
|
|
124
|
+
options.onConnect?.();
|
|
125
|
+
} else if (oldStatus === "open") {
|
|
126
|
+
options.onDisconnect?.();
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
watch(data, () => {
|
|
130
|
+
lastSeen = Date.now();
|
|
131
|
+
});
|
|
132
|
+
let buffer = "";
|
|
133
|
+
watch(data, (raw) => {
|
|
134
|
+
if (raw === null)
|
|
135
|
+
return;
|
|
136
|
+
buffer += raw;
|
|
137
|
+
const frames = buffer.split("\n");
|
|
138
|
+
buffer = frames.pop() ?? "";
|
|
139
|
+
for (const frame of frames) {
|
|
140
|
+
const trimmed = frame.trim();
|
|
141
|
+
if (trimmed) {
|
|
142
|
+
options.onMessage?.(trimmed);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
const pingTimer = setInterval(() => {
|
|
147
|
+
send("ping");
|
|
148
|
+
if (Date.now() - lastSeen > pingTimeoutMs)
|
|
149
|
+
reconnect();
|
|
150
|
+
}, pingIntervalMs);
|
|
151
|
+
onScopeDispose2(() => clearInterval(pingTimer));
|
|
152
|
+
return { status, data, attempts, reconnect, close, send };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/composables/use-cot-stream.ts
|
|
156
|
+
function useCoTStream(url, options = {}) {
|
|
157
|
+
const event = ref2(null);
|
|
158
|
+
const error = ref2(null);
|
|
159
|
+
const connection = useTakConnection(url, {
|
|
160
|
+
...options,
|
|
161
|
+
onMessage: (raw) => {
|
|
162
|
+
try {
|
|
163
|
+
const parsed = parseCoT(raw);
|
|
164
|
+
event.value = parsed;
|
|
165
|
+
error.value = null;
|
|
166
|
+
options.onEvent?.(parsed);
|
|
167
|
+
options.onMessage?.(raw);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
error.value = err;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
return { ...connection, event, error };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/composables/use-entity-store.ts
|
|
177
|
+
import { computed, onScopeDispose as onScopeDispose3, shallowRef, triggerRef } from "vue";
|
|
178
|
+
function useEntityStore() {
|
|
179
|
+
const entities = shallowRef(/* @__PURE__ */ new Map());
|
|
180
|
+
function upsert(event) {
|
|
181
|
+
entities.value.set(event.uid, { ...event, receivedAt: Date.now() });
|
|
182
|
+
triggerRef(entities);
|
|
183
|
+
}
|
|
184
|
+
let rafId = 0;
|
|
185
|
+
function sweep() {
|
|
186
|
+
const now = Date.now();
|
|
187
|
+
let remove = false;
|
|
188
|
+
for (const [uid, entity] of entities.value) {
|
|
189
|
+
if (Date.parse(entity.stale) < now) {
|
|
190
|
+
entities.value.delete(uid);
|
|
191
|
+
remove = true;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (remove) {
|
|
195
|
+
triggerRef(entities);
|
|
196
|
+
}
|
|
197
|
+
rafId = requestAnimationFrame(sweep);
|
|
198
|
+
}
|
|
199
|
+
rafId = requestAnimationFrame(sweep);
|
|
200
|
+
onScopeDispose3(() => cancelAnimationFrame(rafId));
|
|
201
|
+
const list = computed(() => Array.from(entities.value.values()));
|
|
202
|
+
const count = computed(() => entities.value.size);
|
|
203
|
+
const byAffiliation = computed(() => {
|
|
204
|
+
const groups = /* @__PURE__ */ new Map();
|
|
205
|
+
for (const entity of entities.value.values()) {
|
|
206
|
+
const affiliation = entity.type.split("-")[1] ?? "unknown";
|
|
207
|
+
const group = groups.get(affiliation) ?? [];
|
|
208
|
+
group.push(entity);
|
|
209
|
+
groups.set(affiliation, group);
|
|
210
|
+
}
|
|
211
|
+
return groups;
|
|
212
|
+
});
|
|
213
|
+
return { entities, upsert, list, count, byAffiliation };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/composables/use-websocket.ts
|
|
217
|
+
import { onScopeDispose as onScopeDispose4, ref as ref3 } from "vue";
|
|
218
|
+
function useWebSocket(url) {
|
|
219
|
+
const status = ref3("closed");
|
|
220
|
+
const data = ref3(null);
|
|
221
|
+
const ws = new WebSocket(url);
|
|
222
|
+
status.value = "connecting";
|
|
223
|
+
ws.onopen = () => {
|
|
224
|
+
status.value = "open";
|
|
225
|
+
};
|
|
226
|
+
ws.onmessage = (event) => {
|
|
227
|
+
if (typeof event.data === "string") {
|
|
228
|
+
data.value = event.data;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
ws.onclose = () => {
|
|
232
|
+
status.value = "closed";
|
|
233
|
+
};
|
|
234
|
+
ws.onerror = () => {
|
|
235
|
+
console.error("[useWebSocket] connection error");
|
|
236
|
+
};
|
|
237
|
+
function send(payload) {
|
|
238
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
239
|
+
ws.send(payload);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
onScopeDispose4(() => {
|
|
243
|
+
ws.close();
|
|
244
|
+
});
|
|
245
|
+
return { status, data, send };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/protocol/serialize.ts
|
|
249
|
+
function serializeCoT(event) {
|
|
250
|
+
const how = event.how ? ` how="${event.how}"` : "";
|
|
251
|
+
const callsign = event.detail?.contact?.callsign;
|
|
252
|
+
const detail = callsign ? `<detail><contact callsign="${callsign}"/></detail>` : "";
|
|
253
|
+
return `<event version="${event.version}" uid="${event.uid}" type="${event.type}" time="${event.time}" start="${event.start}" stale="${event.stale}"${how}><point lat="${event.point.lat}" lon="${event.point.lon}" hae="${event.point.hae}" ce="${event.point.ce}" le="${event.point.le}"/>${detail}</event>`;
|
|
254
|
+
}
|
|
255
|
+
export {
|
|
256
|
+
parseCoT,
|
|
257
|
+
serializeCoT,
|
|
258
|
+
useCoTStream,
|
|
259
|
+
useEntityStore,
|
|
260
|
+
useReconnectingWebSocket,
|
|
261
|
+
useTakConnection,
|
|
262
|
+
useWebSocket
|
|
263
|
+
};
|
|
264
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/composables/use-cot-stream.ts","../src/protocol/parse.ts","../src/composables/use-tak-connection.ts","../src/composables/use-reconnecting-websocket.ts","../src/utils/backoff.ts","../src/composables/use-entity-store.ts","../src/composables/use-websocket.ts","../src/protocol/serialize.ts"],"sourcesContent":["import type { CoTEvent } from '../protocol/types'\nimport type { TakConnectionOptions } from './use-tak-connection'\nimport { ref } from 'vue'\nimport { parseCoT } from '../protocol/parse'\nimport { useTakConnection } from './use-tak-connection'\n\nexport type CoTStreamOptions = TakConnectionOptions & {\n onEvent?: (event: CoTEvent) => void\n}\n\n/**\n * High-level CoT stream: wraps {@link useTakConnection} and parses each framed\n * message into a typed {@link CoTEvent}. Prefer the lossless `onEvent` callback\n * (fired once per parsed frame) to feed a store; the reactive `event` ref only\n * ever holds the most recent message.\n *\n * @param url - WebSocket URL of the CoT feed.\n * @param options - {@link TakConnectionOptions} plus `onEvent(event)`, called\n * for every successfully parsed CoT message.\n * @returns The underlying connection controls plus reactive `event` (latest\n * parsed `CoTEvent` or `null`) and `error` (last parse error or `null`).\n *\n * @example\n * ```ts\n * const store = useEntityStore()\n * useCoTStream(url, { onEvent: (e) => store.upsert(e) })\n * ```\n */\nexport function useCoTStream(url: string, options: CoTStreamOptions = {}) {\n const event = ref<CoTEvent | null>(null)\n const error = ref<Error | null>(null)\n\n const connection = useTakConnection(url, {\n ...options,\n onMessage: (raw) => {\n try {\n const parsed = parseCoT(raw)\n event.value = parsed\n error.value = null\n options.onEvent?.(parsed)\n options.onMessage?.(raw)\n }\n catch (err) {\n error.value = err as Error\n }\n },\n })\n\n return { ...connection, event, error }\n}\n","import type { CoTEvent } from './types'\nimport { XMLParser } from 'fast-xml-parser'\n\nconst parser = new XMLParser({\n ignoreAttributes: false,\n attributeNamePrefix: '',\n})\n\n/**\n * Parses a CoT (Cursor on Target) XML string into a typed {@link CoTEvent}.\n * Inverse of {@link serializeCoT}.\n *\n * @param xml - A single CoT XML message (an `<event>` element).\n * @returns The parsed, typed event.\n * @throws If the XML has no `<event>` root or is missing its `<point>`.\n */\nexport function parseCoT(xml: string): CoTEvent {\n const raw = parser.parse(xml)\n const event = raw?.event\n if (!event)\n throw new Error('invalid CoT:missing <event> root')\n const point = event.point\n if (!point)\n throw new Error('invalid CoT:<event> missing <point>')\n\n return {\n version: String(event.version),\n uid: String(event.uid),\n type: String(event.type),\n time: String(event.time),\n start: String(event.start),\n stale: String(event.stale),\n how: event.how ? String(event.how) : undefined,\n point: {\n lat: Number(point.lat),\n lon: Number(point.lon),\n hae: Number(point.hae),\n ce: Number(point.ce),\n le: Number(point.le),\n },\n detail: event.detail\n ? {\n contact: event.detail.contact\n ? { callsign: event.detail.contact.callsign }\n : undefined,\n }\n : undefined,\n }\n}\n","import { onScopeDispose, watch } from 'vue'\nimport { useReconnectingWebSocket } from './use-reconnecting-websocket'\n\nexport interface TakConnectionOptions {\n pingIntervalMs?: number\n pingTimeoutMs?: number\n onConnect?: () => void\n onMessage?: (cot: string) => void\n onDisconnect?: () => void\n onError?: () => void\n}\n\n/**\n * Application-level TAK connection on top of {@link useReconnectingWebSocket}.\n * Adds ping keepalive, zombie-connection detection (forces a reconnect when no\n * traffic is seen within `pingTimeoutMs`) and newline-delimited message framing,\n * so `onMessage` fires once per whole CoT message.\n *\n * @param url - WebSocket URL of the TAK/CoT feed.\n * @param options - Lifecycle hooks (`onConnect`, `onMessage`, `onDisconnect`,\n * `onError`) plus `pingIntervalMs` (default 15000) and `pingTimeoutMs` (default 30000).\n * @returns Reactive `status`, `data`, `attempts` and the `reconnect`, `close`, `send` controls.\n */\nexport function useTakConnection(\n url: string,\n options: TakConnectionOptions = {},\n) {\n const connection = useReconnectingWebSocket(url)\n const { status, data, attempts, close, reconnect, send } = connection\n const { pingIntervalMs = 15_000, pingTimeoutMs = 30_000 } = options\n let lastSeen = Date.now()\n\n watch(status, (newStatus, oldStatus) => {\n if (newStatus === 'open') {\n lastSeen = Date.now()\n options.onConnect?.()\n }\n else if (oldStatus === 'open') {\n options.onDisconnect?.()\n }\n })\n watch(data, () => {\n lastSeen = Date.now()\n })\n\n let buffer = ''\n watch(data, (raw) => {\n if (raw === null)\n return\n buffer += raw\n const frames = buffer.split('\\n')\n buffer = frames.pop() ?? ''\n for (const frame of frames) {\n const trimmed = frame.trim()\n if (trimmed) {\n options.onMessage?.(trimmed)\n }\n }\n })\n\n const pingTimer = setInterval(() => {\n send('ping')\n if (Date.now() - lastSeen > pingTimeoutMs)\n reconnect()\n }, pingIntervalMs)\n\n onScopeDispose(() => clearInterval(pingTimer))\n\n return { status, data, attempts, reconnect, close, send }\n}\n","import type { WebSocketStatus } from './use-websocket'\nimport { onScopeDispose, ref } from 'vue'\nimport { backoffDelay } from '../utils/backoff'\n\nexport type ReconnectingStatus = WebSocketStatus | 'reconnecting'\n\nexport interface ReconnectingOptions {\n baseMs?: number\n maxMs?: number\n maxAttempts?: number\n}\n\n/**\n * Like {@link useWebSocket}, but automatically reconnects with exponential\n * backoff and jitter when the connection drops unexpectedly.\n *\n * @param url - WebSocket URL to connect to.\n * @param options - Backoff tuning: `baseMs` (first delay, default 1000),\n * `maxMs` (delay cap, default 15000) and `maxAttempts` (default unlimited).\n * @returns The reactive `status`, `data` and `attempts`, plus `reconnect()`\n * (force an immediate retry), `close()` (stop for good) and `send()`.\n *\n * @example\n * ```ts\n * const { status, attempts, reconnect } = useReconnectingWebSocket(url, { maxAttempts: 10 })\n * ```\n */\nexport function useReconnectingWebSocket(\n url: string,\n options: ReconnectingOptions = {},\n) {\n const {\n baseMs = 1000,\n maxMs = 15_000,\n maxAttempts = Number.POSITIVE_INFINITY,\n } = options\n\n const status = ref<ReconnectingStatus>('closed')\n const data = ref<string | null>(null)\n const attempts = ref(0)\n\n let ws: WebSocket | null = null\n let reconnectTimer: ReturnType<typeof setTimeout> | undefined\n let manualClose = false\n\n function connect() {\n ws = new WebSocket(url)\n status.value = 'connecting'\n\n ws.onopen = () => {\n status.value = 'open'\n attempts.value = 0\n }\n ws.onmessage = (event: MessageEvent) => {\n if (typeof event.data === 'string')\n data.value = event.data\n }\n ws.onerror = () => {\n console.error('[useReconnectingWebSocket] connection error')\n }\n ws.onclose = () => {\n if (manualClose || attempts.value >= maxAttempts) {\n status.value = 'closed'\n return\n }\n const delay = backoffDelay(attempts.value, { baseMs, maxMs })\n attempts.value++\n status.value = 'reconnecting'\n reconnectTimer = setTimeout(connect, delay)\n }\n }\n\n function reconnect() {\n clearTimeout(reconnectTimer)\n attempts.value = 0\n if (ws) {\n ws.onclose = null\n ws.close()\n }\n manualClose = false\n connect()\n }\n function close() {\n manualClose = true\n clearTimeout(reconnectTimer)\n ws?.close()\n }\n\n function send(payload: string) {\n if (ws && ws.readyState === WebSocket.OPEN)\n ws.send(payload)\n }\n\n connect()\n onScopeDispose(close)\n\n return { status, data, attempts, reconnect, close, send }\n}\n","export interface BackoffOptions {\n baseMs?: number\n maxMs?: number\n}\n\n/**\n * Computes an exponential-backoff delay (in milliseconds) for a retry attempt.\n * The delay is `baseMs * 2 ** attempt`, capped at `maxMs`, with jitter added to\n * avoid the thundering-herd problem when many clients reconnect at once.\n *\n * @param attempt - Zero-based retry attempt number.\n * @param options - `baseMs` (default 1000) and `maxMs` (cap, default 15000).\n * @returns The delay to wait before the next attempt, in milliseconds.\n */\nexport function backoffDelay(\n attempt: number,\n options: BackoffOptions = {},\n): number {\n const { baseMs = 1000, maxMs = 15_000 } = options\n const exponential = Math.min(baseMs * 2 ** attempt, maxMs)\n const half = exponential / 2\n return half + Math.random() * half\n}\n","import type { CoTEvent } from '../protocol/types'\nimport { computed, onScopeDispose, shallowRef, triggerRef } from 'vue'\n\nexport type Entity = CoTEvent & { receivedAt: number }\n/**\n * Reactive store of CoT entities keyed by `uid`. Backed by a `shallowRef<Map>`\n * with a single `requestAnimationFrame` loop that sweeps out entities whose\n * `stale` time has passed, so the collection self-expires.\n *\n * @returns `entities` (the reactive `Map<uid, Entity>`), `upsert(event)`\n * (insert or replace, stamping `receivedAt`), `list` (entities as a reactive\n * array), `count` and `byAffiliation` (entities grouped by CoT affiliation).\n *\n * @example\n * ```ts\n * const store = useEntityStore()\n * store.upsert(parseCoT(xml))\n * // store.list.value -> Entity[]\n * ```\n */\nexport function useEntityStore() {\n const entities = shallowRef(new Map<string, Entity>())\n\n function upsert(event: CoTEvent) {\n entities.value.set(event.uid, { ...event, receivedAt: Date.now() })\n triggerRef(entities)\n }\n\n let rafId = 0\n function sweep() {\n const now = Date.now()\n let remove = false\n for (const [uid, entity] of entities.value) {\n if (Date.parse(entity.stale) < now) {\n entities.value.delete(uid)\n remove = true\n }\n }\n if (remove) {\n triggerRef(entities)\n }\n rafId = requestAnimationFrame(sweep)\n }\n rafId = requestAnimationFrame(sweep)\n onScopeDispose(() => cancelAnimationFrame(rafId))\n\n const list = computed(() => Array.from(entities.value.values()))\n const count = computed(() => entities.value.size)\n\n const byAffiliation = computed(() => {\n const groups = new Map<string, Entity[]>()\n for (const entity of entities.value.values()) {\n const affiliation = entity.type.split('-')[1] ?? 'unknown'\n const group = groups.get(affiliation) ?? []\n group.push(entity)\n groups.set(affiliation, group)\n }\n return groups\n })\n\n return { entities, upsert, list, count, byAffiliation }\n}\n","import { onScopeDispose, ref } from 'vue'\n\nexport type WebSocketStatus = 'connecting' | 'open' | 'closing' | 'closed'\n\n/**\n * Reactive wrapper around a single browser `WebSocket`. Opens the connection\n * immediately and exposes its live status and last received message. Does not\n * reconnect — see {@link useReconnectingWebSocket} for that.\n *\n * The socket is closed automatically when the surrounding reactive scope is disposed.\n *\n * @param url - WebSocket URL to connect to, e.g. `wss://tak.example/cot`.\n * @returns `status` (reactive connection state), `data` (latest string message\n * or `null`) and `send` (sends a payload only while the socket is open).\n *\n * @example\n * ```ts\n * const { status, data } = useWebSocket('wss://tak.example/cot')\n * ```\n */\nexport function useWebSocket(url: string) {\n const status = ref<WebSocketStatus>('closed')\n const data = ref<string | null>(null)\n\n const ws = new WebSocket(url)\n status.value = 'connecting'\n\n ws.onopen = () => {\n status.value = 'open'\n }\n ws.onmessage = (event: MessageEvent) => {\n if (typeof event.data === 'string') {\n data.value = event.data\n }\n }\n ws.onclose = () => {\n status.value = 'closed'\n }\n ws.onerror = () => {\n console.error('[useWebSocket] connection error')\n }\n\n function send(payload: string) {\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(payload)\n }\n }\n\n onScopeDispose(() => {\n ws.close()\n })\n\n return { status, data, send }\n}\n","import type { CoTEvent } from './types'\n\n/**\n * Serialises a typed {@link CoTEvent} back into a CoT XML string.\n * Inverse of {@link parseCoT}.\n *\n * @param event - The event to serialise.\n * @returns A CoT XML `<event>` string.\n */\nexport function serializeCoT(event: CoTEvent): string {\n const how = event.how ? ` how=\"${event.how}\"` : ''\n const callsign = event.detail?.contact?.callsign\n const detail = callsign\n ? `<detail><contact callsign=\"${callsign}\"/></detail>`\n : ''\n\n return (\n `<event version=\"${event.version}\" uid=\"${event.uid}\" type=\"${event.type}\" `\n + `time=\"${event.time}\" start=\"${event.start}\" stale=\"${event.stale}\"${how}>`\n + `<point lat=\"${event.point.lat}\" lon=\"${event.point.lon}\" hae=\"${event.point.hae}\" `\n + `ce=\"${event.point.ce}\" le=\"${event.point.le}\"/>${\n detail\n }</event>`\n )\n}\n"],"mappings":"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","names":["ref","onScopeDispose","onScopeDispose","ref","onScopeDispose","onScopeDispose","ref"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vue-cot",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Vue 3 composables and parser for Cursor on Target (CoT) tactical messaging",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/belblue/vue-cot#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/belblue/vue-cot.git",
|
|
11
|
+
"directory": "packages/core"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/belblue/vue-cot/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"vue",
|
|
18
|
+
"vue3",
|
|
19
|
+
"composables",
|
|
20
|
+
"cursor-on-target",
|
|
21
|
+
"cot",
|
|
22
|
+
"tak",
|
|
23
|
+
"atak",
|
|
24
|
+
"wintak",
|
|
25
|
+
"tak-server",
|
|
26
|
+
"tactical",
|
|
27
|
+
"situational-awareness",
|
|
28
|
+
"geospatial",
|
|
29
|
+
"websocket",
|
|
30
|
+
"real-time",
|
|
31
|
+
"xml-parser"
|
|
32
|
+
],
|
|
33
|
+
"sideEffects": false,
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"import": "./dist/index.js",
|
|
38
|
+
"require": "./dist/index.cjs"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist"
|
|
43
|
+
],
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"vue": "^3.5.0"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"fast-xml-parser": "^5.9.3"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"happy-dom": "^20.10.6",
|
|
52
|
+
"tsup": "^8.5.1",
|
|
53
|
+
"vitest": "^4.1.9",
|
|
54
|
+
"vue": "^3.5.0"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsup",
|
|
58
|
+
"typecheck": "tsc --noEmit",
|
|
59
|
+
"test": "vitest run"
|
|
60
|
+
},
|
|
61
|
+
"main": "./dist/index.cjs",
|
|
62
|
+
"module": "./dist/index.js",
|
|
63
|
+
"types": "./dist/index.d.ts"
|
|
64
|
+
}
|