ruby-ulid 0.1.0 → 0.1.5
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 +4 -4
- data/{LICENSE → LICENSE.txt} +0 -0
- data/README.md +44 -9
- data/lib/ulid.rb +97 -23
- data/lib/ulid/crockford_base32.rb +25 -1
- data/lib/ulid/monotonic_generator.rb +48 -22
- data/lib/ulid/uuid.rb +3 -2
- data/lib/ulid/version.rb +1 -1
- data/sig/ulid.rbs +446 -22
- metadata +227 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 01dca4f18f5bb3168b135847b6a1171d00a6ceb03b1d2ef68b6df212975cafc1
|
4
|
+
data.tar.gz: e1b0cb033ded88591d4817395507f517f46b7d3dd3d3a72021a216dcfef09d12
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c9c1a60687c05cfa99ce37f4e692938bff96dd15afa6b0adce40de5b897303b6e57301f069299c06d726bdd7a425e1ed581b7b60e15efc88d69abc417c577005
|
7
|
+
data.tar.gz: 71f333e7a6d6169f346b0e8fba65c9413fa24c32ce6799623e11b789544fb057fa0f67f83ed94a896cbac9df123ee4f9c9598735953217bd6fec332df659ee9f
|
data/{LICENSE → LICENSE.txt}
RENAMED
File without changes
|
data/README.md
CHANGED
@@ -10,7 +10,7 @@ Also providing [ruby/rbs](https://github.com/ruby/rbs) signature files.
|
|
10
10
|
|
11
11
|

|
12
12
|
|
13
|
-

|
14
14
|
[](http://badge.fury.io/rb/ruby-ulid)
|
15
15
|
|
16
16
|
## Universally Unique Lexicographically Sortable Identifier
|
@@ -28,7 +28,7 @@ Instead, herein is proposed ULID:
|
|
28
28
|
- 1.21e+24 unique ULIDs per millisecond
|
29
29
|
- Lexicographically sortable!
|
30
30
|
- Canonically encoded as a 26 character string, as opposed to the 36 character UUID
|
31
|
-
- Uses [Crockford's base32](https://www.crockford.com/base32.html) for better efficiency and readability (5 bits per character)
|
31
|
+
- Uses [Crockford's base32](https://www.crockford.com/base32.html) for better efficiency and readability (5 bits per character)
|
32
32
|
- Case insensitive
|
33
33
|
- No special characters (URL safe)
|
34
34
|
- Monotonic sort order (correctly detects and handles the same millisecond)
|
@@ -49,7 +49,7 @@ Should be installed!
|
|
49
49
|
Add this line to your application/library's `Gemfile` is needed in basic use-case
|
50
50
|
|
51
51
|
```ruby
|
52
|
-
gem 'ruby-ulid', '>= 0.1.
|
52
|
+
gem 'ruby-ulid', '>= 0.1.5', '< 0.2.0'
|
53
53
|
```
|
54
54
|
|
55
55
|
### Generator and Parser
|
@@ -146,6 +146,8 @@ sample_ulids_by_the_time.take(5) #=>
|
|
146
146
|
ulids.sort == ulids #=> true
|
147
147
|
```
|
148
148
|
|
149
|
+
Same generator does not generate duplicated ULIDs even in multi threads environment. It is implemented with [Monitor](https://bugs.ruby-lang.org/issues/16255)
|
150
|
+
|
149
151
|
### Filtering IDs with `Time`
|
150
152
|
|
151
153
|
`ULID` can be element of the `Range`. If you generated the IDs in monotonic generator, ID based filtering is easy and reliable
|
@@ -168,7 +170,7 @@ exclude_end = ULID.range(time1...time2) #=> The end of `Range[ULID]` will be the
|
|
168
170
|
|
169
171
|
# Below patterns are acceptable
|
170
172
|
pinpointing = ULID.range(time1..time1) #=> This will match only for all IDs in `time1`
|
171
|
-
until_the_end = ULID.range(..time1) #=> This will match only for all IDs upto `time1` (The `nil` starting `Range` can be used since Ruby 2.7)
|
173
|
+
# until_the_end = ULID.range(..time1) #=> This will match only for all IDs upto `time1` (The `nil` starting `Range` can be used since Ruby 2.7)
|
172
174
|
until_the_end = ULID.range(ULID.min.to_time..time1) #=> This is same as above for Ruby 2.6
|
173
175
|
until_the_ulid_limit = ULID.range(time1..) # This will match only for all IDs from `time1` to max value of the ULID limit
|
174
176
|
|
@@ -190,7 +192,7 @@ ULID.floor(time) #=> 2000-01-01 00:00:00.123 UTC
|
|
190
192
|
For rough operations, `ULID.scan` might be useful.
|
191
193
|
|
192
194
|
```ruby
|
193
|
-
json
|
195
|
+
json = <<'JSON'
|
194
196
|
{
|
195
197
|
"id": "01F4GNAV5ZR6FJQ5SFQC7WDSY3",
|
196
198
|
"author": {
|
@@ -215,7 +217,7 @@ json =<<'EOD'
|
|
215
217
|
}
|
216
218
|
]
|
217
219
|
}
|
218
|
-
|
220
|
+
JSON
|
219
221
|
|
220
222
|
ULID.scan(json).to_a
|
221
223
|
#=>
|
@@ -311,7 +313,7 @@ ulids.take(10)
|
|
311
313
|
# ULID(2021-04-29 03:18:24.152 UTC: 01F4DT4Z4RA0QV8WFQGRAG63EH),
|
312
314
|
# ULID(2021-05-02 13:27:16.394 UTC: 01F4PM605ABF5SDVMEHBH8JJ9R)]
|
313
315
|
ULID.sample(10, period: ulid1.to_time..ulid2.to_time)
|
314
|
-
#=>
|
316
|
+
#=>
|
315
317
|
# [ULID(2021-04-29 06:44:41.513 UTC: 01F4E5YPD9XQ3MYXWK8ZJKY8SW),
|
316
318
|
# ULID(2021-05-01 00:35:06.629 UTC: 01F4JNKD85SVK1EAEYSJGF53A2),
|
317
319
|
# ULID(2021-05-02 12:45:28.408 UTC: 01F4PHSEYRG9BWBEWMRW1XE6WW),
|
@@ -324,6 +326,34 @@ ULID.sample(10, period: ulid1.to_time..ulid2.to_time)
|
|
324
326
|
# ULID(2021-04-28 15:05:06.808 UTC: 01F4CG68ZRST94T056KRZ5K9S4)]
|
325
327
|
```
|
326
328
|
|
329
|
+
### ULID specification ambiguity around orthographical variants of the format
|
330
|
+
|
331
|
+
I'm afraid so, we should consider [Current ULID spec](https://github.com/ulid/spec/tree/d0c7170df4517939e70129b4d6462cc162f2d5bf#universally-unique-lexicographically-sortable-identifier) has `orthographical variants of the format` possibilities.
|
332
|
+
|
333
|
+
>Uses Crockford's base32 for better efficiency and readability (5 bits per character)
|
334
|
+
|
335
|
+
The original `Crockford's base32` maps `I`, `L` to `1`, `O` to `0`.
|
336
|
+
And accepts freestyle inserting `Hyphens (-)`.
|
337
|
+
To consider this patterns or not is different in each implementations.
|
338
|
+
|
339
|
+
Current parser/validator/matcher aims to cover `subset of Crockford's base32`.
|
340
|
+
I have suggested it would be clarified in [ulid/spec#57](https://github.com/ulid/spec/pull/57).
|
341
|
+
|
342
|
+
>Case insensitive
|
343
|
+
|
344
|
+
I can understand it might be considered in actual use-case.
|
345
|
+
But it is a controversial point, discussing in [ulid/spec#3](https://github.com/ulid/spec/issues/3).
|
346
|
+
|
347
|
+
Be that as it may, this gem provides API for handling the nasty possibilities.
|
348
|
+
|
349
|
+
`ULID.normalize` and `ULID.normalized?`
|
350
|
+
|
351
|
+
```ruby
|
352
|
+
ULID.normalize('-olarz3-noekisv4rrff-q6ig5fav--') #=> "01ARZ3N0EK1SV4RRFFQ61G5FAV"
|
353
|
+
ULID.normalized?('-olarz3-noekisv4rrff-q6ig5fav--') #=> false
|
354
|
+
ULID.normalized?('01ARZ3N0EK1SV4RRFFQ61G5FAV') #=> true
|
355
|
+
```
|
356
|
+
|
327
357
|
### UUIDv4 converter for migration use-cases
|
328
358
|
|
329
359
|
`ULID.from_uuidv4` and `ULID#to_uuidv4` is the converter.
|
@@ -333,7 +363,7 @@ The imported timestamp is meaningless. So ULID's benefit will lost.
|
|
333
363
|
# Currently experimental feature, so needed to load the extension.
|
334
364
|
require 'ulid/uuid'
|
335
365
|
|
336
|
-
# Basically reversible
|
366
|
+
# Basically reversible
|
337
367
|
ulid = ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39') #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
|
338
368
|
ulid.to_uuidv4 #=> "0983d0a2-ff15-4d83-8f37-7dd945b5aa39"
|
339
369
|
|
@@ -401,6 +431,12 @@ NOTE: It is still having precision issue similar as `ulid gem` in the both gener
|
|
401
431
|
1. [Fix to handle timestamp precision in parser](https://github.com/abachman/ulid-ruby/pull/5)
|
402
432
|
1. [Fix to handle timestamp precision in generator](https://github.com/abachman/ulid-ruby/pull/4)
|
403
433
|
|
434
|
+
### Compare performance with them
|
435
|
+
|
436
|
+
See [Benchmark](https://github.com/kachick/ruby-ulid/wiki/Benchmark).
|
437
|
+
|
438
|
+
The results are not something to be proud of.
|
439
|
+
|
404
440
|
## References
|
405
441
|
|
406
442
|
- [Repository](https://github.com/kachick/ruby-ulid)
|
@@ -410,4 +446,3 @@ NOTE: It is still having precision issue similar as `ulid gem` in the both gener
|
|
410
446
|
## Note
|
411
447
|
|
412
448
|
- Another choices for sortable and randomness IDs, [UUIDv6, UUIDv7, UUIDv8 might be the one. (But they are still in draft state)](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-01.html), I will track them in [ruby-ulid#37](https://github.com/kachick/ruby-ulid/issues/37)
|
413
|
-
- Current parser/validator/matcher aims to cover `subset of Crockford's base32`. Suggesting it in [ulid/spec#57](https://github.com/ulid/spec/pull/57). Be that as it may, I might provide special handler or converter for the exception in [ruby-ulid#57](https://github.com/kachick/ruby-ulid/issues/57) and/or [ruby-ulid#78](https://github.com/kachick/ruby-ulid/issues/78)
|
data/lib/ulid.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# coding: us-ascii
|
2
2
|
# frozen_string_literal: true
|
3
|
+
|
3
4
|
# Copyright (C) 2021 Kenichi Kamiya
|
4
5
|
|
5
6
|
require 'securerandom'
|
@@ -15,6 +16,7 @@ class ULID
|
|
15
16
|
class Error < StandardError; end
|
16
17
|
class OverflowError < Error; end
|
17
18
|
class ParserError < Error; end
|
19
|
+
class UnexpectedError < Error; end
|
18
20
|
|
19
21
|
# Excluded I, L, O, U, -.
|
20
22
|
# This is the encoding patterns.
|
@@ -59,6 +61,7 @@ class ULID
|
|
59
61
|
# @return [ULID]
|
60
62
|
def self.at(time)
|
61
63
|
raise ArgumentError, 'ULID.at takes only `Time` instance' unless Time === time
|
64
|
+
|
62
65
|
from_milliseconds_and_entropy(milliseconds: milliseconds_from_time(time), entropy: reasonable_entropy)
|
63
66
|
end
|
64
67
|
|
@@ -90,19 +93,21 @@ class ULID
|
|
90
93
|
# * Do not take random generator for the arguments
|
91
94
|
# * Raising error instead of truncating elements for the given number
|
92
95
|
def self.sample(*args, period: nil)
|
93
|
-
int_generator =
|
94
|
-
|
95
|
-
|
96
|
+
int_generator = (
|
97
|
+
if period
|
98
|
+
ulid_range = range(period)
|
99
|
+
min, max, exclude_end = ulid_range.begin.to_i, ulid_range.end.to_i, ulid_range.exclude_end?
|
96
100
|
|
97
|
-
|
98
|
-
|
101
|
+
possibilities = (max - min) + (exclude_end ? 0 : 1)
|
102
|
+
raise ArgumentError, "given range `#{ulid_range.inspect}` does not have possibilities" unless possibilities.positive?
|
99
103
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
104
|
+
-> {
|
105
|
+
SecureRandom.random_number(possibilities) + min
|
106
|
+
}
|
107
|
+
else
|
108
|
+
RANDOM_INTEGER_GENERATOR
|
109
|
+
end
|
110
|
+
)
|
106
111
|
|
107
112
|
case args.size
|
108
113
|
when 0
|
@@ -133,6 +138,7 @@ class ULID
|
|
133
138
|
string = String.try_convert(string)
|
134
139
|
raise ArgumentError, 'ULID.scan takes only strings' unless string
|
135
140
|
return to_enum(__callee__, string) unless block_given?
|
141
|
+
|
136
142
|
string.scan(SCANNING_PATTERN) do |matched|
|
137
143
|
yield parse(matched)
|
138
144
|
end
|
@@ -155,7 +161,7 @@ class ULID
|
|
155
161
|
milliseconds = n32encoded_timestamp.to_i(32)
|
156
162
|
entropy = n32encoded_randomness.to_i(32)
|
157
163
|
|
158
|
-
new
|
164
|
+
new(milliseconds: milliseconds, entropy: entropy, integer: integer)
|
159
165
|
end
|
160
166
|
|
161
167
|
# @param [Range<Time>, Range<nil>, Range[ULID]] period
|
@@ -163,6 +169,7 @@ class ULID
|
|
163
169
|
# @raise [ArgumentError] if the given period is not a `Range[Time]`, `Range[nil]` or `Range[ULID]`
|
164
170
|
def self.range(period)
|
165
171
|
raise ArgumentError, 'ULID.range takes only `Range[Time]`, `Range[nil]` or `Range[ULID]`' unless Range === period
|
172
|
+
|
166
173
|
begin_element, end_element, exclude_end = period.begin, period.end, period.exclude_end?
|
167
174
|
return period if self === begin_element && self === end_element
|
168
175
|
|
@@ -179,11 +186,7 @@ class ULID
|
|
179
186
|
|
180
187
|
case end_element
|
181
188
|
when Time
|
182
|
-
|
183
|
-
end_ulid = min(end_element)
|
184
|
-
else
|
185
|
-
end_ulid = max(end_element)
|
186
|
-
end
|
189
|
+
end_ulid = exclude_end ? min(end_element) : max(end_element)
|
187
190
|
when nil
|
188
191
|
# The end should be max and include end, because nil end means to cover endless ULIDs until the limit
|
189
192
|
end_ulid = MAX
|
@@ -239,9 +242,8 @@ class ULID
|
|
239
242
|
end
|
240
243
|
end
|
241
244
|
|
242
|
-
# @api private
|
243
245
|
# @return [Integer]
|
244
|
-
def self.reasonable_entropy
|
246
|
+
private_class_method def self.reasonable_entropy
|
245
247
|
SecureRandom.random_number(MAX_ENTROPY)
|
246
248
|
end
|
247
249
|
|
@@ -260,12 +262,65 @@ class ULID
|
|
260
262
|
end
|
261
263
|
|
262
264
|
# @param [String, #to_str] string
|
263
|
-
# @return [
|
264
|
-
|
265
|
+
# @return [String]
|
266
|
+
# @raise [ParserError] if the given format is not correct for ULID specs, even if ignored `orthographical variants of the format`
|
267
|
+
def self.normalize(string)
|
265
268
|
string = String.try_convert(string)
|
269
|
+
raise ArgumentError, 'ULID.normalize takes only strings' unless string
|
270
|
+
|
271
|
+
normalized_in_crockford = CrockfordBase32.normalize(string)
|
272
|
+
# Ensure the ULID correctness, because CrockfordBase32 does not always mean to satisfy ULID format
|
273
|
+
parse(normalized_in_crockford).to_s
|
274
|
+
end
|
275
|
+
|
276
|
+
# @return [Boolean]
|
277
|
+
def self.normalized?(object)
|
278
|
+
normalized = normalize(object)
|
279
|
+
rescue Exception
|
280
|
+
false
|
281
|
+
else
|
282
|
+
normalized == object
|
283
|
+
end
|
284
|
+
|
285
|
+
# @return [Boolean]
|
286
|
+
def self.valid?(object)
|
287
|
+
string = String.try_convert(object)
|
266
288
|
string ? STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string) : false
|
267
289
|
end
|
268
290
|
|
291
|
+
# @param [ULID, #to_ulid] object
|
292
|
+
# @return [ULID, nil]
|
293
|
+
# @raise [TypeError] if `object.to_ulid` did not return ULID instance
|
294
|
+
def self.try_convert(object)
|
295
|
+
begin
|
296
|
+
converted = object.to_ulid
|
297
|
+
rescue NoMethodError
|
298
|
+
nil
|
299
|
+
else
|
300
|
+
if ULID === converted
|
301
|
+
converted
|
302
|
+
else
|
303
|
+
object_class_name = safe_get_class_name(object)
|
304
|
+
converted_class_name = safe_get_class_name(converted)
|
305
|
+
raise TypeError, "can't convert #{object_class_name} to ULID (#{object_class_name}#to_ulid gives #{converted_class_name})"
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
# @param [BasicObject] object
|
311
|
+
# @return [String]
|
312
|
+
private_class_method def self.safe_get_class_name(object)
|
313
|
+
fallback = 'UnknownObject'
|
314
|
+
|
315
|
+
begin
|
316
|
+
name = String.try_convert(object.class.name)
|
317
|
+
rescue Exception
|
318
|
+
fallback
|
319
|
+
else
|
320
|
+
name || fallback
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
269
324
|
# @api private
|
270
325
|
# @param [Integer] milliseconds
|
271
326
|
# @param [Integer] entropy
|
@@ -282,7 +337,7 @@ class ULID
|
|
282
337
|
n32encoded_randomness = entropy.to_s(32).rjust(RANDOMNESS_ENCODED_LENGTH, '0')
|
283
338
|
integer = (n32encoded_timestamp + n32encoded_randomness).to_i(32)
|
284
339
|
|
285
|
-
new
|
340
|
+
new(milliseconds: milliseconds, entropy: entropy, integer: integer)
|
286
341
|
end
|
287
342
|
|
288
343
|
attr_reader :milliseconds, :entropy
|
@@ -353,7 +408,7 @@ class ULID
|
|
353
408
|
def octets
|
354
409
|
digits = @integer.digits(256)
|
355
410
|
(OCTETS_LENGTH - digits.size).times do
|
356
|
-
digits.push
|
411
|
+
digits.push(0)
|
357
412
|
end
|
358
413
|
digits.reverse!
|
359
414
|
end
|
@@ -424,6 +479,25 @@ class ULID
|
|
424
479
|
super
|
425
480
|
end
|
426
481
|
|
482
|
+
# @api private
|
483
|
+
# @return [Integer]
|
484
|
+
def marshal_dump
|
485
|
+
@integer
|
486
|
+
end
|
487
|
+
|
488
|
+
# @api private
|
489
|
+
# @param [Integer] integer
|
490
|
+
# @return [void]
|
491
|
+
def marshal_load(integer)
|
492
|
+
unmarshaled = ULID.from_integer(integer)
|
493
|
+
initialize(integer: unmarshaled.to_i, milliseconds: unmarshaled.milliseconds, entropy: unmarshaled.entropy)
|
494
|
+
end
|
495
|
+
|
496
|
+
# @return [self]
|
497
|
+
def to_ulid
|
498
|
+
self
|
499
|
+
end
|
500
|
+
|
427
501
|
# @return [self]
|
428
502
|
def dup
|
429
503
|
self
|
@@ -1,9 +1,12 @@
|
|
1
1
|
# coding: us-ascii
|
2
2
|
# frozen_string_literal: true
|
3
|
+
|
3
4
|
# Copyright (C) 2021 Kenichi Kamiya
|
4
5
|
|
5
6
|
class ULID
|
6
|
-
#
|
7
|
+
# @see https://www.crockford.com/base32.html
|
8
|
+
#
|
9
|
+
# This module supporting only `subset of original crockford for actual use-case` in ULID context.
|
7
10
|
# Original decoding spec allows other characters.
|
8
11
|
# But I think ULID should allow `subset` of Crockford's Base32.
|
9
12
|
# See below
|
@@ -46,11 +49,24 @@ class ULID
|
|
46
49
|
end
|
47
50
|
end.freeze
|
48
51
|
raise SetupError, 'obvious bug exists in the mapping algorithm' unless N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys == crockford_base32_mappings.keys
|
52
|
+
|
49
53
|
CROCKFORD_BASE32_CHAR_PATTERN = /[#{N32_CHAR_BY_CROCKFORD_BASE32_CHAR.keys.join}]/.freeze
|
50
54
|
|
51
55
|
CROCKFORD_BASE32_CHAR_BY_N32_CHAR = N32_CHAR_BY_CROCKFORD_BASE32_CHAR.invert.freeze
|
52
56
|
N32_CHAR_PATTERN = /[#{CROCKFORD_BASE32_CHAR_BY_N32_CHAR.keys.join}]/.freeze
|
53
57
|
|
58
|
+
STANDARD_BY_VARIANT = {
|
59
|
+
'L' => '1',
|
60
|
+
'l' => '1',
|
61
|
+
'I' => '1',
|
62
|
+
'i' => '1',
|
63
|
+
'O' => '0',
|
64
|
+
'o' => '0',
|
65
|
+
'-' => ''
|
66
|
+
}.freeze
|
67
|
+
VARIANT_PATTERN = /[#{STANDARD_BY_VARIANT.keys.join}]/.freeze
|
68
|
+
|
69
|
+
# @api private
|
54
70
|
# @param [String] string
|
55
71
|
# @return [Integer]
|
56
72
|
def self.decode(string)
|
@@ -58,11 +74,19 @@ class ULID
|
|
58
74
|
n32encoded.to_i(32)
|
59
75
|
end
|
60
76
|
|
77
|
+
# @api private
|
61
78
|
# @param [Integer] integer
|
62
79
|
# @return [String]
|
63
80
|
def self.encode(integer)
|
64
81
|
n32encoded = integer.to_s(32)
|
65
82
|
n32encoded.upcase.gsub(N32_CHAR_PATTERN, CROCKFORD_BASE32_CHAR_BY_N32_CHAR).rjust(ENCODED_LENGTH, '0')
|
66
83
|
end
|
84
|
+
|
85
|
+
# @api private
|
86
|
+
# @param [String] string
|
87
|
+
# @return [String]
|
88
|
+
def self.normalize(string)
|
89
|
+
string.gsub(VARIANT_PATTERN, STANDARD_BY_VARIANT)
|
90
|
+
end
|
67
91
|
end
|
68
92
|
end
|
@@ -1,43 +1,69 @@
|
|
1
1
|
# coding: us-ascii
|
2
2
|
# frozen_string_literal: true
|
3
|
+
|
3
4
|
# Copyright (C) 2021 Kenichi Kamiya
|
4
5
|
|
5
6
|
class ULID
|
6
7
|
class MonotonicGenerator
|
7
|
-
|
8
|
-
|
8
|
+
include MonitorMixin
|
9
|
+
|
10
|
+
# @return [ULID, nil]
|
11
|
+
attr_reader :prev
|
12
|
+
|
13
|
+
undef_method :instance_variable_set
|
9
14
|
|
10
15
|
def initialize
|
11
|
-
|
12
|
-
|
16
|
+
super()
|
17
|
+
@prev = nil
|
13
18
|
end
|
14
19
|
|
20
|
+
# @return [String]
|
21
|
+
def inspect
|
22
|
+
"ULID::MonotonicGenerator(prev: #{@prev.inspect})"
|
23
|
+
end
|
24
|
+
alias_method :to_s, :inspect
|
25
|
+
|
15
26
|
# @param [Time, Integer] moment
|
16
27
|
# @return [ULID]
|
17
28
|
# @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
|
18
|
-
# @raise [
|
29
|
+
# @raise [UnexpectedError] if the generated ULID is an invalid value in monotonicity spec.
|
30
|
+
# Basically will not happen. Just means this feature prefers error rather than invalid value.
|
19
31
|
def generate(moment: ULID.current_milliseconds)
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
if @latest_milliseconds < milliseconds
|
25
|
-
@latest_milliseconds = milliseconds
|
26
|
-
@latest_entropy = ULID.reasonable_entropy
|
27
|
-
else
|
28
|
-
@latest_entropy += 1
|
32
|
+
synchronize do
|
33
|
+
unless @prev
|
34
|
+
@prev = ULID.generate(moment: moment)
|
35
|
+
return @prev
|
29
36
|
end
|
30
|
-
|
37
|
+
|
38
|
+
milliseconds = ULID.milliseconds_from_moment(moment)
|
39
|
+
|
40
|
+
ulid = (
|
41
|
+
if @prev.milliseconds < milliseconds
|
42
|
+
ULID.generate(moment: milliseconds)
|
43
|
+
else
|
44
|
+
ULID.from_milliseconds_and_entropy(milliseconds: @prev.milliseconds, entropy: @prev.entropy.succ)
|
45
|
+
end
|
46
|
+
)
|
47
|
+
|
48
|
+
unless ulid > @prev
|
49
|
+
base_message = "monotonicity broken from unexpected reasons # generated: #{ulid.inspect}, prev: #{@prev.inspect}"
|
50
|
+
additional_information = (
|
51
|
+
if Thread.list == [Thread.main]
|
52
|
+
'# NOTE: looks single thread only exist'
|
53
|
+
else
|
54
|
+
'# NOTE: ran on multi threads, so this might from concurrency issue'
|
55
|
+
end
|
56
|
+
)
|
57
|
+
|
58
|
+
raise UnexpectedError, base_message + additional_information
|
59
|
+
end
|
60
|
+
|
61
|
+
@prev = ulid
|
62
|
+
ulid
|
31
63
|
end
|
32
64
|
end
|
33
65
|
|
34
|
-
|
35
|
-
# @return [void]
|
36
|
-
def reset
|
37
|
-
@latest_milliseconds = 0
|
38
|
-
@latest_entropy = ULID.reasonable_entropy
|
39
|
-
nil
|
40
|
-
end
|
66
|
+
undef_method :freeze
|
41
67
|
|
42
68
|
# @raise [TypeError] always raises exception and does not freeze self
|
43
69
|
# @return [void]
|