/** * In-memory CoT entity store: upsert by id, prune on read by TTL. * Single source of truth; getActiveEntities returns new objects (no mutation of returned refs). */ import { acquire } from './asyncLock.js' import { COT_ENTITY_TTL_MS } from './constants.js' const entities = new Map() /** * Upsert entity by id. Input is not mutated; stored value is a new object. * @param {{ id: string, lat: number, lng: number, label?: string, eventType?: string, type?: string }} parsed */ export async function updateFromCot(parsed) { if (!parsed || typeof parsed.id !== 'string') return const lat = Number(parsed.lat) const lng = Number(parsed.lng) if (!Number.isFinite(lat) || !Number.isFinite(lng)) return await acquire(`cot-${parsed.id}`, async () => { const now = Date.now() const existing = entities.get(parsed.id) const label = typeof parsed.label === 'string' ? parsed.label : (existing?.label ?? parsed.id) const type = typeof parsed.eventType === 'string' ? parsed.eventType : (typeof parsed.type === 'string' ? parsed.type : (existing?.type ?? '')) entities.set(parsed.id, { id: parsed.id, lat, lng, label, type, updatedAt: now, }) }) } /** * Active entities (updated within ttlMs). Prunes expired. Returns new array of new objects. * @param {number} [ttlMs] * @returns {Promise>} Snapshot of active entities. */ export async function getActiveEntities(ttlMs = COT_ENTITY_TTL_MS) { return acquire('cot-prune', async () => { const now = Date.now() const active = [] const expired = [] for (const entity of entities.values()) { if (now - entity.updatedAt <= ttlMs) { active.push({ id: entity.id, lat: entity.lat, lng: entity.lng, label: entity.label ?? entity.id, type: entity.type ?? '', updatedAt: entity.updatedAt, }) } else { expired.push(entity.id) } } for (const id of expired) entities.delete(id) return active }) } /** Clear store (tests only). */ export function clearCotStore() { entities.clear() }