hyperuuid 1.0.0-aarch64-linux-gnu → 1.1.0-aarch64-linux-gnu

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: acf4154ed5298f09e2d529a290ed58aaf28b423ed324514d42284e031a1ce034
4
- data.tar.gz: ceb389657deadaec73fdd68375ee9dfeea621e63647f1d65a8b882171707b71b
3
+ metadata.gz: b9608e59f7992c81542452c00896d1fe2e9b923378578add4657a3ca29f3b3ac
4
+ data.tar.gz: 65c6b2f516f5e324c55039b0631b5893becc8aa15d7f0a02c75aeb164afcfb2b
5
5
  SHA512:
6
- metadata.gz: 8f7dc5e30546568fa1125d5547447c3e21486b63794597873e44948a015b3d26e75753747cbd89b2a172e32a0dd0a590621f591e2ca8a0c85f5d2a0850b202c1
7
- data.tar.gz: dde856853c1fad06e5fcaa6712d9cc82b97174449939ba86aa4daa1f9cd873e84d0b6f59ea262e0b78c522a5caf0bf343f497d98d5db0a9824a1384bd1c1bb14
6
+ metadata.gz: be03eddff6ee0b0f2d2e9cd0526d1dccb1cde03d5d9192c67a14cbf090f6f07ea7bb64fa98dcf9ce51601257354ef6aaf4f6efc8cdb5363030495cf5a5b133b3
7
+ data.tar.gz: 64b5c3aee73b60cd60b3f8056b28541956ceb6817a68811838d16f9256b4de6bfc1db2ffa5daef06894f868571b5ead93a0f348e3e52e1dd0585662150175642
data/README.md CHANGED
@@ -23,15 +23,24 @@ require "hyperuuid"
23
23
  id = HyperUuid.new_v4
24
24
  id2 = HyperUuid.new_v5(HyperUuid::Namespaces::DNS, "example.com")
25
25
  id3 = HyperUuid.new_v7
26
+ puts "v4: #{id}"
27
+ puts "v5: #{id2}"
28
+ puts "v7: #{id3}"
29
+
30
+ puts "timestamp: #{id3.timestamp}" # recover the embedded UTC Time
31
+ # nil instead of raising if id3 isn't an RFC 9562 v7:
32
+ puts "checked timestamp: #{id3.timestamp(raise_on_mismatch: false)}"
33
+ # the RFC 9562 variant and version 7, the guard before trusting its fields:
34
+ puts "is v7: #{id3.rfc?(7)}"
26
35
 
27
- id3.timestamp # recover the embedded UTC Time
28
- id3.timestamp(raise_on_mismatch: false) # nil instead of raising if id3 isn't an RFC 9562 v7
29
- id3.rfc?(7) # the RFC 9562 variant and version 7, the guard before trusting its fields
30
36
  sql = id3.to_sql_order # a version 8 laid out so SQL Server's uniqueidentifier sorts by creation order
31
- sql.timestamp(layout: :sql_server) # read straight from SQL order, no round trip
37
+ puts "sql order: #{sql}"
38
+ puts "sql is v7: #{sql.rfc?(7, layout: :sql_server)}"
39
+ puts "sql timestamp: #{sql.timestamp(layout: :sql_server)}" # read straight from SQL order, no round trip
32
40
 
33
41
  # One native call, one random-bytes fetch, one counter reservation for the whole batch:
34
42
  batch = HyperUuid.new_v7_batch(1000)
43
+ puts "batch: #{batch.length}"
35
44
  ```
36
45
 
37
46
  ## The doors
@@ -107,41 +116,58 @@ one time in 16, and is then converted a second time rather than refused.
107
116
  Send `#to_sql_order`'s result as its raw `#bytes`, not as UUID text. SQL Server stores a UUID
108
117
  string's first three groups byte-reversed, which loses the creation order
109
118
  ([details](https://github.com/SkunkWerkx/HyperUuid#sql-server-ordering)); tiny_tds 3.4 and an ActiveRecord `:uuid` attribute both send text. A binary
110
- literal lands byte for byte:
119
+ literal lands byte for byte (`client` is a connected `TinyTds::Client`):
111
120
 
112
121
  ```ruby
113
122
  sql = HyperUuid.new_v7.to_sql_order
114
123
  client.execute("INSERT INTO orders (id) VALUES (0x#{sql.bytes.unpack1('H*')})").do
115
124
 
116
125
  row = client.execute("SELECT CAST(id AS binary(16)) AS id FROM orders").first
117
- sql = HyperUuid::Uuid.new(row["id"]) # read with layout: :sql_server
126
+ sql = HyperUuid::Uuid.new(row["id"]) # read with layout: :sql_server
118
127
  ```
119
128
 
120
129
  In ActiveRecord, map the column as `:binary`, which quotes as `0x…`. Reading needs the `CAST`
121
130
  too: tiny_tds returns a `uniqueidentifier` as text with those groups reversed.
122
131
 
123
- ### The version 7 batch limit
124
-
125
- `HyperUuid::MAX_V7_BATCH` is 67,108,864 (2²⁶), the size of the counter that orders version 7
126
- UUIDs within a millisecond, and the most one `new_v7_batch` or `new_v7_batch_bytes` call
127
- mints. A larger count is an `ArgumentError` naming the limit, raised before anything is
128
- allocated. Every batch is in strictly increasing order: the counter is one process-wide
129
- sequence, so a batch can straddle the point where it wraps back to 0, and the UUIDs from there
130
- on carry a timestamp one millisecond later than the one supplied rather than sorting before the
131
- ones ahead of them.
132
-
133
- That roll-forward orders one batch, not the stream. The next batch or single `new_v7` in the
134
- same real millisecond starts its counter just past the wrap and carries the supplied timestamp,
135
- so it sorts before the previous batch's tail, which was stamped a millisecond later. Two single
136
- `new_v7` calls either side of the wrap in one millisecond sort in reverse the same way. It
137
- happens at most once per 2²⁶ UUIDs the process mints.
132
+ ### Version 7 ordering and the batch limit
133
+
134
+ Every v7 call, single or batch, reserves from one process-wide stream that is re-seeded at
135
+ random whenever a timestamp moves it forward. A single call consumes one of a millisecond's 2²⁰
136
+ steps and a batch of n consumes ⌈n/64⌉. A batch is always in strictly increasing order. For
137
+ timestamps up to 2⁴⁴ − 2 (the year 2527):
138
+
139
+ - **Ordering.** A call whose timestamp is within 10 s of the newest stream timestamp either
140
+ raises `HyperUuid::CounterExhaustedError` (below) or reserves from the stream, and its UUIDs
141
+ then sort strictly after every UUID from a reservation that completed before it began, on any
142
+ thread. Overlapping reservations are ordered by reservation, which need not be completion
143
+ order.
144
+ - **Embedded timestamp.** At most 11 s ahead of the one supplied: a timestamp up to 11 s behind
145
+ the stream is clamped onto the stream's millisecond, and a millisecond whose steps run out
146
+ splits into the next, so a split's tail carries the millisecond after it (at most 1000 ms
147
+ ahead of the supplied timestamp). An out-of-stream call carries its own timestamp exactly.
148
+ - **Backfill and future-dated calls.** A timestamp more than 11 s behind the stream, or past
149
+ 2⁴⁴ − 2, is minted at itself, outside the stream: it succeeds, leaves the stream alone and is
150
+ ordered against nothing. One call far in the future therefore advances the stream, and until
151
+ the clock catches up, real-time calls are minted out of stream, unordered among themselves.
152
+ - **Exhaustion.** A split that would take the stream more than 1000 ms ahead of the supplied
153
+ timestamp, or past 2⁴⁴ − 2, raises `HyperUuid::CounterExhaustedError`, before anything is
154
+ reserved. A call whose timestamp is past the stream's millisecond succeeds.
155
+ - **Batch size.** `HyperUuid::MAX_V7_BATCH` is 67,108,864 (2²⁶), the most one `new_v7_batch` or
156
+ `new_v7_batch_bytes` call mints. A larger count is an `ArgumentError` naming the limit,
157
+ raised before anything is allocated.
138
158
 
139
159
  ### Errors
140
160
 
141
- Two exceptions describe a failed call, and both carry the same message on every backend:
161
+ Three exceptions describe a failed call, and each carries the same message on every backend:
142
162
 
143
- - `HyperUuid::TimestampOutOfRangeError` — the time cannot be embedded in a version 7: past
144
- its 48-bit millisecond field, or negative (a `Time` before the Unix epoch included).
163
+ - `HyperUuid::TimestampOutOfRangeError` — the time cannot be embedded in a version 7: after
164
+ 9999-12-31T23:59:59.999Z (253,402,300,799,999 ms, the generation cap; the 48-bit field itself
165
+ reaches 10889, and `#timestamp` reads a version 7 another generator dated past the cap as it
166
+ reads a version 6: no timestamp), or negative (a `Time` before the Unix epoch included).
167
+ - `HyperUuid::CounterExhaustedError` — the version 7 stream cannot advance without running
168
+ more than 1000 ms ahead of the supplied time or past its last millisecond (see
169
+ [Version 7 ordering and the batch limit](#version-7-ordering-and-the-batch-limit)). Nothing is
170
+ reserved.
145
171
  - `HyperUuid::RandomSourceError` — the operating system's random source failed.
146
172
 
147
173
  Everything else is a caller bug and raises Ruby's own error for it, checked once in the shared
@@ -188,10 +214,10 @@ The honest trade-off: this gem is native code, not pure Ruby — a precompiled e
188
214
 
189
215
  ```ruby
190
216
  bytes = HyperUuid.new_v7_batch_bytes(1000)
191
- first = bytes[0, 16] # ready for a BINARY(16) bind parameter
217
+ first = bytes[0, 16] # ready for a BINARY(16) bind parameter
192
218
  ```
193
219
 
194
- **About 41x faster than `new_v7_batch`** for a 1000-UUID batch (4.9 µs versus 202 µs). The native call is identical — the difference is that `new_v7_batch` then allocates a `Uuid` object and its byte Strings for every item on top of it. This hands back the bytes the native core already produced, untouched.
220
+ **About 41x faster than `new_v7_batch`** for a 1000-UUID batch (4.9 µs versus 202 µs). The native call is identical — the difference is that `new_v7_batch` then allocates a `Uuid` object and its byte Strings for every item on top of it. This hands back the bytes the native core already produced, untouched. (Measured before the per-millisecond re-seed, core change, 2026-10-27.)
195
221
 
196
222
  The catch, and it inverts the advice: **if you need `Uuid` objects, keep using `new_v7_batch`.** Slicing these bytes into objects yourself just relocates the identical allocations into your own code, and measures no better — sometimes worse. Reach for the byte form only when bytes are the destination: a bind parameter, a wire format, a bulk load.
197
223
 
@@ -209,24 +235,28 @@ Real numbers, `benchmark-ips` on Ruby 4.0.7, linux-x64 on an Intel Core i9-11900
209
235
  |---|---:|---:|
210
236
  | `SecureRandom.uuid` | 943,917 | baseline |
211
237
  | `HyperUuid.new_v4` | 5,236,000 | **5.5x faster** |
212
- | `HyperUuid.new_v7` (explicit ms) | 3,785,000 | **4.0x faster** |
213
- | `HyperUuid.new_v7` (current time) | 3,574,000 | **3.8x faster** |
238
+ | `HyperUuid.new_v7` (explicit ms) † | 3,785,000 | **4.0x faster** |
239
+ | `HyperUuid.new_v7` (current time) † | 3,574,000 | **3.8x faster** |
214
240
  | `HyperUuid.new_v5` | 1,942,000 | 2.1x faster |
215
241
 
242
+ † Measured before the per-millisecond re-seed (core change, 2026-10-27).
243
+
216
244
  A `HyperUuid.new_v4` — real entropy, correct version/variant bits, minted by the shared Rust core — costs less than a fifth of what `SecureRandom.uuid` does, because the Magnus extension is an ordinary native method call with nothing marshalled around it.
217
245
 
218
- The "current time" rows deserve a footnote, because they will not look like this everywhere. Here they land beside the explicit-ms rows: the only difference between the two is one `Process.clock_gettime(CLOCK_REALTIME)` wall-clock read, and on this machine that read is too cheap to see. On a machine with a slow clock it is the whole story — where a virtualized clock defeats the vDSO fast path the same read costs ~1µs, which puts both current-time rows at parity with `SecureRandom.uuid` while the explicit-ms rows stay well ahead of it. `SecureRandom.uuid` never reads a clock — random v4 is the only thing it does. If your clock is slow and you are minting many, read it once and pass the timestamp, or use the batch doors.
246
+ The "current time" rows deserve a footnote, because they will not look like this everywhere. Here they land beside the explicit-ms rows: the only difference between the two is one `Process.clock_gettime(CLOCK_REALTIME)` wall-clock read, and on this machine that read is too cheap to see. On a machine with a slow clock it is the whole story — where a virtualized clock defeats the vDSO fast path the same read costs ~1µs, which puts both current-time rows at parity with `SecureRandom.uuid` while the explicit-ms rows stay well ahead of it. `SecureRandom.uuid` never reads a clock — random v4 is the only thing it does. If your clock is slow and you are minting many, read it once and pass the timestamp, or use the batch doors. (Measured before the per-millisecond re-seed, core change, 2026-10-27.)
219
247
 
220
- The Fiddle fallback (`HYPERUUID_PURE=1`, and any platform without a prebuilt extension) keeps its own diet — a reused thread-local scratch buffer instead of two GC-finalizer-registering mallocs per call, zero-copy `String` passes for read-only inputs, an unsynchronized fast path past the load mutex — landing at 1.16x slower than `SecureRandom.uuid` for v4 (1.24 µs against 1.07 µs in its own run) and about 1.3x slower for v7, with the same structural story as before: `Fiddle`'s interpreted marshalling is the floor, and the batch doors are how you amortize it.
248
+ The Fiddle fallback (`HYPERUUID_PURE=1`, and any platform without a prebuilt extension) keeps its own diet — a reused thread-local scratch buffer instead of two GC-finalizer-registering mallocs per call, zero-copy `String` passes for read-only inputs, an unsynchronized fast path past the load mutex — landing at 1.16x slower than `SecureRandom.uuid` for v4 (1.24 µs against 1.07 µs in its own run) and about 1.3x slower for v7, with the same structural story as before: `Fiddle`'s interpreted marshalling is the floor, and the batch doors are how you amortize it. (Measured before the per-millisecond re-seed, core change, 2026-10-27.)
221
249
 
222
250
  Batch generation still amortizes per-call cost on both backends — one native call for the whole batch:
223
251
 
224
252
  | Call | i/s (Magnus backend) |
225
253
  |---|---:|
226
- | `new_v7` × 1000 (individual) | 3,742 |
227
- | `new_v7_batch(1000)` | 4,850 (**1.3x**) |
254
+ | `new_v7` × 1000 (individual) † | 3,742 |
255
+ | `new_v7_batch(1000)` † | 4,850 (**1.3x**) |
256
+
257
+ † Measured before the per-millisecond re-seed (core change, 2026-10-27).
228
258
 
229
- The multiplier is small on this backend for the best reason available: the individual calls are cheap, so there is little waste left to amortize, and what `new_v7_batch` spends its 206 µs on (in this table's run; 202 µs in the bytes-versus-objects run above) is building a thousand `Uuid` objects — the byte form above does the same native work in 4.9 µs. On the Fiddle backend, where each call costs 1.4 µs, the same batch is 6.7x the loop. If you need v5/v7, need many at once, or need this Ruby service's IDs to agree byte-for-byte with a Go or Python service's, that's what this gem is for — and now it's the fast option too, not just the capable one.
259
+ The multiplier is small on this backend for the best reason available: the individual calls are cheap, so there is little waste left to amortize, and what `new_v7_batch` spends its 206 µs on (in this table's run; 202 µs in the bytes-versus-objects run above) is building a thousand `Uuid` objects — the byte form above does the same native work in 4.9 µs. On the Fiddle backend, where each call costs 1.4 µs, the same batch is 6.7x the loop. If you need v5/v7, need many at once, or need this Ruby service's IDs to agree byte-for-byte with a Go or Python service's, that's what this gem is for — and now it's the fast option too, not just the capable one. (Measured before the per-millisecond re-seed, core change, 2026-10-27.)
230
260
 
231
261
  ## Backends
232
262
 
@@ -280,7 +310,7 @@ gh attestation verify hyperuuid-<version>-<platform>.gem --repo SkunkWerkx/Hyper
280
310
  That is the release's second layer of checking, not the only one: before any gem is built, the
281
311
  same job verifies every native binary it packs — the Fiddle libraries, the Magnus extensions
282
312
  (one per Ruby ABI per platform), the `wasm32-wasip1` archives — against *their own* build
283
- provenance, signed from `SkunkWerkx/.github` by `hyper-build-native.yml` and
313
+ provenance, signed from `SkunkWerkx/.github` by `hyper-core.yml`, `hyper-test-ruby.yml` and
284
314
  `hyper-build-wasm.yml` (so that check needs `--signer-repo SkunkWerkx/.github` added), and
285
315
  refuses to proceed on an unverified one. RubyGems.org has no unpublish and no
286
316
  duplicate-version overwrite, so all of this happens while a bad artifact is still reversible,
@@ -320,7 +350,7 @@ one at `require` time:
320
350
  | 3.4 (until its EOL 2028-03-31) | Magnus, `BACKEND == :native` | platform |
321
351
  | 3.3 (the floor, until its EOL 2027-03-31) | Fiddle | universal |
322
352
 
323
- What stands behind each cell: CI runs the whole suite through each Magnus extension — Ruby 4.0 on every pull request and Ruby 3.4 in the weekly and release builds, on all seven platform-gem platforms, the two musl ones inside each
353
+ What stands behind each cell: CI runs the whole suite through each Magnus extension — Ruby 4.0 on every pull request that touches the gem, the core or the workflows (after the gem's lint passed once, on Linux arm64), and Ruby 3.4 in the weekly and release builds, which run everything — on all seven platform-gem platforms, the two musl ones inside each
324
354
  Ruby's own Alpine image — and the Fiddle suite on Ruby 4.0 on every one of them, plus on Ruby
325
355
  3.3 inside Alpine. Intel macOS has no CI leg: its library is cross-built and tested at the
326
356
  core, and Ruby there runs the universal gem's Fiddle backend over it. Anywhere else — a platform with no native build at
@@ -363,7 +393,7 @@ Fiddle fallback costs the most: measured on win-x64, Ruby 3.4, the Magnus backen
363
393
  `new_v4` in 406ns against Fiddle's 2407ns (**5.9x**) and `new_v7` in 595ns against 2759ns
364
394
  (**4.6x**) — a far wider gap than any Linux or macOS leg shows — and on real Windows-on-ARM
365
395
  hardware, Ruby 4.0.6, `new_v4` in 416ns against Fiddle's 2299ns (**5.5x**) and `new_v7` in
366
- 621ns against 2474ns (**4.0x**), the same shape. Both extensions are built for the `gnullvm`
396
+ 621ns against 2474ns (**4.0x**), the same shape (the `new_v7` figures measured before the per-millisecond re-seed, core change, 2026-10-27). Both extensions are built for the `gnullvm`
367
397
  Rust targets rather than `gnu` — the same mingw-w64/UCRT ABI RubyInstaller's Ruby uses,
368
398
  linked with LLVM and compiler-rt instead of GCC and a statically-linked libgcc, which is what
369
399
  keeps the shipped extension small. The build script, and the two flags that are load-bearing
@@ -422,10 +452,11 @@ which read the clock through WASI.
422
452
  rename them to names of their own when CI builds the archive, which also fails if a newer
423
453
  Rust starts exporting another.
424
454
 
425
- What stands behind it: CI builds each minor's archive from the commit, packs the gem the way
426
- it ships, links it into a fresh interpreter and runs [`wasm-smoke/test.rb`](wasm-smoke/test.rb)
427
- under Node and in headless Chrome (the forge's `hyper-build-wasm.yml`). The published gem is
428
- packed around those attested archives.
455
+ What stands behind it: in the weekly and release builds (not on pull requests), CI builds each
456
+ minor's archive from the commit, packs the gem the way it ships, links it into a fresh
457
+ interpreter and runs [`wasm-smoke/test.rb`](wasm-smoke/test.rb) under Node and in headless
458
+ Chrome (the forge's `hyper-build-wasm.yml`). The published gem is packed around those attested
459
+ archives.
429
460
 
430
461
  ## Development
431
462
 
Binary file
Binary file
@@ -4,8 +4,14 @@ module HyperUuid
4
4
  # arguments are valid. Carries the same message on every backend.
5
5
  class RandomSourceError < StandardError; end
6
6
 
7
- # Raised when a timestamp cannot be embedded in the version 7 UUID being minted: one past
8
- # RFC 9562's 48-bit millisecond field, or a negative one (a `Time` before the Unix epoch
9
- # included). Carries the same message on every backend.
7
+ # Raised when a timestamp cannot be embedded in the version 7 UUID being minted: one after
8
+ # 9999-12-31T23:59:59.999Z (253402300799999 ms, the generation cap; the 48-bit field itself
9
+ # reaches 10889), or a negative one (a `Time` before the Unix epoch included). Carries the
10
+ # same message on every backend.
10
11
  class TimestampOutOfRangeError < StandardError; end
12
+
13
+ # Raised when the version 7 stream cannot advance without running more than 1000 ms ahead of
14
+ # the supplied timestamp or past its last millisecond (2**44 - 2). Nothing is reserved.
15
+ # Carries the same message on every backend.
16
+ class CounterExhaustedError < StandardError; end
11
17
  end
@@ -18,7 +18,7 @@ module HyperUuid
18
18
  # One copy of the out-of-range message for the Fiddle backend and for the doors' own
19
19
  # range check in hyperuuid.rb; the Magnus extension (rust/src/ruby_ext.rs) carries the
20
20
  # same text, and spec/native_backend_spec.rb pins that the two agree.
21
- V7_TIMESTAMP_OUT_OF_RANGE = "unix_millis must fit within the RFC 9562 48-bit field"
21
+ V7_TIMESTAMP_OUT_OF_RANGE = "unix_millis must be between 0 and 253402300799999 (9999-12-31T23:59:59.999Z)"
22
22
 
23
23
  # The two batch-size refusals, shared the same way: a version 7 batch past the 26-bit
24
24
  # counter space (the core's return code 4, which the doors in hyperuuid.rb refuse before
@@ -28,6 +28,12 @@ module HyperUuid
28
28
  V7_BATCH_TOO_LARGE = "a single version 7 batch takes at most 67108864 UUIDs (the 26-bit counter space)"
29
29
  BATCH_TOO_LARGE_TO_ADDRESS = "the batch is too large to address on this platform"
30
30
 
31
+ # The version 7 stream's refusal (the core's return code 6): it cannot advance without
32
+ # running more than 1000 ms ahead of the supplied timestamp or past its last millisecond.
33
+ # The Magnus extension carries the same text, and spec/counter_exhaustion_spec.rb pins it.
34
+ V7_COUNTER_EXHAUSTED = "version 7 counter exhausted: the stream cannot advance without running more " \
35
+ "than 1000 ms ahead of the supplied timestamp or past its last millisecond"
36
+
31
37
  # The SQL Server order conversions' one refusal, in the core's own words (the Magnus
32
38
  # extension raises its SqlOrderError's text): a value that isn't a UUID of the
33
39
  # conversion's version (code 5), an ArgumentError. Every version 7 converts.
@@ -68,6 +74,7 @@ module HyperUuid
68
74
  case rc
69
75
  when 0 then out[0, 16]
70
76
  when 2 then raise TimestampOutOfRangeError, V7_TIMESTAMP_OUT_OF_RANGE
77
+ when 6 then raise CounterExhaustedError, V7_COUNTER_EXHAUSTED
71
78
  else raise random_source_failure("uuid_new_v7")
72
79
  end
73
80
  end
@@ -89,6 +96,7 @@ module HyperUuid
89
96
  when 2 then raise TimestampOutOfRangeError, V7_TIMESTAMP_OUT_OF_RANGE
90
97
  when 3 then raise ArgumentError, BATCH_TOO_LARGE_TO_ADDRESS
91
98
  when 4 then raise ArgumentError, V7_BATCH_TOO_LARGE
99
+ when 6 then raise CounterExhaustedError, V7_COUNTER_EXHAUSTED
92
100
  else raise random_source_failure("uuid_new_v7_batch")
93
101
  end
94
102
  end
@@ -128,7 +128,8 @@ module HyperUuid
128
128
  # variant as well as the version: a 7 in the version nibble under any variant but
129
129
  # +:rfc9562+ is not a version 7 UUID, so it has no timestamp (#rfc? is the same check on
130
130
  # its own). Every other version has none either, a version 6 or 1 from another generator
131
- # included.
131
+ # included, and nor has a version 7 dated past 9999-12-31T23:59:59.999Z, the generation
132
+ # cap: the 48-bit field reaches 10889, and only Runtime.v7_unix_millis reads it that far.
132
133
  #
133
134
  # Raises by default for anything that isn't an RFC 9562 version 7 UUID; pass
134
135
  # `raise_on_mismatch: false` to get `nil` back instead — for a caller that doesn't already
data/lib/hyperuuid.rb CHANGED
@@ -19,7 +19,7 @@ require_relative "hyperuuid/runtime"
19
19
  module HyperUuid
20
20
  # This gem's own version — distinct from the RFC 9562 UUID *versions* (v4/v5/v7) the rest
21
21
  # of this module generates.
22
- VERSION = "1.0.0"
22
+ VERSION = "1.1.0"
23
23
 
24
24
  # The widest batch count, and the longest v5 name in bytes, the native ABI carries (a u32).
25
25
  # (The millisecond count is a u64; unix_millis_from refuses anything wider.)
@@ -27,17 +27,8 @@ module HyperUuid
27
27
  private_constant :U32_MAX
28
28
 
29
29
  # The most UUIDs one version 7 batch (#new_v7_batch, #new_v7_batch_bytes) mints:
30
- # 67,108,864, the size of the 26-bit counter that orders UUIDs within a millisecond. A larger
31
- # count is refused with ArgumentError before anything is allocated.
32
- #
33
- # Every batch up to this size is in strictly increasing order: the counter is one
34
- # process-wide sequence, so a batch can straddle the point where it wraps back to 0, and the
35
- # UUIDs from there on carry a timestamp one millisecond later than the one supplied.
36
- # The roll-forward orders one batch, not the stream: the next batch or single #new_v7 in
37
- # the same real millisecond starts its counter just past the wrap and carries the supplied
38
- # timestamp, so it sorts before this batch's tail, which was stamped a millisecond later.
39
- # Two single #new_v7 calls either side of the wrap in one millisecond sort in reverse the
40
- # same way. It happens at most once per 2**26 UUIDs the process mints.
30
+ # 67,108,864. A larger count is refused with ArgumentError before anything is allocated.
31
+ # See #new_v7_batch for the ordering a batch carries.
41
32
  MAX_V7_BATCH = Runtime::MAX_V7_BATCH
42
33
 
43
34
  # Creates a random UUID version 4 (RFC 9562 §5.4).
@@ -103,33 +94,40 @@ module HyperUuid
103
94
  end
104
95
 
105
96
  # Creates a time-sortable UUID version 7 (RFC 9562 §6.2). Defaults to the current time; pass
106
- # an explicit `Time` or Unix-epoch millisecond integer (non-negative, fitting in 48 bits) to
107
- # embed a specific time instead.
97
+ # an explicit `Time` or Unix-epoch millisecond integer (non-negative, at most 253402300799999:
98
+ # 9999-12-31T23:59:59.999Z, the generation cap) to embed a specific time instead.
99
+ #
100
+ # Every call reserves the next step of one process-wide stream: see #new_v7_batch for the
101
+ # ordering and the embedded timestamp.
108
102
  #
109
- # @raise [TimestampOutOfRangeError] if the time is negative or past the 48-bit v7 field.
103
+ # @raise [TimestampOutOfRangeError] if the time is before 1970 or after 9999-12-31T23:59:59.999Z.
104
+ # @raise [CounterExhaustedError] if the stream cannot advance without running more than
105
+ # 1000 ms ahead of the supplied time or past its last millisecond; nothing is reserved.
110
106
  # @raise [TypeError] if +unix_millis+ isn't a Time, an Integer or nil.
111
107
  def self.new_v7(unix_millis = nil)
112
108
  Uuid.new(Runtime.new_v7(unix_millis_from(unix_millis, Runtime::V7_TIMESTAMP_OUT_OF_RANGE)), true)
113
109
  end
114
110
 
115
111
  # Creates `count` time-sortable version 7 UUIDs sharing one timestamp capture and one
116
- # contiguous block of the monotonic counter — one FFI call and one random-bytes fetch
112
+ # contiguous block of the v7 stream — one FFI call and one random-bytes fetch
117
113
  # instead of `count` of each. Defaults to the current time; pass an explicit `Time` or
118
114
  # Unix-epoch millisecond integer to embed a specific time instead.
119
115
  #
120
- # Every batch is in strictly increasing order. The counter is one process-wide sequence, so
121
- # a batch can straddle the point where it wraps back to 0; the UUIDs from there on carry a
122
- # timestamp one millisecond later than the one supplied rather than sorting before the ones
123
- # ahead of them. That is also why one batch takes at most MAX_V7_BATCH.
116
+ # A batch is always in strictly increasing order, and takes at most MAX_V7_BATCH.
124
117
  #
125
- # The roll-forward orders one batch, not the stream: the next batch or single #new_v7 in
126
- # the same real millisecond starts its counter just past the wrap and carries the supplied
127
- # timestamp, so it sorts before this batch's tail, which was stamped a millisecond later.
128
- # Two single #new_v7 calls either side of the wrap in one millisecond sort in reverse the
129
- # same way. It happens at most once per 2**26 UUIDs the process mints.
118
+ # Every call reserves from one process-wide stream, re-seeded at random whenever a timestamp
119
+ # moves it forward: a single call consumes one of a millisecond's 2**20 steps and a batch of
120
+ # n consumes ceil(n / 64). For timestamps up to 2**44 - 2 (the year 2527), a call whose
121
+ # timestamp is within 10 s of the newest stream timestamp either raises CounterExhaustedError
122
+ # or sorts strictly after every UUID from a reservation that completed before it began. The
123
+ # embedded timestamp is at most 11 s ahead of the one supplied, and a split's tail carries the
124
+ # millisecond after it (at most 1000 ms ahead). A timestamp more than 11 s behind the stream,
125
+ # or past 2**44 - 2, is minted at itself, outside the stream and ordered against nothing.
130
126
  #
131
127
  # @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and MAX_V7_BATCH.
132
- # @raise [TimestampOutOfRangeError] if the time is negative or past the 48-bit v7 field.
128
+ # @raise [TimestampOutOfRangeError] if the time is before 1970 or after 9999-12-31T23:59:59.999Z.
129
+ # @raise [CounterExhaustedError] if the stream cannot advance without running more than
130
+ # 1000 ms ahead of the supplied time or past its last millisecond; nothing is reserved.
133
131
  def self.new_v7_batch(count, unix_millis = nil)
134
132
  bytes = new_v7_batch_bytes(count, unix_millis)
135
133
  Array.new(count) { |i| Uuid.new(bytes[i * 16, 16], true) }
@@ -149,10 +147,12 @@ module HyperUuid
149
147
  # String into them yourself just moves the same allocations into your own code.
150
148
  #
151
149
  # Slice it with `bytes[i * 16, 16]`, which is what #new_v7_batch does internally. The
152
- # order, the possible extra millisecond and the MAX_V7_BATCH limit are #new_v7_batch's.
150
+ # ordering, embedded timestamp and the MAX_V7_BATCH limit are #new_v7_batch's.
153
151
  #
154
152
  # @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and MAX_V7_BATCH.
155
- # @raise [TimestampOutOfRangeError] if the time is negative or past the 48-bit v7 field.
153
+ # @raise [TimestampOutOfRangeError] if the time is before 1970 or after 9999-12-31T23:59:59.999Z.
154
+ # @raise [CounterExhaustedError] if the stream cannot advance without running more than
155
+ # 1000 ms ahead of the supplied time or past its last millisecond; nothing is reserved.
156
156
  def self.new_v7_batch_bytes(count, unix_millis = nil)
157
157
  # First, so any count past the limit (2**32 and beyond included) names the limit itself.
158
158
  raise ArgumentError, Runtime::V7_BATCH_TOO_LARGE if count.is_a?(Integer) && count > MAX_V7_BATCH
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hyperuuid
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: aarch64-linux-gnu
6
6
  authors:
7
7
  - Brian Buvinghausen