169 lines
9.2 KiB
C#
169 lines
9.2 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
|
|
|
/// <summary>
|
|
/// Pure diff between two <see cref="Phase7CompositionResult"/> snapshots — the
|
|
/// <c>previous</c> currently-applied composition and the <c>next</c> 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 <c>RebuildAddressSpace</c> consumes this against a real
|
|
/// <see cref="Commons.OpcUa.IOpcUaAddressSpaceSink"/> binding so re-applies only mutate the
|
|
/// nodes that actually changed — full tear-down + rebuild is reserved for first-boot or
|
|
/// drastic schema flips.
|
|
/// </summary>
|
|
public sealed record Phase7Plan(
|
|
IReadOnlyList<EquipmentNode> AddedEquipment,
|
|
IReadOnlyList<EquipmentNode> RemovedEquipment,
|
|
IReadOnlyList<Phase7Plan.EquipmentDelta> ChangedEquipment,
|
|
IReadOnlyList<DriverInstancePlan> AddedDrivers,
|
|
IReadOnlyList<DriverInstancePlan> RemovedDrivers,
|
|
IReadOnlyList<Phase7Plan.DriverDelta> ChangedDrivers,
|
|
IReadOnlyList<ScriptedAlarmPlan> AddedAlarms,
|
|
IReadOnlyList<ScriptedAlarmPlan> RemovedAlarms,
|
|
IReadOnlyList<Phase7Plan.AlarmDelta> ChangedAlarms,
|
|
IReadOnlyList<GalaxyTagPlan> AddedGalaxyTags,
|
|
IReadOnlyList<GalaxyTagPlan> RemovedGalaxyTags,
|
|
IReadOnlyList<Phase7Plan.GalaxyTagDelta> ChangedGalaxyTags)
|
|
{
|
|
/// <summary>
|
|
/// Equipment-namespace tag diff sets, keyed by <see cref="EquipmentTagPlan.TagId"/>. Added as
|
|
/// init-only members (defaulting empty) rather than positional parameters so existing
|
|
/// <c>Phase7Plan</c> construction sites compile unchanged — consistent with how
|
|
/// <see cref="Phase7CompositionResult.EquipmentTags"/> was added. Without these, an
|
|
/// incremental deploy that changes ONLY equipment tags produced an empty plan and
|
|
/// <c>OpcUaPublishActor.HandleRebuild</c> short-circuited before materialising them.
|
|
/// </summary>
|
|
public IReadOnlyList<EquipmentTagPlan> AddedEquipmentTags { get; init; } = Array.Empty<EquipmentTagPlan>();
|
|
/// <inheritdoc cref="AddedEquipmentTags"/>
|
|
public IReadOnlyList<EquipmentTagPlan> RemovedEquipmentTags { get; init; } = Array.Empty<EquipmentTagPlan>();
|
|
/// <inheritdoc cref="AddedEquipmentTags"/>
|
|
public IReadOnlyList<EquipmentTagDelta> ChangedEquipmentTags { get; init; } = Array.Empty<EquipmentTagDelta>();
|
|
|
|
/// <summary>
|
|
/// Equipment-namespace VirtualTag diff sets, keyed by <see cref="EquipmentVirtualTagPlan.VirtualTagId"/>.
|
|
/// The value-side analogue of <see cref="AddedEquipmentTags"/>: a VirtualTag carries an
|
|
/// <c>Expression</c> evaluated over <c>DependencyRefs</c>, so a deploy that changes ONLY
|
|
/// VirtualTags (e.g. a new computed signal or an edited formula) must still produce a
|
|
/// non-empty plan and drive a rebuild — without these the diff was blind to VirtualTags and
|
|
/// such a deploy silently no-op'd. Added as init-only members (defaulting empty) for the same
|
|
/// compile-compatibility reason as <see cref="AddedEquipmentTags"/>.
|
|
/// </summary>
|
|
public IReadOnlyList<EquipmentVirtualTagPlan> AddedEquipmentVirtualTags { get; init; } = Array.Empty<EquipmentVirtualTagPlan>();
|
|
/// <inheritdoc cref="AddedEquipmentVirtualTags"/>
|
|
public IReadOnlyList<EquipmentVirtualTagPlan> RemovedEquipmentVirtualTags { get; init; } = Array.Empty<EquipmentVirtualTagPlan>();
|
|
/// <inheritdoc cref="AddedEquipmentVirtualTags"/>
|
|
public IReadOnlyList<EquipmentVirtualTagDelta> ChangedEquipmentVirtualTags { get; init; } = Array.Empty<EquipmentVirtualTagDelta>();
|
|
|
|
/// <summary>Gets a value indicating whether the composition plan contains no changes.</summary>
|
|
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 &&
|
|
AddedEquipmentTags.Count == 0 && RemovedEquipmentTags.Count == 0 && ChangedEquipmentTags.Count == 0 &&
|
|
AddedEquipmentVirtualTags.Count == 0 && RemovedEquipmentVirtualTags.Count == 0 && ChangedEquipmentVirtualTags.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 sealed record EquipmentTagDelta(EquipmentTagPlan Previous, EquipmentTagPlan Current);
|
|
public sealed record EquipmentVirtualTagDelta(EquipmentVirtualTagPlan Previous, EquipmentVirtualTagPlan Current);
|
|
}
|
|
|
|
public static class Phase7Planner
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="previous">The previous composition result.</param>
|
|
/// <param name="next">The new composition result.</param>
|
|
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));
|
|
|
|
var (addedEqTags, removedEqTags, changedEqTags) = DiffById(
|
|
previous.EquipmentTags, next.EquipmentTags,
|
|
t => t.TagId,
|
|
(a, b) => new Phase7Plan.EquipmentTagDelta(a, b));
|
|
|
|
// VirtualTags diff by VirtualTagId, mirroring the EquipmentTags pass. EquipmentVirtualTagPlan
|
|
// overrides record equality to compare ALL fields by value — scalars (Expression/DataType/
|
|
// Name/FolderPath) plus DependencyRefs element-wise (SequenceEqual). So a no-op redeploy (fresh
|
|
// list instances, identical contents) correctly diffs to empty; only a real content change is
|
|
// flagged as changed.
|
|
var (addedVTags, removedVTags, changedVTags) = DiffById(
|
|
previous.EquipmentVirtualTags, next.EquipmentVirtualTags,
|
|
t => t.VirtualTagId,
|
|
(a, b) => new Phase7Plan.EquipmentVirtualTagDelta(a, b));
|
|
|
|
return new Phase7Plan(
|
|
addedEq, removedEq, changedEq,
|
|
addedDrv, removedDrv, changedDrv,
|
|
addedAlarm, removedAlarm, changedAlarm,
|
|
addedGalaxy, removedGalaxy, changedGalaxy)
|
|
{
|
|
AddedEquipmentTags = addedEqTags,
|
|
RemovedEquipmentTags = removedEqTags,
|
|
ChangedEquipmentTags = changedEqTags,
|
|
AddedEquipmentVirtualTags = addedVTags,
|
|
RemovedEquipmentVirtualTags = removedVTags,
|
|
ChangedEquipmentVirtualTags = changedVTags,
|
|
};
|
|
}
|
|
|
|
private static (IReadOnlyList<T> Added, IReadOnlyList<T> Removed, IReadOnlyList<TDelta> Changed)
|
|
DiffById<T, TDelta>(
|
|
IReadOnlyList<T> previous,
|
|
IReadOnlyList<T> next,
|
|
Func<T, string> identity,
|
|
Func<T, T, TDelta> deltaFactory) where T : class
|
|
{
|
|
var prevById = previous.ToDictionary(identity, StringComparer.Ordinal);
|
|
var nextById = next.ToDictionary(identity, StringComparer.Ordinal);
|
|
|
|
var added = new List<T>();
|
|
var removed = new List<T>();
|
|
var changed = new List<TDelta>();
|
|
|
|
foreach (var (id, p) in prevById)
|
|
{
|
|
if (!nextById.TryGetValue(id, out var n)) { removed.Add(p); continue; }
|
|
if (!EqualityComparer<T>.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);
|
|
}
|
|
}
|