hyperuuid 0.9.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: 50f08d061f0796dfb71947c4f6efffc233fd2e5927d6faf5930e975118421cbd
4
- data.tar.gz: 5aef737b6fdc53c9f44b2899f4963602f7d3407888c7ca0fa8238072e4c75243
3
+ metadata.gz: b9608e59f7992c81542452c00896d1fe2e9b923378578add4657a3ca29f3b3ac
4
+ data.tar.gz: 65c6b2f516f5e324c55039b0631b5893becc8aa15d7f0a02c75aeb164afcfb2b
5
5
  SHA512:
6
- metadata.gz: 5668103ef23583cd0e5b8b570f6a742776ff0bff860dfe88a69079f55a3147efd1fafd203d1d0aad9b3570cf67fd70034ae791aaee2252194b9cde449fe1931d
7
- data.tar.gz: aca9001c3f265e8f930276b072139eb7bd8ee2b552cedd0c90f9fe26309c2a48b8fa0c8bd5c4bc831e8bd1acd82e2db23e013fcf14c0dcfb46d726c80e3fdfb4
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,54 +116,68 @@ 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 are this gem's own, 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
- Through 0.3.0 these were reachable only as `HyperUuid::Runtime::TimestampOutOfRangeError` and
148
- `HyperUuid::Runtime::RandomSourceError`; those names remain, as aliases of the same classes.
149
-
150
173
  Everything else is a caller bug and raises Ruby's own error for it, checked once in the shared
151
174
  doors rather than left to whichever backend is live: a `TypeError` for a time that is not a
152
175
  `Time`, an Integer or `nil`, for a count that is not an Integer, for a namespace that is not a
153
176
  `Uuid` or a name that is not a `String`, for a version passed to `#rfc?` that is not an
154
- Integer; an `ArgumentError` for a count outside 0..`MAX_V7_BATCH`, for a layout other than
155
- `:rfc9562`/`:sql_server`, and for a batch too large for the platform to address (a 32-bit
156
- target such as ruby.wasm). The SQL Server conversions' one refusal (a value of the wrong
157
- version) comes from the core, as an `ArgumentError` worded the same on both backends.
177
+ Integer; an `ArgumentError` for a count outside 0..`MAX_V7_BATCH` and for a layout other than
178
+ `:rfc9562`/`:sql_server`. Two refusals come from the backend instead, each an `ArgumentError`
179
+ worded the same on both: a batch too large for the platform to address (a 32-bit target such
180
+ as ruby.wasm), and the SQL Server conversions' one refusal, a value of the wrong version.
158
181
 
159
182
  ### Is the native core there?
160
183
 
@@ -191,10 +214,10 @@ The honest trade-off: this gem is native code, not pure Ruby — a precompiled e
191
214
 
192
215
  ```ruby
193
216
  bytes = HyperUuid.new_v7_batch_bytes(1000)
194
- first = bytes[0, 16] # ready for a BINARY(16) bind parameter
217
+ first = bytes[0, 16] # ready for a BINARY(16) bind parameter
195
218
  ```
196
219
 
197
- **About 40x 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.)
198
221
 
199
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.
200
223
 
@@ -212,24 +235,28 @@ Real numbers, `benchmark-ips` on Ruby 4.0.7, linux-x64 on an Intel Core i9-11900
212
235
  |---|---:|---:|
213
236
  | `SecureRandom.uuid` | 943,917 | baseline |
214
237
  | `HyperUuid.new_v4` | 5,236,000 | **5.5x faster** |
215
- | `HyperUuid.new_v7` (explicit ms) | 3,785,000 | **4.0x faster** |
216
- | `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** |
217
240
  | `HyperUuid.new_v5` | 1,942,000 | 2.1x faster |
218
241
 
242
+ † Measured before the per-millisecond re-seed (core change, 2026-10-27).
243
+
219
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.
220
245
 
221
- 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.)
222
247
 
223
- 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.)
224
249
 
225
250
  Batch generation still amortizes per-call cost on both backends — one native call for the whole batch:
226
251
 
227
252
  | Call | i/s (Magnus backend) |
228
253
  |---|---:|
229
- | `new_v7` × 1000 (individual) | 3,742 |
230
- | `new_v7_batch(1000)` | 4,850 (**1.3x**) |
254
+ | `new_v7` × 1000 (individual) † | 3,742 |
255
+ | `new_v7_batch(1000)` † | 4,850 (**1.3x**) |
231
256
 
232
- 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 is building a thousand `Uuid` objects — the byte form above does the same native work in 5 µ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.
257
+ † Measured before the per-millisecond re-seed (core change, 2026-10-27).
258
+
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.)
233
260
 
234
261
  ## Backends
235
262
 
@@ -256,34 +283,42 @@ backend where Ruby threads run the core truly in parallel — each through its o
256
283
  buffer, with the core's v7 counter shared and atomic. `spec/hyperuuid_spec.rb`'s "concurrent
257
284
  callers" examples run under both.
258
285
 
259
- **Ractors.** Main Ractor only, on every backend: called from another Ractor the doors raise
260
- `Ractor::UnsafeError`.
286
+ **Ractors.** Main Ractor only, on every backend: called from another Ractor the doors raise a
287
+ `Ractor::Error` (`Ractor::IsolationError`, or `Ractor::UnsafeError`, by door and backend).
261
288
 
262
289
  ## Verifying provenance
263
290
 
264
- Every gem RubyGems.org serves — the universal fallback, each of the seven precompiled
265
- platform gems and `hyperuuid-wasm` — carries its own GitHub build-provenance attestation, signed directly by
266
- this repo's own `release.yml` (the `rubygems-publish` job attests `ruby/pkg/*.gem` right
267
- before the push), so plain `--repo` verifies any of them:
291
+ Every gem RubyGems.org serves — the universal fallback, each of the seven precompiled platform
292
+ gems and `hyperuuid-wasm` — verifies from the release tag alone: the release attestation,
293
+ signed by this repository's `release.yml` at the tag, names every one of them, with the
294
+ release inventory as its predicate.
295
+
296
+ ```sh
297
+ gem fetch hyperuuid -v <version> --platform <platform> # or omit --platform for the universal gem
298
+ gh attestation verify hyperuuid-<version>-<platform>.gem --repo SkunkWerkx/HyperUuid \
299
+ --source-ref refs/tags/v<version> --predicate-type https://github.com/SkunkWerkx/.github/predicates/release/v1
300
+ ```
301
+
302
+ Each gem's build provenance is this repository's too: the `ruby-pack` job attests
303
+ `ruby/pkg/*.gem` as soon as they are built and `rubygems-publish` pushes those preserved
304
+ bytes, so plain `--repo` verifies it:
268
305
 
269
306
  ```sh
270
- gem fetch hyperuuid -v X.Y.Z --platform <platform> # or omit --platform for the universal gem
271
- gh attestation verify hyperuuid-X.Y.Z-<platform>.gem --repo SkunkWerkx/HyperUuid
307
+ gh attestation verify hyperuuid-<version>-<platform>.gem --repo SkunkWerkx/HyperUuid
272
308
  ```
273
309
 
274
- That's the release's second layer of checking, not the only one: before any gem gets built,
275
- the same job verifies every native binary it packs — the Fiddle libraries, the Magnus
276
- extensions (one per Ruby ABI per platform), the `wasm32-wasip1` archives — against *their own*
277
- attestations — those are signed from `SkunkWerkx/.github` by `hyper-build-native.yml` and
278
- `hyper-build-wasm.yml`, so
279
- that check needs `--signer-repo SkunkWerkx/.github` added — and refuses to proceed on an
280
- unverified one. RubyGems.org has no unpublish and no
281
- duplicate-version overwrite, so this all happens while a bad artifact is still reversible.
282
- The release run's job summary then re-fetches every gem from the CDN and records
283
- attested-vs-served digests, turning "rubygems.org stores an upload verbatim" into a
284
- per-release measurement rather than an assumption — see
285
- [csharp/README.md's provenance section](../csharp/README.md#native-binary-provenance) for
286
- more on why `--signer-repo` is needed for some artifacts here and not others.
310
+ That is the release's second layer of checking, not the only one: before any gem is built, the
311
+ same job verifies every native binary it packs — the Fiddle libraries, the Magnus extensions
312
+ (one per Ruby ABI per platform), the `wasm32-wasip1` archives — against *their own* build
313
+ provenance, signed from `SkunkWerkx/.github` by `hyper-core.yml`, `hyper-test-ruby.yml` and
314
+ `hyper-build-wasm.yml` (so that check needs `--signer-repo SkunkWerkx/.github` added), and
315
+ refuses to proceed on an unverified one. RubyGems.org has no unpublish and no
316
+ duplicate-version overwrite, so all of this happens while a bad artifact is still reversible,
317
+ and the push itself is a separate dispatch from the preserved bytes. The release run then
318
+ re-fetches every gem from rubygems.org and fails unless each matches the attested bytes
319
+ exactly, turning "rubygems.org stores an upload verbatim" into a per-release check rather than
320
+ an assumption. The root README's [Provenance](../README.md#provenance) section has the whole
321
+ picture.
287
322
 
288
323
  ## Install
289
324
 
@@ -315,7 +350,7 @@ one at `require` time:
315
350
  | 3.4 (until its EOL 2028-03-31) | Magnus, `BACKEND == :native` | platform |
316
351
  | 3.3 (the floor, until its EOL 2027-03-31) | Fiddle | universal |
317
352
 
318
- 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
319
354
  Ruby's own Alpine image — and the Fiddle suite on Ruby 4.0 on every one of them, plus on Ruby
320
355
  3.3 inside Alpine. Intel macOS has no CI leg: its library is cross-built and tested at the
321
356
  core, and Ruby there runs the universal gem's Fiddle backend over it. Anywhere else — a platform with no native build at
@@ -358,7 +393,7 @@ Fiddle fallback costs the most: measured on win-x64, Ruby 3.4, the Magnus backen
358
393
  `new_v4` in 406ns against Fiddle's 2407ns (**5.9x**) and `new_v7` in 595ns against 2759ns
359
394
  (**4.6x**) — a far wider gap than any Linux or macOS leg shows — and on real Windows-on-ARM
360
395
  hardware, Ruby 4.0.6, `new_v4` in 416ns against Fiddle's 2299ns (**5.5x**) and `new_v7` in
361
- 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`
362
397
  Rust targets rather than `gnu` — the same mingw-w64/UCRT ABI RubyInstaller's Ruby uses,
363
398
  linked with LLVM and compiler-rt instead of GCC and a statically-linked libgcc, which is what
364
399
  keeps the shipped extension small. The build script, and the two flags that are load-bearing
@@ -401,6 +436,12 @@ which read the clock through WASI.
401
436
  - **No Rust toolchain.** The gem's `extconf.rb` only hands rbwasm the prebuilt archive, and
402
437
  rbwasm downloads its own wasi-sdk. The first `rbwasm build` compiles Ruby itself and takes
403
438
  15–20 minutes; later builds reuse it.
439
+ - **macOS with Homebrew: limit pkg-config to `openssl@3`.** Run the build as
440
+ `PKG_CONFIG_LIBDIR="$(brew --prefix openssl@3)/lib/pkgconfig" bundle exec rbwasm build …`.
441
+ Without it Homebrew's pkg-config files reach into the build twice: its `openssl` is OpenSSL
442
+ 4, which the native Ruby rbwasm builds first does not compile against, and its `libyaml`
443
+ is a macOS library that psych then links into the wasm interpreter, which fails for Ruby
444
+ 4.0.
404
445
  - **Static linking only,** into ruby.wasm's default `wasm32-unknown-wasip1` interpreter.
405
446
  rbwasm's dynamic-linking build (a pic target, for the component model) is not supported.
406
447
  - **Size.** rbwasm packs every gem's files into the interpreter, the archives included, so the
@@ -411,10 +452,11 @@ which read the clock through WASI.
411
452
  rename them to names of their own when CI builds the archive, which also fails if a newer
412
453
  Rust starts exporting another.
413
454
 
414
- What stands behind it: CI builds each minor's archive from the commit, packs the gem the way
415
- it ships, links it into a fresh interpreter and runs [`wasm-smoke/test.rb`](wasm-smoke/test.rb)
416
- under Node and in headless Chrome (the forge's `hyper-build-wasm.yml`). The published gem is
417
- 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.
418
460
 
419
461
  ## Development
420
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
@@ -15,17 +15,10 @@ module HyperUuid
15
15
  # A Ruby gem's files are plain files on disk once installed, so native/{rid}/{lib} can be
16
16
  # dlopen'd directly, with no extraction step.
17
17
  module Runtime
18
- # The package's two exceptions live on HyperUuid itself (errors.rb). These are the names
19
- # they had through 0.3.0, kept as aliases of the very same classes so a
20
- # `rescue HyperUuid::Runtime::TimestampOutOfRangeError` written against an earlier
21
- # release still catches.
22
- RandomSourceError = HyperUuid::RandomSourceError
23
- TimestampOutOfRangeError = HyperUuid::TimestampOutOfRangeError
24
-
25
18
  # One copy of the out-of-range message for the Fiddle backend and for the doors' own
26
19
  # range check in hyperuuid.rb; the Magnus extension (rust/src/ruby_ext.rs) carries the
27
20
  # same text, and spec/native_backend_spec.rb pins that the two agree.
28
- 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)"
29
22
 
30
23
  # The two batch-size refusals, shared the same way: a version 7 batch past the 26-bit
31
24
  # counter space (the core's return code 4, which the doors in hyperuuid.rb refuse before
@@ -35,6 +28,12 @@ module HyperUuid
35
28
  V7_BATCH_TOO_LARGE = "a single version 7 batch takes at most 67108864 UUIDs (the 26-bit counter space)"
36
29
  BATCH_TOO_LARGE_TO_ADDRESS = "the batch is too large to address on this platform"
37
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
+
38
37
  # The SQL Server order conversions' one refusal, in the core's own words (the Magnus
39
38
  # extension raises its SqlOrderError's text): a value that isn't a UUID of the
40
39
  # conversion's version (code 5), an ArgumentError. Every version 7 converts.
@@ -75,6 +74,7 @@ module HyperUuid
75
74
  case rc
76
75
  when 0 then out[0, 16]
77
76
  when 2 then raise TimestampOutOfRangeError, V7_TIMESTAMP_OUT_OF_RANGE
77
+ when 6 then raise CounterExhaustedError, V7_COUNTER_EXHAUSTED
78
78
  else raise random_source_failure("uuid_new_v7")
79
79
  end
80
80
  end
@@ -96,6 +96,7 @@ module HyperUuid
96
96
  when 2 then raise TimestampOutOfRangeError, V7_TIMESTAMP_OUT_OF_RANGE
97
97
  when 3 then raise ArgumentError, BATCH_TOO_LARGE_TO_ADDRESS
98
98
  when 4 then raise ArgumentError, V7_BATCH_TOO_LARGE
99
+ when 6 then raise CounterExhaustedError, V7_COUNTER_EXHAUSTED
99
100
  else raise random_source_failure("uuid_new_v7_batch")
100
101
  end
101
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 = "0.9.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: 0.9.0
4
+ version: 1.1.0
5
5
  platform: aarch64-linux-gnu
6
6
  authors:
7
7
  - Brian Buvinghausen