|
| 1 | +namespace UUIDv7 |
| 2 | + |
| 3 | +// https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-01.html#name-uuidv7-layout-and-bit-order |
| 4 | +// UUIDv7 has microsecond resolution, 64-bit timestamp, 16-bit sequence number, and 64-bit node ID. |
| 5 | + |
| 6 | +open System |
| 7 | +open System.Diagnostics |
| 8 | +open System.Security.Cryptography |
| 9 | +open System.Text |
| 10 | + |
| 11 | +module internal HexConverter = |
| 12 | + let hexCharsUpper = "0123456789ABCDEF" |
| 13 | + let hexCharsLower = "0123456789abcdef" |
| 14 | + |
| 15 | + [<Literal>] |
| 16 | + let Mask = 0xF000_0000_0000_0000UL |
| 17 | + |
| 18 | + let private guidBuilder = StringBuilder(36) |
| 19 | + |
| 20 | + let private formatGuidB (hexChars: string) (high: uint64) (low: uint64) (sb: StringBuilder) = |
| 21 | + |
| 22 | + let rec loopHigh i x = |
| 23 | + match i with |
| 24 | + | 0 -> () |
| 25 | + | _ -> |
| 26 | + sb.Append(hexChars.[int ((x &&& Mask) >>> 60)]) |
| 27 | + |> ignore |
| 28 | + |
| 29 | + if i = 9 || i = 5 || i = 1 then |
| 30 | + sb.Append('-') |> ignore |
| 31 | + |
| 32 | + loopHigh (i - 1) (x <<< 4) |
| 33 | + |
| 34 | + let rec loopLow i x = |
| 35 | + match i with |
| 36 | + | 0 -> () |
| 37 | + | _ -> |
| 38 | + sb.Append(hexChars.[int ((x &&& Mask) >>> 60)]) |
| 39 | + |> ignore |
| 40 | + |
| 41 | + if i = 13 then sb.Append('-') |> ignore |
| 42 | + loopLow (i - 1) (x <<< 4) |
| 43 | + |
| 44 | + loopHigh 16 high |
| 45 | + loopLow 16 low |
| 46 | + |
| 47 | + let formatGuid uppercase high low = |
| 48 | + let hexChars = |
| 49 | + if uppercase then |
| 50 | + hexCharsUpper |
| 51 | + else |
| 52 | + hexCharsLower |
| 53 | + |
| 54 | + guidBuilder.Clear() |> ignore |
| 55 | + formatGuidB hexChars high low guidBuilder |
| 56 | + guidBuilder.ToString() |
| 57 | + |
| 58 | +[<Struct>] |
| 59 | +type UUIDv7(high: uint64, low: uint64) = |
| 60 | + member _.High = high |
| 61 | + member _.Low = low |
| 62 | + |
| 63 | + member _.Version = (high <<< 48) >>> 60 |
| 64 | + |
| 65 | + member _.Variant = low >>> 62 |
| 66 | + |
| 67 | + member _.ToUnixTimeSeconds() = |
| 68 | + DateTimeOffset.FromUnixTimeSeconds(int64 (high >>> 28)) |
| 69 | + |
| 70 | + member _.FullTimestamp = |
| 71 | + let low12 = (high &&& 0xFFFUL) |
| 72 | + let seconds = TimeSpan.FromSeconds(float (high >>> 28)) |
| 73 | + |
| 74 | + let usec = |
| 75 | + let x = (((high >>> 16) &&& 0xFFFUL) <<< 12) ||| low12 |
| 76 | + TimeSpan.FromMilliseconds((float x) / 1000.0) |
| 77 | + |
| 78 | + DateTimeOffset.UnixEpoch + seconds + usec |
| 79 | + |
| 80 | + override _.ToString() = HexConverter.formatGuid false high low |
| 81 | + |
| 82 | +module private UUIDv7Helpers = |
| 83 | + |
| 84 | + // Figure 4: UUIDv7 Field and Bit Layout - Encoding Example (Microsecond Precision) |
| 85 | + // 0 1 2 3 |
| 86 | + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 |
| 87 | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
| 88 | + // | unixts | |
| 89 | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
| 90 | + // |unixts | usec | ver | usec | |
| 91 | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
| 92 | + // |var| seq | rand | |
| 93 | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
| 94 | + // | rand | |
| 95 | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
| 96 | + |
| 97 | + |
| 98 | + [<Literal>] |
| 99 | + let UuidVer = 0x7000UL |
| 100 | + |
| 101 | + [<Literal>] |
| 102 | + let TSTopMask = 0xFFFFFFFFFFFF0000UL |
| 103 | + |
| 104 | + [<Literal>] |
| 105 | + let TSBottomMask = 0xFFFUL |
| 106 | + |
| 107 | + [<Literal>] |
| 108 | + let SeqInit = 0b1000_0000_0000_0000UL |
| 109 | + |
| 110 | + [<Literal>] |
| 111 | + let SeqMax = 0b1011_1111_1111_1111UL |
| 112 | + |
| 113 | + [<Literal>] |
| 114 | + let RandMask = 0xFFFF_FFFF_FFFFUL |
| 115 | + |
| 116 | + type LowBits(rng: RandomNumberGenerator) = |
| 117 | + let mutable i = SeqInit |
| 118 | + let gate = obj () |
| 119 | + |
| 120 | + let mutable lastTs = 0UL |
| 121 | + let rand = Array.zeroCreate<byte> (8 * 256) |
| 122 | + let mutable randI = 0uy |
| 123 | + |
| 124 | + let fromSeq seqId = |
| 125 | + let span = Span(rand) |
| 126 | + |
| 127 | + if randI = 0uy then |
| 128 | + rng.GetNonZeroBytes(span) |
| 129 | + |
| 130 | + let x = |
| 131 | + (seqId <<< 48) |
| 132 | + ||| ((BitConverter.ToUInt64(span.Slice((int randI) <<< 3, 8))) |
| 133 | + &&& RandMask) |
| 134 | + |
| 135 | + randI <- randI + 1uy |
| 136 | + x |
| 137 | + |
| 138 | + new() = LowBits(RandomNumberGenerator.Create()) |
| 139 | + |
| 140 | + member _.TryNext(timeStamp) = |
| 141 | + lock gate (fun () -> |
| 142 | + if timeStamp <> lastTs then |
| 143 | + lastTs <- timeStamp |
| 144 | + i <- SeqInit |
| 145 | + ValueSome(fromSeq SeqInit) |
| 146 | + else |
| 147 | + match i with |
| 148 | + | SeqMax -> ValueNone |
| 149 | + | x -> |
| 150 | + i <- i + 1UL |
| 151 | + ValueSome(fromSeq x) |
| 152 | + |
| 153 | + ) |
| 154 | + |
| 155 | + type private HighBits() = |
| 156 | + let mutable unixTime = 0UL |
| 157 | + let mutable msTicks = 0L |
| 158 | + |
| 159 | + let updateTime () = |
| 160 | + let now = DateTimeOffset.UtcNow |
| 161 | + unixTime <- uint64 (now.ToUnixTimeSeconds()) <<< 28 |
| 162 | + |
| 163 | + msTicks <- |
| 164 | + TimeSpan |
| 165 | + .FromMilliseconds( |
| 166 | + float now.Millisecond |
| 167 | + ) |
| 168 | + .Ticks |
| 169 | + |
| 170 | + do updateTime () |
| 171 | + |
| 172 | + let usFreq = |
| 173 | + match Stopwatch.Frequency / 1_000_000L with |
| 174 | + | 0L -> |
| 175 | + raise ( |
| 176 | + PlatformNotSupportedException( |
| 177 | + "Stopwatch.Frequency < 1_000_000L. High resolution timer is required." |
| 178 | + ) |
| 179 | + ) |
| 180 | + | usFreq -> usFreq // highestSetBit (uint64 usFreq) |
| 181 | + |
| 182 | + let stopwatch = Stopwatch.StartNew() |
| 183 | + |
| 184 | + member _.Next() = |
| 185 | + let uSecs = uint64 ((msTicks + stopwatch.ElapsedTicks) / usFreq) |
| 186 | + |
| 187 | + let uSecs = |
| 188 | + if uSecs > 10_000_000UL then |
| 189 | + lock stopwatch (fun () -> |
| 190 | + // Correct for tick drift |
| 191 | + updateTime () |
| 192 | + |
| 193 | + stopwatch.Restart() |
| 194 | + uint64 ((msTicks + stopwatch.ElapsedTicks) / usFreq)) |
| 195 | + else |
| 196 | + uSecs |
| 197 | + |
| 198 | + let bottom12 = uSecs &&& TSBottomMask |
| 199 | + let ts = (uSecs <<< 4) + unixTime |
| 200 | + let top48 = ts &&& TSTopMask |
| 201 | + top48 ||| UuidVer ||| bottom12 |
| 202 | + |
| 203 | + let mutable private timestamp = Unchecked.defaultof<_> |
| 204 | + let mutable private clock = Unchecked.defaultof<_> |
| 205 | + |
| 206 | + let rec nextUuid () = |
| 207 | + if obj.ReferenceEquals(timestamp, null) then |
| 208 | + timestamp <- HighBits() |
| 209 | + clock <- LowBits() |
| 210 | + |
| 211 | + let high = timestamp.Next() |
| 212 | + |
| 213 | + match clock.TryNext high with |
| 214 | + | ValueSome low -> UUIDv7(high, low) |
| 215 | + | ValueNone -> nextUuid () |
| 216 | + |
| 217 | +type UUIDv7 with |
| 218 | + static member New() = UUIDv7Helpers.nextUuid () |
0 commit comments