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