@if (_result is not null)
{
@if (_result.Ok)
{
OK · @_result.LatencyMs?.ToString("F0") ms
}
else
{
Failed · @DisplayText.Abbreviate(_result.Message, 60)
}
}
@code {
/// Driver type key — must match an installed IDriverProbe.DriverType.
[Parameter, EditorRequired] public string DriverType { get; set; } = "";
/// Callback that returns the current form config as JSON. Called at click time.
[Parameter, EditorRequired] public Func GetConfigJson { get; set; } = () => "{}";
///
/// Fallback probe budget, used only when the config JSON carries no positive
/// ProbeTimeoutSeconds and the driver has no documented default (TestConnectBudget.DriverDefaults). The configured value wins (it is every driver's documented "Test Connect
/// probe timeout"), so it can lengthen the budget as well as shorten it. Clamped to [1, 60] s.
///
[Parameter] public int TimeoutSeconds { get; set; } = TestConnectBudget.DefaultSeconds;
private const int ClearAfterMs = 30_000;
private bool _busy;
private int? _lastBudgetSeconds;
private TestDriverConnectResult? _result;
// System.Threading.Timer (it replaced an Elapsed-event timer with an async void lambda): DisposeAsync awaits an
// in-flight callback, and the callback body never throws onto the thread pool (R04 L2). The generation
// makes a superseded timer's late fire a no-op, so it cannot clear a newer result.
private System.Threading.Timer? _clearTimer;
private int _resultGeneration;
private volatile bool _disposed;
private async Task OnClickAsync()
{
_busy = true;
_result = null;
StateHasChanged();
try
{
var json = GetConfigJson() ?? "{}";
var budget = TestConnectBudget.Resolve(json, TimeoutSeconds, DriverType);
_lastBudgetSeconds = budget;
_result = await Probe.TestAsync(DriverType, json, budget);
}
catch (Exception ex)
{
_result = new TestDriverConnectResult(false, ex.Message, null, Guid.Empty);
}
finally
{
_busy = false;
ScheduleClear();
if (!_disposed) StateHasChanged();
}
}
private void ScheduleClear()
{
if (_disposed) return;
var generation = ++_resultGeneration;
_clearTimer?.Dispose();
_clearTimer = new System.Threading.Timer(_ => _ = ClearAsync(generation), null, ClearAfterMs, Timeout.Infinite);
}
private async Task ClearAsync(int generation)
{
try
{
await InvokeAsync(() =>
{
if (_disposed || generation != _resultGeneration) return;
_result = null;
StateHasChanged();
});
}
catch (Exception ex) when (ex is ObjectDisposedException or InvalidOperationException)
{
// The circuit went away between the fire and the render — nothing to clear.
}
}
///
public async ValueTask DisposeAsync()
{
_disposed = true;
if (_clearTimer is not null) await _clearTimer.DisposeAsync();
}
}