fix(ui): disposal guard on Alarm Summary live callback, matching the DebugView pattern (plan R2-07 T9)
This commit is contained in:
@@ -382,15 +382,20 @@
|
|||||||
_liveSubscription = null;
|
_liveSubscription = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// N7: set BEFORE teardown so a live callback racing Dispose is dropped both
|
||||||
|
// before the InvokeAsync marshal and inside it (mirrors DebugView.razor).
|
||||||
|
private volatile bool _disposed;
|
||||||
|
|
||||||
// Raised on the aggregator's thread — marshal onto the circuit before touching state.
|
// Raised on the aggregator's thread — marshal onto the circuit before touching state.
|
||||||
private void OnLiveAlarmsChanged(int siteId)
|
private void OnLiveAlarmsChanged(int siteId)
|
||||||
{
|
{
|
||||||
|
if (_disposed) return;
|
||||||
_ = InvokeAsync(() =>
|
_ = InvokeAsync(() =>
|
||||||
{
|
{
|
||||||
// Drop stale callbacks for a site we've since navigated away from, and
|
// 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
|
// let the poll drive until the aggregator has actually seeded (so we never
|
||||||
// clobber a good poll snapshot with an empty pre-seed list).
|
// clobber a good poll snapshot with an empty pre-seed list).
|
||||||
if (_selectedSiteId != siteId || !LiveAlarmCache.IsLive(siteId))
|
if (_disposed || _selectedSiteId != siteId || !LiveAlarmCache.IsLive(siteId))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -525,6 +530,7 @@
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
_disposed = true;
|
||||||
StopTimer();
|
StopTimer();
|
||||||
DisposeLiveSubscription();
|
DisposeLiveSubscription();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -278,6 +278,30 @@ public class AlarmSummaryRenderTests : BunitContext
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── arch-review R2 N7: disposal guard on the live callback ─────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LiveCallbackRacingDispose_IsDropped_NoRebuildNoThrow()
|
||||||
|
{
|
||||||
|
var cut = RenderWithSiteSelected();
|
||||||
|
_liveCache.PushAlarms(new List<AlarmStateChanged>
|
||||||
|
{
|
||||||
|
new("Gamma", "G-alarm", AlarmState.Active, 300, DateTimeOffset.UtcNow),
|
||||||
|
});
|
||||||
|
cut.WaitForAssertion(() => Assert.Equal("Gamma", FirstRowInstance(cut)));
|
||||||
|
|
||||||
|
// Snapshot the callback BEFORE disposal — this models a delta thread that
|
||||||
|
// already read the delegate when Dispose ran (arch-review R2 N7).
|
||||||
|
var inFlight = _liveCache.CapturedCallback!;
|
||||||
|
cut.Instance.Dispose();
|
||||||
|
|
||||||
|
_liveCache.PushAlarms(new List<AlarmStateChanged>()); // mutate the fake's snapshot
|
||||||
|
var ex = Record.Exception(inFlight);
|
||||||
|
Assert.Null(ex);
|
||||||
|
// The disposed page must not have re-applied the (now empty) live snapshot.
|
||||||
|
Assert.Equal("Gamma", FirstRowInstance(cut));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Controllable in-memory fake of <see cref="ISiteAlarmLiveCache"/>: records
|
/// Controllable in-memory fake of <see cref="ISiteAlarmLiveCache"/>: records
|
||||||
/// subscribe/dispose counts and lets a test push a live snapshot (which flips
|
/// subscribe/dispose counts and lets a test push a live snapshot (which flips
|
||||||
@@ -293,6 +317,7 @@ public class AlarmSummaryRenderTests : BunitContext
|
|||||||
public int SubscribeCount { get; private set; }
|
public int SubscribeCount { get; private set; }
|
||||||
public int DisposeCount { get; private set; }
|
public int DisposeCount { get; private set; }
|
||||||
public int? LastSubscribedSite { get; private set; }
|
public int? LastSubscribedSite { get; private set; }
|
||||||
|
public Action? CapturedCallback => _onChanged;
|
||||||
|
|
||||||
public IDisposable Subscribe(int siteId, Action onChanged)
|
public IDisposable Subscribe(int siteId, Action onChanged)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user