Rewrite into TypeScript

This commit is contained in:
Wessel T
2019-08-01 16:49:14 +02:00
parent 9e7df46abb
commit 496cd5641c
29 changed files with 863 additions and 174 deletions

48
lib/Master.ts Normal file
View 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
View 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

File diff suppressed because one or more lines are too long

6
lib/snowflakey.ts Normal file
View 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
View 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
View 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;
};