using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming; namespace ZB.MOM.WW.ScadaBridge.Communication; /// /// Central, transient, in-memory per-site live alarm cache (plan #10, Task 4). /// A DI singleton shared by every Alarm Summary Blazor circuit on the active central /// node. For each site with at least one active viewer it runs ONE shared per-site /// aggregator that seeds from the existing debug-snapshot fan-out and stays warm on a /// single site-wide, alarm-only gRPC stream (SubscribeSite), so the page /// reflects alarm transitions in near-real-time instead of re-polling every 15s. /// /// Hard constraint (locked [PERM]): there is NO persisted central alarm /// store. This cache is purely in-memory, per-node — no EF entity/table/migration backs /// it. In routine operation only the active node (behind Traefik) hosts viewers, so only /// it runs aggregators; browsing the standby node directly (diagnostic ports) starts an /// independent read-only aggregator there — accepted (arch review 02 round 2, N8): /// bounded (one stream/site, viewer-capped), read-only, and torn down by the viewer /// linger. On a NodeA↔NodeB failover the new active node re-seeds from scratch. /// /// /// Reference-counted lifecycle: the first for a site /// starts its aggregator (open stream + snapshot seed); the last subscriber leaving /// stops it after a short linger (see /// ) to avoid re-seed thrash. /// One gRPC stream per site, not per browser circuit. /// /// public interface ISiteAlarmLiveCache { /// /// Registers a viewer for a site's live alarm feed and returns an /// that unregisters it. The first subscriber for a site /// starts the shared aggregator; the last subscriber's /// schedules a linger-delayed stop. is raised (on the /// aggregator's thread) after each applied change — the handler must not block and /// should marshal any UI work onto its own dispatcher (mirrors /// IDeploymentStatusNotifier). Disposing the returned handle is idempotent. /// /// The numeric site id whose alarms the viewer wants. /// Callback invoked whenever the site's cached alarm set changes. /// A disposable that unregisters this viewer (idempotent). IDisposable Subscribe(int siteId, Action onChanged); /// /// Returns the current in-memory alarm snapshot for a site — exactly what the page /// should render. Empty when the site has no aggregator or has not yet completed its /// first seed. Safe to call from Blazor render threads: it returns an immutable /// point-in-time list (the aggregator publishes fresh immutable lists; readers never /// see a partially-mutated collection). /// /// The numeric site id. /// The current cached alarms (possibly empty), never null. IReadOnlyList GetCurrentAlarms(int siteId); /// /// Whether the site's aggregator has completed its initial seed and is serving live /// state. The page can use this to decide whether to keep its 15s poll fallback /// (Task 5): false means "not live yet — keep polling"; true means the /// cache is authoritative. /// /// The numeric site id. /// true once the site's aggregator has seeded and published at least once. bool IsLive(int siteId); }