diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..91a196c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = false \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..04c01ba --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..195eb39 --- /dev/null +++ b/.eslintrc @@ -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 + } +} \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..c924aba --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -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) \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..a51e87b --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +patreon: wessel +ko_fi: wessel diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..f87fa4c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -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: + + + +- [ ] I have also tested the issue on latest master, commit hash: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..7b57c16 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -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. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..1f65093 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -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. \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..04c01ba --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9ffbed9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,89 @@ +/* + General vscode settings for all my projects + Made by Wessel "wesselgame" T (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", +} \ No newline at end of file diff --git a/.vscode/snippets.code-snippets b/.vscode/snippets.code-snippets new file mode 100644 index 0000000..84e0db8 --- /dev/null +++ b/.vscode/snippets.code-snippets @@ -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" + // } +} \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..18dfcd1 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @PassTheWessel \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..f4fc9c2 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -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 diff --git a/LICENSE b/LICENSE index bcd4705..672d702 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ The MIT License (MIT) -Copyright (c) 2019-present Wessel "wesselgame" T +Copyright (c) 2019-present Wessel "wesselgame" T Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 0c6315a..8463fec 100644 --- a/README.md +++ b/README.md @@ -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? diff --git a/generator.js b/generator.js deleted file mode 100644 index adb300a..0000000 --- a/generator.js +++ /dev/null @@ -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 }; - } -}; \ No newline at end of file diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..9f2710b --- /dev/null +++ b/index.d.ts @@ -0,0 +1,68 @@ +// Type definitions for Snowflakey 0.1.0 +// Project: Snowflakey +// Definitions by: Wessel "wesselgame" T +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; + } +} \ No newline at end of file diff --git a/lib/Master.ts b/lib/Master.ts new file mode 100644 index 0000000..fc9ea19 --- /dev/null +++ b/lib/Master.ts @@ -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)); + } + } +} + diff --git a/lib/Worker.ts b/lib/Worker.ts new file mode 100644 index 0000000..eb1aa1e --- /dev/null +++ b/lib/Worker.ts @@ -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 { + 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 { + 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 }; + } +}; \ No newline at end of file diff --git a/fake_node_modules/bigInt.js b/lib/bigInt.js similarity index 100% rename from fake_node_modules/bigInt.js rename to lib/bigInt.js diff --git a/lib/snowflakey.ts b/lib/snowflakey.ts new file mode 100644 index 0000000..9fb358f --- /dev/null +++ b/lib/snowflakey.ts @@ -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(); +}; + diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 0000000..03b0861 --- /dev/null +++ b/lib/types.ts @@ -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; +} \ No newline at end of file diff --git a/lib/util.ts b/lib/util.ts new file mode 100644 index 0000000..c37114b --- /dev/null +++ b/lib/util.ts @@ -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; +}; \ No newline at end of file diff --git a/package.json b/package.json index aa29d08..a9bcebd 100644 --- a/package.json +++ b/package.json @@ -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 ", - "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": {} } diff --git a/test.js b/test.js deleted file mode 100644 index e3e9913..0000000 --- a/test.js +++ /dev/null @@ -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 ) ); \ No newline at end of file diff --git a/tests/main.test.ts b/tests/main.test.ts new file mode 100644 index 0000000..bae1f7b --- /dev/null +++ b/tests/main.test.ts @@ -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()) \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f0d5303 --- /dev/null +++ b/tsconfig.json @@ -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" ] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..dcda6a2 --- /dev/null +++ b/yarn.lock @@ -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==