namespace DodoSSH.Client.Domain; /// The merged entry, and everything that had to be overridden to produce it. /// The entry to store and push. /// Empty when the two sides were reconcilable field by field. public sealed record ConnectionLogMergeResult( ConnectionLogSecret Merged, IReadOnlyList Conflicts) { /// Whether anything had to be overridden. public bool HasConflicts => Conflicts.Count > 0; } /// public sealed record ActivityLogMergeResult( ActivityLogSecret Merged, IReadOnlyList Conflicts) { /// Whether anything had to be overridden. public bool HasConflicts => Conflicts.Count > 0; } /// /// Merges two divergent versions of a connection log entry. /// /// /// /// This exists because the item-kind pipeline requires it, and it should never run. A log entry is /// written once, at the moment a connection closes, and nothing updates one — so there is no second version /// for a first to diverge from. Reaching this code means two clients wrote different records under one /// entity id, and entity ids are v7 GUIDs minted independently on each machine. /// /// /// It is still a real merge rather than a throw. The reconciler runs inside a sync pass, and an exception /// there would strand every item queued behind this one — for a situation that is a bug in some client and /// not an emergency. So the remote side wins, the difference is recorded like any other, and somebody reads /// a conflict notice about a log entry, which is the loudest signal this could reasonably give. /// /// /// Nothing is redacted. Every field is already an audit record of something that happened, and a notice that /// hid which of two records was dropped would defeat the point of noticing. /// /// public static class ConnectionLogSecretMerge { /// Produces the merged entry. /// The version both sides branched from. /// The pending local version. /// The server's current version. public static ConnectionLogMergeResult Merge( ConnectionLogSecret ancestor, ConnectionLogSecret local, ConnectionLogSecret remote) { ArgumentNullException.ThrowIfNull(ancestor); ArgumentNullException.ThrowIfNull(local); ArgumentNullException.ThrowIfNull(remote); // Whole-value, not field by field. The fields of one entry describe one event, and a merge that took // the host from one side and the duration from the other would invent a connection nobody made — // which is a worse outcome than losing the record this machine happened to hold. if (local == remote) { return new ConnectionLogMergeResult(remote, []); } return new ConnectionLogMergeResult( remote, [ new HostFieldConflict( "Entry", MergeSide.Local, remote.Label, local.Label, DiscardedWasRemoval: false), ]); } } /// /// Merges two divergent versions of an activity log entry. /// /// public static class ActivityLogSecretMerge { /// public static ActivityLogMergeResult Merge( ActivityLogSecret ancestor, ActivityLogSecret local, ActivityLogSecret remote) { ArgumentNullException.ThrowIfNull(ancestor); ArgumentNullException.ThrowIfNull(local); ArgumentNullException.ThrowIfNull(remote); if (local == remote) { return new ActivityLogMergeResult(remote, []); } return new ActivityLogMergeResult( remote, [ new HostFieldConflict( "Entry", MergeSide.Local, remote.Label, local.Label, DiscardedWasRemoval: false), ]); } }