block-uuidv7 0.1.0
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/.rspec +3 -0
- data/.rubocop.yml +23 -0
- data/AGENTS.md +88 -0
- data/README.md +163 -0
- data/Rakefile +10 -0
- data/block-uuidv7.gemspec +32 -0
- data/lib/uuidv7/base62.rb +53 -0
- data/lib/uuidv7/generator.rb +54 -0
- data/lib/uuidv7/monotonic_generator.rb +55 -0
- data/lib/uuidv7/version.rb +5 -0
- data/lib/uuidv7.rb +46 -0
- metadata +59 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ae447bd13b7cd778a06726790f667823627a9b3bac0adbce6d309b3e367b2e3d
|
|
4
|
+
data.tar.gz: b526111fb89415d3efff2bc315af518c43f9dfb7a432a6363b582bbdd746dc1f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b4b93d3b5a7aaf4007cf70ada05fc001561d4a9a24df2594a6011fcce0644f74f2a7eae9466323e52ef7bdf302bfb1b8ce8a4aaeaaf34e75454f9e8407006ec6
|
|
7
|
+
data.tar.gz: 71d36f1e1395312cf3348cc00cb6b254792c3c9836a9e590990f12e7d760325a3161976857b497f6c322fdaa515eeaead9e14e3da0d91aea8e6d5f189bd28189
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
TargetRubyVersion: 3.0
|
|
3
|
+
NewCops: enable
|
|
4
|
+
SuggestExtensions: false
|
|
5
|
+
|
|
6
|
+
Style/Documentation:
|
|
7
|
+
Enabled: false
|
|
8
|
+
|
|
9
|
+
Metrics/BlockLength:
|
|
10
|
+
Exclude:
|
|
11
|
+
- 'spec/**/*'
|
|
12
|
+
|
|
13
|
+
Metrics/MethodLength:
|
|
14
|
+
Max: 25
|
|
15
|
+
|
|
16
|
+
Metrics/AbcSize:
|
|
17
|
+
Max: 30
|
|
18
|
+
|
|
19
|
+
Style/FormatStringToken:
|
|
20
|
+
Enabled: false
|
|
21
|
+
|
|
22
|
+
Layout/LineLength:
|
|
23
|
+
Max: 120
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Agent Guide - Ruby Implementation
|
|
2
|
+
|
|
3
|
+
For the multi-language overview, see [root AGENTS.md](../AGENTS.md).
|
|
4
|
+
|
|
5
|
+
## Quick Commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd ruby
|
|
9
|
+
bundle install # Install dependencies
|
|
10
|
+
bundle exec rake spec # Run tests
|
|
11
|
+
bundle exec rake rubocop # Run linter
|
|
12
|
+
bundle exec rake # Run both tests and linter
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Prerequisites
|
|
16
|
+
|
|
17
|
+
- Ruby 3.0 or later
|
|
18
|
+
- Bundler
|
|
19
|
+
|
|
20
|
+
## Project Structure
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
ruby/
|
|
24
|
+
├── lib/
|
|
25
|
+
│ ├── uuidv7.rb # Main entry point
|
|
26
|
+
│ └── uuidv7/
|
|
27
|
+
│ ├── version.rb # Version constant
|
|
28
|
+
│ ├── generator.rb # High-performance UUID v7 (no ordering guarantees)
|
|
29
|
+
│ ├── monotonic_generator.rb # Monotonic UUID v7 (strict ordering)
|
|
30
|
+
│ └── base62.rb # Base62 encoding/decoding
|
|
31
|
+
├── spec/
|
|
32
|
+
│ ├── spec_helper.rb
|
|
33
|
+
│ ├── uuidv7_spec.rb
|
|
34
|
+
│ ├── monotonic_uuidv7_spec.rb
|
|
35
|
+
│ └── base62_spec.rb
|
|
36
|
+
├── uuidv7.gemspec
|
|
37
|
+
├── Gemfile
|
|
38
|
+
├── Rakefile
|
|
39
|
+
├── README.md
|
|
40
|
+
└── AGENTS.md # This file
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Key Implementation Details
|
|
44
|
+
|
|
45
|
+
### Random Number Generation Strategy
|
|
46
|
+
|
|
47
|
+
- Uses `SecureRandom` from Ruby stdlib for all random bits
|
|
48
|
+
- Ruby's `rand()` is not thread-safe, so `SecureRandom` is preferred
|
|
49
|
+
- Performance is still excellent for typical use cases
|
|
50
|
+
|
|
51
|
+
### Monotonic Counter Behavior
|
|
52
|
+
|
|
53
|
+
- Counter occupies 12 bits (rand_a field): 0-4095
|
|
54
|
+
- Counter increments with each generation in same millisecond
|
|
55
|
+
- Counter resets to **random value** when timestamp advances (not zero!)
|
|
56
|
+
- If counter overflows (4096 in same ms), waits for next millisecond
|
|
57
|
+
- Uses `Mutex` for thread safety
|
|
58
|
+
|
|
59
|
+
### Thread Safety
|
|
60
|
+
|
|
61
|
+
- `UUIDv7.generate`: Thread-safe (no shared mutable state)
|
|
62
|
+
- `MonotonicUUIDv7.generate`: Thread-safe via Mutex synchronization
|
|
63
|
+
|
|
64
|
+
### Clock Injection
|
|
65
|
+
|
|
66
|
+
Uses block-based clock injection for testability:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
UUIDv7.generate { 1700000000000 }
|
|
70
|
+
MonotonicUUIDv7.generate { Time.now.to_i * 1000 }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Testing Notes
|
|
74
|
+
|
|
75
|
+
- `MonotonicGenerator.reset_state!` is available for test isolation
|
|
76
|
+
- Always reset state before monotonic tests to ensure deterministic behavior
|
|
77
|
+
|
|
78
|
+
## Gem Publishing
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
gem build uuidv7.gemspec
|
|
82
|
+
gem push uuidv7-x.x.x.gem
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Further Reading
|
|
86
|
+
|
|
87
|
+
- [README.md](README.md) - API documentation and usage examples
|
|
88
|
+
- [Root AGENTS.md](../AGENTS.md) - Shared algorithm and design principles
|
data/README.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# UUIDv7 - Ruby
|
|
2
|
+
|
|
3
|
+
A minimal, high-performance UUID v7 implementation for Ruby.
|
|
4
|
+
|
|
5
|
+
## Introduction
|
|
6
|
+
|
|
7
|
+
[UUID v7](https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-7) is a time-ordered UUID format that encodes a Unix timestamp in the most significant 48 bits, making UUIDs naturally sortable by creation time. This is useful for:
|
|
8
|
+
|
|
9
|
+
- Database indexed fields that benefit from sequential ordering
|
|
10
|
+
- Distributed systems where time-based ordering is valuable
|
|
11
|
+
- Event logs and audit trails where chronological sorting is important
|
|
12
|
+
|
|
13
|
+
### Compact Base62 Format
|
|
14
|
+
|
|
15
|
+
**Recommended for APIs, databases, and anywhere IDs are stored or transmitted as text.**
|
|
16
|
+
|
|
17
|
+
#### Why Compact Strings?
|
|
18
|
+
|
|
19
|
+
UUIDs are 128-bit values. When you need to store or transmit them, you have two choices:
|
|
20
|
+
|
|
21
|
+
1. **Binary (16 bytes)**: Most efficient, but not human-readable and requires binary-safe storage
|
|
22
|
+
2. **String**: Human-readable and universally supported, but takes more space
|
|
23
|
+
|
|
24
|
+
If you must use strings (APIs, URLs, text database columns, JSON, logs), the standard UUID format (`01936c0a-5e0c-7b3a-8f9d-2e1c4a6b8d0f`) uses 36 characters. The compact format reduces this to **22 characters** while preserving all the benefits of UUID v7.
|
|
25
|
+
|
|
26
|
+
#### How It Works
|
|
27
|
+
|
|
28
|
+
The compact format uses **Base62 encoding** (digits `0-9`, uppercase `A-Z`, lowercase `a-z`) to represent the 128-bit UUID value. This is similar to how Base64 works, but without special characters like `+`, `/`, or `=`.
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
Standard: 01936c0a-5e0c-7b3a-8f9d-2e1c4a6b8d0f (36 chars)
|
|
32
|
+
Compact: 01JDQYZ9M6K7TCJK2F3W8N (22 chars)
|
|
33
|
+
↑
|
|
34
|
+
Same UUID, different encoding
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The encoding preserves **lexicographic ordering**: if UUID A was generated before UUID B, then `compact(A) < compact(B)` in string comparison. This means database indexes on compact strings maintain time-ordering, and APIs can sort by ID to get chronological order.
|
|
38
|
+
|
|
39
|
+
#### When to Use Compact Strings
|
|
40
|
+
|
|
41
|
+
| Use Case | Recommended Format |
|
|
42
|
+
|----------|-------------------|
|
|
43
|
+
| Database binary column (BINARY(16), BLOB) | Binary (16 bytes) |
|
|
44
|
+
| Database text/VARCHAR column | **Compact (22 chars)** |
|
|
45
|
+
| REST API responses | **Compact (22 chars)** |
|
|
46
|
+
| URLs and query parameters | **Compact (22 chars)** |
|
|
47
|
+
| Logs and debugging | Standard (36 chars) for readability |
|
|
48
|
+
| Interop with systems expecting standard UUIDs | Standard (36 chars) |
|
|
49
|
+
|
|
50
|
+
#### Benefits Summary
|
|
51
|
+
|
|
52
|
+
- **39% shorter** than standard UUID strings (22 vs 36 characters)
|
|
53
|
+
- **Lexicographically sortable** - preserves time-based ordering in databases and APIs
|
|
54
|
+
- **URL-safe** - no special characters, hyphens, or encoding needed
|
|
55
|
+
- **Database-friendly** - smaller indexes, faster queries, less storage
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
Add this line to your application's Gemfile:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
gem 'uuidv7'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
And then execute:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
bundle install
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Or install it yourself as:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
gem install uuidv7
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Usage
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
require 'uuidv7'
|
|
81
|
+
|
|
82
|
+
# Generate a UUID v7 (high performance, no ordering guarantees within same millisecond)
|
|
83
|
+
uuid = UUIDv7.generate
|
|
84
|
+
# => "01936c0a-5e0c-7b3a-8f9d-2e1c4a6b8d0f"
|
|
85
|
+
|
|
86
|
+
# Generate with monotonic ordering (for database primary keys)
|
|
87
|
+
uuid = MonotonicUUIDv7.generate
|
|
88
|
+
# => "01936c0a-5e0c-7b3a-8f9d-2e1c4a6b8d0f"
|
|
89
|
+
|
|
90
|
+
# Extract the timestamp (milliseconds since Unix epoch)
|
|
91
|
+
timestamp = UUIDv7.timestamp(uuid)
|
|
92
|
+
# => 1700000000000
|
|
93
|
+
|
|
94
|
+
# Generate a compact string directly (22 characters, Base62)
|
|
95
|
+
compact_id = UUIDv7.generate_compact
|
|
96
|
+
# => "01JDQYZ9M6K7TCJK2F3W8N"
|
|
97
|
+
|
|
98
|
+
# Convert UUID to compact string (preserves sort order)
|
|
99
|
+
compact = UUIDv7.to_compact(uuid)
|
|
100
|
+
|
|
101
|
+
# Convert compact string back to UUID
|
|
102
|
+
uuid = UUIDv7.from_compact(compact)
|
|
103
|
+
|
|
104
|
+
# Custom clock for testing
|
|
105
|
+
uuid = UUIDv7.generate { 1700000000000 }
|
|
106
|
+
|
|
107
|
+
# Monotonic with custom clock
|
|
108
|
+
uuid = MonotonicUUIDv7.generate { 1700000000000 }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Design Principles
|
|
112
|
+
|
|
113
|
+
**Minimal API Surface**: Simple module methods that return standard UUID strings. No custom UUID class - maximum compatibility with existing code.
|
|
114
|
+
|
|
115
|
+
**Separate Modules for Different Use Cases**: Two distinct implementations for different performance/ordering trade-offs:
|
|
116
|
+
- **`UUIDv7`**: Maximum performance with no synchronization overhead. UUIDs generated in the same millisecond may not be strictly ordered, but uniqueness is maintained through random bits. Ideal for high-throughput scenarios and distributed systems.
|
|
117
|
+
- **`MonotonicUUIDv7`**: Uses a mutex-protected counter to ensure strict ordering within the same millisecond, following RFC 9562 recommendations. Best for database primary keys and scenarios requiring guaranteed sequential ordering.
|
|
118
|
+
|
|
119
|
+
**Timestamp Extraction**: UUIDs contain timing information, and this library makes it easy to extract this for debugging, observability, and time-based queries.
|
|
120
|
+
|
|
121
|
+
**Flexible Generation**: Block-based clock injection for testing or custom time sources.
|
|
122
|
+
|
|
123
|
+
## API Reference
|
|
124
|
+
|
|
125
|
+
### UUIDv7 (High Performance, No Ordering Guarantees)
|
|
126
|
+
|
|
127
|
+
| Method | Description |
|
|
128
|
+
|--------|-------------|
|
|
129
|
+
| `UUIDv7.generate(&clock)` | Generate a new UUID v7 string. Optional block for custom clock. |
|
|
130
|
+
| `UUIDv7.generate_compact(&clock)` | Generate a new UUID v7 as 22-character compact string. |
|
|
131
|
+
| `UUIDv7.timestamp(uuid)` | Extract millisecond timestamp from UUID v7 string. |
|
|
132
|
+
| `UUIDv7.to_compact(uuid)` | Convert UUID string to 22-character compact string. |
|
|
133
|
+
| `UUIDv7.from_compact(compact)` | Convert compact string back to UUID string. |
|
|
134
|
+
|
|
135
|
+
### MonotonicUUIDv7 (Sequential Ordering Guaranteed)
|
|
136
|
+
|
|
137
|
+
| Method | Description |
|
|
138
|
+
|--------|-------------|
|
|
139
|
+
| `MonotonicUUIDv7.generate(&clock)` | Generate a new UUID v7 with strict ordering. Optional block for custom clock. |
|
|
140
|
+
| `MonotonicUUIDv7.generate_compact(&clock)` | Generate with strict ordering as compact string. |
|
|
141
|
+
|
|
142
|
+
## Implementation Details
|
|
143
|
+
|
|
144
|
+
### Format
|
|
145
|
+
|
|
146
|
+
UUID v7 follows RFC 9562:
|
|
147
|
+
- **Bits 0-47**: Unix timestamp in milliseconds (48 bits)
|
|
148
|
+
- **Bits 48-51**: Version field (0111 for v7)
|
|
149
|
+
- **Bits 52-63**: Counter or random bits (12 bits, called `rand_a`)
|
|
150
|
+
- **Bits 64-65**: Variant field (10 for RFC 4122)
|
|
151
|
+
- **Bits 66-127**: Random bits (62 bits, called `rand_b`)
|
|
152
|
+
|
|
153
|
+
### Monotonic Counter Behavior
|
|
154
|
+
|
|
155
|
+
- Counter occupies 12 bits (rand_a field): 0-4095
|
|
156
|
+
- Counter increments with each generation in same millisecond
|
|
157
|
+
- Counter resets to **random value** when timestamp advances
|
|
158
|
+
- If counter overflows (4096 in same ms), waits for next millisecond
|
|
159
|
+
- Thread-safe via Mutex synchronization
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
Apache-2.0
|
data/Rakefile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/uuidv7/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'block-uuidv7'
|
|
7
|
+
spec.version = UUIDv7::VERSION
|
|
8
|
+
spec.authors = ['Block, Inc.']
|
|
9
|
+
spec.email = ['opensource@block.xyz']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'A minimal, high-performance UUID v7 implementation for Ruby'
|
|
12
|
+
spec.description = 'UUID v7 is a time-ordered UUID format that encodes a Unix timestamp in the most significant ' \
|
|
13
|
+
'48 bits, making UUIDs naturally sortable by creation time. This library provides both ' \
|
|
14
|
+
'high-performance and monotonic (strictly ordered) variants.'
|
|
15
|
+
spec.homepage = 'https://github.com/block/uuidv7'
|
|
16
|
+
spec.license = 'Apache-2.0'
|
|
17
|
+
spec.required_ruby_version = '>= 3.0.0'
|
|
18
|
+
|
|
19
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
20
|
+
spec.metadata['source_code_uri'] = 'https://github.com/block/uuidv7/tree/main/ruby'
|
|
21
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
22
|
+
|
|
23
|
+
spec.files = Dir.chdir(__dir__) do
|
|
24
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
25
|
+
(File.expand_path(f) == __FILE__) ||
|
|
26
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
spec.bindir = 'exe'
|
|
30
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
31
|
+
spec.require_paths = ['lib']
|
|
32
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UUIDv7
|
|
4
|
+
module Base62
|
|
5
|
+
ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
|
6
|
+
BASE = 62
|
|
7
|
+
COMPACT_LENGTH = 22
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def encode(uuid)
|
|
11
|
+
raise InvalidUUIDError, 'UUID cannot be nil' if uuid.nil?
|
|
12
|
+
|
|
13
|
+
hex = uuid.delete('-')
|
|
14
|
+
raise InvalidUUIDError, 'Invalid UUID format' unless hex.match?(/\A[0-9a-f]{32}\z/i)
|
|
15
|
+
|
|
16
|
+
value = hex.to_i(16)
|
|
17
|
+
result = []
|
|
18
|
+
|
|
19
|
+
while value.positive?
|
|
20
|
+
value, remainder = value.divmod(BASE)
|
|
21
|
+
result << ALPHABET[remainder]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
result << '0' while result.length < COMPACT_LENGTH
|
|
25
|
+
|
|
26
|
+
result.reverse.join
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def decode(compact_string)
|
|
30
|
+
raise InvalidCompactStringError, 'Compact string cannot be nil' if compact_string.nil?
|
|
31
|
+
|
|
32
|
+
unless compact_string.length == COMPACT_LENGTH
|
|
33
|
+
raise InvalidCompactStringError,
|
|
34
|
+
"Compact string must be exactly #{COMPACT_LENGTH} characters (got #{compact_string.length})"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
value = 0
|
|
38
|
+
compact_string.each_char do |char|
|
|
39
|
+
digit = ALPHABET.index(char)
|
|
40
|
+
raise InvalidCompactStringError, "Invalid compact string character: #{char}" if digit.nil?
|
|
41
|
+
|
|
42
|
+
value = (value * BASE) + digit
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
msb = (value >> 64) & 0xFFFFFFFFFFFFFFFF
|
|
46
|
+
lsb = value & 0xFFFFFFFFFFFFFFFF
|
|
47
|
+
|
|
48
|
+
hex = format('%016x%016x', msb, lsb)
|
|
49
|
+
"#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module UUIDv7
|
|
6
|
+
module Generator
|
|
7
|
+
class << self
|
|
8
|
+
def generate(&clock)
|
|
9
|
+
timestamp = clock ? clock.call : (Time.now.to_f * 1000).to_i
|
|
10
|
+
rand_a = rand(4096)
|
|
11
|
+
rand_b = SecureRandom.random_number(2**62)
|
|
12
|
+
|
|
13
|
+
build(timestamp, rand_a, rand_b)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def generate_compact(&clock)
|
|
17
|
+
Base62.encode(generate(&clock))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def timestamp(uuid)
|
|
21
|
+
raise InvalidUUIDError, 'UUID cannot be nil' if uuid.nil?
|
|
22
|
+
|
|
23
|
+
hex = uuid.delete('-')
|
|
24
|
+
raise InvalidUUIDError, 'Invalid UUID format' unless hex.match?(/\A[0-9a-f]{32}\z/i)
|
|
25
|
+
|
|
26
|
+
msb = hex[0, 16].to_i(16)
|
|
27
|
+
version = (msb >> 12) & 0xF
|
|
28
|
+
|
|
29
|
+
raise InvalidUUIDError, "UUID is not version 7 (got version #{version})" unless version == 7
|
|
30
|
+
|
|
31
|
+
msb >> 16
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def build(timestamp, rand_a, rand_b)
|
|
35
|
+
rand_a &= 0xFFF
|
|
36
|
+
|
|
37
|
+
msb = (timestamp << 16) | rand_a
|
|
38
|
+
lsb = rand_b
|
|
39
|
+
|
|
40
|
+
msb = (msb & 0xFFFFFFFFFFFF0FFF) | 0x0000000000007000
|
|
41
|
+
lsb = (lsb & 0x3FFFFFFFFFFFFFFF) | 0x8000000000000000
|
|
42
|
+
|
|
43
|
+
format_uuid(msb, lsb)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def format_uuid(msb, lsb)
|
|
49
|
+
hex = format('%016x%016x', msb, lsb)
|
|
50
|
+
"#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module UUIDv7
|
|
6
|
+
module MonotonicGenerator
|
|
7
|
+
COUNTER_MAX = 0xFFF # 12 bits = 4095
|
|
8
|
+
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
@last_timestamp = 0
|
|
11
|
+
@counter = 0
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def generate(&clock)
|
|
15
|
+
@mutex.synchronize do
|
|
16
|
+
timestamp = clock ? clock.call : (Time.now.to_f * 1000).to_i
|
|
17
|
+
counter_value = nil
|
|
18
|
+
|
|
19
|
+
if timestamp == @last_timestamp
|
|
20
|
+
@counter = (@counter + 1) & COUNTER_MAX
|
|
21
|
+
|
|
22
|
+
if @counter.zero?
|
|
23
|
+
loop do
|
|
24
|
+
timestamp = clock ? clock.call : (Time.now.to_f * 1000).to_i
|
|
25
|
+
break if timestamp != @last_timestamp
|
|
26
|
+
|
|
27
|
+
sleep(0.0001) unless clock
|
|
28
|
+
end
|
|
29
|
+
@counter = SecureRandom.random_number(COUNTER_MAX + 1)
|
|
30
|
+
end
|
|
31
|
+
counter_value = @counter
|
|
32
|
+
else
|
|
33
|
+
@counter = SecureRandom.random_number(COUNTER_MAX + 1)
|
|
34
|
+
counter_value = @counter
|
|
35
|
+
@last_timestamp = timestamp
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
rand_b = SecureRandom.random_number(2**62)
|
|
39
|
+
Generator.build(timestamp, counter_value, rand_b)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def generate_compact(&clock)
|
|
44
|
+
Base62.encode(generate(&clock))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def reset_state!
|
|
48
|
+
@mutex.synchronize do
|
|
49
|
+
@last_timestamp = 0
|
|
50
|
+
@counter = 0
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/uuidv7.rb
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'uuidv7/version'
|
|
4
|
+
require_relative 'uuidv7/base62'
|
|
5
|
+
require_relative 'uuidv7/generator'
|
|
6
|
+
require_relative 'uuidv7/monotonic_generator'
|
|
7
|
+
|
|
8
|
+
module UUIDv7
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
class InvalidUUIDError < Error; end
|
|
11
|
+
class InvalidCompactStringError < Error; end
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def generate(&clock)
|
|
15
|
+
Generator.generate(&clock)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def generate_compact(&clock)
|
|
19
|
+
Generator.generate_compact(&clock)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def timestamp(uuid)
|
|
23
|
+
Generator.timestamp(uuid)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_compact(uuid)
|
|
27
|
+
Base62.encode(uuid)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def from_compact(compact_string)
|
|
31
|
+
Base62.decode(compact_string)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
module MonotonicUUIDv7
|
|
37
|
+
class << self
|
|
38
|
+
def generate(&clock)
|
|
39
|
+
UUIDv7::MonotonicGenerator.generate(&clock)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def generate_compact(&clock)
|
|
43
|
+
UUIDv7::MonotonicGenerator.generate_compact(&clock)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: block-uuidv7
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Block, Inc.
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-01-29 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: UUID v7 is a time-ordered UUID format that encodes a Unix timestamp in
|
|
14
|
+
the most significant 48 bits, making UUIDs naturally sortable by creation time.
|
|
15
|
+
This library provides both high-performance and monotonic (strictly ordered) variants.
|
|
16
|
+
email:
|
|
17
|
+
- opensource@block.xyz
|
|
18
|
+
executables: []
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- ".rspec"
|
|
23
|
+
- ".rubocop.yml"
|
|
24
|
+
- AGENTS.md
|
|
25
|
+
- README.md
|
|
26
|
+
- Rakefile
|
|
27
|
+
- block-uuidv7.gemspec
|
|
28
|
+
- lib/uuidv7.rb
|
|
29
|
+
- lib/uuidv7/base62.rb
|
|
30
|
+
- lib/uuidv7/generator.rb
|
|
31
|
+
- lib/uuidv7/monotonic_generator.rb
|
|
32
|
+
- lib/uuidv7/version.rb
|
|
33
|
+
homepage: https://github.com/block/uuidv7
|
|
34
|
+
licenses:
|
|
35
|
+
- Apache-2.0
|
|
36
|
+
metadata:
|
|
37
|
+
homepage_uri: https://github.com/block/uuidv7
|
|
38
|
+
source_code_uri: https://github.com/block/uuidv7/tree/main/ruby
|
|
39
|
+
rubygems_mfa_required: 'true'
|
|
40
|
+
post_install_message:
|
|
41
|
+
rdoc_options: []
|
|
42
|
+
require_paths:
|
|
43
|
+
- lib
|
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: 3.0.0
|
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
requirements: []
|
|
55
|
+
rubygems_version: 3.4.19
|
|
56
|
+
signing_key:
|
|
57
|
+
specification_version: 4
|
|
58
|
+
summary: A minimal, high-performance UUID v7 implementation for Ruby
|
|
59
|
+
test_files: []
|