mirror of
https://github.com/Wessel/Snowflakey.git
synced 2026-07-22 00:08:15 +02:00
Rewrite into TypeScript
This commit is contained in:
48
lib/Master.ts
Normal file
48
lib/Master.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { SnowflakeWorker } from './types';
|
||||
|
||||
export default class SnowflakeMaster extends EventEmitter {
|
||||
public workers: any[]
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.setMaxListeners(1000);
|
||||
this.workers = [];
|
||||
}
|
||||
|
||||
addWorkers(...workers: SnowflakeWorker[]): void {
|
||||
for (const worker of workers) {
|
||||
this.workers.push(worker);
|
||||
}
|
||||
|
||||
return this.refresh();
|
||||
}
|
||||
|
||||
listWorkers(): SnowflakeWorker[] {
|
||||
return this.workers;
|
||||
}
|
||||
|
||||
removeWorkers(...identities: string[] | number[]): { 'removed': number } {
|
||||
let found = 0;
|
||||
|
||||
for (const identity of identities) {
|
||||
for (let i in this.workers) {
|
||||
const worker = this.workers[i];
|
||||
if (worker.options.name === identity || worker.options.workerId === identity) {
|
||||
found++;
|
||||
this.workers.splice(parseInt(i), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { removed: found }
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
for (let worker of this.workers) {
|
||||
worker.on('newSnowflake', (...args) => this.emit('newSnowflake', ...args));
|
||||
worker.on('deconstructedFlake', (...args) => this.emit('deconstructedFlake', ...args));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
156
lib/Worker.ts
Normal file
156
lib/Worker.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import big from './bigInt';
|
||||
import { EventEmitter } from 'events';
|
||||
import { sleep, getBits } from './util'
|
||||
import { Snowflake, SnowflakeConfig, SnowflakeMutable } from './types';
|
||||
|
||||
export default class SnowflakeWorker extends EventEmitter {
|
||||
public options: SnowflakeConfig;
|
||||
private _mutable: SnowflakeMutable;
|
||||
private _maxIncrement: Number;
|
||||
public workerId: number;
|
||||
public processId: number;
|
||||
|
||||
constructor(options: SnowflakeConfig) {
|
||||
super();
|
||||
// The default options for the generator
|
||||
this.setMaxListeners(100);
|
||||
this.options = {
|
||||
name: undefined,
|
||||
async: false,
|
||||
epoch: null,
|
||||
workerId: 0,
|
||||
processId: 0,
|
||||
stringify: true,
|
||||
workerBits: 5,
|
||||
processBits: 5,
|
||||
incrementBits: 12,
|
||||
...options
|
||||
};
|
||||
|
||||
// an object containing mutable (unfrozen) properties
|
||||
this._mutable = {
|
||||
locks: [],
|
||||
locked: false,
|
||||
increment: big.zero.subtract(1),
|
||||
lastTimestamp: Date.now()
|
||||
};
|
||||
|
||||
if (this.options.incrementBits + this.options.processBits + this.options.workerBits !== 22) throw new Error('incrementBits, processBits, and workerBits must add up to 22.');
|
||||
// ensure that ids conform to the number of bits
|
||||
this.options.workerId = this.options.workerId % (2 ** this.options.workerBits);
|
||||
this.options.processId = this.options.processId % (2 ** this.options.processBits);
|
||||
|
||||
// check if NaN
|
||||
if (isNaN(this.options.workerId)) this.options.workerId = 0;
|
||||
if (isNaN(this.options.processId)) this.options.processId = 0;
|
||||
|
||||
// store the maximum increment bound
|
||||
this._maxIncrement = 2 ** this.options.incrementBits;
|
||||
|
||||
// calculate the shifted worker/process ids for later reference
|
||||
this.workerId = big(this.options.workerId).shiftLeft(this.options.incrementBits + this.options.processBits);
|
||||
this.processId = big(this.options.processId).shiftLeft(this.options.incrementBits);
|
||||
|
||||
// freeze immutable objects to prevent tampering
|
||||
Object.freeze(this.options);
|
||||
// Object.freeze(this);
|
||||
}
|
||||
|
||||
get increment(): number {
|
||||
return this._mutable.increment = this._mutable.increment.next().mod(this._maxIncrement);
|
||||
}
|
||||
|
||||
generate(): Snowflake | Promise<Snowflake> {
|
||||
if (this.options.async) return this._generateAsync();
|
||||
else return this._generate();
|
||||
}
|
||||
|
||||
_generate(date: number = Date.now(), increment: number = this.increment): Snowflake {
|
||||
// 0000000000000000000000000000000000000000000000000000000000000000
|
||||
// aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000000000000000
|
||||
let flake: any = big(date).minus(this.options.epoch).shiftLeft(22)
|
||||
// aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbb00000000000000000
|
||||
.add(this.workerId)
|
||||
// aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbccccc000000000000
|
||||
.add(this.processId)
|
||||
// aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbcccccdddddddddddd
|
||||
.add(increment);
|
||||
|
||||
this.emit('newSnowflake', {
|
||||
worker: this,
|
||||
method: 'sync',
|
||||
snowflake: flake.toString(),
|
||||
});
|
||||
|
||||
if (this.options.stringify) flake = flake.toString();
|
||||
return flake;
|
||||
}
|
||||
|
||||
_lock() {
|
||||
if (this._mutable.locked) return new Promise(res => this._mutable.locks.push(res));
|
||||
else this._mutable.locked = true;
|
||||
}
|
||||
|
||||
_unlock(): void {
|
||||
if (this._mutable.locks.length > 0) this._mutable.locks.shift()();
|
||||
else this._mutable.locked = false;
|
||||
}
|
||||
|
||||
async _generateAsync(): Promise<Snowflake> {
|
||||
let lock = this._lock();
|
||||
if (lock) await lock;
|
||||
let now = Date.now();
|
||||
// check if increment should be reset
|
||||
if (this._mutable.lastTimestamp !== now) {
|
||||
// last timestamp didnt match, reset increment
|
||||
this._mutable.increment = big.zero;
|
||||
this._mutable.lastTimestamp = now;
|
||||
} else {
|
||||
// last timestamp matched, increase increment
|
||||
this._mutable.increment = this._mutable.increment.next();
|
||||
// check if increment exceeds max bounds
|
||||
if (this._mutable.increment.greaterOrEquals(this._maxIncrement)) {
|
||||
// sleep for 2ms - 1ms has a risk of timestamp not incrementing for some reason?
|
||||
await sleep(2 / 1000);
|
||||
// reset increment
|
||||
this._mutable.increment = big.zero;
|
||||
now = this._mutable.lastTimestamp = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
// generate a snowflake with the new increment
|
||||
let flake: Snowflake = this._generate(now, this._mutable.increment);
|
||||
this._unlock();
|
||||
this.emit('newSnowflake', {
|
||||
worker: this,
|
||||
method: 'async',
|
||||
snowflake: flake.toString(),
|
||||
});
|
||||
|
||||
return flake;
|
||||
}
|
||||
|
||||
deconstruct(snowflake: Snowflake): object {
|
||||
// turn snowflake into a bigint
|
||||
let flake = big(snowflake);
|
||||
// shift right, and add epoch to obtain timestamp
|
||||
let timestamp = flake.shiftRight(22).add(this.options.epoch);
|
||||
|
||||
//obtain workerId
|
||||
let wBitShift = this.options.incrementBits + this.options.processBits;
|
||||
let workerId = flake.and(big(getBits(this.options.workerBits)).shiftLeft(wBitShift)).shiftRight(wBitShift);
|
||||
|
||||
// obtain processId
|
||||
let processId = flake.and(big(getBits(this.options.processBits)).shiftLeft(this.options.incrementBits)).shiftRight(this.options.incrementBits);
|
||||
|
||||
// obtain increment
|
||||
let increment = flake.and(getBits(this.options.incrementBits));
|
||||
|
||||
this.emit('deconstructedFlake', {
|
||||
worker: this,
|
||||
method: 'sync',
|
||||
timestamp, workerId, processId, increment
|
||||
});
|
||||
return { timestamp, workerId, processId, increment };
|
||||
}
|
||||
};
|
||||
1
lib/bigInt.js
Normal file
1
lib/bigInt.js
Normal file
File diff suppressed because one or more lines are too long
6
lib/snowflakey.ts
Normal file
6
lib/snowflakey.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const Master = require('./Master').default;
|
||||
export const Worker = require('./Worker').default;
|
||||
export const lookup = (flake: number, epoch: number): string => {
|
||||
return new Date((flake / 4194304) + epoch).toLocaleString();
|
||||
};
|
||||
|
||||
33
lib/types.ts
Normal file
33
lib/types.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export interface SnowflakeConfig {
|
||||
name?: string;
|
||||
async?: boolean;
|
||||
epoch?: number;
|
||||
workerId?: any,
|
||||
processId?: number,
|
||||
stringify?: boolean,
|
||||
workerBits: number,
|
||||
processBits: number,
|
||||
incrementBits: number
|
||||
}
|
||||
|
||||
export interface SnowflakeMutable {
|
||||
locks: any;
|
||||
locked: boolean;
|
||||
increment: any;
|
||||
lastTimestamp: number;
|
||||
}
|
||||
|
||||
export type Snowflake = number;
|
||||
|
||||
export interface SnowflakeWorker {
|
||||
options: SnowflakeConfig;
|
||||
workerId: number;
|
||||
_mutable: SnowflakeMutable;
|
||||
processId: number;
|
||||
_maxIncrement: number;
|
||||
_lock(): void;
|
||||
_unlock(): void;
|
||||
generate(): Snowflake;
|
||||
_generate(): Snowflake;
|
||||
_generateAsync(): Snowflake;
|
||||
}
|
||||
18
lib/util.ts
Normal file
18
lib/util.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Halt the event loop for `duration` seconds
|
||||
*
|
||||
* @param {number} [duration=1] - The duration in seconds to wait for
|
||||
*/
|
||||
export const sleep = (duration: number = 1): void => {
|
||||
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, (duration * 1000));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all bits from `bits`
|
||||
*
|
||||
* @param {number} bits - The bits to get bits from
|
||||
* @returns {number} - The found bits
|
||||
*/
|
||||
export const getBits = (bits: number): number => {
|
||||
return (2 ** bits) - 1;
|
||||
};
|
||||
Reference in New Issue
Block a user