fix(anpr): share one camera snapshot across bridge + advisory paths
On a vehicle entry, two paths captured the SAME Hikvision camera within ~1s — the ANPR bridge (barrier-driving) and the advisory snapshotAsync (evidence/ telemetry) — each from a separate adapter instance. Hikvision serves snapshots single-threaded, so the second concurrent GET returned HTTP 503; the bridge then fail-softed and burned its 12s debounce, producing a ~74s "slow" subscriber entry (observed 2026-06-25, Qazim Mulleti / AB816NN — plate read was instant at conf 1.000; the delay was the 503/debounce churn, not recognition). Add captureSnapshotShared() in snapshot.ts: a module-level, deviceId-keyed cache that both paths call. It coalesces in-flight captures (the 2nd caller awaits the 1st's pull → no concurrent 503), serves a brief freshness window (1500ms) so the bridge→advisory sequence for one vehicle reuses one frame, never caches a failure (next caller retries), and keys by deviceId (no cross-camera/stale-vehicle reuse). Wired into anpr-entry.ts (bridge) and snapshot.ts (advisory). Tests: snapshot.test.ts (concurrent coalescing, TTL reuse, TTL-lapse re-pull, failure-not-cached, per-camera keying); anpr-entry.test.ts mock updated. 168 server tests green. NOTE: this removes the latency (the 503 collision). The separate double-entry (two signed vehicle_entry for one car) — debounce-too-short / stamp-before- success — is still open; less likely now but not eliminated. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -3,7 +3,7 @@ import { devices, deviceEvents as deviceEventsTable, eq, siteConfig, type Db, ty
|
||||
import type { FastifyBaseLogger } from "fastify";
|
||||
import { deviceEvents, type DeviceReadEvent } from "./device-events.js";
|
||||
import { directionOf, type FlowDirection } from "./device-resolve.js";
|
||||
import { buildCamera } from "./snapshot.js";
|
||||
import { buildCamera, captureSnapshotShared } from "./snapshot.js";
|
||||
import type { SubscriptionFlow } from "./subscription-flow.js";
|
||||
import type { VisionClient } from "./vision-client.js";
|
||||
|
||||
@@ -100,7 +100,9 @@ export class AnprBridge {
|
||||
// "both" collapses to entry purely for the capture hint (it doesn't pick the lane —
|
||||
// the gated flow infers the verb from the camera's bound relay direction).
|
||||
const direction: FlowDirection = directionOf(this.#db, row) === "exit" ? "exit" : "entry";
|
||||
const shot = await camera.captureSnapshot({ direction });
|
||||
// Shared capture (deviceId-keyed): coalesces with the advisory snapshotAsync for
|
||||
// the SAME vehicle so the single-threaded camera isn't hit twice (→ HTTP 503).
|
||||
const shot = await captureSnapshotShared(deviceId, camera, { direction });
|
||||
const result = await this.#vision.analyze(shot.bytes, shot.contentType);
|
||||
if (!result || !result.plate) return; // nothing read
|
||||
|
||||
|
||||
Reference in New Issue
Block a user