ruby-ulid 0.1.0 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
![ULIDlogo](https://raw.githubusercontent.com/kachick/ruby-ulid/main/logo.png)
|
12
12
|
|
13
|
-
![Build Status](https://github.com/kachick/ruby-ulid/actions/workflows/
|
13
|
+
![Build Status](https://github.com/kachick/ruby-ulid/actions/workflows/test_behaviors.yml/badge.svg?branch=main)
|
14
14
|
[![Gem Version](https://badge.fury.io/rb/ruby-ulid.png)](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]
|