//
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
//
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.Json.Serialization;
namespace StellaOps.HybridLogicalClock;
///
/// Hybrid Logical Clock timestamp providing monotonic, causally-ordered time
/// across distributed nodes even under clock skew.
///
///
///
/// HLC combines the benefits of physical time (human-readable, bounded drift)
/// with logical clocks (guaranteed causality, no rollback). The timestamp
/// consists of three components:
///
///
/// - PhysicalTime: Unix milliseconds UTC, advances with wall clock
/// - NodeId: Unique identifier for the generating node
/// - LogicalCounter: Increments when events occur at same physical time
///
///
/// Total ordering is defined as: (PhysicalTime, LogicalCounter, NodeId)
///
///
[JsonConverter(typeof(HlcTimestampJsonConverter))]
public readonly record struct HlcTimestamp : IComparable, IComparable
{
///
/// Physical time component (Unix milliseconds UTC).
///
public required long PhysicalTime { get; init; }
///
/// Unique node identifier (e.g., "scheduler-east-1").
///
public required string NodeId { get; init; }
///
/// Logical counter for events at same physical time.
///
public required int LogicalCounter { get; init; }
///
/// Gets the physical time as a .
///
[JsonIgnore]
public DateTimeOffset PhysicalDateTime =>
DateTimeOffset.FromUnixTimeMilliseconds(PhysicalTime);
///
/// Gets a zero/uninitialized timestamp.
///
public static HlcTimestamp Zero => new()
{
PhysicalTime = 0,
NodeId = string.Empty,
LogicalCounter = 0
};
///
/// String representation for storage: "0001704067200000-scheduler-east-1-000042".
/// Format: {PhysicalTime:D13}-{NodeId}-{LogicalCounter:D6}
///
/// A sortable string representation.
public string ToSortableString()
{
return string.Create(
CultureInfo.InvariantCulture,
$"{PhysicalTime:D13}-{NodeId}-{LogicalCounter:D6}");
}
///
/// Parse from sortable string format.
///
/// The sortable string to parse.
/// The parsed .
/// Thrown when value is null.
/// Thrown when value is not in valid format.
public static HlcTimestamp Parse(string value)
{
ArgumentNullException.ThrowIfNull(value);
if (!TryParse(value, out var result))
{
throw new FormatException($"Invalid HLC timestamp format: '{value}'");
}
return result;
}
///
/// Try to parse from sortable string format.
///
/// The sortable string to parse.
/// The parsed timestamp if successful.
/// True if parsing succeeded; otherwise false.
public static bool TryParse(
[NotNullWhen(true)] string? value,
out HlcTimestamp result)
{
result = default;
if (string.IsNullOrEmpty(value))
{
return false;
}
// Format: {PhysicalTime:D13}-{NodeId}-{LogicalCounter:D6}
// Example: 0001704067200000-scheduler-east-1-000042
// The NodeId can contain hyphens, so we parse from both ends
var firstDash = value.IndexOf('-', StringComparison.Ordinal);
if (firstDash < 1)
{
return false;
}
var lastDash = value.LastIndexOf('-');
if (lastDash <= firstDash || lastDash >= value.Length - 1)
{
return false;
}
var physicalTimeStr = value[..firstDash];
var nodeId = value[(firstDash + 1)..lastDash];
var counterStr = value[(lastDash + 1)..];
if (!long.TryParse(physicalTimeStr, NumberStyles.None, CultureInfo.InvariantCulture, out var physicalTime))
{
return false;
}
if (string.IsNullOrEmpty(nodeId))
{
return false;
}
if (!int.TryParse(counterStr, NumberStyles.None, CultureInfo.InvariantCulture, out var counter))
{
return false;
}
result = new HlcTimestamp
{
PhysicalTime = physicalTime,
NodeId = nodeId,
LogicalCounter = counter
};
return true;
}
///
/// Compare for total ordering.
/// Order: (PhysicalTime, LogicalCounter, NodeId).
///
/// The other timestamp to compare.
/// Comparison result.
public int CompareTo(HlcTimestamp other)
{
// Primary: physical time
var physicalCompare = PhysicalTime.CompareTo(other.PhysicalTime);
if (physicalCompare != 0)
{
return physicalCompare;
}
// Secondary: logical counter
var counterCompare = LogicalCounter.CompareTo(other.LogicalCounter);
if (counterCompare != 0)
{
return counterCompare;
}
// Tertiary: node ID (for stable tie-breaking)
return string.Compare(NodeId, other.NodeId, StringComparison.Ordinal);
}
///
public int CompareTo(object? obj)
{
if (obj is null)
{
return 1;
}
if (obj is HlcTimestamp other)
{
return CompareTo(other);
}
throw new ArgumentException($"Object must be of type {nameof(HlcTimestamp)}", nameof(obj));
}
///
/// Less than operator.
///
public static bool operator <(HlcTimestamp left, HlcTimestamp right) => left.CompareTo(right) < 0;
///
/// Less than or equal operator.
///
public static bool operator <=(HlcTimestamp left, HlcTimestamp right) => left.CompareTo(right) <= 0;
///
/// Greater than operator.
///
public static bool operator >(HlcTimestamp left, HlcTimestamp right) => left.CompareTo(right) > 0;
///
/// Greater than or equal operator.
///
public static bool operator >=(HlcTimestamp left, HlcTimestamp right) => left.CompareTo(right) >= 0;
///
public override string ToString() => ToSortableString();
}