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);
}
}