You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[bug] : Sortable generator makes timestamps its own decoder rejects, and one bad clock reading breaks the generator for good #55
There are two linked problems in createSortableId.
1. It makes timestamps its own decoder rejects.
Since #29, getTimestamp refuses decoded values above 2^48−1 ms (the year 10889). The generator only checks that the time fits the character width, and 9 base62 characters can hold far more than 2^48.
consts=createSortableId({now: ()=>2**48})("evt");// "evt_1HvWXNAa8rJNAzRwphhk3h53w", with no errorgetTimestamp(s);// undefinedconstus=createSortableId({now: ()=>1760000000000000})("evt");// a microsecond clockgetDate(us);// undefinedgetTimestampOrThrow(us);// TypeError: prefid: "evt_…" is not a well-formed sortable id.
The same happens with timestampSize: 12 plus now: () => 2 ** 50, and with BASE32_CROCKFORD at 2^48 ("e_8000000000…"). A monotonic spill-over at exactly 2^48−1 also makes unreadable IDs. The value 2^48−1 itself round-trips.
2. One bad clock reading breaks the generator for good.
In src/generators/sortable.ts, lastTime and lastRandom are saved beforeencodeTime checks the value. After one out-of-range reading, every later call reuses the bad time.
letfirst=true;constg=createSortableId({now: ()=>(first ? ((first=false),62**9) : Date.now())});for(leti=0;i<4;i++){try{g("e");}catch(e){console.log(e.message);}}// 4 x: prefid: timestamp 13537086546263552 does not fit in 9 "0".."z" characters.
The clock is back to Date.now(), but every call still throws. If the first reading is 2 ** 48 + 5 instead, nothing throws, but every later ID is pinned above 2^48 and decodes to undefined:
now is documented for custom clocks and epochs. Passing microseconds by mistake is plausible, and then every ID is "not well-formed" with no error when it's created. One clock glitch shouldn't poison a long-running process.
What to do
In the generator, compute the candidate time and random indices first. Check them (time <= SORTABLE_TIME_MAX, and the value fits the width). Only then save lastTime/lastRandom. Throw:
RangeError: prefid: timestamp X exceeds the sortable range (2^48-1 ms).
Important: if you add the throw without moving the state update, fixing problem 1 triggers problem 2, so fix both together. Raising the decoder's ceiling instead would undo #29.
Acceptance criteria
now: () => 2 ** 48 throws a RangeError with default options, with timestampSize: 12 and with BASE32_CROCKFORD.
2^48−1 still works.
A spill-over past 2^48−1 throws, and the next call with a normal clock works.
Both poisoning repros above: after one bad reading, the next normal reading gives a correct, decodable ID.
Correction to the acceptance criteria: after a spill-over past 2^48-1, the generator should keep throwing, not recover — no valid ID can sort after the ones it already issued. On an error it should simply not save the bad value, without resetting its state. Details in #57.
Activity
suhailopensource commentedon Oct 13, 2026
Correction to the acceptance criteria: after a spill-over past 2^48-1, the generator should keep throwing, not recover — no valid ID can sort after the ones it already issued. On an error it should simply not save the bad value, without resetting its state. Details in #57.