Files
processor/src/db/migrations/0002_positions_faulty.sql
T
julian fbb1f34e9a feat(live): task 1.5.4 — broadcast consumer group and fan-out
Adds the per-instance Redis Stream consumer group (live-broadcast-{instance_id})
that reads the telemetry stream and fans out each position to subscribed
WebSocket connections without affecting the durable-write consumer path.

Key changes:
- src/shared/codec.ts: moved decodePosition/CodecError out of src/core/ so
  src/live/broadcast.ts can decode positions without crossing the enforced
  src/core/ ↔ src/live/ boundary; src/core/codec.ts now re-exports from there
- src/shared/types.ts: added Position and AttributeValue (same move, same reason);
  src/core/types.ts re-exports both to preserve existing import paths
- src/live/broadcast.ts: createBroadcastConsumer factory — XREADGROUP loop,
  immediate ACK semantics, toPositionMessage mapper, fanOut per event/topic
- src/live/device-event-map.ts: createDeviceEventMap factory — in-memory cache
  of entry_devices × entries join, refreshed every LIVE_DEVICE_EVENT_REFRESH_MS
- src/db/migrations/0002_positions_faulty.sql: adds faulty boolean column and
  positions_device_ts_idx for snapshot-on-subscribe query (task 1.5.5)
- src/main.ts: wired authClient, authzClient, registry, liveServer,
  deviceEventMap, broadcastConsumer; shutdown chain: liveServer → deviceEventMap
  + broadcastConsumer → durable-write consumer → metricsServer → Redis → Postgres
- test/live-broadcast.test.ts: 4 unit tests covering single subscriber, multiple
  subscribers, orphan device, and multi-event device fan-out
2026-05-02 18:36:52 +02:00

20 lines
1.0 KiB
SQL

-- Migration: 0002_positions_faulty
-- Adds the faulty column to positions and ensures the (device_id, ts DESC) index
-- needed by the snapshot-on-subscribe query exists.
--
-- The faulty column is set post-hoc by operators in Directus when a position is
-- flagged as unrealistic. The snapshot-on-subscribe query (task 1.5.5) filters
-- WHERE faulty = false to exclude flagged positions from the initial map state.
-- The live broadcast path (Redis stream → fan-out) never touches this column
-- because faulty flags are applied after the fact.
ALTER TABLE positions
ADD COLUMN IF NOT EXISTS faulty boolean NOT NULL DEFAULT false;
-- Index for the snapshot DISTINCT ON query:
-- SELECT DISTINCT ON (device_id) ... ORDER BY device_id, ts DESC
-- TimescaleDB scans only the latest chunks for devices with recent activity,
-- but the (device_id, ts DESC) index makes per-device latest-position lookups
-- efficient regardless of chunk age.
CREATE INDEX IF NOT EXISTS positions_device_ts_idx ON positions (device_id, ts DESC);