using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Commons.Browsing; /// /// In-memory that records the streamed discovery tree /// for the universal browser instead of materializing OPC UA nodes. Single-threaded like /// every builder a driver streams into during DiscoverAsync. Enforces a node cap: once /// exceeded it stops recording (drivers keep streaming harmlessly) and marks the tree /// truncated. See docs/plans/2026-07-15-universal-discovery-browser-design.md §4.1. /// public sealed class CapturingAddressSpaceBuilder : IAddressSpaceBuilder { private sealed class CaptureState(int nodeCap) { public int Count; public bool Truncated; public readonly int NodeCap = nodeCap; public readonly Dictionary ById = new(StringComparer.Ordinal); public bool TryReserve() { if (Count >= NodeCap) { Truncated = true; return false; } Count++; return true; } } private readonly CaptureState _state; private readonly CapturedNode _scope; /// Create a root capturing builder with the given node cap (default 50 000). /// Maximum number of nodes recorded before the tree is marked truncated. public CapturingAddressSpaceBuilder(int nodeCap = 50_000) { _state = new CaptureState(nodeCap); _scope = new CapturedNode { Id = "", Name = "", DisplayName = "", IsLeaf = false }; } private CapturingAddressSpaceBuilder(CaptureState state, CapturedNode scope) { _state = state; _scope = scope; } /// public IAddressSpaceBuilder Folder(string browseName, string displayName) { var node = new CapturedNode { Id = _scope.Id + "/" + browseName, Name = browseName, DisplayName = displayName, IsLeaf = false, }; if (_state.TryReserve()) { _scope.Children.Add(node); _state.ById.TryAdd(node.Id, node); } // Over cap: the child builder writes into a detached node — recording stops, streaming doesn't break. return new CapturingAddressSpaceBuilder(_state, node); } /// public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo) { var node = new CapturedNode { Id = attributeInfo.FullName, Name = browseName, DisplayName = displayName, IsLeaf = true, Attribute = attributeInfo, }; if (_state.TryReserve()) { _scope.Children.Add(node); _state.ById.TryAdd(node.Id, node); } return new CapturedVariableHandle(node); } /// public void AddProperty(string browseName, DriverDataType dataType, object? value) => _scope.Properties.Add((browseName, dataType, value)); /// Snapshot the captured tree. Only valid on the root builder (the one the browser constructed). /// An immutable-after-build . public CapturedTree Build() => new() { Root = _scope, Truncated = _state.Truncated, Count = _state.Count, ById = _state.ById, }; private sealed class CapturedVariableHandle(CapturedNode node) : IVariableHandle { public string FullReference => node.Id; public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) { node.IsAlarmMarked = true; return NoopAlarmSink.Instance; } } private sealed class NoopAlarmSink : IAlarmConditionSink { public static readonly NoopAlarmSink Instance = new(); public void OnTransition(AlarmEventArgs args) { } // browse never delivers live transitions } }