import { and, eq, laneDevices, type Db } from "@parking/db"; // Resolves a device instance id (lane_devices.id) to its lane number. // // Device pushes/events carry the `lane_devices` id (which device fired), not a // lane. The event log wants the lane, so we keep a small in-memory id->lane map // rebuilt from the DB at startup and refreshed whenever assignments change // (assign/unassign). It's tiny (one row per device) and read on the hot path of // every input event, so a cached map beats a per-event DB lookup. export class LaneMap { readonly #db: Db; #byDeviceId = new Map(); constructor(db: Db) { this.#db = db; } /** (Re)load the id->lane map from the lane_devices table. */ refresh(): void { const rows = this.#db.select().from(laneDevices).all(); const next = new Map(); for (const r of rows) next.set(r.id, r.lane); this.#byDeviceId = next; } /** Lane for a device instance id, or null if the device isn't known. */ laneFor(deviceId: string): number | null { return this.#byDeviceId.get(deviceId) ?? null; } } /** * The lane a reader/scanner belongs to, IF that lane has an access (barrier) * device to open — shared by the read-driven flows (exit + permit). A read is an * identity signal; it only drives a barrier where there's one to drive. Returns * the lane number or null. (Distinguishing entry- vs. exit-readers per lane is a * later lane-direction model.) */ export async function readerLaneWithAccess(db: Db, deviceId: string): Promise { const row = await db.select().from(laneDevices).where(eq(laneDevices.id, deviceId)).get(); if (!row || !row.enabled) return null; const access = await db .select() .from(laneDevices) .where(and(eq(laneDevices.category, "access"), eq(laneDevices.lane, row.lane))) .get(); return access && access.enabled ? row.lane : null; }