diff --git a/examples/benchmark/benchmark.js b/examples/benchmark/benchmark.js index 3765cef2..911dfc3d 100644 --- a/examples/benchmark/benchmark.js +++ b/examples/benchmark/benchmark.js @@ -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); }) diff --git a/examples/benchmark/package-lock.json b/examples/benchmark/package-lock.json index 7cbd4eea..015637bd 100644 --- a/examples/benchmark/package-lock.json +++ b/examples/benchmark/package-lock.json @@ -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", diff --git a/src/test/v1-random.test.ts b/src/test/v1-random.test.ts deleted file mode 100644 index 3d26fc9d..00000000 --- a/src/test/v1-random.test.ts +++ /dev/null @@ -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'); - }); -}); diff --git a/src/test/v1-rng.test.ts b/src/test/v1-rng.test.ts deleted file mode 100644 index 7157b313..00000000 --- a/src/test/v1-rng.test.ts +++ /dev/null @@ -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'); - }); -}); diff --git a/src/test/v1.test.ts b/src/test/v1.test.ts index 11d44681..709b1680 100644 --- a/src/test/v1.test.ts +++ b/src/test/v1.test.ts @@ -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('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('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('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('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'); + 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'); }); - 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}`); + } + }); }); diff --git a/src/test/v6.test.ts b/src/test/v6.test.ts index aaad4521..5ae3929e 100644 --- a/src/test/v6.test.ts +++ b/src/test/v6.test.ts @@ -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', () => { diff --git a/src/test/v7.test.ts b/src/test/v7.test.ts index 5b7d280d..062b95a7 100644 --- a/src/test/v7.test.ts +++ b/src/test/v7.test.ts @@ -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); diff --git a/src/v1.ts b/src/v1.ts index eaa22b6f..0218251d 100644 --- a/src/v1.ts +++ b/src/v1.ts @@ -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 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; + let bytes: Uint8Array; + + // 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)(); - - // Randomize node - if (node == null) { - node = Uint8Array.of( - seedBytes[0], - seedBytes[1], - seedBytes[2], - seedBytes[3], - seedBytes[4], - seedBytes[5] - ); - - // 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 - - _nodeId = node; - } - } + 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(); + + updateV1State(_state, now, rnds); + + // 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 + ); + } - // 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; - } + return buf ? bytes : unsafeStringify(bytes); +} + +// (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; - // 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"); - } + return state; +} - _lastMSecs = msecs; - _lastNSecs = nsecs; - _clockseq = clockseq; +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.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; diff --git a/src/v7.ts b/src/v7.ts index f4fe5bba..f24ba3af 100644 --- a/src/v7.ts +++ b/src/v7.ts @@ -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];