Skip to content

Commit c4c1a5f

Browse files
committed
Improve monotonic API
1 parent 524f6b2 commit c4c1a5f

4 files changed

Lines changed: 136 additions & 66 deletions

File tree

README.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Instead, herein is proposed ULID:
2727
- Uses Crockford's base32 for better efficiency and readability (5 bits per character)
2828
- Case insensitive
2929
- No special characters (URL safe)
30+
- Monotonic sort order (correctly detects and handles the same millisecond)
3031

3132
## JavaScript
3233

@@ -43,8 +44,13 @@ import ulid from 'ulid'
4344

4445
ulid() // 01ARZ3NDEKTSV4RRFFQ69G5FAV
4546

46-
// You can also input a seed time which will consistently give you the same time component
47-
// This is useful for migrating to ulid
47+
// You can also input a seed time which will consistently
48+
// give you the same string for the time component. This is
49+
// useful for migrating to ulid.
50+
//
51+
// Note that multiple calls with the same seed time will
52+
// still monotonically increase the random component for
53+
// strict sort order.
4854
ulid(1469918176385) // 01ARYZ6S41TSV4RRFFQ69G5FAV
4955
```
5056

@@ -118,6 +124,18 @@ Below is the current specification of ULID as implemented in this repository.
118124

119125
The left-most character must be sorted first, and the right-most character sorted last (lexical order). The default ASCII character set must be used. Within the same millisecond, sort order is not guaranteed
120126

127+
#### Monotonicity
128+
129+
When generating a ULID within the same millisecond, we can provide some
130+
guarantees regarding sort order. Namely, if the same millisecond is detected, the `random` component is incremented by 1 bit in the least significant bit position (with carrying). For example:
131+
132+
```javascript
133+
ulid() // 01BX5ZZKBKACTAV9WEVGEMMVRZ
134+
ulid() // 01BX5ZZKBKACTAV9WEVGEMMVS0
135+
```
136+
137+
If, in the extremely unlikely event that, you manage to generate at most 80 ^ 2 ULIDs within the same millisecond, or cause the random component to overflow in any other way, the generation will fail.
138+
121139
### Canonical String Representation
122140

123141
```

index.js

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -92,23 +92,30 @@ var factory = function (prng) {
9292
}
9393
return time;
9494
}
95-
var lastEncodedTime;
96-
var lastRandom;
95+
function createMonotonic() {
96+
var lastTime = 0;
97+
var lastRandom;
98+
return function ulid(seedTime) {
99+
if (isNaN(seedTime)) {
100+
seedTime = Date.now();
101+
}
102+
if (seedTime <= lastTime) {
103+
var incrementedRandom = lastRandom = incrementBase32(lastRandom);
104+
return encodeTime(lastTime, TIME_LEN) + incrementedRandom;
105+
}
106+
lastTime = seedTime;
107+
var newRandom = lastRandom = encodeRandom(RANDOM_LEN);
108+
return encodeTime(seedTime, TIME_LEN) + newRandom;
109+
};
110+
}
97111
var ulid = function ulid(seedTime) {
98-
if (isNaN(seedTime) === false) {
99-
return encodeTime(seedTime, TIME_LEN) + encodeRandom(RANDOM_LEN);
100-
}
101-
var currTime = Date.now();
102-
var encodedTime = encodeTime(currTime, TIME_LEN);
103-
if (encodedTime === lastEncodedTime) {
104-
var incrementedRandom = lastRandom = incrementBase32(lastRandom);
105-
return encodedTime + incrementedRandom;
112+
if (isNaN(seedTime)) {
113+
seedTime = Date.now();
106114
}
107-
lastEncodedTime = encodedTime;
108-
var newRandom = lastRandom = encodeRandom(RANDOM_LEN);
109-
return encodedTime + newRandom;
115+
return encodeTime(seedTime, TIME_LEN) + encodeRandom(RANDOM_LEN);
110116
};
111117
ulid.prng = prng;
118+
ulid.createMonotonic = createMonotonic;
112119
ulid.incrementBase32 = incrementBase32;
113120
ulid.randomChar = randomChar;
114121
ulid.encodeTime = encodeTime;

index.ts

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
11
"use strict";
22

3-
const factory = (prng) => {
3+
interface PRNG {
4+
(): number
5+
}
6+
7+
interface ULID {
8+
(seedTime?: number): string
9+
}
10+
11+
interface ExportedObject extends ULID {
12+
createMonotonic(): ULID
13+
prng: PRNG
14+
incrementBase32(str: string): string
15+
randomChar(): string
16+
encodeTime(now: number, len: number): string
17+
encodeRandom(len: number): string
18+
decodeTime(id: string): number
19+
factory(prng: any): ExportedObject
20+
}
21+
22+
const factory = (prng: PRNG): ExportedObject => {
423

524
// These values should NEVER change. If
625
// they do, we're no longer making ulids!
@@ -101,36 +120,32 @@ const factory = (prng) => {
101120
return time
102121
}
103122

104-
interface ULID {
105-
(seedTime?: number): string
106-
prng(): number
107-
incrementBase32(str: string): string
108-
randomChar(): string
109-
encodeTime(now: number, len: number): string
110-
encodeRandom(len: number): string
111-
decodeTime(id: string): number
112-
factory(prng: any): ULID
123+
function createMonotonic(): ULID {
124+
let lastTime: number = 0
125+
let lastRandom: string
126+
return function ulid(seedTime?: number): string {
127+
if (isNaN(seedTime)) {
128+
seedTime = Date.now()
129+
}
130+
if (seedTime <= lastTime) {
131+
const incrementedRandom = lastRandom = incrementBase32(lastRandom)
132+
return encodeTime(lastTime, TIME_LEN) + incrementedRandom
133+
}
134+
lastTime = seedTime
135+
const newRandom = lastRandom = encodeRandom(RANDOM_LEN)
136+
return encodeTime(seedTime, TIME_LEN) + newRandom
137+
}
113138
}
114139

115-
let lastEncodedTime: string
116-
let lastRandom: string
117-
118140
const ulid = function ulid(seedTime?: number): string {
119-
if (isNaN(seedTime) === false) {
120-
return encodeTime(seedTime, TIME_LEN) + encodeRandom(RANDOM_LEN)
121-
}
122-
const currTime = Date.now()
123-
const encodedTime = encodeTime(currTime, TIME_LEN)
124-
if (encodedTime === lastEncodedTime) {
125-
const incrementedRandom = lastRandom = incrementBase32(lastRandom)
126-
return encodedTime + incrementedRandom
141+
if (isNaN(seedTime)) {
142+
seedTime = Date.now()
127143
}
128-
lastEncodedTime = encodedTime
129-
const newRandom = lastRandom = encodeRandom(RANDOM_LEN)
130-
return encodedTime + newRandom
131-
} as ULID
144+
return encodeTime(seedTime, TIME_LEN) + encodeRandom(RANDOM_LEN)
145+
} as ExportedObject
132146

133147
ulid.prng = prng
148+
ulid.createMonotonic = createMonotonic
134149
ulid.incrementBase32 = incrementBase32
135150
ulid.randomChar = randomChar
136151
ulid.encodeTime = encodeTime

test.js

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,9 @@ describe('ulid', function() {
1010
assert.strictEqual(false, isNaN(ulid.prng()))
1111
})
1212

13-
it('should be between 0 and 1, tested many times', function() {
14-
for (var x = 0; x < 1000; x++) {
15-
var num = ulid.prng()
16-
assert(num >= 0 && num <= 1)
17-
}
13+
it('should be between 0 and 1', function() {
14+
var num = ulid.prng()
15+
assert(num >= 0 && num <= 1)
1816
})
1917

2018
})
@@ -45,7 +43,7 @@ describe('ulid', function() {
4543

4644
var sample = {}
4745

48-
for (var x = 0; x < 100000; x++) {
46+
for (var x = 0; x < 320000; x++) {
4947
char = String(ulid.randomChar()) // for if it were to ever return undefined
5048
if (sample[char] === undefined) {
5149
sample[char] = 0
@@ -170,33 +168,65 @@ describe('ulid', function() {
170168
}
171169

172170
var stubbedUlid = ulid.factory(prng)
173-
var clock
174171

175-
before(function() {
176-
clock = lolex.install({
177-
now: 1469918176385,
178-
toFake: ['Date']
179-
})
180-
})
172+
describe('without seedTime', function() {
181173

182-
after(function() {
183-
clock.uninstall()
184-
})
174+
var monotonic = stubbedUlid.createMonotonic()
175+
var clock
185176

186-
it('first call', function() {
187-
assert.strictEqual('01ARYZ6S41YYYYYYYYYYYYYYYY', stubbedUlid())
188-
})
177+
before(function() {
178+
clock = lolex.install({
179+
now: 1469918176385,
180+
toFake: ['Date']
181+
})
182+
})
189183

190-
it('second call', function() {
191-
assert.strictEqual('01ARYZ6S41YYYYYYYYYYYYYYYZ', stubbedUlid())
192-
})
184+
after(function() {
185+
clock.uninstall()
186+
})
187+
188+
it('first call', function() {
189+
assert.strictEqual('01ARYZ6S41YYYYYYYYYYYYYYYY', monotonic())
190+
})
191+
192+
it('second call', function() {
193+
assert.strictEqual('01ARYZ6S41YYYYYYYYYYYYYYYZ', monotonic())
194+
})
195+
196+
it('third call', function() {
197+
assert.strictEqual('01ARYZ6S41YYYYYYYYYYYYYYZ0', monotonic())
198+
})
199+
200+
it('fourth call', function() {
201+
assert.strictEqual('01ARYZ6S41YYYYYYYYYYYYYYZ1', monotonic())
202+
})
193203

194-
it('third call', function() {
195-
assert.strictEqual('01ARYZ6S41YYYYYYYYYYYYYYZ0', stubbedUlid())
196204
})
197205

198-
it('fourth call', function() {
199-
assert.strictEqual('01ARYZ6S41YYYYYYYYYYYYYYZ1', stubbedUlid())
206+
describe('with seedTime', function() {
207+
208+
var monotonic = stubbedUlid.createMonotonic()
209+
210+
it('first call', function() {
211+
assert.strictEqual('01ARYZ6S41YYYYYYYYYYYYYYYY', monotonic(1469918176385))
212+
})
213+
214+
it('second call with the same', function() {
215+
assert.strictEqual('01ARYZ6S41YYYYYYYYYYYYYYYZ', monotonic(1469918176385))
216+
})
217+
218+
it('third call with less than', function() {
219+
assert.strictEqual('01ARYZ6S41YYYYYYYYYYYYYYZ0', monotonic(100000000))
220+
})
221+
222+
it('fourth call with even more less than', function() {
223+
assert.strictEqual('01ARYZ6S41YYYYYYYYYYYYYYZ1', monotonic(10000))
224+
})
225+
226+
it('fifth call with 1 greater than', function() {
227+
assert.strictEqual('01ARYZ6S42YYYYYYYYYYYYYYYY', monotonic(1469918176386))
228+
})
229+
200230
})
201231

202232
})

0 commit comments

Comments
 (0)