15da8d4f0d
Task 6: CapturedTree/CapturedNode model + CapturingAddressSpaceBuilder — an in-memory
IAddressSpaceBuilder that records a driver's DiscoverAsync stream (folder nesting,
leaf FullName as node id, alarm mark, node-cap truncation, no-op alarm sink).
Task 7: CapturedTreeBrowseSession — serves the captured snapshot through IBrowseSession
(Root/Expand/Attributes from memory, truncation marker, shared-immutable-tree dispose).
13 unit tests green.
113 lines
4.0 KiB
C#
113 lines
4.0 KiB
C#
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Commons.Browsing;
|
|
|
|
/// <summary>
|
|
/// In-memory <see cref="IAddressSpaceBuilder"/> 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.
|
|
/// </summary>
|
|
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<string, CapturedNode> 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;
|
|
|
|
/// <summary>Create a root capturing builder with the given node cap (default 50 000).</summary>
|
|
/// <param name="nodeCap">Maximum number of nodes recorded before the tree is marked truncated.</param>
|
|
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;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void AddProperty(string browseName, DriverDataType dataType, object? value) =>
|
|
_scope.Properties.Add((browseName, dataType, value));
|
|
|
|
/// <summary>Snapshot the captured tree. Only valid on the root builder (the one the browser constructed).</summary>
|
|
/// <returns>An immutable-after-build <see cref="CapturedTree"/>.</returns>
|
|
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
|
|
}
|
|
}
|