Skip to content

Instantly share code, notes, and snippets.

@seia-soto
Last active June 28, 2023 19:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save seia-soto/57e2ee628b9179f293cdee960baafbd4 to your computer and use it in GitHub Desktop.
Save seia-soto/57e2ee628b9179f293cdee960baafbd4 to your computer and use it in GitHub Desktop.
Snowflake ID generator in JavaScript using BigInt
/* 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