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

8
.editorconfig Normal file
View File

@@ -0,0 +1,8 @@
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = false

2
.eslintignore Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
dist/

65
.eslintrc Normal file
View File

@@ -0,0 +1,65 @@
{
"parser": "@typescript-eslint/parser",
"env": {
"es6": true,
"amd": true,
"node": true,
"mongo": true,
"jquery": true,
"browser": true,
"commonjs": true
},
"parserOptions": {
"ecmaVersion": 9,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
"forOf": true,
"spread": true,
"modules": true,
"classes": true,
"generators": true,
"restParams": true,
"regexUFlag": true,
"regexYFlag": true,
"globalReturn": true,
"destructuring": true,
"impliedStrict": true,
"blockBindings": true,
"defaultParams": true,
"octalLiterals": true,
"arrowFunctions": true,
"binaryLiterals": true,
"templateStrings": true,
"superInFunctions": true,
"unicodeCodePointEscapes": true,
"objectLiteralShorthandMethods": true,
"objectLiteralComputedProperties": true,
"objectLiteralDuplicateProperties": true,
"objectLiteralShorthandProperties": true
}
},
"plugins": [],
"rules": {
"semi": "warn",
"indent": [ 0, 2 ],
"strict": "off",
"eqeqeq": "error",
"no-var": "warn",
"no-undef": "warn",
"valid-jsdoc": "warn",
"comma-dangle": "warn",
"no-dupe-args": "warn",
"no-dupe-keys": "warn",
"require-await": "warn",
"spaced-comment": "error",
"space-in-parens": "error",
"no-global-assign": "warn",
"no-duplicate-imports": "error",
"no-dupe-class-members": "error"
},
"globals": {
"_config": false,
"console": false
}
}

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

14
.github/CONTRIBUTING.md vendored Normal file
View File

@@ -0,0 +1,14 @@
# Contributing
**The issue tracker is only for bug reports and enhancement suggestions. If you have a question, please ask it in the [Discord](https://discord.gg/SV7DAE9) instead of opening an issue you will get redirected there anyway.**
If you wish to contribute to Aimaina, feel free to fork the repository and submit a pull request.
[ESLint](https://eslint.org/) is used to correct most typo's you make, so it would be helpful if you added [ESLint](https://eslint.org/) to your editor of choice)
## Setup
To get ready to work on the codebase, please do the following:
1. Fork & clone the repository, and make sure you're on the **master** branch
2. Run `yarn --dev` or `npm install --dev`
4. Code your heart out and test using `yarn test` or `npm run test`!
6. [Submit a pull request](https://github.com/PassTheWessel/aimaina/compare)

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
patreon: wessel
ko_fi: wessel

26
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,26 @@
---
name: Bug Report
about: Report an issue with Aimaina
title: "[BUG] Somenthing broke"
labels: bug
assignees: PassTheWessel
---
**Please describe the problem you are having in as much detail as possible:**
**Include a reproducible code sample here, if possible:**
```js
// Place your code here
```
**Further details:**
- Node.js version:
- Operating system:
<!--
If this applies to you, please check the respective checkbox: [ ] becomes [x].
-->
- [ ] I have also tested the issue on latest master, commit hash:

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Request a feature for Aimaina
title: "[ENHANCEMENT] This is an amazing new idea!"
labels: enhancement
assignees: PassTheWessel
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the ideal solution**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

10
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,10 @@
**Please describe the changes this PR makes and why it should be merged:**
**Status**
- [ ] Code changes have been tested and there aren't any typos in it
**Semantic versioning classification:**
- [ ] This PR changes wumpfetch's core codebase (methods or parameters added)
- [ ] This PR includes breaking changes (methods removed or renamed, parameters moved or removed)
- [ ] This PR **only** includes non-code changes, like changes to documentation, README, etc.

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
dist/

89
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,89 @@
/*
General vscode settings for all my projects
Made by Wessel "wesselgame" T <discord@go2it.eu> (https://github.com/PassTheWessel)
Extensions: rainglow, eslint, tslint, material icon theme, gitlens, fish-vscode, Docker,
Popping and Locking theme, Sass, stylelint, SVG viewer, Trailing spaces, Auto renaming tags
Color themes: Banner (rainglow), Hyrule (rainglow), Azure (rainglow), Github (rainglow),
Heroku (rainglow), Popping and Locking
*/
{
// Window
"window.zoomLevel": 0,
// Editor
"editor.fontSize": 13,
"editor.fontWeight": "200",
"editor.fontFamily": "'SFMono-Regular','Consolas','Liberation Mono','Menlo','Courier','monospace'",
"editor.lineHeight": 20,
"editor.fontLigatures": true,
"editor.cursorStyle": "line",
"editor.cursorWidth": 0,
"editor.cursorBlinking": "blink",
"editor.multiCursorModifier": "ctrlCmd",
"editor.minimap.enabled": true,
"editor.smoothScrolling": true,
"editor.minimap.renderCharacters": true,
"editor.tabSize": 2,
"editor.autoIndent": false,
"editor.insertSpaces": true,
"editor.tabCompletion": "on",
"editor.formatOnPaste": true,
"editor.detectIndentation": false,
"editor.wordWrap": "off",
"editor.matchBrackets": true,
"editor.renderWhitespace": "none",
"editor.autoClosingBrackets": "always",
"editor.tokenColorCustomizations": {
"types": "#C59F61",
"strings": "#F6EB90",
"numbers": "#C54121",
"keywords": "#C59F49",
"comments": "#6a737d",
"variables": "#C55F45",
"functions": "#C59F55"
},
//Workbench
"workbench.iconTheme": "material-icon-theme",
"workbench.colorTheme": "Banner (rainglow)",
"workbench.editor.showTabs": true,
"workbench.editor.tabSizing": "fit",
"workbench.sideBar.location": "left",
// Debug
"debug.toolBarLocation": "floating",
"debug.allowBreakpointsEverywhere": true,
// Console
"terminal.integrated.shell.windows": "C:\\Windows\\System32\\bash.exe", // C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe
// Files
"files.autoSave": "off",
"files.exclude": {
"**/.git": false,
"**/.svn": false,
"**/.hg": false,
"**/CVS": false,
"**/.DS_Store": false
},
// Breadcrumbs
"breadcrumbs.enabled": true,
"breadcrumbs.filePath": "last",
"breadcrumbs.symbolPath": "on",
"breadcrumbs.symbolSortOrder": "name",
// Git
"git.enableSmartCommit": true,
"git.ignoreLimitWarning": true,
// ESlint
"eslint.enable": true,
"eslint.packageManager": "yarn",
// Languages
"[yaml]": {
"editor.tabSize": 2,
"editor.autoIndent": false,
"editor.insertSpaces": true
},
"[markdown]": {
"editor.wordWrap": "on",
"editor.quickSuggestions": false
},
// Live share
"liveshare.featureSet": "insiders",
}

18
.vscode/snippets.code-snippets vendored Normal file
View File

@@ -0,0 +1,18 @@
{
// Place your global snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
// Placeholders with the same ids are connected.
// Example:
// "Print to console": {
// "scope": "javascript,typescript",
// "prefix": "log",
// "body": [
// "console.log('$1');",
// "$2"
// ],
// "description": "Log output to console"
// }
}

1
CODEOWNERS Normal file
View File

@@ -0,0 +1 @@
* @PassTheWessel

76
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at discord@go2it.eu. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

View File

@@ -1,7 +1,7 @@
The MIT License (MIT)
Copyright (c) 2019-present Wessel "wesselgame" T
Copyright (c) 2019-present Wessel "wesselgame" T <discord@go2it.eu>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -5,38 +5,58 @@
## Installing
```sh
$ yarn add snowflakey # Install w/ Yarn (the superior package manager)
$ yarn add snowflakey # Install w/ Yarn
$ npm i snowflakey # Install w/ NPM
```
## Usage
##### Code
##### Using a worker
```js
// require & generate the instance
const Snowflake = require( './generator' );
const snowflake = new Snowflake.generator({
processBits: 0,
// Declare snowflakey
const snowflakey = require('snowflakey');
// Create the worker instance
const Worker = new snowflakey.Worker({
name: 'starling',
epoch: 1420070400000,
workerId: process.env.CLUSTER_ID || 31,
processId: process.pid || undefined,
workerBits: 8,
incrementBits: 14,
workerId: process.env.CLUSTER_ID || 31
processBits: 0,
incrementBits: 14
});
// exports for global use
exports.makeSnowflake = ( date ) => { return snowflake._generate( date ); };
exports.unmakeSnowflake = ( flake ) => { let decon = snowflake.deconstruct( flake ); return decon.timestamp.valueOf(); };
// Generate the snowflake
const flake = Worker.generate();
console.log(`Created snowflake: ${flake}`);
console.log(`Creation date : ${snowflakey.lookup(flake, worker.options.epoch)}`);
console.log(`Deconstructed : ${Worker.deconstruct(flake).timestamp.valueOf()}`);
```
// example
const flake = this.makeSnowflake( Date.now() );
console.log( flake );
console.log( `Creation date: ${Snowflake.lookup( flake, 1420070400000 )}` );
console.log( this.unmakeSnowflake( flake ) );
##### Using a master
```js
// ... Worker code
// Create the master instance and add the worker
const Master = new snowflakey.Master();
master.addWorker(Worker);
// Listen to the events
master.on('newSnowflake', (data) => {
console.log(`created snowflake: ${data.snowflake} by Worker ${data.worker.options.name || data.worker.options.workerId}`)
console.log(`Creation date : ${Snowflake.lookup(flake, data.worker.options.epoch)}`);
data.worker.deconstruct(data.snowflake);
});
master.on('deconstructedFlake', (data) => {
console.log(`Deconstructed : ${data.timestamp.valueOf()} by Worker ${data.worker.options.name || data.worker.options.workerId}`);
});
// Make the worker generate a snowflake
worker.generate();
```
##### Result
```sh
$ node test.js
534760094454759424
Creation date: 2019-1-15 16:45:41
1547567141880
Created snowflake: 534760094454759424
Creation date : 2019-1-15 16:45:41
Deconstructed : 1547567141880
```
### What is a snowflake?

View File

@@ -1,129 +0,0 @@
const big = require( './fake_node_modules/bigInt' );
const sleep = ( time = 1 ) => { return new Promise( res => { setTimeout( res, time ); } ); };
const getBits = ( bits ) => { return ( 2 ** bits ) - 1; };
exports.lookup = ( flake = 0, epoch = 1420070400000 ) => { return new Date( ( flake / 4194304 ) + epoch ).toLocaleString(); };
exports.generator = class Snowflake {
constructor( options = {} ) {
this.options = Object.assign({
async : false,
epoch : 1420070400000,
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 options and this object, to prevent tampering
Object.freeze( this.options );
Object.freeze( this );
}
get increment() { return this.mutable.increment = this.mutable.increment.next().mod( this.maxIncrement ); }
generate() {
if ( this.options.async ) return this._generateAsync();
else return this._generate();
}
_generate( date, increment = null ) {
// 0000000000000000000000000000000000000000000000000000000000000000
// aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000000000000000
let flake = big( date || Date.now() ).minus( this.options.epoch ).shiftLeft( 22 )
// aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbb00000000000000000
.add( this.workerId )
// aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbccccc000000000000
.add( this.processId )
// aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbcccccdddddddddddd
.add( increment || this.increment );
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() {
if ( this.mutable.locks.length > 0 ) this.mutable.locks.shift()();
else this.mutable.locked = false;
}
async _generateAsync() {
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 );
// reset increment
this.mutable.increment = big.zero;
now = this.mutable.lastTimestamp = Date.now();
}
}
// generate a snowflake with the new increment
let flake = this._generate( now, this.mutable.increment );
this._unlock();
return flake;
}
deconstruct( snowflake ) {
// 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));
return { timestamp, workerId, processId, increment };
}
};

68
index.d.ts vendored Normal file
View File

@@ -0,0 +1,68 @@
// Type definitions for Snowflakey 0.1.0
// Project: Snowflakey
// Definitions by: Wessel "wesselgame" T <discord@go2it.eu>
declare module 'snowflakey' {
import { EventEmitter } from 'events';
export type Snowflake = number;
export function lookup(flake: number, epoch: number);
export class Master extends EventEmitter {
public workers: SnowflakeWorker[];
public refresh(): void;
public listWorkers(): SnowflakeWorker[];
public addWorkers(...workers: any[]): void;
public removeWorkers(...workers: string[] | number[]): { removed: number };
}
export class Worker extends EventEmitter {
public options: SnowflakeConfig;
private _mutable: SnowflakeMutable;
constructor(options: SnowflakeConfig);
private _lock(): void;
private _unlock(): void;
public generate(): Snowflake;
public deconstruct(flake: Snowflake): DeconstructedSnowflake;
private _generate(): Snowflake;
private _generateAsync(): Snowflake;
}
export interface DeconstructedSnowflake {
workerId: number,
timestamp: number,
processId: number,
increment: number
}
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 interface SnowflakeWorker {
options: SnowflakeConfig;
workerId: number;
_mutable: SnowflakeMutable;
processId: number;
_maxIncrement: number;
_lock(): void;
_unlock(): void;
generate(): Snowflake;
_generate(): Snowflake;
_generateAsync(): Snowflake;
}
}

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 };
}
};

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;
};

View File

@@ -1,14 +1,15 @@
{
"name": "snowflakey",
"version": "0.0.2",
"description": "❄️ Snowflake generation/lookup",
"version": "0.1.1",
"description": "❄️ Fast and easy snowflake generation and lookup",
"author": "Wessel \"wesselgame\" T <discord@go2it.eu>",
"main": "generator.js",
"main": "dist/lib/snowflakey.js",
"types": "index.d.ts",
"license": "MIT",
"files": [
"generator.js",
"LICENSE",
"fake_node_modules/"
"dist/lib",
"index.d.ts"
],
"bugs": {
"url": "https://www.github.com/PassTheWessel/Snowflakey/issues"
@@ -21,9 +22,17 @@
"keywords": [
"snowflake",
"generator",
"cli",
"accounts",
"lookup",
"lightweight"
]
],
"scripts": {
"test": "cd ./dist/tests && node main.test.js",
"compile": "tsc"
},
"devDependencies": {
"@types/node": "^12.6.8",
"@typescript-eslint/parser": "^1.13.0"
},
"dependencies": {}
}

18
test.js
View File

@@ -1,18 +0,0 @@
// require & generate the instance
const Snowflake = require( './generator' );
const snowflake = new Snowflake.generator({
processBits: 0,
workerBits: 8,
incrementBits: 14,
workerId: process.env.CLUSTER_ID || 31
});
// exports for global use
exports.makeSnowflake = ( date ) => { return snowflake._generate( date ); };
exports.unmakeSnowflake = ( flake ) => { let decon = snowflake.deconstruct( flake ); return decon.timestamp.valueOf(); };
// example
const flake = this.makeSnowflake( Date.now() );
console.log( flake );
console.log( `Creation date: ${Snowflake.lookup( flake, 1420070400000 )}` );
console.log( this.unmakeSnowflake( flake ) );

40
tests/main.test.ts Normal file
View File

@@ -0,0 +1,40 @@
// require & generate the instance
import * as Snowflake from '../lib/snowflakey';
const master = new Snowflake.Master();
const worker = new Snowflake.Worker({
name: 'starling',
epoch: 1420070400000,
workerId: process.env.CLUSTER_ID || 31,
processId: process.pid || undefined,
workerBits: 8,
processBits: 0,
incrementBits: 14
});
master.addWorkers(worker);
// Using the worker directly
const flake = worker.generate();
const epoch = 1420070400000;
console.log('----------[ Worker ]----------')
console.log(`Created snowflake: ${flake}`);
console.log(`Creation date : ${Snowflake.lookup(flake, worker.options.epoch)}`);
console.log(`Deconstructed : ${worker.deconstruct(flake).timestamp.valueOf()}`);
// Using the master to get events
console.log('----------[ Master ]----------')
master.on('newSnowflake', (data) => {
console.log(`created snowflake: ${data.snowflake} by Worker ${data.worker.options.name || data.worker.options.workerId}`)
console.log(`Creation date : ${Snowflake.lookup(flake, data.worker.options.epoch)}`);
data.worker.deconstruct(data.snowflake);
});
master.on('deconstructedFlake', (data) => {
console.log(`Deconstructed : ${data.timestamp.valueOf()} by Worker ${data.worker.options.name || data.worker.options.workerId}`);
});
worker.generate();
master.removeWorkers(worker.options.name);
console.log(master.listWorkers())

23
tsconfig.json Normal file
View File

@@ -0,0 +1,23 @@
{
"compilerOptions": {
"lib": [ "es2015", "es2016", "es2017", "esnext" ],
"watch": true,
"types": [ "node" ],
"outDir": "./dist",
"target": "esnext",
"module": "commonjs",
"strict": false,
"allowJs": true,
"rootDirs": [ "./lib", "./tests" ],
"declaration": false,
"noImplicitAny": false,
"removeComments": true,
"moduleResolution": "node",
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true
},
"include": [ "lib/**/*", "tests/**/*" ],
"exclude": [ "node_modules" ]
}

80
yarn.lock Normal file
View File

@@ -0,0 +1,80 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@types/eslint-visitor-keys@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==
"@types/json-schema@^7.0.3":
version "7.0.3"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636"
integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==
"@types/node@^12.6.8":
version "12.6.8"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.6.8.tgz#e469b4bf9d1c9832aee4907ba8a051494357c12c"
integrity sha512-aX+gFgA5GHcDi89KG5keey2zf0WfZk/HAQotEamsK2kbey+8yGKcson0hbK8E+v0NArlCJQCqMP161YhV6ZXLg==
"@typescript-eslint/experimental-utils@1.13.0":
version "1.13.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz#b08c60d780c0067de2fb44b04b432f540138301e"
integrity sha512-zmpS6SyqG4ZF64ffaJ6uah6tWWWgZ8m+c54XXgwFtUv0jNz8aJAVx8chMCvnk7yl6xwn8d+d96+tWp7fXzTuDg==
dependencies:
"@types/json-schema" "^7.0.3"
"@typescript-eslint/typescript-estree" "1.13.0"
eslint-scope "^4.0.0"
"@typescript-eslint/parser@^1.13.0":
version "1.13.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-1.13.0.tgz#61ac7811ea52791c47dc9fd4dd4a184fae9ac355"
integrity sha512-ITMBs52PCPgLb2nGPoeT4iU3HdQZHcPaZVw+7CsFagRJHUhyeTgorEwHXhFf3e7Evzi8oujKNpHc8TONth8AdQ==
dependencies:
"@types/eslint-visitor-keys" "^1.0.0"
"@typescript-eslint/experimental-utils" "1.13.0"
"@typescript-eslint/typescript-estree" "1.13.0"
eslint-visitor-keys "^1.0.0"
"@typescript-eslint/typescript-estree@1.13.0":
version "1.13.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz#8140f17d0f60c03619798f1d628b8434913dc32e"
integrity sha512-b5rCmd2e6DCC6tCTN9GSUAuxdYwCM/k/2wdjHGrIRGPSJotWMCe/dGpi66u42bhuh8q3QBzqM4TMA1GUUCJvdw==
dependencies:
lodash.unescape "4.0.1"
semver "5.5.0"
eslint-scope@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848"
integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==
dependencies:
esrecurse "^4.1.0"
estraverse "^4.1.1"
eslint-visitor-keys@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
integrity sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==
esrecurse@^4.1.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf"
integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==
dependencies:
estraverse "^4.1.0"
estraverse@^4.1.0, estraverse@^4.1.1:
version "4.2.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=
lodash.unescape@4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c"
integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=
semver@5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==