feat(opcua): surgical RemoveEquipmentSubtree with notifier demotion (R2-07 T10)
This commit is contained in:
@@ -87,7 +87,7 @@
|
||||
{
|
||||
"id": "T10",
|
||||
"subject": "Phase 2: OtOpcUaNodeManager.RemoveEquipmentSubtree (descendants + notifier demotion) + SdkAddressSpaceSink forwards (high-risk)",
|
||||
"status": "pending",
|
||||
"status": "completed",
|
||||
"blockedBy": [
|
||||
"T8",
|
||||
"T9"
|
||||
|
||||
@@ -1847,7 +1847,73 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ISurgicalAddressSpaceSink.RemoveEquipmentSubtree"/>
|
||||
public bool RemoveEquipmentSubtree(string equipmentNodeId) => false;
|
||||
public bool RemoveEquipmentSubtree(string equipmentNodeId)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(equipmentNodeId);
|
||||
EnsureAddressSpaceCreated();
|
||||
|
||||
GeneralModelChangeEventState e;
|
||||
lock (Lock)
|
||||
{
|
||||
// Unknown equipment id ⇒ map drift ⇒ false (caller rebuilds).
|
||||
if (!_folders.ContainsKey(equipmentNodeId)) return false;
|
||||
|
||||
// Folder-scoped NodeIds mean every descendant's id is the equipment id itself or begins with
|
||||
// "<equipmentId>/" (sub-folders, variables, condition nodes). Snapshot each map's in-scope keys
|
||||
// first (ToList) so we don't mutate while enumerating.
|
||||
var prefix = equipmentNodeId + "/";
|
||||
bool InScope(string id) => id == equipmentNodeId || id.StartsWith(prefix, StringComparison.Ordinal);
|
||||
|
||||
// Value variables (+ their historized-tagname registrations).
|
||||
foreach (var id in _variables.Keys.Where(InScope).ToList())
|
||||
{
|
||||
if (_variables.TryRemove(id, out var v))
|
||||
{
|
||||
v.Parent?.RemoveChild(v);
|
||||
PredefinedNodes?.Remove(v.NodeId);
|
||||
}
|
||||
_historizedTagnames.TryRemove(id, out _);
|
||||
}
|
||||
|
||||
// Part 9 condition nodes (+ their native flags).
|
||||
foreach (var id in _alarmConditions.Keys.Where(InScope).ToList())
|
||||
{
|
||||
if (_alarmConditions.TryRemove(id, out var c))
|
||||
{
|
||||
c.Parent?.RemoveChild(c);
|
||||
PredefinedNodes?.Remove(c.NodeId);
|
||||
}
|
||||
_nativeAlarmNodeIds.Remove(id);
|
||||
}
|
||||
|
||||
// Notifier demotion BEFORE dropping the folders: sever the Server↔folder HasNotifier ref for
|
||||
// every promoted folder in the subtree (an equipment folder, or a sub-folder that hosted an
|
||||
// alarm), else the removal leaks an orphaned root-notifier reference on the Server object.
|
||||
// _notifierFolders is keyed by the folder's NodeId; match on its string identifier.
|
||||
foreach (var kvp in _notifierFolders.Where(kv => InScope(kv.Key.Identifier?.ToString() ?? string.Empty)).ToList())
|
||||
{
|
||||
RemoveRootNotifier(kvp.Value);
|
||||
_notifierFolders.Remove(kvp.Key);
|
||||
}
|
||||
// Drop the event-history source registrations for the subtree (keyed by the folder id string).
|
||||
foreach (var src in _eventNotifierSources.Keys.Where(InScope).ToList())
|
||||
_eventNotifierSources.TryRemove(src, out _);
|
||||
|
||||
// Sub-folders + the equipment folder itself (children already detached above).
|
||||
foreach (var id in _folders.Keys.Where(InScope).ToList())
|
||||
{
|
||||
if (_folders.TryRemove(id, out var f))
|
||||
{
|
||||
f.Parent?.RemoveChild(f);
|
||||
PredefinedNodes?.Remove(f.NodeId);
|
||||
}
|
||||
}
|
||||
|
||||
e = BuildNodesRemovedModelChange(equipmentNodeId);
|
||||
}
|
||||
ReportModelChangeOutsideLock(e, equipmentNodeId);
|
||||
return true;
|
||||
}
|
||||
|
||||
private FolderState ResolveParentFolder(string? parentNodeId)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user