diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..52aa6b3 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "tabWidth": 4, + "useTabs": false, + "singleQuote": true +} + \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 05cdbbb..dbecacd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,13 +1,8 @@ { "files.exclude": { - "lib": true, "yarn.lock": true, "yarn-error.log": true, - ".prettierrc": true, - ".gitignore": true, "node_modules": true, - ".vscode": true, - ".github": false, "coverage": true } } \ No newline at end of file diff --git a/README.md b/README.md index 4747765..faab5d9 100644 --- a/README.md +++ b/README.md @@ -40,3 +40,4 @@ yarn add sunflake ## LICENSE This package is licensed under the [GNU Lesser General Public License](https://www.gnu.org/licenses/lgpl-3.0). +Hexadecimal to decimal convertion within this package is sourced from [hex2dec](http://www.danvk.org/hex2dec.html), which is licensed under the [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0.html) license. diff --git a/package.json b/package.json index bc1e041..6280176 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "devDependencies": { "@types/jest": "^27.0.3", "@types/node": "^17.0.2", + "@types/yup": "^0.29.13", "@typescript-eslint/parser": "^5.2.0", "chalk": "4.0.0", "eslint": "^8.4.0", @@ -35,7 +36,8 @@ "jest": "^27.4.4", "ts-jest": "^27.1.1", "ts-node": "^10.4.0", - "typescript": "^4.5.2" + "typescript": "^4.5.2", + "yup": "^0.32.11" }, "dependencies": {}, "version": "0.0.1" diff --git a/src/hex2dec.ts b/src/hex2dec.ts new file mode 100644 index 0000000..6f6fc24 --- /dev/null +++ b/src/hex2dec.ts @@ -0,0 +1,91 @@ +// Adds two arrays for the given base (10 or 16), returning the result. +// This turns out to be the only "primitive" operation we need. +const add = (x: number[], y: number[], base: number) => { + let z = []; + let n = Math.max(x.length, y.length); + let carry = 0; + let i = 0; + while (i < n || carry) { + let xi = i < x.length ? x[i] : 0; + let yi = i < y.length ? y[i] : 0; + let zi = carry + xi + yi; + z.push(zi % base); + carry = Math.floor(zi / base); + i++; + } + + return z; +}; + +// Returns a*x, where x is an array of decimal digits and a is an ordinary +// JavaScript number. base is the number base of the array x. +const multiplyByNumber = (num: number, x: number[], base: number): number[] => { + if (num == 0 || num < 0) return []; + + let result: number[] = []; + let power = x; + + while (num !== 0) { + if (num & 1) { + result = add(result, power, base); + } + + num >>= 1; + + if (num === 0) break; + + power = add(power, power, base); + } + + return result; +}; + +const parseToDigitsArray = (str: string, base: number) => { + let digits = str.split(''); + let ary = []; + for (let i = digits.length - 1; i >= 0; i--) { + let n = parseInt(digits[i], base); + + if (isNaN(n)) return null; + + ary.push(n); + } + + return ary; +}; + +const convertBase = (str: string, fromBase: number, toBase: number) => { + let digits = parseToDigitsArray(str, fromBase); + + if (digits === null) return null; + + let outArray: number[] = []; + let power = [1]; + for (let i = 0; i < digits.length; i++) { + // inletiant: at this point, fromBase^i = power + if (digits[i]) { + outArray = add( + outArray, + multiplyByNumber(digits[i] as number, power, toBase), + toBase + ); + } + + power = multiplyByNumber(fromBase, power, toBase); + } + + let out = ''; + for (let i = outArray.length - 1; i >= 0; i--) { + out += outArray[i].toString(toBase); + } + + return out; +}; + +export const hexToDec = (hexStr: string) => { + if (hexStr.startsWith('0x')) hexStr = hexStr.substring(2); + + hexStr = hexStr.toLowerCase(); + + return convertBase(hexStr, 16, 10); +}; diff --git a/src/index.ts b/src/index.ts index fe9cc25..093c035 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,63 @@ -export const Sunflake = {}; \ No newline at end of file +import { hexToDec } from './hex2dec'; + +let lastTime: number = 0; +let seq: number = 0; + +export type SunflakeConfig = { + machineID: number; + epoch: number; + time?: number; +}; + +export const generateSunflake = async (config: SunflakeConfig) => { + let { machineID, epoch, time } = Object.assign< + SunflakeConfig, + Partial + >( + { + epoch: 0, + machineID: 0, + time: 1, + }, + config + ) as Required; + + lastTime = time; + machineID = machineID % 1023; + + const bTime = (time - epoch).toString(2); + + // Get the sequence number + if (lastTime == time) { + seq++; + + if (seq > 4095) { + seq = 0; + + // Make system wait till time is been shifted by one millisecond + while (Date.now() <= time) { + await new Promise((acc) => setImmediate(acc)); + } + } + } else { + seq = 0; + } + + lastTime = time; + + let bSeq = seq.toString(2); + let bMid = machineID.toString(2); + + // Create sequence binary bit + while (bSeq.length < 12) bSeq = '0' + bSeq; + while (bMid.length < 10) bMid = '0' + bMid; + + const bid = bTime + bMid + bSeq; + + let id = ''; + for (let i = bid.length; i > 0; i -= 4) { + id = parseInt(bid.substring(i - 4, i), 2).toString(16) + id; + } + + return hexToDec(id); +}; diff --git a/tests/snowflake.test.ts b/tests/snowflake.test.ts deleted file mode 100644 index f69eab4..0000000 --- a/tests/snowflake.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Sunflake } from '../src'; - -it('Exports Sunflake', () => { - expect(Sunflake); -}); \ No newline at end of file diff --git a/tests/sunflake.test.ts b/tests/sunflake.test.ts new file mode 100644 index 0000000..b64ffd8 --- /dev/null +++ b/tests/sunflake.test.ts @@ -0,0 +1,58 @@ +import { generateSunflake } from '../src'; + +const EPOCH: number = 1640988001000; // First second of 2022 + + +it('Exports Sunflake', () => { + expect(generateSunflake); +}); + +it('Generates two snowflake value', async () => { + const flake1 = await generateSunflake({ machineID: 1, epoch: EPOCH }); + const flake2 = await generateSunflake({ machineID: 1, epoch: EPOCH }); + + expect(flake1 != flake2); +}); + +it('Generates two snowflake value in sync', async () => { + const [flake1, flake2] = await Promise.all([ + generateSunflake({ machineID: 1, epoch: EPOCH }), + generateSunflake({ machineID: 1, epoch: EPOCH }), + ]); + + expect(flake1 != flake2); +}); + +it('Generates two snowflake value in sync with same time', async () => { + const time = Date.now(); + const [flake1, flake2] = await Promise.all([ + generateSunflake({ machineID: 1, epoch: EPOCH, time }), + generateSunflake({ machineID: 1, epoch: EPOCH, time }), + ]); + + expect(flake1 !== flake2); +}); + +it('Generates 500 snowflake value in sync with same time', async () => { + const time = Date.now(); + const hugeList = []; + for (let i = 0; i <= 500; i++) { + hugeList.push(generateSunflake({ machineID: 1, epoch: EPOCH, time })); + } + + const list = await Promise.all(hugeList); + + expect(new Set(list).size === list.length); +}); + +it('Generates 5200 snowflake value in sync with same time', async () => { + const time = Date.now(); + const hugeList = []; + for (let i = 0; i <= 5200; i++) { + hugeList.push(generateSunflake({ machineID: 1, epoch: EPOCH, time })); + } + + const list = await Promise.all(hugeList); + + expect(new Set(list).size === list.length); +}); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 2da7abb..0e0a62a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -257,6 +257,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/runtime@^7.15.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa" + integrity sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.16.7", "@babel/template@^7.3.3": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" @@ -653,6 +660,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== +"@types/lodash@^4.14.175": + version "4.14.178" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8" + integrity sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw== + "@types/node@*", "@types/node@^17.0.2": version "17.0.6" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.6.tgz#cc1589c9ee853b389e67e8fb4384e0f250a139b9" @@ -685,6 +697,11 @@ dependencies: "@types/yargs-parser" "*" +"@types/yup@^0.29.13": + version "0.29.13" + resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.29.13.tgz#21b137ba60841307a3c8a1050d3bf4e63ad561e9" + integrity sha512-qRyuv+P/1t1JK1rA+elmK1MmCL1BapEzKKfbEhDBV/LMMse4lmhZ/XbgETI39JveDJRpLjmToOI6uFtMW/WR2g== + "@typescript-eslint/experimental-utils@^5.0.0": version "5.8.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.8.1.tgz#01861eb2f0749f07d02db342b794145a66ed346f" @@ -2425,6 +2442,11 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + lodash.memoize@4.x: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -2528,6 +2550,11 @@ multimap@^1.1.0: resolved "https://registry.yarnpkg.com/multimap/-/multimap-1.1.0.tgz#5263febc085a1791c33b59bb3afc6a76a2a10ca8" integrity sha512-0ZIR9PasPxGXmRsEF8jsDzndzHDj7tIav+JUmvIFB/WHswliFnquxECT/De7GR4yg99ky/NlRKJT82G1y271bw== +nanoclone@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4" + integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -2734,6 +2761,11 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" +property-expr@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.4.tgz#37b925478e58965031bb612ec5b3260f8241e910" + integrity sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg== + psl@^1.1.33: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" @@ -2773,6 +2805,11 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" +regenerator-runtime@^0.13.4: + version "0.13.9" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" + integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== + regexp-tree@^0.1.23, regexp-tree@~0.1.1: version "0.1.24" resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.24.tgz#3d6fa238450a4d66e5bc9c4c14bb720e2196829d" @@ -3090,6 +3127,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +toposort@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" + integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA= + tough-cookie@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" @@ -3370,3 +3412,16 @@ yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +yup@^0.32.11: + version "0.32.11" + resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.11.tgz#d67fb83eefa4698607982e63f7ca4c5ed3cf18c5" + integrity sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg== + dependencies: + "@babel/runtime" "^7.15.4" + "@types/lodash" "^4.14.175" + lodash "^4.17.21" + lodash-es "^4.17.21" + nanoclone "^0.2.1" + property-expr "^2.0.4" + toposort "^2.0.2"