Commit 1f241d28 authored by Thomas Randolph's avatar Thomas Randolph

Add a seedable UUIDv4 generator

Most UUID generators assume that you want fully
random UUIDs. In most cases, this is true.

The `uuid` package allows
a consumer to pass in `random` values (an array
of 16 numbers 0-255), or a generator that outputs
16 random bytes.

This is our hook into being able to provide
"random" values. We just need a way to get
"random" values that are actually random in
most cases, but that we can control if we want
to.

Enter: the Mersenne Twister.
Mersenne Twisters can be seeded with a number
to start. They will derive all of their future twisted
states from that initial seed. So: we still get
"randomness," but we can also seed it to make
the output deterministic.

This `random.js` file outputs a single function
(for now) called `uuids` that will generate a
random UUIDv4 string or - if provided seeds -
will generate the correct resulting UUIDv4
given those seeds.

Consumers can request multiple values
to avoid having to constantly call the function
and/or constantly reconstruct the internal
Twister.
parent d7ed9fff
/**
* @module uuids
*/
/**
* A string or number representing a start state for a random generator
* @typedef {(Number|String)} Seed
*/
/**
* A UUIDv4 string in the format <code>Hex{8}-Hex{4}-4Hex{3}-[89ab]Hex{3}-Hex{12}</code>
* @typedef {String} UUIDv4
*/
// https://gitlab.com/gitlab-org/frontend/rfcs/-/issues/20
/* eslint-disable import/prefer-default-export */
import MersenneTwister from 'mersenne-twister';
import stringHash from 'string-hash';
import { isString } from 'lodash';
import { v4 } from 'uuid';
function getSeed(seeds) {
return seeds.reduce((seedling, seed, i) => {
let thisSeed = 0;
if (Number.isInteger(seed)) {
thisSeed = seed;
} else if (isString(seed)) {
thisSeed = stringHash(seed);
}
return seedling + (seeds.length - i) * thisSeed;
}, 0);
}
function getPseudoRandomNumberGenerator(...seeds) {
let seedNumber;
if (seeds.length) {
seedNumber = getSeed(seeds);
} else {
seedNumber = Math.floor(Math.random() * 10 ** 15);
}
return new MersenneTwister(seedNumber);
}
function randomValuesForUuid(prng) {
const randomValues = [];
for (let i = 0; i <= 3; i += 1) {
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint32(0, prng.random_int());
randomValues.push(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3));
}
return randomValues;
}
/**
* Get an array of UUIDv4s
* @param {Object} [options={}]
* @param {Seed[]} [options.seeds=[]] - A list of mixed strings or numbers to seed the UUIDv4 generator
* @param {Number} [options.count=1] - A total number of UUIDv4s to generate
* @returns {UUIDv4[]} An array of UUIDv4s
*/
export function uuids({ seeds = [], count = 1 } = {}) {
const rng = getPseudoRandomNumberGenerator(...seeds);
return (
// Create an array the same size as the number of UUIDs requested
Array(count)
.fill(0)
// Replace each slot in the array with a UUID which needs 16 (pseudo)random values to generate
.map(() => v4({ random: randomValuesForUuid(rng) }))
);
}
...@@ -626,3 +626,9 @@ ...@@ -626,3 +626,9 @@
:why: :why:
:versions: [] :versions: []
:when: 2019-11-08 10:03:31.787226000 Z :when: 2019-11-08 10:03:31.787226000 Z
- - :whitelist
- CC0-1.0
- :who: Thomas Randolph
:why: This license is public domain
:versions: []
:when: 2020-06-03 05:04:44.632875345 Z
...@@ -102,6 +102,7 @@ ...@@ -102,6 +102,7 @@
"lodash": "^4.17.15", "lodash": "^4.17.15",
"marked": "^0.3.12", "marked": "^0.3.12",
"mermaid": "^8.5.1", "mermaid": "^8.5.1",
"mersenne-twister": "1.1.0",
"mitt": "^1.2.0", "mitt": "^1.2.0",
"monaco-editor": "^0.18.1", "monaco-editor": "^0.18.1",
"monaco-editor-webpack-plugin": "^1.7.0", "monaco-editor-webpack-plugin": "^1.7.0",
...@@ -120,6 +121,7 @@ ...@@ -120,6 +121,7 @@
"sortablejs": "^1.10.2", "sortablejs": "^1.10.2",
"sql.js": "^0.4.0", "sql.js": "^0.4.0",
"stickyfilljs": "^2.1.0", "stickyfilljs": "^2.1.0",
"string-hash": "1.1.3",
"style-loader": "^1.1.3", "style-loader": "^1.1.3",
"svg4everybody": "2.1.9", "svg4everybody": "2.1.9",
"swagger-ui-dist": "^3.24.3", "swagger-ui-dist": "^3.24.3",
...@@ -133,6 +135,7 @@ ...@@ -133,6 +135,7 @@
"tributejs": "4.1.3", "tributejs": "4.1.3",
"unfetch": "^4.1.0", "unfetch": "^4.1.0",
"url-loader": "^3.0.0", "url-loader": "^3.0.0",
"uuid": "8.1.0",
"visibilityjs": "^1.2.4", "visibilityjs": "^1.2.4",
"vue": "^2.6.10", "vue": "^2.6.10",
"vue-apollo": "^3.0.3", "vue-apollo": "^3.0.3",
......
import { uuids } from '~/diffs/utils/uuids';
const HEX = /[a-f0-9]/i;
const HEX_RE = HEX.source;
const UUIDV4 = new RegExp(
`${HEX_RE}{8}-${HEX_RE}{4}-4${HEX_RE}{3}-[89ab]${HEX_RE}{3}-${HEX_RE}{12}`,
'i',
);
describe('UUIDs Util', () => {
describe('uuids', () => {
const SEQUENCE_FOR_GITLAB_SEED = [
'a1826a44-316c-480e-a93d-8cdfeb36617c',
'e049db1f-a4cf-4cba-aa60-6d95e3b547dc',
'6e3c737c-13a7-4380-b17d-601f187d7e69',
'bee5cc7f-c486-45c0-8ad3-d1ac5402632d',
'af248c9f-a3a6-4d4f-a311-fe151ffab25a',
];
const SEQUENCE_FOR_12345_SEED = [
'edfb51e2-e3e1-4de5-90fd-fd1d21760881',
'2f154da4-0a2d-4da9-b45e-0ffed391517e',
'91566d65-8836-4222-9875-9e1df4d0bb01',
'f6ea6c76-7640-4d71-a736-9d3bec7a1a8e',
'bfb85869-5fb9-4c5b-a750-5af727ac5576',
];
it('returns version 4 UUIDs', () => {
expect(uuids()[0]).toMatch(UUIDV4);
});
it('outputs an array of UUIDs', () => {
const ids = uuids({ count: 11 });
expect(ids.length).toEqual(11);
expect(ids.every(id => UUIDV4.test(id))).toEqual(true);
});
it.each`
seeds | uuid
${['some', 'special', 'seed']} | ${'6fa53e51-0f70-4072-9c84-1c1eee1b9934'}
${['magic']} | ${'fafae8cd-7083-44f3-b82d-43b30bd27486'}
${['seeded']} | ${'e06ed291-46c5-4e42-836b-e7c772d48b49'}
${['GitLab']} | ${'a1826a44-316c-480e-a93d-8cdfeb36617c'}
${['JavaScript']} | ${'12dfb297-1560-4c38-9775-7178ef8472fb'}
${[99, 169834, 2619]} | ${'3ecc8ad6-5b7c-4c9b-94a8-c7271c2fa083'}
${[12]} | ${'2777374b-723b-469b-bd73-e586df964cfd'}
${[9876, 'mixed!', 7654]} | ${'865212e0-4a16-4934-96f9-103cf36a6931'}
${[123, 1234, 12345, 6]} | ${'40aa2ee6-0a11-4e67-8f09-72f5eba04244'}
${[0]} | ${'8c7f0aac-97c4-4a2f-b716-a675d821ccc0'}
`(
'should always output the UUID $uuid when the options.seeds argument is $seeds',
({ uuid, seeds }) => {
expect(uuids({ seeds })[0]).toEqual(uuid);
},
);
describe('unseeded UUID randomness', () => {
const nonRandom = Array(6)
.fill(0)
.map((_, i) => uuids({ seeds: [i] })[0]);
const random = uuids({ count: 6 });
const moreRandom = uuids({ count: 6 });
it('is different from a seeded result', () => {
random.forEach((id, i) => {
expect(id).not.toEqual(nonRandom[i]);
});
});
it('is different from other random results', () => {
random.forEach((id, i) => {
expect(id).not.toEqual(moreRandom[i]);
});
});
it('never produces any duplicates', () => {
expect(new Set(random).size).toEqual(random.length);
});
});
it.each`
seed | sequence
${'GitLab'} | ${SEQUENCE_FOR_GITLAB_SEED}
${12345} | ${SEQUENCE_FOR_12345_SEED}
`(
'should output the same sequence of UUIDs for the given seed "$seed"',
({ seed, sequence }) => {
expect(uuids({ seeds: [seed], count: 5 })).toEqual(sequence);
},
);
});
});
...@@ -7774,6 +7774,11 @@ mermaid@^8.5.1: ...@@ -7774,6 +7774,11 @@ mermaid@^8.5.1:
moment-mini "^2.22.1" moment-mini "^2.22.1"
scope-css "^1.2.1" scope-css "^1.2.1"
mersenne-twister@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/mersenne-twister/-/mersenne-twister-1.1.0.tgz#f916618ee43d7179efcf641bec4531eb9670978a"
integrity sha1-+RZhjuQ9cXnvz2Qb7EUx65Zwl4o=
methods@~1.1.2: methods@~1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
...@@ -10629,6 +10634,11 @@ streamroller@^1.0.6: ...@@ -10629,6 +10634,11 @@ streamroller@^1.0.6:
fs-extra "^7.0.1" fs-extra "^7.0.1"
lodash "^4.17.14" lodash "^4.17.14"
string-hash@1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b"
integrity sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=
string-length@^2.0.0: string-length@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
...@@ -11687,6 +11697,11 @@ uuid@3.3.2, uuid@^3.0.1, uuid@^3.3.2: ...@@ -11687,6 +11697,11 @@ uuid@3.3.2, uuid@^3.0.1, uuid@^3.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
uuid@8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d"
integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==
v8-compile-cache@2.0.3, v8-compile-cache@^2.0.3: v8-compile-cache@2.0.3, v8-compile-cache@^2.0.3:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment