namespace ZB.MOM.WW.OtOpcUa.Commons.OpcUa; /// /// Wrapper that defers to an inner sink swapped in at /// runtime. Needed because the production sink (SdkAddressSpaceSink) wraps an /// OtOpcUaNodeManager that only exists after the SDK StandardServer has /// started — but Akka actors resolve their sink dependency at construction time, before /// the hosted service has booted the SDK. /// /// Bound as a singleton in DI on driver-role hosts; the OPC UA hosted service calls /// once the server is up. Until that swap happens, every call is a /// no-op against , so the actor stays safe to /// receive messages from the moment it boots. /// public sealed class DeferredAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgicalAddressSpaceSink { private volatile IOpcUaAddressSpaceSink _inner = NullOpcUaAddressSpaceSink.Instance; /// Swap in the production sink. Pass null to revert to the null sink /// (used during graceful shutdown so post-stop writes don't hit a half-disposed manager). /// The sink implementation to use, or null to use the null sink. public void SetSink(IOpcUaAddressSpaceSink? sink) => _inner = sink ?? NullOpcUaAddressSpaceSink.Instance; /// public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm) => _inner.WriteValue(nodeId, value, quality, sourceTimestampUtc, realm); /// public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm) => _inner.WriteAlarmCondition(alarmNodeId, state, sourceTimestampUtc, realm); /// public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, AddressSpaceRealm realm, bool isNative = false) => _inner.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity, realm, isNative); /// // Forward the WP4 multi-notifier wiring to the inner sink. Without this the native-alarm fan-out to the // referencing equipment folders ships INERT on every driver-role host (actors inject THIS wrapper, not the // inner SdkAddressSpaceSink) — the F10b / PR#423 forwarding trap the reflection guard exists to catch. public void WireAlarmNotifiers(string alarmNodeId, AddressSpaceRealm alarmRealm, IReadOnlyList notifierFolderNodeIds, AddressSpaceRealm notifierFolderRealm) => _inner.WireAlarmNotifiers(alarmNodeId, alarmRealm, notifierFolderNodeIds, notifierFolderRealm); /// public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm) => _inner.EnsureFolder(folderNodeId, parentNodeId, displayName, realm); /// public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, AddressSpaceRealm realm, string? historianTagname = null, bool isArray = false, uint? arrayLength = null) => _inner.EnsureVariable(variableNodeId, parentFolderNodeId, displayName, dataType, writable, realm, historianTagname, isArray, arrayLength); /// public void RebuildAddressSpace() => _inner.RebuildAddressSpace(); /// public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm) => _inner.RaiseNodesAddedModelChange(affectedNodeId, realm); /// public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") => _inner.AddReference(sourceNodeId, sourceRealm, targetNodeId, targetRealm, referenceType); /// // Forwards to the inner sink when it supports the surgical capability; returns false otherwise — // before the real SdkAddressSpaceSink is swapped in (inner is still the null sink), or any inner // sink that isn't surgical — so the caller (AddressSpaceApplier) falls back to a full rebuild. // Without this forward the surgical optimization is inert on every driver-role host, because // actors inject THIS wrapper, not the inner sink. ALL six args (including the DataType/array-shape // ones) MUST be forwarded — a partial forward silently drops the shape update on every driver-role host. public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength, AddressSpaceRealm realm) => _inner is ISurgicalAddressSpaceSink surgical && surgical.UpdateTagAttributes(variableNodeId, writable, historianTagname, dataType, isArray, arrayLength, realm); /// // Forwards to the inner sink when it supports the surgical capability; returns false otherwise — // before the real SdkAddressSpaceSink is swapped in (inner is still the null sink), or any inner // sink that isn't surgical — so the caller (AddressSpaceApplier) falls back to a full rebuild. // Without this forward the rename-refresh optimization is inert on every driver-role host, because // actors inject THIS wrapper, not the inner sink. public bool UpdateFolderDisplayName(string folderNodeId, string displayName, AddressSpaceRealm realm) => _inner is ISurgicalAddressSpaceSink surgical && surgical.UpdateFolderDisplayName(folderNodeId, displayName, realm); // R2-07 T7 — surgical remove forwards. Same capability-sniffing pattern as UpdateTagAttributes / // UpdateFolderDisplayName: forward when the inner sink supports the surgical capability; return false // otherwise (before the real SdkAddressSpaceSink is swapped in, or any non-surgical inner) so the caller // (AddressSpaceApplier) falls back to a full rebuild. Without these forwards the surgical remove path is // inert on every driver-role host — the F10b / PR#423 trap the reflection guard exists to catch. /// public bool RemoveVariableNode(string variableNodeId, AddressSpaceRealm realm) => _inner is ISurgicalAddressSpaceSink surgical && surgical.RemoveVariableNode(variableNodeId, realm); /// public bool RemoveAlarmConditionNode(string alarmNodeId, AddressSpaceRealm realm) => _inner is ISurgicalAddressSpaceSink surgical && surgical.RemoveAlarmConditionNode(alarmNodeId, realm); /// public bool RemoveEquipmentSubtree(string equipmentNodeId, AddressSpaceRealm realm) => _inner is ISurgicalAddressSpaceSink surgical && surgical.RemoveEquipmentSubtree(equipmentNodeId, realm); }