diff --git a/src/FastIDs.TypeId/TypeId.Core/TypeId.Core.csproj b/src/FastIDs.TypeId/TypeId.Core/TypeId.Core.csproj index 624dbe7..ffabd52 100644 --- a/src/FastIDs.TypeId/TypeId.Core/TypeId.Core.csproj +++ b/src/FastIDs.TypeId/TypeId.Core/TypeId.Core.csproj @@ -33,7 +33,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/src/FastIDs.TypeId/TypeId.Core/TypeIdDecoded.cs b/src/FastIDs.TypeId/TypeId.Core/TypeIdDecoded.cs index 630e613..1cf34e3 100644 --- a/src/FastIDs.TypeId/TypeId.Core/TypeIdDecoded.cs +++ b/src/FastIDs.TypeId/TypeId.Core/TypeIdDecoded.cs @@ -1,13 +1,14 @@ using System.Runtime.InteropServices; using System.Text.Unicode; -using UUIDNext; -using UUIDNext.Generator; +using FastIDs.TypeId.Uuid; namespace FastIDs.TypeId; [StructLayout(LayoutKind.Auto)] public readonly struct TypeIdDecoded : IEquatable, ISpanFormattable, IUtf8SpanFormattable { + private static readonly UuidGenerator UuidGenerator = new(); + /// /// The type part of the TypeId. /// @@ -66,7 +67,7 @@ public int GetSuffix(Span utf8Output) /// DateTimeOffset representing the ID generation timestamp. public DateTimeOffset GetTimestamp() { - var (timestampMs, _) = UuidV7Generator.Decode(Id); + var timestampMs = UuidDecoder.DecodeTimestamp(Id); return DateTimeOffset.FromUnixTimeMilliseconds(timestampMs); } @@ -194,7 +195,7 @@ public bool TryFormat(Span utf8Destination, out int bytesWritten, ReadOnly /// /// This method validates the type. If you are sure that type is valid use to skip type validation. /// - public static TypeIdDecoded New(string type) => FromUuidV7(type, Uuid.NewSequential()); + public static TypeIdDecoded New(string type) => FromUuidV7(type, UuidGenerator.New()); /// /// Generates new TypeId with the specified type and random UUIDv7. If is false, type is not validated. @@ -207,7 +208,7 @@ public bool TryFormat(Span utf8Destination, out int bytesWritten, ReadOnly /// Use this method with set to false when you are sure that is valid. /// This method is a bit faster than (especially for longer types) because it skips type validation. /// - public static TypeIdDecoded New(string type, bool validateType) => validateType ? New(type) : new TypeIdDecoded(type, Uuid.NewSequential()); + public static TypeIdDecoded New(string type, bool validateType) => validateType ? New(type) : new TypeIdDecoded(type, UuidGenerator.New()); /// /// Generates new TypeId with the specified type and UUIDv7. diff --git a/src/FastIDs.TypeId/TypeId.Core/Uuid/GuidConverter.cs b/src/FastIDs.TypeId/TypeId.Core/Uuid/GuidConverter.cs new file mode 100644 index 0000000..a72c5a7 --- /dev/null +++ b/src/FastIDs.TypeId/TypeId.Core/Uuid/GuidConverter.cs @@ -0,0 +1,37 @@ +using System.Runtime.CompilerServices; + +namespace FastIDs.TypeId.Uuid; + +// The UUIDv7 implementation is extracted from https://github.com/mareek/UUIDNext to prevent transient dependency. +// TypeID doesn't require any UUID implementations except UUIDv7. +internal static class GuidConverter +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Guid CreateGuidFromBigEndianBytes(Span bytes) + { + SetVersion(bytes); + SetVariant(bytes); + return new Guid(bytes, true); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SetVersion(Span bytes) + { + const byte uuidVersion = 7; + const int versionByteIndex = 6; + //Erase upper 4 bits + bytes[versionByteIndex] &= 0b0000_1111; + //Set 4 upper bits to version + bytes[versionByteIndex] |= uuidVersion << 4; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SetVariant(Span bytes) + { + const int variantByteIndex = 8; + //Erase upper 2 bits + bytes[variantByteIndex] &= 0b0011_1111; + //Set 2 upper bits to variant + bytes[variantByteIndex] |= 0b1000_0000; + } +} \ No newline at end of file diff --git a/src/FastIDs.TypeId/TypeId.Core/Uuid/UuidDecoder.cs b/src/FastIDs.TypeId/TypeId.Core/Uuid/UuidDecoder.cs new file mode 100644 index 0000000..b76f7fd --- /dev/null +++ b/src/FastIDs.TypeId/TypeId.Core/Uuid/UuidDecoder.cs @@ -0,0 +1,21 @@ +using System.Buffers.Binary; + +namespace FastIDs.TypeId.Uuid; + +// The UUIDv7 implementation is extracted from https://github.com/mareek/UUIDNext to prevent transient dependency. +// TypeID doesn't require any UUID implementations except UUIDv7. +internal static class UuidDecoder +{ + public static long DecodeTimestamp(Guid guid) + { + // Allocating 2 bytes more to prepend timestamp data. + Span bytes = stackalloc byte[18]; + guid.TryWriteBytes(bytes[2..], bigEndian: true, out _); + + var timestampBytes = bytes[..8]; + var timestampMs = BinaryPrimitives.ReadInt64BigEndian(timestampBytes); + + return timestampMs; + } + +} \ No newline at end of file diff --git a/src/FastIDs.TypeId/TypeId.Core/Uuid/UuidGenerator.cs b/src/FastIDs.TypeId/TypeId.Core/Uuid/UuidGenerator.cs new file mode 100644 index 0000000..9cf4103 --- /dev/null +++ b/src/FastIDs.TypeId/TypeId.Core/Uuid/UuidGenerator.cs @@ -0,0 +1,117 @@ +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; + +namespace FastIDs.TypeId.Uuid; + +// The UUIDv7 implementation is extracted from https://github.com/mareek/UUIDNext to prevent transient dependency. +// TypeID doesn't require any UUID implementations except UUIDv7. + +// All timestamps of type `long` in this class are Unix milliseconds unless stated otherwise. +internal class UuidGenerator +{ + private const int SequenceBitSize = 7; + private const int SequenceMaxValue = (1 << SequenceBitSize) - 1; + + private long _lastUsedTimestamp; + private long _timestampOffset; + private ushort _monotonicSequence; + + public Guid New() + { + // Allocating 2 bytes more to prepend timestamp data. + Span buffer = stackalloc byte[18]; + + // Offset bytes that are used in ID. + var idBytes = buffer[2..]; + + var timestamp = GetCurrentUnixMilliseconds(); + SetSequence(idBytes[6..8], ref timestamp); + SetTimestamp(buffer[..8], timestamp); // Using full buffer because we need to account for two zero-bytes in front. + RandomNumberGenerator.Fill(idBytes[8..]); + + return GuidConverter.CreateGuidFromBigEndianBytes(idBytes); + } + + // The implementation copied from DateTimeOffset.ToUnixTimeMilliseconds() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private long GetCurrentUnixMilliseconds() => DateTime.UtcNow.Ticks / 10000L - 62135596800000L; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SetTimestamp(Span bytes, long unixMs) + { + BinaryPrimitives.TryWriteInt64BigEndian(bytes, unixMs); + } + + private void SetSequence(Span bytes, ref long timestamp) + { + ushort sequence; + var originalTimestamp = timestamp; + + lock (this) + { + sequence = GetSequenceNumber(ref timestamp); + if (sequence > SequenceMaxValue) + { + // if the sequence is greater than the max value, we take advantage + // of the anti-rewind mechanism to simulate a slight change in clock time + timestamp = originalTimestamp + 1; + sequence = GetSequenceNumber(ref timestamp); + } + } + + BinaryPrimitives.TryWriteUInt16BigEndian(bytes, sequence); + } + + private ushort GetSequenceNumber(ref long timestamp) + { + EnsureTimestampNeverMoveBackward(ref timestamp); + + if (timestamp == _lastUsedTimestamp) + { + _monotonicSequence += 1; + } + else + { + _lastUsedTimestamp = timestamp; + _monotonicSequence = GetSequenceSeed(); + } + + return _monotonicSequence; + } + + private void EnsureTimestampNeverMoveBackward(ref long timestamp) + { + var lastUsedTs = _lastUsedTimestamp; + if (_timestampOffset > 0 && timestamp > lastUsedTs) + { + // reset the offset to reduce the drift with the actual time when possible + _timestampOffset = 0; + return; + } + + var offsetTimestamp = timestamp + _timestampOffset; + if (offsetTimestamp < lastUsedTs) + { + // if the computer clock has moved backward since the last generated UUID, + // we add an offset to ensure the timestamp always move forward (See RFC Section 6.2) + _timestampOffset = lastUsedTs - timestamp; + timestamp = lastUsedTs; + return; + } + + // Happy path + timestamp = offsetTimestamp; + } + + + private static ushort GetSequenceSeed() + { + // following section 6.2 on "Fixed-Length Dedicated Counter Seeding", the initial value of the sequence is randomized + Span buffer = stackalloc byte[2]; + RandomNumberGenerator.Fill(buffer); + // Setting the highest bit to 0 mitigate the risk of a sequence overflow (see section 6.2) + buffer[0] &= 0b0000_0111; + return BinaryPrimitives.ReadUInt16BigEndian(buffer); + } +} \ No newline at end of file