Skip to content

Commit

Permalink
fix!: refactor v1 internal state and options logic
Browse files Browse the repository at this point in the history
  • Loading branch information
broofa committed Jul 18, 2024
1 parent 7eff835 commit 62d6734
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 125 deletions.
35 changes: 0 additions & 35 deletions src/test/v1-rng.test.ts

This file was deleted.

30 changes: 30 additions & 0 deletions src/test/v1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,36 @@ describe('v1', () => {
);
});

test('explicit options.random produces expected id', () => {
function rng() {
return Uint8Array.of(
0x10,
0x91,
0x56,
0xbe,
0xc4,
0xfb,
0xc1,
0xea,
0x71,
0xb4,
0xef,
0xe1,
0x67,
0x1c,
0x58,
0x36
);
}

const id = v1({
msecs: 1321651533573,
nsecs: 5432,
rng,
});
assert.strictEqual(id, 'd9428888-122b-11e1-81ea-119156bec4fb');
});

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 });
Expand Down
221 changes: 131 additions & 90 deletions src/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,133 +7,174 @@ 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 count 100-nanosecond intervals since the Gregorian
// epoch, (1582-10-15 00:00). JS Numbers aren't precise enough for this, so we
// represent them internally using 'msecs' (integer milliseconds, 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 = {
msecs: -Infinity,
nsecs: 0,
};

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,
isV6 ? undefined : _state.clockseq,
isV6 ? undefined : _state.node,
buf,
offset
);
}

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;
}
export function updateV1State(state: V1State, now: number, rnds: Uint8Array) {
// 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
// selecting a new 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;
}
state.msecs = now;

// 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();

// 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;
// Init node (do this after timestamp update which may reset the node)
if (!state.node) {
state.node = rnds.slice(10, 16);

// Time since last uuid creation (in msecs)
const dt = msecs - _lastMSecs + (nsecs - _lastNSecs) / 10000;
// Set multicast bit
// https://www.rfc-editor.org/rfc/rfc9562.html#section-6.10-3
state.node[0] |= 0x01; // Set multicast bit

// 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 = undefined;
}

// 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;
// Init clock sequence to random value (do this after node initialization,
// which may reset the clock sequence)
// https://www.rfc-editor.org/rfc/rfc9562.html#section-5.1-7
if (state.clockseq === undefined) {
state.clockseq = ((rnds[6] << 8) | rnds[7]) & 0x3fff;
} else if (now < state.msecs) {
// Bump clockseq on clock regression
// https://www.rfc-editor.org/rfc/rfc9562.html#section-5.1-7
state.clockseq = (state.clockseq + 1) & 0x3fff;
}

// 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[6] << 8) | rnds[7]) & 0x3fff;
node ??= rnds.slice(0, 6);

// 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;

0 comments on commit 62d6734

Please sign in to comment.