mirror of
https://github.com/hanzoai/uuid.git
synced 2026-09-24 17:04:02 +00:00
fix!: refactor v1 internal state and options logic (#780)
This commit is contained in:
@@ -104,9 +104,6 @@ export default function benchmark(uuid, Benchmark) {
|
||||
.add('uuid.v1ToV6()', function () {
|
||||
uuid.v1ToV6(V1_ID);
|
||||
})
|
||||
.add('uuid.v1ToV6() w/ randomization', function () {
|
||||
uuid.v1ToV6(V1_ID, true);
|
||||
})
|
||||
.add('uuid.v6ToV1()', function () {
|
||||
uuid.v6ToV1(V6_ID);
|
||||
})
|
||||
|
||||
Generated
+24
-34
@@ -25,34 +25,29 @@
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.24.6",
|
||||
"@babel/core": "7.24.6",
|
||||
"@babel/eslint-parser": "7.24.6",
|
||||
"@babel/plugin-syntax-import-attributes": "7.24.6",
|
||||
"@babel/preset-env": "7.24.6",
|
||||
"@babel/eslint-parser": "7.24.7",
|
||||
"@commitlint/cli": "19.3.0",
|
||||
"@commitlint/config-conventional": "19.2.2",
|
||||
"@eslint/js": "9.5.0",
|
||||
"@types/eslint__js": "8.42.3",
|
||||
"@types/random-seed": "0.3.5",
|
||||
"@wdio/browserstack-service": "7.16.10",
|
||||
"@wdio/cli": "7.16.10",
|
||||
"@wdio/jasmine-framework": "7.16.6",
|
||||
"@wdio/local-runner": "7.16.10",
|
||||
"@wdio/spec-reporter": "7.16.9",
|
||||
"@wdio/static-server-service": "7.16.6",
|
||||
"@wdio/browserstack-service": "8.39.0",
|
||||
"@wdio/cli": "8.39.0",
|
||||
"@wdio/jasmine-framework": "8.39.0",
|
||||
"@wdio/local-runner": "8.39.0",
|
||||
"@wdio/spec-reporter": "8.39.0",
|
||||
"@wdio/static-server-service": "8.39.0",
|
||||
"bundlewatch": "0.3.3",
|
||||
"eslint": "9.5.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-prettier": "5.1.3",
|
||||
"globals": "15.3.0",
|
||||
"globals": "15.6.0",
|
||||
"husky": "9.0.11",
|
||||
"jest": "29.7.0",
|
||||
"lint-staged": "15.2.5",
|
||||
"neostandard": "0.5.1",
|
||||
"lint-staged": "15.2.7",
|
||||
"neostandard": "0.7.2",
|
||||
"npm-run-all": "4.1.5",
|
||||
"optional-dev-dependency": "2.0.1",
|
||||
"prettier": "3.3.0",
|
||||
"random-seed": "0.3.0",
|
||||
"prettier": "3.3.2",
|
||||
"runmd": "1.3.9",
|
||||
"standard-version": "9.5.0",
|
||||
"typescript": "5.4.5",
|
||||
@@ -112,34 +107,29 @@
|
||||
"uuid": {
|
||||
"version": "file:../../.local/uuid",
|
||||
"requires": {
|
||||
"@babel/cli": "7.24.6",
|
||||
"@babel/core": "7.24.6",
|
||||
"@babel/eslint-parser": "7.24.6",
|
||||
"@babel/plugin-syntax-import-attributes": "7.24.6",
|
||||
"@babel/preset-env": "7.24.6",
|
||||
"@babel/eslint-parser": "7.24.7",
|
||||
"@commitlint/cli": "19.3.0",
|
||||
"@commitlint/config-conventional": "19.2.2",
|
||||
"@eslint/js": "9.5.0",
|
||||
"@types/eslint__js": "8.42.3",
|
||||
"@types/random-seed": "0.3.5",
|
||||
"@wdio/browserstack-service": "7.16.10",
|
||||
"@wdio/cli": "7.16.10",
|
||||
"@wdio/jasmine-framework": "7.16.6",
|
||||
"@wdio/local-runner": "7.16.10",
|
||||
"@wdio/spec-reporter": "7.16.9",
|
||||
"@wdio/static-server-service": "7.16.6",
|
||||
"@wdio/browserstack-service": "8.39.0",
|
||||
"@wdio/cli": "8.39.0",
|
||||
"@wdio/jasmine-framework": "8.39.0",
|
||||
"@wdio/local-runner": "8.39.0",
|
||||
"@wdio/spec-reporter": "8.39.0",
|
||||
"@wdio/static-server-service": "8.39.0",
|
||||
"bundlewatch": "0.3.3",
|
||||
"eslint": "9.5.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-prettier": "5.1.3",
|
||||
"globals": "15.3.0",
|
||||
"globals": "15.6.0",
|
||||
"husky": "9.0.11",
|
||||
"jest": "29.7.0",
|
||||
"lint-staged": "15.2.5",
|
||||
"neostandard": "0.5.1",
|
||||
"lint-staged": "15.2.7",
|
||||
"neostandard": "0.7.2",
|
||||
"npm-run-all": "4.1.5",
|
||||
"optional-dev-dependency": "2.0.1",
|
||||
"prettier": "3.3.0",
|
||||
"random-seed": "0.3.0",
|
||||
"prettier": "3.3.2",
|
||||
"runmd": "1.3.9",
|
||||
"standard-version": "9.5.0",
|
||||
"typescript": "5.4.5",
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import * as assert from 'assert';
|
||||
import test, { describe } from 'node:test';
|
||||
import v1 from '../v1.js';
|
||||
|
||||
// Since the clockseq is cached in the module this test must run in a separate file in order to
|
||||
// initialize the v1 clockseq with controlled random data.
|
||||
describe('v1-random', () => {
|
||||
const randomBytesFixture = Uint8Array.of(
|
||||
0x10,
|
||||
0x91,
|
||||
0x56,
|
||||
0xbe,
|
||||
0xc4,
|
||||
0xfb,
|
||||
0xc1,
|
||||
0xea,
|
||||
0x71,
|
||||
0xb4,
|
||||
0xef,
|
||||
0xe1,
|
||||
0x67,
|
||||
0x1c,
|
||||
0x58,
|
||||
0x36
|
||||
);
|
||||
|
||||
test('explicit options.random produces expected id', () => {
|
||||
const id = v1({
|
||||
msecs: 1321651533573,
|
||||
nsecs: 5432,
|
||||
random: randomBytesFixture,
|
||||
});
|
||||
assert.strictEqual(id, 'd9428888-122b-11e1-81ea-119156bec4fb');
|
||||
});
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
import * as assert from 'assert';
|
||||
import test, { describe } from 'node:test';
|
||||
import v1 from '../v1.js';
|
||||
|
||||
// Since the clockseq is cached in the module this test must run in a separate file in order to
|
||||
// initialize the v1 clockseq with controlled random data.
|
||||
describe('v1-rng', () => {
|
||||
const randomBytesFixture = Uint8Array.of(
|
||||
0x10,
|
||||
0x91,
|
||||
0x56,
|
||||
0xbe,
|
||||
0xc4,
|
||||
0xfb,
|
||||
0xc1,
|
||||
0xea,
|
||||
0x71,
|
||||
0xb4,
|
||||
0xef,
|
||||
0xe1,
|
||||
0x67,
|
||||
0x1c,
|
||||
0x58,
|
||||
0x36
|
||||
);
|
||||
|
||||
test('explicit options.random produces expected id', () => {
|
||||
const id = v1({
|
||||
msecs: 1321651533573,
|
||||
nsecs: 5432,
|
||||
rng: () => randomBytesFixture,
|
||||
});
|
||||
assert.strictEqual(id, 'd9428888-122b-11e1-81ea-119156bec4fb');
|
||||
});
|
||||
});
|
||||
+128
-94
@@ -1,20 +1,56 @@
|
||||
import * as assert from 'assert';
|
||||
import test, { describe } from 'node:test';
|
||||
import v1 from '../v1.js';
|
||||
import parse from '../parse.js';
|
||||
import v1, { updateV1State } from '../v1.js';
|
||||
|
||||
// Verify ordering of v1 ids created with explicit times
|
||||
const TIME = 1321644961388; // 2011-11-18 11:36:01.388-08:00
|
||||
|
||||
// Fixture values for testing with the rfc v1 UUID example:
|
||||
// https://www.rfc-editor.org/rfc/rfc9562.html#name-example-of-a-uuidv1-value
|
||||
const RFC_V1 = 'c232ab00-9414-11ec-b3c8-9f68deced846';
|
||||
const RFC_V1_BYTES = parse(RFC_V1);
|
||||
|
||||
// `options` for producing the above RFC UUID
|
||||
const RFC_OPTIONS = {
|
||||
msecs: 0x17f22e279b0,
|
||||
nsecs: 0,
|
||||
clockseq: 0x33c8,
|
||||
node: Uint8Array.of(0x9f, 0x68, 0xde, 0xce, 0xd8, 0x46),
|
||||
};
|
||||
|
||||
// random bytes for producing the above RFC UUID
|
||||
const RFC_RANDOM = Uint8Array.of(
|
||||
// unused
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
|
||||
// clock seq
|
||||
RFC_OPTIONS.clockseq >> 8,
|
||||
RFC_OPTIONS.clockseq & 0xff,
|
||||
|
||||
// node
|
||||
...RFC_OPTIONS.node
|
||||
);
|
||||
|
||||
// Compare v1 timestamp fields chronologically
|
||||
function compareV1TimeField(a: string, b: string) {
|
||||
a = a.split('-').slice(0, 3).reverse().join('');
|
||||
b = b.split('-').slice(0, 3).reverse().join('');
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
}
|
||||
|
||||
describe('v1', () => {
|
||||
test('v1 sort order (default)', () => {
|
||||
const ids = [v1(), v1(), v1(), v1(), v1()];
|
||||
|
||||
const sorted = [...ids].sort((a, b) => {
|
||||
a = a.split('-').reverse().join('-');
|
||||
b = b.split('-').reverse().join('-');
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
});
|
||||
|
||||
const sorted = [...ids].sort(compareV1TimeField);
|
||||
assert.deepEqual(ids, sorted);
|
||||
});
|
||||
|
||||
@@ -28,110 +64,108 @@ describe('v1', () => {
|
||||
v1({ msecs: TIME + 28 * 24 * 3600 * 1000 }),
|
||||
];
|
||||
|
||||
const sorted = [...ids].sort((a, b) => {
|
||||
a = a.split('-').reverse().join('-');
|
||||
b = b.split('-').reverse().join('-');
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
});
|
||||
|
||||
const sorted = [...ids].sort(compareV1TimeField);
|
||||
assert.deepEqual(ids, sorted);
|
||||
});
|
||||
|
||||
test('msec', () => {
|
||||
assert.ok(
|
||||
v1({ msecs: TIME }) !== v1({ msecs: TIME }),
|
||||
'IDs created at same msec are different'
|
||||
);
|
||||
test('v1(options)', () => {
|
||||
assert.equal(v1({ msecs: RFC_OPTIONS.msecs, random: RFC_RANDOM }), RFC_V1, 'minimal options');
|
||||
assert.equal(v1(RFC_OPTIONS), RFC_V1, 'full options');
|
||||
});
|
||||
|
||||
test('exception thrown when > 10k ids created in 1ms', () => {
|
||||
assert.throws(function () {
|
||||
v1({ msecs: TIME, nsecs: 10000 });
|
||||
}, 'throws when > 10K ids created in 1 ms');
|
||||
test('v1(options) equality', () => {
|
||||
assert.notEqual(v1({ msecs: TIME }), v1({ msecs: TIME }), 'UUIDs with minimal options differ');
|
||||
assert.equal(v1(RFC_OPTIONS), v1(RFC_OPTIONS), 'UUIDs with full options are identical');
|
||||
});
|
||||
|
||||
test('clock regression by msec', () => {
|
||||
// Verify clock regression bumps clockseq
|
||||
const uidt = v1({ msecs: TIME });
|
||||
const uidtb = v1({ msecs: TIME - 1 });
|
||||
assert.ok(
|
||||
parseInt(uidtb.split('-')[3], 16) - parseInt(uidt.split('-')[3], 16) === 1,
|
||||
'Clock regression by msec increments the clockseq'
|
||||
);
|
||||
});
|
||||
|
||||
test('clock regression by nsec', () => {
|
||||
// Verify clock regression bumps clockseq
|
||||
const uidtn = v1({ msecs: TIME, nsecs: 10 });
|
||||
const uidtnb = v1({ msecs: TIME, nsecs: 9 });
|
||||
assert.ok(
|
||||
parseInt(uidtnb.split('-')[3], 16) - parseInt(uidtn.split('-')[3], 16) === 1,
|
||||
'Clock regression by nsec increments the clockseq'
|
||||
);
|
||||
});
|
||||
|
||||
const fullOptions = {
|
||||
msecs: 1321651533573,
|
||||
nsecs: 5432,
|
||||
clockseq: 0x385c,
|
||||
node: Uint8Array.of(0x61, 0xcd, 0x3c, 0xbb, 0x32, 0x10),
|
||||
};
|
||||
|
||||
test('explicit options produce expected id', () => {
|
||||
// Verify explicit options produce expected id
|
||||
const id = v1(fullOptions);
|
||||
assert.ok(
|
||||
id === 'd9428888-122b-11e1-b85c-61cd3cbb3210',
|
||||
'Explicit options produce expected id'
|
||||
);
|
||||
});
|
||||
|
||||
test('ids spanning 1ms boundary are 100ns apart', () => {
|
||||
// Verify adjacent ids across a msec boundary are 1 time unit apart
|
||||
const u0 = v1({ msecs: TIME, nsecs: 9999 });
|
||||
const u1 = v1({ msecs: TIME + 1, nsecs: 0 });
|
||||
|
||||
const before = u0.split('-')[0];
|
||||
const after = u1.split('-')[0];
|
||||
const dt = parseInt(after, 16) - parseInt(before, 16);
|
||||
assert.ok(dt === 1, 'Ids spanning 1ms boundary are 100ns apart');
|
||||
});
|
||||
|
||||
const expectedBytes = Uint8Array.of(
|
||||
217,
|
||||
66,
|
||||
136,
|
||||
136,
|
||||
18,
|
||||
43,
|
||||
17,
|
||||
225,
|
||||
184,
|
||||
92,
|
||||
97,
|
||||
205,
|
||||
60,
|
||||
187,
|
||||
50,
|
||||
16
|
||||
);
|
||||
|
||||
test('fills one UUID into a buffer as expected', () => {
|
||||
const buffer = new Uint8Array(16);
|
||||
const result = v1(fullOptions, buffer);
|
||||
assert.deepEqual(buffer, expectedBytes);
|
||||
const result = v1(RFC_OPTIONS, buffer);
|
||||
assert.deepEqual(buffer, RFC_V1_BYTES);
|
||||
assert.strictEqual(buffer, result);
|
||||
});
|
||||
|
||||
test('fills two UUIDs into a buffer as expected', () => {
|
||||
const buffer = new Uint8Array(32);
|
||||
v1(fullOptions, buffer, 0);
|
||||
v1(fullOptions, buffer, 16);
|
||||
v1(RFC_OPTIONS, buffer, 0);
|
||||
v1(RFC_OPTIONS, buffer, 16);
|
||||
|
||||
const expectedBuf = new Uint8Array(32);
|
||||
expectedBuf.set(expectedBytes);
|
||||
expectedBuf.set(expectedBytes, 16);
|
||||
expectedBuf.set(RFC_V1_BYTES);
|
||||
expectedBuf.set(RFC_V1_BYTES, 16);
|
||||
|
||||
assert.deepEqual(buffer, expectedBuf);
|
||||
});
|
||||
|
||||
test('v1() state transitions', () => {
|
||||
// Test fixture for internal state passed into updateV1State function
|
||||
const PRE_STATE = {
|
||||
msecs: 10,
|
||||
nsecs: 20,
|
||||
clockseq: 0x1234,
|
||||
node: Uint8Array.of(0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc),
|
||||
};
|
||||
|
||||
// Note: The test code, below, passes RFC_RANDOM as the `rnds` argument for
|
||||
// convenience. This allows us to test that fields have been initialized from
|
||||
// the rnds argument by testing for RFC_OPTIONS values in the output state.
|
||||
|
||||
const tests = [
|
||||
{
|
||||
title: 'initial state',
|
||||
state: {},
|
||||
now: 10,
|
||||
expected: {
|
||||
msecs: 10, // -> now
|
||||
nsecs: 0, // -> init
|
||||
clockseq: RFC_OPTIONS.clockseq, // -> random
|
||||
node: RFC_OPTIONS.node, // -> random
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'same time interval',
|
||||
state: { ...PRE_STATE },
|
||||
now: PRE_STATE.msecs,
|
||||
expected: {
|
||||
...PRE_STATE,
|
||||
nsecs: 21, // -> +1
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'new time interval',
|
||||
state: { ...PRE_STATE },
|
||||
now: PRE_STATE.msecs + 1,
|
||||
expected: {
|
||||
...PRE_STATE,
|
||||
msecs: PRE_STATE.msecs + 1, // -> +1
|
||||
nsecs: 0, // -> init
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'same time interval (nsecs overflow)',
|
||||
state: { ...PRE_STATE, nsecs: 9999 },
|
||||
now: PRE_STATE.msecs,
|
||||
expected: {
|
||||
...PRE_STATE,
|
||||
nsecs: 0, // -> init
|
||||
clockseq: RFC_OPTIONS.clockseq, // -> init
|
||||
node: RFC_OPTIONS.node, // -> init
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'time regression',
|
||||
state: { ...PRE_STATE },
|
||||
now: PRE_STATE.msecs - 1,
|
||||
expected: {
|
||||
...PRE_STATE,
|
||||
msecs: PRE_STATE.msecs - 1, // -> now
|
||||
clockseq: RFC_OPTIONS.clockseq, // -> init
|
||||
node: RFC_OPTIONS.node, // -> init
|
||||
},
|
||||
},
|
||||
];
|
||||
for (const { title, state, now, expected } of tests) {
|
||||
assert.deepStrictEqual(updateV1State(state, now, RFC_RANDOM), expected, `Failed: ${title}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+18
-18
@@ -9,29 +9,29 @@ describe('v6', () => {
|
||||
const V6_ID = '1ef21d2f-1207-6660-8c4f-419efbd44d48';
|
||||
|
||||
const fullOptions = {
|
||||
msecs: 1321651533573,
|
||||
nsecs: 5432,
|
||||
msecs: 0x133b891f705,
|
||||
nsecs: 0x1538,
|
||||
clockseq: 0x385c,
|
||||
node: Uint8Array.of(0x61, 0xcd, 0x3c, 0xbb, 0x32, 0x10),
|
||||
};
|
||||
|
||||
const EXPECTED_BYTES = Uint8Array.of(
|
||||
30,
|
||||
17,
|
||||
34,
|
||||
189,
|
||||
148,
|
||||
40,
|
||||
104,
|
||||
136,
|
||||
184,
|
||||
92,
|
||||
97,
|
||||
205,
|
||||
60,
|
||||
187,
|
||||
50,
|
||||
16
|
||||
0x1e,
|
||||
0x11,
|
||||
0x22,
|
||||
0xbd,
|
||||
0x94,
|
||||
0x28,
|
||||
0x68,
|
||||
0x88,
|
||||
0xb8,
|
||||
0x5c,
|
||||
0x61,
|
||||
0xcd,
|
||||
0x3c,
|
||||
0xbb,
|
||||
0x32,
|
||||
0x10
|
||||
);
|
||||
|
||||
test('default behavior', () => {
|
||||
|
||||
+32
-32
@@ -134,7 +134,38 @@ describe('v7', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('internal state updates properly', () => {
|
||||
test('can supply seq', () => {
|
||||
let seq = 0x12345;
|
||||
let uuid = v7({
|
||||
msecs: RFC_MSECS,
|
||||
seq,
|
||||
});
|
||||
|
||||
assert.strictEqual(uuid.substr(0, 25), '017f22e2-79b0-7000-848d-1');
|
||||
|
||||
seq = 0x6fffffff;
|
||||
uuid = v7({
|
||||
msecs: RFC_MSECS,
|
||||
seq,
|
||||
});
|
||||
|
||||
assert.strictEqual(uuid.substring(0, 25), '017f22e2-79b0-76ff-bfff-f');
|
||||
});
|
||||
|
||||
test('internal seq is reset upon timestamp change', () => {
|
||||
v7({
|
||||
msecs: RFC_MSECS,
|
||||
seq: 0x6fffffff,
|
||||
});
|
||||
|
||||
const uuid = v7({
|
||||
msecs: RFC_MSECS + 1,
|
||||
});
|
||||
|
||||
assert.ok(uuid.indexOf('fff') !== 15);
|
||||
});
|
||||
|
||||
test('v7() state transitions', () => {
|
||||
const tests = [
|
||||
{
|
||||
title: 'new time interval',
|
||||
@@ -191,37 +222,6 @@ describe('v7', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('can supply seq', () => {
|
||||
let seq = 0x12345;
|
||||
let uuid = v7({
|
||||
msecs: RFC_MSECS,
|
||||
seq,
|
||||
});
|
||||
|
||||
assert.strictEqual(uuid.substr(0, 25), '017f22e2-79b0-7000-848d-1');
|
||||
|
||||
seq = 0x6fffffff;
|
||||
uuid = v7({
|
||||
msecs: RFC_MSECS,
|
||||
seq,
|
||||
});
|
||||
|
||||
assert.strictEqual(uuid.substring(0, 25), '017f22e2-79b0-76ff-bfff-f');
|
||||
});
|
||||
|
||||
test('internal seq is reset upon timestamp change', () => {
|
||||
v7({
|
||||
msecs: RFC_MSECS,
|
||||
seq: 0x6fffffff,
|
||||
});
|
||||
|
||||
const uuid = v7({
|
||||
msecs: RFC_MSECS + 1,
|
||||
});
|
||||
|
||||
assert.ok(uuid.indexOf('fff') !== 15);
|
||||
});
|
||||
|
||||
test('flipping bits changes the result', () => {
|
||||
// convert uint8array to BigInt (BE)
|
||||
const asBigInt = (buf: Uint8Array) => buf.reduce((acc, v) => (acc << 8n) | BigInt(v), 0n);
|
||||
|
||||
@@ -7,133 +7,180 @@ import { unsafeStringify } from './stringify.js';
|
||||
// Inspired by https://github.com/LiosK/UUID.js
|
||||
// and http://docs.python.org/library/uuid.html
|
||||
|
||||
let _nodeId: Uint8Array;
|
||||
let _clockseq: number;
|
||||
type V1State = {
|
||||
node?: Uint8Array; // node id (47-bit random)
|
||||
clockseq?: number; // sequence number (14-bit)
|
||||
|
||||
// Previous uuid creation time
|
||||
let _lastMSecs = 0;
|
||||
let _lastNSecs = 0;
|
||||
// v1 & v6 timestamps are a pain to deal with. They specify time from the
|
||||
// Gregorian epoch in 100ns intervals, which requires values with 57+ bits of
|
||||
// precision. But that's outside the precision of IEEE754 floats (i.e. JS
|
||||
// numbers). To work around this, we represent them internally using 'msecs'
|
||||
// (milliseconds since unix epoch) and 'nsecs' (100-nanoseconds offset from
|
||||
// `msecs`).
|
||||
|
||||
msecs?: number; // timestamp (milliseconds, unix epoch)
|
||||
nsecs?: number; // timestamp (100-nanoseconds offset from 'msecs')
|
||||
};
|
||||
|
||||
const _state: V1State = {};
|
||||
|
||||
function v1(options?: Version1Options, buf?: undefined, offset?: number): string;
|
||||
function v1(options?: Version1Options, buf?: Uint8Array, offset?: number): Uint8Array;
|
||||
function v1(options?: Version1Options, buf?: Uint8Array, offset?: number): UUIDTypes {
|
||||
options ??= {};
|
||||
let bytes: Uint8Array;
|
||||
|
||||
let i = (buf && offset) || 0;
|
||||
const b = buf || new Uint8Array(16);
|
||||
|
||||
let node = options.node;
|
||||
let clockseq = options.clockseq;
|
||||
|
||||
// v1 only: Use cached `node` and `clockseq` values
|
||||
if (!options._v6) {
|
||||
if (!node) {
|
||||
node = _nodeId;
|
||||
}
|
||||
if (clockseq == null) {
|
||||
clockseq = _clockseq;
|
||||
// Extract _v6 flag from options, clearing options if appropriate
|
||||
const isV6 = options?._v6 ?? false;
|
||||
if (options) {
|
||||
const optionsKeys = Object.keys(options);
|
||||
if (optionsKeys.length === 1 && optionsKeys[0] === '_v6') {
|
||||
options = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle cases where we need entropy. We do this lazily to minimize issues
|
||||
// related to insufficient system entropy. See #189
|
||||
if (node == null || clockseq == null) {
|
||||
const seedBytes = options.random || (options.rng || rng)();
|
||||
if (options) {
|
||||
// With options: Make UUID independent of internal state
|
||||
bytes = v1Bytes(
|
||||
options.random ?? options.rng?.() ?? rng(),
|
||||
options.msecs,
|
||||
options.nsecs,
|
||||
options.clockseq,
|
||||
options.node,
|
||||
buf,
|
||||
offset
|
||||
);
|
||||
} else {
|
||||
// Without options: Make UUID from internal state
|
||||
const now = Date.now();
|
||||
const rnds = rng();
|
||||
|
||||
// Randomize node
|
||||
if (node == null) {
|
||||
node = Uint8Array.of(
|
||||
seedBytes[0],
|
||||
seedBytes[1],
|
||||
seedBytes[2],
|
||||
seedBytes[3],
|
||||
seedBytes[4],
|
||||
seedBytes[5]
|
||||
);
|
||||
updateV1State(_state, now, rnds);
|
||||
|
||||
// v1 only: cache node value for reuse
|
||||
if (!_nodeId && !options._v6) {
|
||||
// per RFC4122 4.5: Set MAC multicast bit (v1 only)
|
||||
node[0] |= 0x01; // Set multicast bit
|
||||
// Geenerate UUID. Note that v6 uses random values for `clockseq` and
|
||||
// `node`.
|
||||
//
|
||||
// https://www.rfc-editor.org/rfc/rfc9562.html#section-5.6-4
|
||||
bytes = v1Bytes(
|
||||
rnds,
|
||||
_state.msecs,
|
||||
_state.nsecs,
|
||||
// v6 UUIDs get random `clockseq` and `node` for every UUID
|
||||
// https://www.rfc-editor.org/rfc/rfc9562.html#section-5.6-4
|
||||
isV6 ? undefined : _state.clockseq,
|
||||
isV6 ? undefined : _state.node,
|
||||
buf,
|
||||
offset
|
||||
);
|
||||
}
|
||||
|
||||
_nodeId = node;
|
||||
}
|
||||
}
|
||||
return buf ? bytes : unsafeStringify(bytes);
|
||||
}
|
||||
|
||||
// Randomize clockseq
|
||||
if (clockseq == null) {
|
||||
// Per 4.2.2, randomize (14 bit) clockseq
|
||||
clockseq = ((seedBytes[6] << 8) | seedBytes[7]) & 0x3fff;
|
||||
if (_clockseq === undefined && !options._v6) {
|
||||
_clockseq = clockseq;
|
||||
}
|
||||
// (Private!) Do not use. This method is only exported for testing purposes
|
||||
// and may change without notice.
|
||||
export function updateV1State(state: V1State, now: number, rnds: Uint8Array) {
|
||||
state.msecs ??= -Infinity;
|
||||
state.nsecs ??= 0;
|
||||
|
||||
// Update timestamp
|
||||
if (now === state.msecs) {
|
||||
// Same msec-interval = simulate higher clock resolution by bumping `nsecs`
|
||||
// https://www.rfc-editor.org/rfc/rfc9562.html#section-6.1-2.6
|
||||
state.nsecs++;
|
||||
|
||||
// Check for `nsecs` overflow (nsecs is capped at 10K intervals / msec)
|
||||
if (state.nsecs >= 10000) {
|
||||
// Prior to uuid@11 this would throw an error, however the RFCs allow for
|
||||
// changing the node in this case. This slightly breaks monotonicity at
|
||||
// msec granularity, but that's not a significant concern.
|
||||
// https://www.rfc-editor.org/rfc/rfc9562.html#section-6.1-2.16
|
||||
state.node = undefined;
|
||||
state.nsecs = 0;
|
||||
}
|
||||
} else if (now > state.msecs) {
|
||||
// Reset nsec counter when clock advances to a new msec interval
|
||||
state.nsecs = 0;
|
||||
} else if (now < state.msecs) {
|
||||
// Handle clock regression
|
||||
// https://www.rfc-editor.org/rfc/rfc9562.html#section-6.1-2.7
|
||||
//
|
||||
// Note: Unsetting node here causes both it and clockseq to be randomized,
|
||||
// below.
|
||||
state.node = undefined;
|
||||
}
|
||||
|
||||
// v1 & v6 timestamps are 100 nano-second units since the Gregorian epoch,
|
||||
// (1582-10-15 00:00). JSNumbers aren't precise enough for this, so time is
|
||||
// handled internally as 'msecs' (integer milliseconds) and 'nsecs'
|
||||
// (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00.
|
||||
let msecs = options.msecs !== undefined ? options.msecs : Date.now();
|
||||
// Init node and clock sequence (do this after timestamp update which may
|
||||
// reset the node) https://www.rfc-editor.org/rfc/rfc9562.html#section-5.1-7
|
||||
//
|
||||
// Note:
|
||||
if (!state.node) {
|
||||
state.node = rnds.slice(10, 16);
|
||||
|
||||
// Per 4.2.1.2, use count of uuid's generated during the current clock
|
||||
// cycle to simulate higher resolution clock
|
||||
let nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1;
|
||||
// Set multicast bit
|
||||
// https://www.rfc-editor.org/rfc/rfc9562.html#section-6.10-3
|
||||
state.node[0] |= 0x01; // Set multicast bit
|
||||
|
||||
// Time since last uuid creation (in msecs)
|
||||
const dt = msecs - _lastMSecs + (nsecs - _lastNSecs) / 10000;
|
||||
|
||||
// Per 4.2.1.2, Bump clockseq on clock regression
|
||||
if (dt < 0 && options.clockseq === undefined) {
|
||||
clockseq = (clockseq + 1) & 0x3fff;
|
||||
// Clock sequence must be randomized
|
||||
// https://www.rfc-editor.org/rfc/rfc9562.html#section-5.1-8
|
||||
state.clockseq = ((rnds[8] << 8) | rnds[9]) & 0x3fff;
|
||||
}
|
||||
|
||||
// Reset nsecs if clock regresses (new clockseq) or we've moved onto a new
|
||||
// time interval
|
||||
if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) {
|
||||
nsecs = 0;
|
||||
state.msecs = now;
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function v1Bytes(
|
||||
rnds: Uint8Array,
|
||||
msecs?: number,
|
||||
nsecs?: number,
|
||||
clockseq?: number,
|
||||
node?: Uint8Array,
|
||||
buf?: Uint8Array,
|
||||
offset = 0
|
||||
) {
|
||||
// Defaults
|
||||
if (!buf) {
|
||||
buf = new Uint8Array(16);
|
||||
offset = 0;
|
||||
}
|
||||
msecs ??= Date.now();
|
||||
nsecs ??= 0;
|
||||
clockseq ??= ((rnds[8] << 8) | rnds[9]) & 0x3fff;
|
||||
node ??= rnds.slice(10, 16);
|
||||
|
||||
// Per 4.2.1.2 Throw error if too many uuids are requested
|
||||
if (nsecs >= 10000) {
|
||||
throw new Error("uuid.v1(): Can't create more than 10M uuids/sec");
|
||||
}
|
||||
|
||||
_lastMSecs = msecs;
|
||||
_lastNSecs = nsecs;
|
||||
_clockseq = clockseq;
|
||||
|
||||
// Per 4.1.4 - Convert from unix epoch to Gregorian epoch
|
||||
// Offset to Gregorian epoch
|
||||
// https://www.rfc-editor.org/rfc/rfc9562.html#section-5.1-1
|
||||
msecs += 12219292800000;
|
||||
|
||||
// `time_low`
|
||||
const tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000;
|
||||
b[i++] = (tl >>> 24) & 0xff;
|
||||
b[i++] = (tl >>> 16) & 0xff;
|
||||
b[i++] = (tl >>> 8) & 0xff;
|
||||
b[i++] = tl & 0xff;
|
||||
buf[offset++] = (tl >>> 24) & 0xff;
|
||||
buf[offset++] = (tl >>> 16) & 0xff;
|
||||
buf[offset++] = (tl >>> 8) & 0xff;
|
||||
buf[offset++] = tl & 0xff;
|
||||
|
||||
// `time_mid`
|
||||
const tmh = ((msecs / 0x100000000) * 10000) & 0xfffffff;
|
||||
b[i++] = (tmh >>> 8) & 0xff;
|
||||
b[i++] = tmh & 0xff;
|
||||
buf[offset++] = (tmh >>> 8) & 0xff;
|
||||
buf[offset++] = tmh & 0xff;
|
||||
|
||||
// `time_high_and_version`
|
||||
b[i++] = ((tmh >>> 24) & 0xf) | 0x10; // include version
|
||||
b[i++] = (tmh >>> 16) & 0xff;
|
||||
buf[offset++] = ((tmh >>> 24) & 0xf) | 0x10; // include version
|
||||
buf[offset++] = (tmh >>> 16) & 0xff;
|
||||
|
||||
// `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant)
|
||||
b[i++] = (clockseq >>> 8) | 0x80;
|
||||
// `clock_seq_hi_and_reserved` | variant
|
||||
buf[offset++] = (clockseq >>> 8) | 0x80;
|
||||
|
||||
// `clock_seq_low`
|
||||
b[i++] = clockseq & 0xff;
|
||||
buf[offset++] = clockseq & 0xff;
|
||||
|
||||
// `node`
|
||||
for (let n = 0; n < 6; ++n) {
|
||||
b[i + n] = node[n];
|
||||
buf[offset++] = node[n];
|
||||
}
|
||||
|
||||
return buf || unsafeStringify(b);
|
||||
return buf;
|
||||
}
|
||||
|
||||
export default v1;
|
||||
|
||||
@@ -3,14 +3,11 @@ import rng from './rng.js';
|
||||
import { unsafeStringify } from './stringify.js';
|
||||
|
||||
type V7State = {
|
||||
msecs: number; // time, milliseconds
|
||||
seq: number; // sequence number (32-bits)
|
||||
msecs?: number; // time, milliseconds
|
||||
seq?: number; // sequence number (32-bits)
|
||||
};
|
||||
|
||||
const _state: V7State = {
|
||||
msecs: -Infinity,
|
||||
seq: 0,
|
||||
};
|
||||
const _state: V7State = {};
|
||||
|
||||
function v7(options?: Version7Options, buf?: undefined, offset?: number): string;
|
||||
function v7(options?: Version7Options, buf?: Uint8Array, offset?: number): Uint8Array;
|
||||
@@ -42,6 +39,9 @@ function v7(options?: Version7Options, buf?: Uint8Array, offset?: number): UUIDT
|
||||
// (Private!) Do not use. This method is only exported for testing purposes
|
||||
// and may change without notice.
|
||||
export function updateV7State(state: V7State, now: number, rnds: Uint8Array) {
|
||||
state.msecs ??= -Infinity;
|
||||
state.seq ??= 0;
|
||||
|
||||
if (now > state.msecs) {
|
||||
// Time has moved on! Pick a new random sequence number
|
||||
state.seq = (rnds[6] << 23) | (rnds[7] << 16) | (rnds[8] << 8) | rnds[9];
|
||||
|
||||
Reference in New Issue
Block a user