feat(live): task 1.5.5 — snapshot-on-subscribe

Adds snapshot provider that queries the latest non-faulty position per device
registered to an event, returned in the `subscribed` reply so the SPA map is
populated immediately rather than waiting for the first live broadcast batch.

Key changes:
- src/live/snapshot.ts: createSnapshotProvider factory using DISTINCT ON
  (device_id) ... ORDER BY device_id, ts DESC with WHERE faulty=false; converts
  Date ts to epoch ms; omits speed/course when 0 (matching broadcast convention)
- src/main.ts: injects createSnapshotProvider(pool) into createSubscriptionRegistry
- test/live-snapshot.test.ts: 7 unit tests covering: two-device result, empty
  event, faulty exclusion, DISTINCT ON semantics, parameterized query, metrics
  observation, and error propagation

The snapshot query requires the positions_device_ts_idx created in migration 0002
(task 1.5.4).  Snapshot failures fail open — registry.fetchSnapshot returns [] so
the subscription still succeeds with an empty initial state.
This commit is contained in:
2026-05-02 17:54:44 +02:00
parent fbb1f34e9a
commit b3d6410af6
3 changed files with 354 additions and 1 deletions
+3 -1
View File
@@ -24,6 +24,7 @@ import { createAuthzClient } from './live/authz.js';
import { createSubscriptionRegistry } from './live/registry.js';
import { createBroadcastConsumer } from './live/broadcast.js';
import { createDeviceEventMap } from './live/device-event-map.js';
import { createSnapshotProvider } from './live/snapshot.js';
// -------------------------------------------------------------------------
// Startup: validate config (fail fast on bad env), build logger
@@ -139,7 +140,8 @@ async function main(): Promise<void> {
// 10. Build the live WebSocket server (tasks 1.5.2 and 1.5.3).
const authClient = createAuthClient(config, logger, metrics);
const authzClient = createAuthzClient(config, logger, metrics);
const registry = createSubscriptionRegistry(authzClient, config, logger, metrics);
const snapshotProvider = createSnapshotProvider(pool, logger, metrics);
const registry = createSubscriptionRegistry(authzClient, config, logger, metrics, snapshotProvider);
const messageHandler = async (
conn: LiveConnection,