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