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.
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: []