merge(deferred-10): aggregated live alarm stream for Alarm Summary
Merges plan #10 (feat/live-alarm-stream) into main alongside plan #22. Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -5,9 +5,11 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums
|
||||
@using ZB.MOM.WW.ScadaBridge.Communication
|
||||
@implements IDisposable
|
||||
@inject IAlarmSummaryService AlarmSummaryService
|
||||
@inject ISiteRepository SiteRepository
|
||||
@inject ISiteAlarmLiveCache LiveAlarmCache
|
||||
|
||||
<div class="container-fluid mt-3" data-test="alarm-summary">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
@@ -259,13 +261,23 @@
|
||||
_notReporting = Array.Empty<string>();
|
||||
_rollup = new AlarmRollup(0, 0, 0, new Dictionary<AlarmKind, int>());
|
||||
StopTimer();
|
||||
DisposeLiveSubscription();
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedSiteId = siteId;
|
||||
ClearFilters();
|
||||
// Subscribe to the shared live cache FIRST so the aggregator (and its
|
||||
// seed-then-stream) starts warming while the initial poll runs.
|
||||
SubscribeLive(siteId);
|
||||
await RefreshAsync();
|
||||
StartTimer();
|
||||
// If another circuit already warmed the cache for this site, the initial
|
||||
// poll may be staler than what's live — apply the live snapshot on top.
|
||||
if (LiveAlarmCache.IsLive(siteId))
|
||||
{
|
||||
ApplyLiveSnapshot(siteId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshAsync()
|
||||
@@ -322,6 +334,64 @@
|
||||
_refreshTimer = null;
|
||||
}
|
||||
|
||||
// ── Live cache (plan #10, Task 5) ──────────────────────────────────────────
|
||||
// The page is live-cache-first with the 15s poll kept as a fallback/safety net.
|
||||
// Reconciliation model:
|
||||
// • The live cache pushes onChanged deltas (near-real-time) whenever the site's
|
||||
// aggregated alarm set changes. We rebuild _rows/_rollup/_visibleRows from the
|
||||
// immutable live snapshot — but deliberately DO NOT touch _notReporting, since
|
||||
// the alarm-only live cache can't compute it.
|
||||
// • The 15s poll (RefreshAsync) still runs untouched: it is the authority for
|
||||
// _notReporting and the safety net when the cache is not live (pre-seed or a
|
||||
// degraded/failed stream — IsLive == false). When live, the poll simply
|
||||
// re-affirms the same snapshot; the live path just makes updates arrive sooner.
|
||||
// • Both paths mutate shared state only via the Blazor dispatcher (the poll via
|
||||
// its InvokeAsync callback, the live delta via OnLiveChanged's InvokeAsync), so
|
||||
// they are serialized and never race. Each rebuild is an idempotent snapshot, so
|
||||
// a live rebuild immediately followed by a poll rebuild (or vice-versa) is safe.
|
||||
private IDisposable? _liveSubscription;
|
||||
|
||||
private void SubscribeLive(int siteId)
|
||||
{
|
||||
DisposeLiveSubscription();
|
||||
_liveSubscription = LiveAlarmCache.Subscribe(siteId, () => OnLiveAlarmsChanged(siteId));
|
||||
}
|
||||
|
||||
private void DisposeLiveSubscription()
|
||||
{
|
||||
_liveSubscription?.Dispose();
|
||||
_liveSubscription = null;
|
||||
}
|
||||
|
||||
// Raised on the aggregator's thread — marshal onto the circuit before touching state.
|
||||
private void OnLiveAlarmsChanged(int siteId)
|
||||
{
|
||||
_ = InvokeAsync(() =>
|
||||
{
|
||||
// Drop stale callbacks for a site we've since navigated away from, and
|
||||
// let the poll drive until the aggregator has actually seeded (so we never
|
||||
// clobber a good poll snapshot with an empty pre-seed list).
|
||||
if (_selectedSiteId != siteId || !LiveAlarmCache.IsLive(siteId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyLiveSnapshot(siteId);
|
||||
StateHasChanged();
|
||||
});
|
||||
}
|
||||
|
||||
// Rebuilds rows/rollup/visible-rows from the live snapshot. Leaves _notReporting
|
||||
// as the last poll computed it (the alarm-only cache can't know it). Idempotent.
|
||||
private void ApplyLiveSnapshot(int siteId)
|
||||
{
|
||||
var current = LiveAlarmCache.GetCurrentAlarms(siteId);
|
||||
var result = AlarmSummaryService.BuildFromLiveAlarms(current);
|
||||
_rows = result.Alarms;
|
||||
_rollup = AlarmSummaryService.ComputeRollup(_rows);
|
||||
RecomputeVisibleRows();
|
||||
}
|
||||
|
||||
private IEnumerable<string> DistinctInstances =>
|
||||
_rows.Select(r => r.InstanceUniqueName).Distinct().OrderBy(n => n, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
@@ -434,5 +504,9 @@
|
||||
_ => "Computed"
|
||||
};
|
||||
|
||||
public void Dispose() => StopTimer();
|
||||
public void Dispose()
|
||||
{
|
||||
StopTimer();
|
||||
DisposeLiveSubscription();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user