@@ -7,7 +7,139 @@ export function generateFriendlyId(prefix: string, size?: number) {
77 return `${ prefix } _${ idGenerator ( size ) } ` ;
88}
99
10- export function generateInternalId ( ) {
10+ // Injected by the server split-mode bootstrap. Default false =
11+ // today's behavior (cuid). Isomorphic file: never read process.env here.
12+ let ksuidMintEnabled = false ;
13+
14+ /** Server bootstrap calls this once with isSplitEnabled(). Off by default. */
15+ export function setKsuidMintEnabled ( enabled : boolean ) : void {
16+ ksuidMintEnabled = enabled ;
17+ }
18+
19+ /** Test/diagnostic read-back of the current mint mode. */
20+ export function isKsuidMintEnabled ( ) : boolean {
21+ return ksuidMintEnabled ;
22+ }
23+
24+ // KSUID epoch (2014-05-13T16:53:20Z) — seconds offset applied to the unix timestamp.
25+ const KSUID_EPOCH = 1_400_000_000 ;
26+ const KSUID_TIMESTAMP_BYTES = 4 ;
27+ export const KSUID_PAYLOAD_BYTES = 16 ;
28+ const KSUID_TOTAL_BYTES = KSUID_TIMESTAMP_BYTES + KSUID_PAYLOAD_BYTES ;
29+ const KSUID_STRING_LENGTH = 27 ;
30+ const BASE62_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" ;
31+
32+ /** Encode raw bytes as base62 (big-endian), left-padded to the given length. */
33+ function base62Encode ( bytes : Uint8Array , length : number ) : string {
34+ const digits = Array . from ( bytes ) ;
35+ let result = "" ;
36+
37+ while ( digits . length > 0 ) {
38+ let remainder = 0 ;
39+ const quotient : number [ ] = [ ] ;
40+
41+ for ( let i = 0 ; i < digits . length ; i ++ ) {
42+ const acc = ( digits [ i ] ?? 0 ) + remainder * 256 ;
43+ const q = Math . floor ( acc / 62 ) ;
44+ remainder = acc % 62 ;
45+
46+ if ( quotient . length > 0 || q > 0 ) {
47+ quotient . push ( q ) ;
48+ }
49+ }
50+
51+ result = BASE62_ALPHABET . charAt ( remainder ) + result ;
52+ digits . length = 0 ;
53+ digits . push ( ...quotient ) ;
54+ }
55+
56+ return result . padStart ( length , BASE62_ALPHABET . charAt ( 0 ) ) ;
57+ }
58+
59+ /**
60+ * 27-char, base62, time-ordered KSUID body (length-disjoint from the 25-char cuid): a 4-byte
61+ * timestamp (seconds since the KSUID epoch) + a 16-byte payload; ids from different seconds
62+ * sort in mint order. Payload defaults to CSPRNG entropy; callers may supply up to
63+ * KSUID_PAYLOAD_BYTES metadata bytes (written first, remainder stays random for uniqueness).
64+ */
65+ export function generateKsuidId ( payload ?: Uint8Array ) : string {
66+ const bytes = new Uint8Array ( KSUID_TOTAL_BYTES ) ;
67+
68+ const timestamp = Math . floor ( Date . now ( ) / 1000 ) - KSUID_EPOCH ;
69+ bytes [ 0 ] = ( timestamp >>> 24 ) & 0xff ;
70+ bytes [ 1 ] = ( timestamp >>> 16 ) & 0xff ;
71+ bytes [ 2 ] = ( timestamp >>> 8 ) & 0xff ;
72+ bytes [ 3 ] = timestamp & 0xff ;
73+
74+ if ( payload && payload . length > KSUID_PAYLOAD_BYTES ) {
75+ throw new Error (
76+ `KSUID payload must be at most ${ KSUID_PAYLOAD_BYTES } bytes (got ${ payload . length } )`
77+ ) ;
78+ }
79+ const reserved = payload ?. length ?? 0 ;
80+ if ( payload && reserved > 0 ) {
81+ bytes . set ( payload , KSUID_TIMESTAMP_BYTES ) ;
82+ }
83+ if ( reserved < KSUID_PAYLOAD_BYTES ) {
84+ globalThis . crypto . getRandomValues ( bytes . subarray ( KSUID_TIMESTAMP_BYTES + reserved ) ) ;
85+ }
86+
87+ return base62Encode ( bytes , KSUID_STRING_LENGTH ) ;
88+ }
89+
90+ /** Decoded parts of a KSUID body: its mint timestamp and 16-byte payload. */
91+ export interface DecodedKsuid {
92+ timestampSeconds : number ;
93+ timestamp : Date ;
94+ payload : Uint8Array ;
95+ }
96+
97+ /**
98+ * Decode a KSUID body (or a `prefix_<body>` friendly id) into its timestamp + 16-byte payload.
99+ * The inverse of generateKsuidId's layout. Throws if the body is not 27 base62 chars.
100+ */
101+ export function decodeKsuid ( idOrFriendlyId : string ) : DecodedKsuid {
102+ const underscore = idOrFriendlyId . indexOf ( "_" ) ;
103+ const body = underscore === - 1 ? idOrFriendlyId : idOrFriendlyId . slice ( underscore + 1 ) ;
104+ if ( body . length !== KSUID_STRING_LENGTH ) {
105+ throw new Error (
106+ `Not a KSUID body: expected ${ KSUID_STRING_LENGTH } base62 chars, got ${ body . length } `
107+ ) ;
108+ }
109+
110+ let n = 0n ;
111+ for ( const ch of body ) {
112+ const digit = BASE62_ALPHABET . indexOf ( ch ) ;
113+ if ( digit < 0 ) {
114+ throw new Error ( `Invalid base62 character in KSUID body: ${ ch } ` ) ;
115+ }
116+ n = n * 62n + BigInt ( digit ) ;
117+ }
118+
119+ const bytes = new Uint8Array ( KSUID_TOTAL_BYTES ) ;
120+ for ( let i = KSUID_TOTAL_BYTES - 1 ; i >= 0 ; i -- ) {
121+ bytes [ i ] = Number ( n & 0xffn ) ;
122+ n >>= 8n ;
123+ }
124+
125+ const timestampSeconds =
126+ ( bytes [ 0 ] ?? 0 ) * 0x1000000 +
127+ ( bytes [ 1 ] ?? 0 ) * 0x10000 +
128+ ( bytes [ 2 ] ?? 0 ) * 0x100 +
129+ ( bytes [ 3 ] ?? 0 ) +
130+ KSUID_EPOCH ;
131+
132+ return {
133+ timestampSeconds,
134+ timestamp : new Date ( timestampSeconds * 1000 ) ,
135+ payload : bytes . slice ( KSUID_TIMESTAMP_BYTES ) ,
136+ } ;
137+ }
138+
139+ export function generateInternalId ( useKsuidWhenEnabled = false ) : string {
140+ if ( useKsuidWhenEnabled && ksuidMintEnabled ) {
141+ return generateKsuidId ( ) ;
142+ }
11143 return cuid ( ) ;
12144}
13145
@@ -58,10 +190,13 @@ export function fromFriendlyId(friendlyId: string, expectedEntityName?: string):
58190}
59191
60192export class IdUtil {
61- constructor ( private entityName : string ) { }
193+ constructor (
194+ private entityName : string ,
195+ private ksuidWhenEnabled = false
196+ ) { }
62197
63198 generate ( ) {
64- const internalId = generateInternalId ( ) ;
199+ const internalId = generateInternalId ( this . ksuidWhenEnabled ) ;
65200
66201 return {
67202 id : internalId ,
@@ -90,9 +225,9 @@ export class IdUtil {
90225export const BackgroundWorkerId = new IdUtil ( "worker" ) ;
91226export const CheckpointId = new IdUtil ( "checkpoint" ) ;
92227export const QueueId = new IdUtil ( "queue" ) ;
93- export const RunId = new IdUtil ( "run" ) ;
228+ export const RunId = new IdUtil ( "run" , true ) ;
94229export const SnapshotId = new IdUtil ( "snapshot" ) ;
95- export const WaitpointId = new IdUtil ( "waitpoint" ) ;
230+ export const WaitpointId = new IdUtil ( "waitpoint" , true ) ;
96231export const BatchId = new IdUtil ( "batch" ) ;
97232export const BulkActionId = new IdUtil ( "bulk" ) ;
98233export const AttemptId = new IdUtil ( "attempt" ) ;
0 commit comments