heldar_kernel/services/consumer.rs
1//! The kernel's perception-consumer seam.
2//!
3//! The media + AI-task kernel is domain-agnostic: it samples frames, accepts detections from workers,
4//! and persists them. Anything that *interprets* those detections into domain events — the zone engine
5//! (spatial rules), the ANPR/access-control engine (plate authorization), and other domain
6//! verticals — plug in here as a [`DetectionConsumer`] rather than being wired
7//! into the ingest handler or [`crate::state::AppState`] directly.
8//!
9//! This inverts the dependency: ingest fans a committed batch out to a registry of consumers; a
10//! consumer self-declares which `task_type`s it cares about. New apps are *added* to the registry (and,
11//! after the crate split, linked in by the composing binary or fed over the event stream) — the kernel
12//! never gains an `if task_type == "..."` branch.
13
14use chrono::{DateTime, Utc};
15
16use crate::models::DetectionIngest;
17
18/// One committed batch of detections handed to consumers after it is persisted. Carries the
19/// site/camera/task context so a consumer needs no extra lookups (and so the seam is tenant-aware for
20/// distributed deployments).
21pub struct DetectionBatch<'a> {
22 pub camera_id: &'a str,
23 pub site_id: Option<&'a str>,
24 /// The task type that produced this batch (consumers self-select on it; a vertical may also
25 /// inspect it). Part of the stable seam contract.
26 pub task_type: &'a str,
27 pub detections: &'a [DetectionIngest],
28 /// Worker-supplied capture time (engines that need trustworthy timing use server time instead;
29 /// time-windowing consumers use this).
30 pub timestamp: DateTime<Utc>,
31}
32
33/// A pluggable interpreter of detection batches. Implementors live in their own module/crate (zones is
34/// kernel-open; ANPR/entry is a proprietary app) and are registered into [`crate::state::AppState`].
35#[async_trait::async_trait]
36pub trait DetectionConsumer: Send + Sync {
37 /// Stable name for logs/metrics.
38 fn name(&self) -> &'static str;
39
40 /// Whether this consumer wants batches of the given `task_type`. Return `true` for all types when
41 /// the consumer is task-agnostic (e.g. the zone engine evaluates any tracked detection).
42 fn interested_in(&self, task_type: &str) -> bool;
43
44 /// Process a persisted batch. Must not panic; errors are the consumer's own to log/handle.
45 async fn consume(&self, batch: &DetectionBatch<'_>);
46}