fix(central-ui): resolve CentralUI-020..025 — auth-ping idle logout, DebugView race, push-handler disposal guard, JS-interop catch narrowing, claim-constant helper, SessionExpiry tests
This commit is contained in:
@@ -204,6 +204,17 @@
|
||||
private int _totalPages;
|
||||
private const int PageSize = 25;
|
||||
|
||||
// CentralUI-022: IDeploymentStatusNotifier is a process singleton that
|
||||
// raises StatusChanged on the DeploymentManager service thread. Dispose()
|
||||
// unsubscribes, but the notifier can read its subscriber list and begin
|
||||
// invoking OnDeploymentStatusChanged just before this component is disposed.
|
||||
// The handler then runs against a disposed component and InvokeAsync throws
|
||||
// ObjectDisposedException as an unobserved fire-and-forget task exception.
|
||||
// This flag (set first in Dispose()) makes a racing callback no-op, and the
|
||||
// dispatch swallows the residual ObjectDisposedException — mirroring the
|
||||
// DebugView (CentralUI-009) and ToastNotification (CentralUI-010) guards.
|
||||
private volatile bool _disposed;
|
||||
|
||||
// CentralUI-006: deployment status updates are push-based, not polled.
|
||||
// DeploymentManager raises IDeploymentStatusNotifier.StatusChanged on every
|
||||
// deployment-record status write; this page subscribes to it and reloads,
|
||||
@@ -220,12 +231,34 @@
|
||||
|
||||
private void OnDeploymentStatusChanged(ScadaLink.DeploymentManager.DeploymentStatusChange change)
|
||||
{
|
||||
if (!_autoRefresh) return;
|
||||
_ = InvokeAsync(async () =>
|
||||
// CentralUI-022: a callback racing disposal must not touch the component.
|
||||
if (_disposed || !_autoRefresh) return;
|
||||
_ = DispatchReloadAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads the deployment table on the renderer's dispatcher, guarded
|
||||
/// against the component being disposed mid-flight (CentralUI-022):
|
||||
/// <c>InvokeAsync</c> throws <see cref="ObjectDisposedException"/> once the
|
||||
/// circuit is gone, and this handler runs fire-and-forget so that exception
|
||||
/// would otherwise go unobserved on the DeploymentManager thread.
|
||||
/// </summary>
|
||||
private async Task DispatchReloadAsync()
|
||||
{
|
||||
if (_disposed) return;
|
||||
try
|
||||
{
|
||||
await LoadDataAsync();
|
||||
StateHasChanged();
|
||||
});
|
||||
await InvokeAsync(async () =>
|
||||
{
|
||||
if (_disposed) return;
|
||||
await LoadDataAsync();
|
||||
StateHasChanged();
|
||||
});
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Component disposed between the guard and the dispatch — ignore.
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleAutoRefresh()
|
||||
@@ -316,8 +349,10 @@
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Unsubscribe so a status change after the circuit is gone does not
|
||||
// touch a disposed component (the notifier is a process singleton).
|
||||
// CentralUI-022: set the guard first so a callback already in flight on
|
||||
// the DeploymentManager thread no-ops, then unsubscribe so no further
|
||||
// status change reaches this disposed component.
|
||||
_disposed = true;
|
||||
DeploymentStatusNotifier.StatusChanged -= OnDeploymentStatusChanged;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user