using System.Security.Cryptography; namespace Just.Core; /// /// Generates time-based sequential IDs with entropy /// ID Structure (64 bits): /// /// [1 bit] Always 0 (positive signed longs) /// [41 bits] Milliseconds since epoch (covers ~69 years) /// [8 bits] Sequence counter (0-255 per millisecond) /// [14 bits] Random entropy (0-16383) /// /// /// /// Important behaviors: /// /// Guarantees monotonic ordering within same millisecond /// Throws on backward time jumps /// Sequence resets when timestamp advances /// Thread-safe through locking /// /// public sealed class SeqId(DateTime epoch) { private const int TimestampBits = 41; private const int TimestampShift = 63 - TimestampBits; private const long TimestampMask = 0x000001FF_FFFFFFFF; private const int SeqBits = 8; private const int SeqShift = TimestampShift - SeqBits; private const long SeqMask = 0x00000000_000000FF; private const int RandExclusiveUpper = 1 << SeqShift; /// /// Default epoch (2025-01-01 UTC) used for instance /// public static DateTime DefaultEpoch { get; } = new(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc); /// /// Default instance using /// public static SeqId Default { get; } = new(DefaultEpoch); /// /// Generates ID using default instance and current UTC time /// /// Entropy quality (default: Strong) /// 64-bit sequential ID with random component /// /// Thrown if more than 255 IDs generated in 1ms /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static long NextId(RngEntropy entropy = RngEntropy.Strong) => Default.Next(DateTime.UtcNow, entropy); #if NET9_0_OR_GREATER private readonly Lock _lock = new(); #else private readonly object _lock = new(); #endif private readonly DateTime _epoch = epoch; private int _seqId = 0; private long _lastTimestamp = -1L; /// /// Generates next ID using current UTC time /// /// Entropy quality (default: Strong) /// 64-bit sequential ID with random component /// /// Thrown if more than 255 IDs generated in 1ms /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public long Next(RngEntropy entropy = RngEntropy.Strong) => Next(DateTime.UtcNow, entropy); /// /// Generates next ID with explicit timestamp /// /// Timestamp basis for ID generation /// Entropy quality (default: Strong) /// 64-bit sequential ID with random component /// /// Thrown if is earlier than last used timestamp /// /// /// Thrown if more than 255 IDs generated in 1ms /// public long Next(DateTime dateTime, RngEntropy entropy = RngEntropy.Strong) { var epoch = dateTime.Subtract(_epoch); var timestamp = ((epoch.Ticks / TimeSpan.TicksPerMillisecond) & TimestampMask) << TimestampShift; long currentSeq; lock (_lock) { if (timestamp > _lastTimestamp) { _lastTimestamp = timestamp; _seqId = 0; } else if (timestamp < _lastTimestamp) { throw new InvalidOperationException("Refused to create new SeqId. Last timestamp is in the future."); } if (_seqId == SeqMask) { throw new IndexOutOfRangeException("Refused to create new SeqId. Sequence exhausted."); } currentSeq = ((_seqId++) & SeqMask) << SeqShift; } long currentRand = entropy == RngEntropy.Strong ? RandomNumberGenerator.GetInt32(RandExclusiveUpper) : Random.Shared.Next(RandExclusiveUpper); return timestamp | currentSeq | currentRand; } }