Last active
June 28, 2023 19:17
-
-
Save seia-soto/57e2ee628b9179f293cdee960baafbd4 to your computer and use it in GitHub Desktop.
Snowflake ID generator in JavaScript using BigInt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable no-bitwise */ | |
export enum SnowflakeErrors { | |
EXCEEDED_MAXIMUM_SEQUENCE = 'Exceeded Maximum Sequence', | |
INVALID_MACHINE_ID = 'Invalid Machine Id', | |
INVALID_MACHINE_RANGE = 'Invalid Machine Range', | |
INVALID_PARAMETER_RANGES = 'Invalid Parameter Ranges', | |
INVALID_SEQUENCE_RANGE = 'Invalid Sequence Range', | |
INVALID_TIMESTAMP_EPOCH = 'Invalid Timestamp Epoch', | |
INVALID_TIMESTAMP_RANGE = 'Invalid Timestamp Range', | |
} | |
export type SnowflakeRanges = { | |
machine: number; | |
sequence: number; | |
timestamp: number; | |
}; | |
export type SnowflakeMetadata = { | |
machine: number; | |
sequence: number; | |
timestamp: number; | |
}; | |
export const getBitMask = (size: number) => (BigInt(1) << BigInt(size)) - BigInt(1); | |
export const generateSnowflake = (epoch: number, ranges: SnowflakeRanges, metadata: SnowflakeMetadata) => | |
(BigInt(metadata.timestamp - epoch) << BigInt(ranges.sequence + ranges.machine)) | |
| (BigInt(metadata.machine) << BigInt(ranges.sequence)) | |
| (BigInt(metadata.sequence)); | |
export const parseSnowflake = (epoch: number, ranges: SnowflakeRanges, snowflake: bigint) => ({ | |
machine: (snowflake & getBitMask(ranges.machine + ranges.sequence)) >> BigInt(ranges.sequence), | |
sequence: (snowflake & getBitMask(ranges.sequence)), | |
timestamp: (snowflake >> BigInt(ranges.sequence + ranges.machine)) + BigInt(epoch), | |
}); | |
export class SnowflakeGenerator { | |
sequence = 0; | |
sequenceAt = Date.now(); | |
constructor( | |
readonly epoch: number, | |
readonly machine: number, | |
readonly ranges: SnowflakeRanges = { | |
machine: 8, | |
sequence: 12, | |
timestamp: 44, | |
}, | |
) { | |
if (ranges.machine + ranges.sequence + ranges.timestamp !== 64) { | |
throw new Error(SnowflakeErrors.INVALID_PARAMETER_RANGES + ': The sum of bit ranges should be 64!'); | |
} | |
if (ranges.timestamp < 31) { | |
throw new Error(SnowflakeErrors.INVALID_TIMESTAMP_RANGE + ': The bit range of timestamp should be larger than 31!'); | |
} | |
if (ranges.sequence < 7) { | |
throw new Error(SnowflakeErrors.INVALID_SEQUENCE_RANGE + ': The bit range of sequence should be larger than 7!'); | |
} | |
if (ranges.machine < 7) { | |
throw new Error(SnowflakeErrors.INVALID_MACHINE_RANGE + ': The bit range of machine should be larger than 7!'); | |
} | |
if (epoch < 0 || epoch > Date.now()) { | |
throw new Error(SnowflakeErrors.INVALID_TIMESTAMP_EPOCH + ': The timestamp epoch should be set between zero and current timestamp!'); | |
} | |
if (machine < 0 || machine > (1 << ranges.machine - 1)) { | |
throw new Error(SnowflakeErrors.INVALID_MACHINE_ID + ': The machine id should be set between zero and maximum representable value of bit range of machine!'); | |
} | |
} | |
generate() { | |
const now = Date.now(); | |
if (now !== this.sequenceAt) { | |
this.sequence = 0; | |
this.sequenceAt = now; | |
} | |
const sequence = this.sequence++; | |
if (sequence > (1 << this.ranges.sequence - 1)) { | |
throw new Error(SnowflakeErrors.EXCEEDED_MAXIMUM_SEQUENCE + ': Exceeded maximum representative range of sequence, you can set allowNextTick to disable error and retry automatically in next tick!'); | |
} | |
return generateSnowflake(this.epoch, this.ranges, { | |
machine: this.machine, | |
sequence, | |
timestamp: now, | |
}); | |
} | |
parse(n: bigint) { | |
return parseSnowflake(this.epoch, this.ranges, n); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment