feat(browse): Wave-0 Batch E — UntilStable settle loop + BrowserSessionService fallback
Task 9: DiscoverTreeAsync UntilStable settle loop (FOCAS) — re-capture on a 1s interval until
the node set is non-empty and stable across two passes, bounded by open-timeout; on
timeout with a prior non-empty capture, return it (best-effort) instead of failing.
Task 11: BrowserSessionService resolves bespoke-first, falls back to IUniversalDriverBrowser
when no bespoke browser matches and CanBrowse is true; new CanBrowse on the service.
16 unit tests green (4 settle + 12 service).
This commit is contained in:
@@ -13,7 +13,8 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Browsing;
|
||||
public sealed class BrowserSessionService(
|
||||
IEnumerable<IDriverBrowser> browsers,
|
||||
BrowseSessionRegistry registry,
|
||||
ILogger<BrowserSessionService> logger) : IBrowserSessionService
|
||||
ILogger<BrowserSessionService> logger,
|
||||
IUniversalDriverBrowser universalBrowser) : IBrowserSessionService
|
||||
{
|
||||
/// <summary>Upper bound on a single root/expand/attributes call.</summary>
|
||||
public static readonly TimeSpan PerCallTimeout = TimeSpan.FromSeconds(20);
|
||||
@@ -25,7 +26,7 @@ public sealed class BrowserSessionService(
|
||||
public async Task<BrowseOpenResult> OpenAsync(string driverType, string configJson, CancellationToken ct)
|
||||
{
|
||||
if (!_browsersByType.TryGetValue(driverType, out var browser))
|
||||
return new(false, $"No browser registered for driver type '{driverType}'.", Guid.Empty);
|
||||
return await OpenUniversalAsync(driverType, configJson, ct).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var session = await browser.OpenAsync(configJson, ct).ConfigureAwait(false);
|
||||
@@ -40,6 +41,28 @@ public sealed class BrowserSessionService(
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanBrowse(string driverType, string configJson) =>
|
||||
_browsersByType.ContainsKey(driverType) || universalBrowser.CanBrowse(driverType, configJson);
|
||||
|
||||
private async Task<BrowseOpenResult> OpenUniversalAsync(string driverType, string configJson, CancellationToken ct)
|
||||
{
|
||||
if (!universalBrowser.CanBrowse(driverType, configJson))
|
||||
return new(false, $"No browser registered for driver type '{driverType}'.", Guid.Empty);
|
||||
try
|
||||
{
|
||||
var session = await universalBrowser.OpenAsync(driverType, configJson, ct).ConfigureAwait(false);
|
||||
registry.Register(session);
|
||||
return new(true, null, session.Token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogInformation(ex,
|
||||
"Universal browse open failed for driverType={DriverType}: {Message}", driverType, ex.Message);
|
||||
return new(false, ex.Message, Guid.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<BrowseNode>> RootAsync(Guid token, CancellationToken ct) =>
|
||||
InvokeAsync(token, ct, (s, c) => s.RootAsync(c));
|
||||
|
||||
@@ -54,6 +54,14 @@ public interface IBrowserSessionService
|
||||
/// <param name="token">Registry handle for the browse session to close.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
Task CloseAsync(Guid token);
|
||||
|
||||
/// <summary>True when a browse affordance exists for this driver type: a bespoke browser
|
||||
/// is registered, or the universal browser can serve it. Cheap (no connect); never throws.
|
||||
/// Drives the picker's Browse-button visibility.</summary>
|
||||
/// <param name="driverType">The driver type name.</param>
|
||||
/// <param name="configJson">The authoring DriverConfig JSON (may be mid-edit).</param>
|
||||
/// <returns>True when the picker should offer a Browse affordance for this driver.</returns>
|
||||
bool CanBrowse(string driverType, string configJson);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user