Skip to content

Commit

Permalink
fix: v7 options API
Browse files Browse the repository at this point in the history
  • Loading branch information
broofa committed Jul 16, 2024
1 parent f8b253a commit 62dc290
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 132 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,10 +338,10 @@ Create an RFC version 7 (random) UUID
| | |
| --- | --- |
| [`options`] | `Object` with one or more of the following properties: |
| [`options.msecs`] | RFC "timestamp" field (`Number` of milliseconds, unix epoch) |
| [`options.msecs`] | RFC "timestamp" field (`Number` of milliseconds, unix epoch). Default = `Date.now()` |
| [`options.random`] | `Array` of 16 random bytes (0-255) |
| [`options.rng`] | Alternative to `options.random`, a `Function` that returns an `Array` of 16 random bytes (0-255) |
| [`options.seq`] | 31 bit monotonic sequence counter as `Number` between 0 - 0x7fffffff |
| [`options.seq`] | 32-bit sequence `Number` between 0 - 0xffffffff. Default = random value. |
| [`buffer`] | `Array \| Buffer` If specified, uuid will be written here in byte-form, starting at `offset` |
| [`offset` = 0] | `Number` Index to start writing UUID bytes in `buffer` |
| _returns_ | UUID `String` if no `buffer` is specified, otherwise returns `buffer` |
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@
"test:browser": "wdio run ./wdio.conf.js",
"test:node": "npm-run-all --parallel examples:node:**",
"test:pack": "./scripts/testpack.sh",
"test:watch": "node --test --watch dist/esm/test",
"test": "node --test dist/esm/test"
"test:watch": "node --test --enable-source-maps --watch dist/esm/test",
"test": "node --test --enable-source-maps dist/esm/test"
},
"repository": {
"type": "git",
Expand Down
15 changes: 9 additions & 6 deletions src/test/v7.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as assert from 'assert';
import test, { describe } from 'node:test';
import { Version7Options } from '../_types.js';
import stringify, { unsafeStringify } from '../stringify.js';
import v7 from '../v7.js';
import stringify from '../stringify.js';

/**
* fixture bit layout:
Expand All @@ -26,7 +26,7 @@ import stringify from '../stringify.js';
*/

describe('v7', () => {
const msecsFixture = 1645557742000;
const msecsFixture = 0x17f22e279b0;
const seqFixture = 0x661b189b;

const randomBytesFixture = Uint8Array.of(
Expand All @@ -48,6 +48,7 @@ describe('v7', () => {
0x8f
);

// const expectedBytes = parse('017f22e2-79b0-7cc3-98c4-dc0c0c07398f');
const expectedBytes = Uint8Array.of(
1,
127,
Expand Down Expand Up @@ -79,7 +80,7 @@ describe('v7', () => {
msecs: msecsFixture,
seq: seqFixture,
});
assert.strictEqual(id, '017f22e2-79b0-7cc3-98c4-dc0c0c07398f');
assert.strictEqual(id, '017f22e2-79b0-7661-ac62-6c0c0c07398f');
});

test('explicit options.rng produces expected result', () => {
Expand All @@ -88,7 +89,7 @@ describe('v7', () => {
msecs: msecsFixture,
seq: seqFixture,
});
assert.strictEqual(id, '017f22e2-79b0-7cc3-98c4-dc0c0c07398f');
assert.strictEqual(id, '017f22e2-79b0-7661-ac62-6c0c0c07398f');
});

test('explicit options.msecs produces expected result', () => {
Expand All @@ -108,8 +109,10 @@ describe('v7', () => {
},
buffer
);
assert.deepEqual(buffer, expectedBytes);
assert.strictEqual(buffer, result);
stringify(buffer);

assert.deepEqual(unsafeStringify(buffer), unsafeStringify(expectedBytes));
// assert.strictEqual(buffer, result);
});

test('fills two UUIDs into a buffer as expected', () => {
Expand Down
190 changes: 68 additions & 122 deletions src/v7.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,154 +2,100 @@ import { UUIDTypes, Version7Options } from './_types.js';
import rng from './rng.js';
import { unsafeStringify } from './stringify.js';

/**
* UUID V7 - Unix Epoch time-based UUID
*
* The IETF has published RFC9562, introducing 3 new UUID versions (6,7,8). This
* implementation of V7 is based on the accepted, though not yet approved,
* revisions.
*
* RFC 9562:https://www.rfc-editor.org/rfc/rfc9562.html Universally Unique
* IDentifiers (UUIDs)
*
* Sample V7 value:
* https://www.rfc-editor.org/rfc/rfc9562.html#name-example-of-a-uuidv7-value
*
* Monotonic Bit Layout: RFC rfc9562.6.2 Method 1, Dedicated Counter Bits ref:
* https://www.rfc-editor.org/rfc/rfc9562.html#section-6.2-5.1
*
* 0 1 2 3 0 1 2 3 4 5 6
* 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | unix_ts_ms |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | unix_ts_ms | ver | seq_hi |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |var| seq_low | rand |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | rand |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*
* seq is a 31 bit serialized counter; comprised of 12 bit seq_hi and 19 bit
* seq_low, and randomly initialized upon timestamp change. 31 bit counter size
* was selected as any bitwise operations in node are done as _signed_ 32 bit
* ints. we exclude the sign bit.
*/

let _seqLow: number | null = null;
let _seqHigh: number | null = null;
let _msecs = 0;
// Internally-maintained generator state
let _msecs = -Infinity; // -Infinity here forces seq initialization
let _seq = 0;

function v7(options?: Version7Options, buf?: undefined, offset?: number): string;
function v7(options?: Version7Options, buf?: Uint8Array, offset?: number): Uint8Array;
function v7(options?: Version7Options, buf?: Uint8Array, offset?: number): UUIDTypes {
options ??= {};

// initialize buffer and pointer
let i = (buf && offset) || 0;
const b = buf || new Uint8Array(16);

// rnds is Uint8Array(16) filled with random bytes
const rnds = options.random || (options.rng || rng)();

// milliseconds since unix epoch, 1970-01-01 00:00
const msecs = options.msecs !== undefined ? options.msecs : Date.now();

// seq is user provided 31 bit counter
let seq = options.seq !== undefined ? options.seq : null;

// initialize local seq high/low parts
let seqHigh = _seqHigh;
let seqLow = _seqLow;

// check if clock has advanced and user has not provided msecs
if (msecs > _msecs && options.msecs === undefined) {
_msecs = msecs;

// unless user provided seq, reset seq parts
if (seq !== null) {
seqHigh = null;
seqLow = null;
let bytes: Uint8Array;

if (options) {
// console.log('options', options);
// Options supplied = UUID is independent of internal state
bytes = v7Bytes(
options.random ?? options.rng?.() ?? rng(),
options.msecs,
options.seq,
buf,
offset
);
} else {
// console.log('no options');
// No options = Use internal state
const msecs = Date.now();

let rnds: Uint8Array | undefined;

// Randomize sequence counter when time interval changes
if (msecs > _msecs) {
rnds = rng();
_seq = (rnds[6] << 23) | (rnds[7] << 16) | (rnds[8] << 8) | rnds[9];
} else {
_seq++;
}
}

// if we have a user provided seq
if (seq !== null) {
// trim provided seq to 31 bits of value, avoiding overflow
if (seq > 0x7fffffff) {
seq = 0x7fffffff;
}
_msecs = msecs;

// split provided seq into high/low parts
seqHigh = (seq >>> 19) & 0xfff;
seqLow = seq & 0x7ffff;
bytes = v7Bytes(rnds || rng(), _msecs, _seq, buf, offset);
}

// randomly initialize seq
if (seqHigh === null || seqLow === null) {
seqHigh = rnds[6] & 0x7f;
seqHigh = (seqHigh << 8) | rnds[7];

seqLow = rnds[8] & 0x3f; // pad for var
seqLow = (seqLow << 8) | rnds[9];
seqLow = (seqLow << 5) | (rnds[10] >>> 3);
}
return buf ? bytes : unsafeStringify(bytes);
}

// increment seq if within msecs window
if (msecs + 10000 > _msecs && seq === null) {
if (++seqLow > 0x7ffff) {
seqLow = 0;
function v7Bytes(
rnds: Uint8Array,
msecs: number | undefined,
seq: number | undefined,
buf = new Uint8Array(16),
offset = 0
) {
msecs ??= Date.now();

if (++seqHigh > 0xfff) {
seqHigh = 0;
seq ??= ((rnds[6] * 0x7f) << 24) | (rnds[7] << 16) | (rnds[8] << 8) | rnds[9];

// increment internal _msecs. this allows us to continue incrementing
// while staying monotonic. Note, once we hit 10k milliseconds beyond system
// clock, we will reset breaking monotonicity (after (2^31)*10000 generations)
_msecs++;
}
}
} else {
// resetting; we have advanced more than
// 10k milliseconds beyond system clock
_msecs = msecs;
if (seq > 0x7fffffff) {
seq = 0x7fffffff;
}

_seqHigh = seqHigh;
_seqLow = seqLow;
// const seqHigh = (seq >>> 20) & 0xfff; // 12 bits
// const seqLow = seq & 0xfffff; // 20 bits
const seqHigh = (seq >>> 19) & 0xfff; // 12 bits
const seqLow = seq & 0x7ffff; // 19 bits
console.log('SEQ', seqHigh.toString(16), seqLow.toString(16));

// [bytes 0-5] 48 bits of local timestamp
b[i++] = (_msecs / 0x10000000000) & 0xff;
b[i++] = (_msecs / 0x100000000) & 0xff;
b[i++] = (_msecs / 0x1000000) & 0xff;
b[i++] = (_msecs / 0x10000) & 0xff;
b[i++] = (_msecs / 0x100) & 0xff;
b[i++] = _msecs & 0xff;
buf[offset++] = (msecs / 0x10000000000) & 0xff;
buf[offset++] = (msecs / 0x100000000) & 0xff;
buf[offset++] = (msecs / 0x1000000) & 0xff;
buf[offset++] = (msecs / 0x10000) & 0xff;
buf[offset++] = (msecs / 0x100) & 0xff;
buf[offset++] = msecs & 0xff;

// [byte 6] - set 4 bits of version (7) with first 4 bits seq_hi
b[i++] = ((seqHigh >>> 8) & 0x0f) | 0x70;
buf[offset++] = ((seqHigh >>> 8) & 0x0f) | 0x70;

// [byte 7] remaining 8 bits of seq_hi
b[i++] = seqHigh & 0xff;
buf[offset++] = seqHigh & 0xff;

// [byte 8] - variant (2 bits), first 6 bits seq_low
b[i++] = ((seqLow >>> 13) & 0x3f) | 0x80;
// [byte 8] - variant (2 bits), first 6 bits seqLow
buf[offset++] = ((seqLow >>> 13) & 0x3f) | 0x80;

// [byte 9] 8 bits seq_low
b[i++] = (seqLow >>> 5) & 0xff;
// [byte 9] 8 bits seqLow
buf[offset++] = (seqLow >>> 5) & 0xff;

// [byte 10] remaining 5 bits seq_low, 3 bits random
b[i++] = ((seqLow << 3) & 0xff) | (rnds[10] & 0x07);
// [byte 10] remaining 5 bits seqLow, 3 bits random
buf[offset++] = ((seqLow << 3) & 0xff) | (rnds[10] & 0x03);

// [bytes 11-15] always random
b[i++] = rnds[11];
b[i++] = rnds[12];
b[i++] = rnds[13];
b[i++] = rnds[14];
b[i++] = rnds[15];
buf[offset++] = rnds[11];
buf[offset++] = rnds[12];
buf[offset++] = rnds[13];
buf[offset++] = rnds[14];
buf[offset++] = rnds[15];

return buf || unsafeStringify(b);
return buf;
}

export default v7;

0 comments on commit 62dc290

Please sign in to comment.