/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Collections.Generic; using System.Threading; namespace Opc.Ua.Server.NodeManager { /// /// Aggregates model change events that occur during a service call /// so they can be reported in a single GeneralModelChangeEventType /// notification at the end of the call. /// /// /// Per Part 5 ยง6.4.32, servers should batch model changes per /// transaction or publish cycle rather than emitting one event per /// change. This aggregator collects entries safely from concurrent /// callers and can be drained on the publish cycle boundary. It /// is used by every CustomNodeManager / AsyncCustomNodeManager /// (not just alarm node managers) and lives in /// Opc.Ua.Server.NodeManager to reflect its general scope. /// public sealed class ModelChangeAggregator { private readonly Lock m_lock = new(); private List m_pending = []; /// /// Adds a model change to the pending batch. /// public void Add(ModelChangeStructureDataType change) { if (change == null) { throw new ArgumentNullException(nameof(change)); } lock (m_lock) { m_pending.Add(change); } } /// /// Records the addition of a node. /// public void RecordNodeAdded(NodeId affected, NodeId? typeDefinition) { if (affected.IsNull) { throw new ArgumentNullException(nameof(affected)); } Add(new ModelChangeStructureDataType { Affected = affected, AffectedType = typeDefinition ?? NodeId.Null, Verb = (byte)ModelChangeVerbs.NodeAdded }); } /// /// Records the deletion of a node. /// public void RecordNodeDeleted(NodeId affected, NodeId? typeDefinition) { if (affected.IsNull) { throw new ArgumentNullException(nameof(affected)); } Add(new ModelChangeStructureDataType { Affected = affected, AffectedType = typeDefinition ?? NodeId.Null, Verb = (byte)ModelChangeVerbs.NodeDeleted }); } /// /// Records the addition of a reference. /// public void RecordReferenceAdded(NodeId affected, NodeId? typeDefinition = null) { if (affected.IsNull) { throw new ArgumentNullException(nameof(affected)); } Add(new ModelChangeStructureDataType { Affected = affected, AffectedType = typeDefinition ?? NodeId.Null, Verb = (byte)ModelChangeVerbs.ReferenceAdded }); } /// /// Records the deletion of a reference. /// public void RecordReferenceDeleted(NodeId affected, NodeId? typeDefinition = null) { if (affected.IsNull) { throw new ArgumentNullException(nameof(affected)); } Add(new ModelChangeStructureDataType { Affected = affected, AffectedType = typeDefinition ?? NodeId.Null, Verb = (byte)ModelChangeVerbs.ReferenceDeleted }); } /// /// Records a DataType change on a variable. /// public void RecordDataTypeChanged(NodeId affected, NodeId? typeDefinition = null) { if (affected.IsNull) { throw new ArgumentNullException(nameof(affected)); } Add(new ModelChangeStructureDataType { Affected = affected, AffectedType = typeDefinition ?? NodeId.Null, Verb = (byte)ModelChangeVerbs.DataTypeChanged }); } /// /// True when there are pending changes to drain. /// public bool HasPending { get { lock (m_lock) { return m_pending.Count > 0; } } } /// /// Drains all pending changes, returning a snapshot suitable /// for use as the Changes array of a /// GeneralModelChangeEventType. /// public ArrayOf Drain() { lock (m_lock) { if (m_pending.Count == 0) { return []; } List drained = m_pending; m_pending = []; return new ArrayOf(drained.ToArray()); } } } /// /// Bit values for the Verb field of /// ModelChangeStructureDataType. /// [Flags] public enum ModelChangeVerbs : byte { /// /// No change. /// None = 0, /// /// A new node was added. /// NodeAdded = 1, /// /// An existing node was deleted. /// NodeDeleted = 2, /// /// A reference was added. /// ReferenceAdded = 4, /// /// A reference was deleted. /// ReferenceDeleted = 8, /// /// The DataType attribute changed. /// DataTypeChanged = 16, } }