vue-cot 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/index.cjs +4 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ by NATO/US tactical systems such as ATAK, WinTAK and TAK Server.
|
|
|
6
6
|
> **Status:** feature-complete **v0.1**. A well-scoped, self-contained library — not a
|
|
7
7
|
> work in progress. Actively maintained.
|
|
8
8
|
|
|
9
|
+
**Live demo:** [vue-cot-demo.vercel.app](https://vue-cot-demo.vercel.app/) — runs entirely in the browser with a built-in mock CoT feed.
|
|
10
|
+
|
|
9
11
|
Related packages (`@vue-tactical/map`, `@vue-tactical/symbols`, `@vue-tactical/coords`)
|
|
10
12
|
would be interesting to build but are **not currently planned** — open an issue if you'd
|
|
11
13
|
like to collaborate. PRs welcome.
|
|
@@ -60,6 +62,7 @@ releases if there's demand — file an issue.
|
|
|
60
62
|
|
|
61
63
|
## Links
|
|
62
64
|
|
|
65
|
+
- Live demo: https://vue-cot-demo.vercel.app/
|
|
63
66
|
- Repository & issues: https://github.com/belblue/vue-cot
|
|
64
67
|
- Contributing: issues and PRs welcome; safe fixes and dependency updates get merged,
|
|
65
68
|
larger changes are best discussed in an issue first.
|
package/dist/index.cjs
CHANGED
|
@@ -42,11 +42,11 @@ var parser = new import_fast_xml_parser.XMLParser({
|
|
|
42
42
|
function parseCoT(xml) {
|
|
43
43
|
const raw = parser.parse(xml);
|
|
44
44
|
const event = raw?.event;
|
|
45
|
-
if (!event)
|
|
46
|
-
|
|
45
|
+
if (!event) throw new Error("invalid CoT:missing <event> root");
|
|
46
|
+
if (event.uid == null) throw new Error("invalid CoT: <event> missing uid");
|
|
47
|
+
if (event.type == null) throw new Error("invalid CoT: <event> missing type");
|
|
47
48
|
const point = event.point;
|
|
48
|
-
if (!point)
|
|
49
|
-
throw new Error("invalid CoT:<event> missing <point>");
|
|
49
|
+
if (!point) throw new Error("invalid CoT:<event> missing <point>");
|
|
50
50
|
return {
|
|
51
51
|
version: String(event.version),
|
|
52
52
|
uid: String(event.uid),
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +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"]}
|
|
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) throw new Error(\"invalid CoT:missing <event> root\");\n if (event.uid == null) throw new Error(\"invalid CoT: <event> missing uid\");\n if (event.type == null) throw new Error(\"invalid CoT: <event> missing type\");\n const point = event.point;\n if (!point) 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.js
CHANGED
|
@@ -10,11 +10,11 @@ var parser = new XMLParser({
|
|
|
10
10
|
function parseCoT(xml) {
|
|
11
11
|
const raw = parser.parse(xml);
|
|
12
12
|
const event = raw?.event;
|
|
13
|
-
if (!event)
|
|
14
|
-
|
|
13
|
+
if (!event) throw new Error("invalid CoT:missing <event> root");
|
|
14
|
+
if (event.uid == null) throw new Error("invalid CoT: <event> missing uid");
|
|
15
|
+
if (event.type == null) throw new Error("invalid CoT: <event> missing type");
|
|
15
16
|
const point = event.point;
|
|
16
|
-
if (!point)
|
|
17
|
-
throw new Error("invalid CoT:<event> missing <point>");
|
|
17
|
+
if (!point) throw new Error("invalid CoT:<event> missing <point>");
|
|
18
18
|
return {
|
|
19
19
|
version: String(event.version),
|
|
20
20
|
uid: String(event.uid),
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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"]}
|
|
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) throw new Error(\"invalid CoT:missing <event> root\");\n if (event.uid == null) throw new Error(\"invalid CoT: <event> missing uid\");\n if (event.type == null) throw new Error(\"invalid CoT: <event> missing type\");\n const point = event.point;\n if (!point) 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
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vue-cot",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.1",
|
|
5
5
|
"description": "Vue 3 composables and parser for Cursor on Target (CoT) tactical messaging",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"homepage": "https://
|
|
7
|
+
"homepage": "https://vue-cot-demo.vercel.app",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "git+https://github.com/belblue/vue-cot.git",
|