hyperuuid-wasm 0.6.1
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +431 -0
- data/ext/hyperuuid_native/3.4/hyperuuid_native.a.gz +0 -0
- data/ext/hyperuuid_native/4.0/hyperuuid_native.a.gz +0 -0
- data/ext/hyperuuid_native/extconf.rb +107 -0
- data/lib/hyperuuid/errors.rb +12 -0
- data/lib/hyperuuid/namespaces.rb +13 -0
- data/lib/hyperuuid/native_platform.rb +28 -0
- data/lib/hyperuuid/runtime.rb +279 -0
- data/lib/hyperuuid/uuid.rb +179 -0
- data/lib/hyperuuid.rb +247 -0
- metadata +87 -0
data/lib/hyperuuid.rb
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
require_relative "hyperuuid/errors"
|
|
2
|
+
require_relative "hyperuuid/uuid"
|
|
3
|
+
require_relative "hyperuuid/namespaces"
|
|
4
|
+
require_relative "hyperuuid/native_platform"
|
|
5
|
+
require_relative "hyperuuid/runtime"
|
|
6
|
+
|
|
7
|
+
# RFC 9562 UUID v4 (random), v5 (deterministic), v6 and v7 (time-sortable) generation from one
|
|
8
|
+
# Rust core, reached through whichever of two backends this install can load: the core
|
|
9
|
+
# linked straight into a Magnus native extension (shipped precompiled in the platform gems),
|
|
10
|
+
# or the native libhyperuuid shared library called through Fiddle (the universal gem bundles
|
|
11
|
+
# one per supported platform, picked by HyperUuid::NativePlatform). No runtime bridge in
|
|
12
|
+
# either, and nothing compiled at install time. HyperUuid::BACKEND names the one that loaded; the selection logic
|
|
13
|
+
# is at the bottom of this file.
|
|
14
|
+
#
|
|
15
|
+
# The doors below — with Uuid, Namespaces and the two exception classes — are the public
|
|
16
|
+
# surface, and are shared by every backend, argument validation included: a caller bug raises
|
|
17
|
+
# the same error whichever backend is live.
|
|
18
|
+
module HyperUuid
|
|
19
|
+
# This gem's own version — distinct from the RFC 9562 UUID *versions* (v4/v5/v6/v7) the
|
|
20
|
+
# rest of this module generates.
|
|
21
|
+
VERSION = "0.6.1"
|
|
22
|
+
|
|
23
|
+
# The widest batch count, and the longest v5 name in bytes, the native ABI carries (a u32).
|
|
24
|
+
# (The millisecond count is a u64; unix_millis_from refuses anything wider.)
|
|
25
|
+
U32_MAX = 0xFFFF_FFFF
|
|
26
|
+
private_constant :U32_MAX
|
|
27
|
+
|
|
28
|
+
# Creates a random UUID version 4 (RFC 9562 §5.4).
|
|
29
|
+
def self.new_v4
|
|
30
|
+
Uuid.new(Runtime.new_v4, true)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Creates a deterministic UUID version 5 (RFC 9562 §5.5) from a namespace and a name. The
|
|
34
|
+
# same (namespace, name) pair always produces the same UUID. `name` may be a text String
|
|
35
|
+
# (encoded as UTF-8) or already-raw ASCII-8BIT bytes, which are used as-is; it may be empty.
|
|
36
|
+
#
|
|
37
|
+
# @raise [TypeError] if +namespace+ isn't a HyperUuid::Uuid or +name+ isn't a String.
|
|
38
|
+
def self.new_v5(namespace, name)
|
|
39
|
+
raise TypeError, "namespace must be a HyperUuid::Uuid; got #{namespace.class}" unless namespace.is_a?(Uuid)
|
|
40
|
+
raise TypeError, "name must be a String; got #{name.class}" unless name.is_a?(String)
|
|
41
|
+
|
|
42
|
+
name_bytes =
|
|
43
|
+
if name.encoding == Encoding::ASCII_8BIT
|
|
44
|
+
name
|
|
45
|
+
else
|
|
46
|
+
name.encode(Encoding::UTF_8).dup.force_encoding(Encoding::BINARY)
|
|
47
|
+
end
|
|
48
|
+
raise ArgumentError, "name must be at most #{U32_MAX} bytes" if name_bytes.bytesize > U32_MAX
|
|
49
|
+
|
|
50
|
+
Uuid.new(Runtime.new_v5(namespace.bytes, name_bytes), true)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Converts +value+ to a Unix-epoch millisecond integer: +nil+ becomes the current time, a
|
|
54
|
+
# +Time+ is converted exactly (via its own Rational seconds, avoiding float rounding), and
|
|
55
|
+
# an Integer millisecond count passes through unchanged. Shared by every
|
|
56
|
+
# `new_v6`/`new_v7`/batch door below so a caller can pass either a `Time` or a raw
|
|
57
|
+
# millisecond count interchangeably.
|
|
58
|
+
#
|
|
59
|
+
# This is also where a caller bug is caught once, for every backend: anything else is a
|
|
60
|
+
# TypeError, and a count that cannot cross the ABI as a u64 at all — negative (a `Time`
|
|
61
|
+
# before the epoch included) or past 64 bits — is the same TimestampOutOfRangeError, with
|
|
62
|
+
# the same +out_of_range+ message, the core raises for a value it can see but not embed.
|
|
63
|
+
# Left to the backends those cases diverged: a RangeError from the extension, a wrapped
|
|
64
|
+
# value from Fiddle.
|
|
65
|
+
#
|
|
66
|
+
# The two common arguments go first and cheapest: no argument is the clock, which needs no
|
|
67
|
+
# range check, and an Integer is checked by its bit length rather than against 2**64 - 1: a
|
|
68
|
+
# comparison with a number that size is a bignum comparison, and cost more than the mint.
|
|
69
|
+
private_class_method def self.unix_millis_from(value, out_of_range)
|
|
70
|
+
return Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond) if value.nil?
|
|
71
|
+
|
|
72
|
+
millis =
|
|
73
|
+
if value.is_a?(Integer) then value
|
|
74
|
+
elsif value.is_a?(Time) then (value.to_r * 1000).floor
|
|
75
|
+
else raise TypeError, "unix_millis must be a Time, an Integer or nil; got #{value.class}"
|
|
76
|
+
end
|
|
77
|
+
raise TimestampOutOfRangeError, out_of_range unless millis >= 0 && millis.bit_length <= 64
|
|
78
|
+
|
|
79
|
+
millis
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Checks a batch +count+ once, for every backend: an Integer the ABI's u32 can carry.
|
|
83
|
+
private_class_method def self.batch_count(count)
|
|
84
|
+
raise TypeError, "count must be an Integer; got #{count.class}" unless count.is_a?(Integer)
|
|
85
|
+
raise ArgumentError, "count must be between 0 and #{U32_MAX}; got #{count}" unless count.between?(0, U32_MAX)
|
|
86
|
+
|
|
87
|
+
count
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Creates a time-sortable UUID version 6 (RFC 9562 §5.6), a field-compatible reordering of
|
|
91
|
+
# version 1 for better sort/index locality. Defaults to the current time; pass an explicit
|
|
92
|
+
# `Time` or Unix-epoch millisecond integer to embed a specific time instead. `clock_seq` and
|
|
93
|
+
# `node` are randomly generated on every call — unlike version 7, there is no monotonic
|
|
94
|
+
# counter, so calls within the same millisecond are not guaranteed to sort in creation order.
|
|
95
|
+
#
|
|
96
|
+
# @raise [TimestampOutOfRangeError] if the time is negative or past the 60-bit v6 field.
|
|
97
|
+
# @raise [TypeError] if +unix_millis+ isn't a Time, an Integer or nil.
|
|
98
|
+
def self.new_v6(unix_millis = nil)
|
|
99
|
+
Uuid.new(Runtime.new_v6(unix_millis_from(unix_millis, Runtime::V6_TIMESTAMP_OUT_OF_RANGE)), true)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Creates `count` time-sortable version 6 UUIDs sharing one timestamp capture — one FFI call
|
|
103
|
+
# and one random-bytes fetch instead of `count` of each. Defaults to the current time; pass
|
|
104
|
+
# an explicit `Time` or Unix-epoch millisecond integer to embed a specific time instead.
|
|
105
|
+
#
|
|
106
|
+
# @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and 2**32 - 1.
|
|
107
|
+
# @raise [TimestampOutOfRangeError] if the time is negative or past the 60-bit v6 field.
|
|
108
|
+
def self.new_v6_batch(count, unix_millis = nil)
|
|
109
|
+
bytes = new_v6_batch_bytes(count, unix_millis)
|
|
110
|
+
Array.new(count) { |i| Uuid.new(bytes[i * 16, 16], true) }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Creates a time-sortable UUID version 7 (RFC 9562 §6.2). Defaults to the current time; pass
|
|
114
|
+
# an explicit `Time` or Unix-epoch millisecond integer (non-negative, fitting in 48 bits) to
|
|
115
|
+
# embed a specific time instead.
|
|
116
|
+
#
|
|
117
|
+
# @raise [TimestampOutOfRangeError] if the time is negative or past the 48-bit v7 field.
|
|
118
|
+
# @raise [TypeError] if +unix_millis+ isn't a Time, an Integer or nil.
|
|
119
|
+
def self.new_v7(unix_millis = nil)
|
|
120
|
+
Uuid.new(Runtime.new_v7(unix_millis_from(unix_millis, Runtime::V7_TIMESTAMP_OUT_OF_RANGE)), true)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Creates `count` time-sortable version 7 UUIDs sharing one timestamp capture and one
|
|
124
|
+
# contiguous block of the monotonic counter — one FFI call and one random-bytes fetch
|
|
125
|
+
# instead of `count` of each. Defaults to the current time; pass an explicit `Time` or
|
|
126
|
+
# Unix-epoch millisecond integer to embed a specific time instead.
|
|
127
|
+
#
|
|
128
|
+
# @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and 2**32 - 1.
|
|
129
|
+
# @raise [TimestampOutOfRangeError] if the time is negative or past the 48-bit v7 field.
|
|
130
|
+
def self.new_v7_batch(count, unix_millis = nil)
|
|
131
|
+
bytes = new_v7_batch_bytes(count, unix_millis)
|
|
132
|
+
Array.new(count) { |i| Uuid.new(bytes[i * 16, 16], true) }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Returns `count` version 7 UUIDs as one binary String of raw RFC 9562-ordered bytes,
|
|
136
|
+
# 16 per UUID, instead of an Array of Uuid objects.
|
|
137
|
+
#
|
|
138
|
+
# Far faster than #new_v7_batch for a large batch — the README's "Bulk generation into
|
|
139
|
+
# bytes" section carries the measured figures. The difference is not the native call —
|
|
140
|
+
# that is identical — it is that #new_v7_batch then allocates `count` Uuid objects and
|
|
141
|
+
# their byte Strings on top of it. This hands back the bytes the native core already
|
|
142
|
+
# produced, untouched.
|
|
143
|
+
#
|
|
144
|
+
# Use it when bytes are the destination: a BYTEA/uniqueidentifier bind parameter, a wire
|
|
145
|
+
# format, a bulk COPY. If you need Uuid objects, keep using #new_v7_batch — slicing this
|
|
146
|
+
# String into them yourself just moves the same allocations into your own code.
|
|
147
|
+
#
|
|
148
|
+
# Slice it with `bytes[i * 16, 16]`, which is what #new_v7_batch does internally.
|
|
149
|
+
#
|
|
150
|
+
# @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and 2**32 - 1.
|
|
151
|
+
# @raise [TimestampOutOfRangeError] if the time is negative or past the 48-bit v7 field.
|
|
152
|
+
def self.new_v7_batch_bytes(count, unix_millis = nil)
|
|
153
|
+
count = batch_count(count)
|
|
154
|
+
Runtime.new_v7_batch(count, unix_millis_from(unix_millis, Runtime::V7_TIMESTAMP_OUT_OF_RANGE))
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Returns `count` version 6 UUIDs as one binary String of raw RFC 9562-ordered bytes,
|
|
158
|
+
# 16 per UUID. The version 6 counterpart to #new_v7_batch_bytes, with the same rationale and
|
|
159
|
+
# the same guidance about when it is the right call.
|
|
160
|
+
#
|
|
161
|
+
# clock_seq and node are independently random per item; unlike version 7 there is no
|
|
162
|
+
# monotonic counter, so items minted in the same millisecond are not guaranteed to sort in
|
|
163
|
+
# creation order.
|
|
164
|
+
#
|
|
165
|
+
# @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and 2**32 - 1.
|
|
166
|
+
# @raise [TimestampOutOfRangeError] if the time is negative or past the 60-bit v6 field.
|
|
167
|
+
def self.new_v6_batch_bytes(count, unix_millis = nil)
|
|
168
|
+
count = batch_count(count)
|
|
169
|
+
Runtime.new_v6_batch(count, unix_millis_from(unix_millis, Runtime::V6_TIMESTAMP_OUT_OF_RANGE))
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# The version of the native core actually loaded, as "major.minor.patch" — read from the
|
|
173
|
+
# library itself (its `hyperuuid_version` export), not from this gem, so a consumer can
|
|
174
|
+
# prove the core behind the doors is the one this binding was built against (and name the
|
|
175
|
+
# mismatch against HyperUuid::VERSION when it is not). The cheapest possible probe that the
|
|
176
|
+
# backend resolved at all: takes nothing, mints nothing, cannot fail once the backend loads.
|
|
177
|
+
#
|
|
178
|
+
# @raise [LoadError] if no backend could be loaded for this platform.
|
|
179
|
+
def self.native_version
|
|
180
|
+
word = Runtime.packed_version
|
|
181
|
+
"#{word >> 16}.#{(word >> 8) & 0xFF}.#{word & 0xFF}"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Whether a backend actually loaded and exports the ABI this binding was built against —
|
|
185
|
+
# what a consumer with a fallback of its own (SecureRandom.uuid, say) checks before
|
|
186
|
+
# committing to these doors. Probed once (a native_version round trip), cached, and never
|
|
187
|
+
# raises: a missing shared library, an unsupported platform, or an older core without the
|
|
188
|
+
# version export all answer false. The doors themselves keep their own behavior — the
|
|
189
|
+
# first call on an unavailable backend raises its precise error — this only answers the
|
|
190
|
+
# question quietly.
|
|
191
|
+
def self.available?
|
|
192
|
+
return @available unless @available.nil?
|
|
193
|
+
|
|
194
|
+
@available = begin
|
|
195
|
+
native_version.is_a?(String)
|
|
196
|
+
rescue LoadError, StandardError
|
|
197
|
+
false
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# --- backend selection: the Magnus extension, when present, replaces the Runtime methods
|
|
203
|
+
# above in place (no delegation layer) — Fiddle's measured per-call marshalling floor drops
|
|
204
|
+
# to an ordinary extension call, while everything above Runtime (Uuid, the module doors,
|
|
205
|
+
# batch slicing) stays shared byte-for-byte between backends. The pure-Fiddle definitions
|
|
206
|
+
# remain the universal zero-compile fallback — the last resort, for a Ruby or a platform no
|
|
207
|
+
# platform gem covers; precompiled platform gems are how the extension ships without ever
|
|
208
|
+
# making a consumer compile anything, and they carry no Fiddle library at all.
|
|
209
|
+
#
|
|
210
|
+
# HYPERUUID_PURE forces Fiddle. It is a testing and diagnostic switch — CI runs the whole suite
|
|
211
|
+
# through it, and it is how a suspected extension bug is ruled in or out — not a setting a
|
|
212
|
+
# deployment needs. It is read for presence, not value, so "0" and the empty string force it
|
|
213
|
+
# too. Inside a platform gem there is no library for it to load: BACKEND still reads :fiddle,
|
|
214
|
+
# HyperUuid.available? answers false, and the first door call raises a LoadError naming the
|
|
215
|
+
# universal gem, which is where the Fiddle backend lives.
|
|
216
|
+
HyperUuid::BACKEND =
|
|
217
|
+
if ENV["HYPERUUID_PURE"]
|
|
218
|
+
:fiddle
|
|
219
|
+
else
|
|
220
|
+
# Two layouts, and both have to work. A released platform gem is a "fat" gem carrying one
|
|
221
|
+
# extension per supported Ruby ABI under lib/hyperuuid/<minor>/ (see the Rakefile's
|
|
222
|
+
# native:gem task for why an ABI-per-file is unavoidable — Magnus has no `abi3`
|
|
223
|
+
# equivalent), and `rake native:dev` — the local dev loop — stages its own build at that
|
|
224
|
+
# same versioned path. CI's in-job staging additionally drops a single extension flat at
|
|
225
|
+
# lib/, which is also where a hand copy of `cargo ruby-ext`'s output goes (the alias only
|
|
226
|
+
# builds into rust/target/ruby/release/; nothing but that rake task copies it here).
|
|
227
|
+
# Trying the versioned path first and the flat one second means neither has to know the
|
|
228
|
+
# other exists.
|
|
229
|
+
#
|
|
230
|
+
# A miss on both is not an error: it means this Ruby/platform combination has no
|
|
231
|
+
# precompiled extension, which is precisely what the Fiddle backend below is for. A
|
|
232
|
+
# platform with no shared library either still lands on Fiddle, so the first call raises
|
|
233
|
+
# its own precise error — a "not found" LoadError naming the path, or
|
|
234
|
+
# NativePlatform::UnsupportedPlatformError naming the platform. HyperUuid.available? is
|
|
235
|
+
# the quiet way to ask.
|
|
236
|
+
begin
|
|
237
|
+
require "hyperuuid/#{RUBY_VERSION[/\d+\.\d+/]}/hyperuuid_native"
|
|
238
|
+
:native
|
|
239
|
+
rescue LoadError
|
|
240
|
+
begin
|
|
241
|
+
require "hyperuuid_native"
|
|
242
|
+
:native
|
|
243
|
+
rescue LoadError
|
|
244
|
+
:fiddle
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: hyperuuid-wasm
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.6.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Brian Buvinghausen
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rake
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '13.0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '13.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: yard
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.9'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.9'
|
|
40
|
+
description: |
|
|
41
|
+
The hyperuuid gem's Magnus extension, prebuilt for wasm32-wasip1, for a ruby.wasm
|
|
42
|
+
interpreter built with `rbwasm build`: put this gem in that Gemfile instead of hyperuuid
|
|
43
|
+
and the extension is linked into the interpreter. Same API and the same Rust core as the
|
|
44
|
+
hyperuuid gem. Not for a host Ruby, where the hyperuuid gem is the one to install.
|
|
45
|
+
executables: []
|
|
46
|
+
extensions:
|
|
47
|
+
- ext/hyperuuid_native/extconf.rb
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- LICENSE
|
|
51
|
+
- README.md
|
|
52
|
+
- ext/hyperuuid_native/3.4/hyperuuid_native.a.gz
|
|
53
|
+
- ext/hyperuuid_native/4.0/hyperuuid_native.a.gz
|
|
54
|
+
- ext/hyperuuid_native/extconf.rb
|
|
55
|
+
- lib/hyperuuid.rb
|
|
56
|
+
- lib/hyperuuid/errors.rb
|
|
57
|
+
- lib/hyperuuid/namespaces.rb
|
|
58
|
+
- lib/hyperuuid/native_platform.rb
|
|
59
|
+
- lib/hyperuuid/runtime.rb
|
|
60
|
+
- lib/hyperuuid/uuid.rb
|
|
61
|
+
homepage: https://github.com/SkunkWerkx/HyperUuid
|
|
62
|
+
licenses:
|
|
63
|
+
- MIT
|
|
64
|
+
metadata:
|
|
65
|
+
source_code_uri: https://github.com/SkunkWerkx/HyperUuid
|
|
66
|
+
changelog_uri: https://github.com/SkunkWerkx/HyperUuid/blob/master/CHANGELOG.md
|
|
67
|
+
bug_tracker_uri: https://github.com/SkunkWerkx/HyperUuid/issues
|
|
68
|
+
documentation_uri: https://github.com/SkunkWerkx/HyperUuid/tree/master/ruby#ruby-in-the-browser
|
|
69
|
+
rubygems_mfa_required: 'true'
|
|
70
|
+
rdoc_options: []
|
|
71
|
+
require_paths:
|
|
72
|
+
- lib
|
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '3.3'
|
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
requirements: []
|
|
84
|
+
rubygems_version: 4.0.20
|
|
85
|
+
specification_version: 4
|
|
86
|
+
summary: 'HyperUuid for ruby.wasm: the Magnus extension, linked in by rbwasm build'
|
|
87
|
+
test_files: []
|