namespace ZB.MOM.WW.OtOpcUa.OpcUaServer; /// /// Pure diff between two snapshots — the /// previous currently-applied composition and the next from a freshly-applied /// deployment. Three lists per entity class (Equipment / DriverInstance / ScriptedAlarm) /// captured by stable identity: added items are new, removed items have to be torn down, /// changed items have the same identity but at least one field differs. /// /// OpcUaPublishActor's RebuildAddressSpace consumes this against a real /// binding so re-applies only mutate the /// nodes that actually changed — full tear-down + rebuild is reserved for first-boot or /// drastic schema flips. /// public sealed record Phase7Plan( IReadOnlyList AddedEquipment, IReadOnlyList RemovedEquipment, IReadOnlyList ChangedEquipment, IReadOnlyList AddedDrivers, IReadOnlyList RemovedDrivers, IReadOnlyList ChangedDrivers, IReadOnlyList AddedAlarms, IReadOnlyList RemovedAlarms, IReadOnlyList ChangedAlarms, IReadOnlyList AddedGalaxyTags, IReadOnlyList RemovedGalaxyTags, IReadOnlyList ChangedGalaxyTags) { public bool IsEmpty => AddedEquipment.Count == 0 && RemovedEquipment.Count == 0 && ChangedEquipment.Count == 0 && AddedDrivers.Count == 0 && RemovedDrivers.Count == 0 && ChangedDrivers.Count == 0 && AddedAlarms.Count == 0 && RemovedAlarms.Count == 0 && ChangedAlarms.Count == 0 && AddedGalaxyTags.Count == 0 && RemovedGalaxyTags.Count == 0 && ChangedGalaxyTags.Count == 0; public sealed record EquipmentDelta(EquipmentNode Previous, EquipmentNode Current); public sealed record DriverDelta(DriverInstancePlan Previous, DriverInstancePlan Current); public sealed record AlarmDelta(ScriptedAlarmPlan Previous, ScriptedAlarmPlan Current); public sealed record GalaxyTagDelta(GalaxyTagPlan Previous, GalaxyTagPlan Current); } public static class Phase7Planner { /// /// Diff two compositions, emitting Added/Removed/Changed sets per entity class. /// Identity is the entity's stable id (EquipmentId, DriverInstanceId, ScriptedAlarmId). /// Element equality on the projection records doubles as the "did this change" check, /// so any field difference moves an item from "stable" to ChangedX. /// public static Phase7Plan Compute(Phase7CompositionResult previous, Phase7CompositionResult next) { ArgumentNullException.ThrowIfNull(previous); ArgumentNullException.ThrowIfNull(next); var (addedEq, removedEq, changedEq) = DiffById( previous.EquipmentNodes, next.EquipmentNodes, n => n.EquipmentId, (a, b) => new Phase7Plan.EquipmentDelta(a, b)); var (addedDrv, removedDrv, changedDrv) = DiffById( previous.DriverInstancePlans, next.DriverInstancePlans, d => d.DriverInstanceId, (a, b) => new Phase7Plan.DriverDelta(a, b)); var (addedAlarm, removedAlarm, changedAlarm) = DiffById( previous.ScriptedAlarmPlans, next.ScriptedAlarmPlans, a => a.ScriptedAlarmId, (a, b) => new Phase7Plan.AlarmDelta(a, b)); var (addedGalaxy, removedGalaxy, changedGalaxy) = DiffById( previous.GalaxyTags, next.GalaxyTags, t => t.TagId, (a, b) => new Phase7Plan.GalaxyTagDelta(a, b)); return new Phase7Plan( addedEq, removedEq, changedEq, addedDrv, removedDrv, changedDrv, addedAlarm, removedAlarm, changedAlarm, addedGalaxy, removedGalaxy, changedGalaxy); } private static (IReadOnlyList Added, IReadOnlyList Removed, IReadOnlyList Changed) DiffById( IReadOnlyList previous, IReadOnlyList next, Func identity, Func deltaFactory) where T : class { var prevById = previous.ToDictionary(identity, StringComparer.Ordinal); var nextById = next.ToDictionary(identity, StringComparer.Ordinal); var added = new List(); var removed = new List(); var changed = new List(); foreach (var (id, p) in prevById) { if (!nextById.TryGetValue(id, out var n)) { removed.Add(p); continue; } if (!EqualityComparer.Default.Equals(p, n)) changed.Add(deltaFactory(p, n)); } foreach (var (id, n) in nextById) { if (!prevById.ContainsKey(id)) added.Add(n); } added.Sort((a, b) => string.CompareOrdinal(identity(a), identity(b))); removed.Sort((a, b) => string.CompareOrdinal(identity(a), identity(b))); return (added, removed, changed); } }