diff --git a/.local/uuid/v7.js b/.local/uuid/v7.js new file mode 120000 index 00000000..c54100ca --- /dev/null +++ b/.local/uuid/v7.js @@ -0,0 +1 @@ +../../v7.js \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index 49780785..5538ca13 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,4 @@ dist/ node_modules/ README.md +*.sh \ No newline at end of file diff --git a/AUTHORS b/AUTHORS index 5a105230..d61b4eb3 100644 --- a/AUTHORS +++ b/AUTHORS @@ -3,3 +3,4 @@ Christoph Tavan AJ ONeal Vincent Voyer Roman Shtylman +Patrick McCarren diff --git a/README.md b/README.md index 4f51e098..816ece0f 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ For the creation of [RFC4122](https://www.ietf.org/rfc/rfc4122.txt) UUIDs -- **Complete** - Support for RFC4122 version 1, 3, 4, and 5 UUIDs +- **Complete** - Support for RFC4122 version 1, 3, 4, 5, and 7 UUIDs - **Cross-platform** - Support for ... - CommonJS, [ECMAScript Modules](#ecmascript-modules) and [CDN builds](#cdn-builds) - NodeJS 12+ ([LTS releases](https://github.com/nodejs/Release)) @@ -59,6 +59,7 @@ For timestamp UUIDs, namespace UUIDs, and other options read on ... | [`uuid.v3()`](#uuidv3name-namespace-buffer-offset) | Create a version 3 (namespace w/ MD5) UUID | | | [`uuid.v4()`](#uuidv4options-buffer-offset) | Create a version 4 (random) UUID | | | [`uuid.v5()`](#uuidv5name-namespace-buffer-offset) | Create a version 5 (namespace w/ SHA-1) UUID | | +| [`uuid.v7()`](#uuidv7options-buffer-offset) | Create a version 7 (Unix Epoch time-based) UUID | `experimental support` | | [`uuid.validate()`](#uuidvalidatestr) | Test a string to see if it is a valid UUID | New in `uuid@8.3` | | [`uuid.version()`](#uuidversionstr) | Detect RFC version of a UUID | New in `uuid@8.3` | @@ -251,6 +252,29 @@ import { v5 as uuidv5 } from 'uuid'; uuidv5('https://www.w3.org/', uuidv5.URL); // ⇨ 'c106a26a-21bb-5538-8bf2-57095d1976c1' ``` +### uuid.v7([options[, buffer[, offset]]]) + +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.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 | +| [`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` | + +Example: + +```javascript +import { v7 as uuidv7 } from 'uuid'; + +uuidv7(); // ⇨ '01695553-c90c-7aad-9bdd-330d7b3dcb6d' +``` + ### uuid.validate(str) Test a string to see if it is a valid UUID @@ -325,6 +349,7 @@ Usage: uuid v3 uuid v4 uuid v5 + uuid v7 uuid --help Note: may be "URL" or "DNS" to use the corresponding UUIDs diff --git a/README_js.md b/README_js.md index 46f9033e..88c0c3dd 100644 --- a/README_js.md +++ b/README_js.md @@ -21,7 +21,7 @@ require('crypto').randomUUID = undefined; For the creation of [RFC4122](https://www.ietf.org/rfc/rfc4122.txt) UUIDs -- **Complete** - Support for RFC4122 version 1, 3, 4, and 5 UUIDs +- **Complete** - Support for RFC4122 version 1, 3, 4, 5, and 7 UUIDs - **Cross-platform** - Support for ... - CommonJS, [ECMAScript Modules](#ecmascript-modules) and [CDN builds](#cdn-builds) - NodeJS 12+ ([LTS releases](https://github.com/nodejs/Release)) @@ -73,6 +73,7 @@ For timestamp UUIDs, namespace UUIDs, and other options read on ... | [`uuid.v3()`](#uuidv3name-namespace-buffer-offset) | Create a version 3 (namespace w/ MD5) UUID | | | [`uuid.v4()`](#uuidv4options-buffer-offset) | Create a version 4 (random) UUID | | | [`uuid.v5()`](#uuidv5name-namespace-buffer-offset) | Create a version 5 (namespace w/ SHA-1) UUID | | +| [`uuid.v7()`](#uuidv7options-buffer-offset) | Create a version 7 (Unix Epoch time-based) UUID | `experimental support` | | [`uuid.validate()`](#uuidvalidatestr) | Test a string to see if it is a valid UUID | New in `uuid@8.3` | | [`uuid.version()`](#uuidversionstr) | Detect RFC version of a UUID | New in `uuid@8.3` | @@ -259,6 +260,29 @@ import { v5 as uuidv5 } from 'uuid'; uuidv5('https://www.w3.org/', uuidv5.URL); // RESULT ``` +### uuid.v7([options[, buffer[, offset]]]) + +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.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 | +| [`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` | + +Example: + +```javascript --run +import { v7 as uuidv7 } from 'uuid'; + +uuidv7(); // RESULT +``` + ### uuid.validate(str) Test a string to see if it is a valid UUID @@ -333,6 +357,7 @@ Usage: uuid v3 uuid v4 uuid v5 + uuid v7 uuid --help Note: may be "URL" or "DNS" to use the corresponding UUIDs diff --git a/bundlewatch.config.json b/bundlewatch.config.json index 80b49a7a..b4fb5c20 100644 --- a/bundlewatch.config.json +++ b/bundlewatch.config.json @@ -20,6 +20,8 @@ "path": "./examples/browser-rollup/dist/v5-size.js", "maxSize": "1.5 kB" }, + { "path": "./examples/browser-rollup/dist/v7-size.js", "maxSize": "0.8 kB" }, + { "path": "./examples/browser-webpack/dist/v1-size.js", "maxSize": "1.0 kB" @@ -35,6 +37,7 @@ { "path": "./examples/browser-webpack/dist/v5-size.js", "maxSize": "1.5 kB" - } + }, + { "path": "./examples/browser-webpack/dist/v7-size.js", "maxSize": "0.8 kB" } ] } diff --git a/examples/benchmark/benchmark.js b/examples/benchmark/benchmark.js index 076f3b96..beda5105 100644 --- a/examples/benchmark/benchmark.js +++ b/examples/benchmark/benchmark.js @@ -57,12 +57,26 @@ export default function benchmark(uuid, Benchmark) { .add('uuid.v4() fill existing array', function () { uuid.v4(null, array, 0); }) + .add('uuid.v4() without native generation', function () { + uuid.v4({}); // passing an object instead of null bypasses native.randomUUID + }) .add('uuid.v3()', function () { uuid.v3('hello.example.com', uuid.v3.DNS); }) .add('uuid.v5()', function () { uuid.v5('hello.example.com', uuid.v5.DNS); }) + .add('uuid.v7()', function () { + uuid.v7(); + }) + .add('uuid.v7() fill existing array', function () { + uuid.v7(null, array, 0); + }) + .add('uuid.v7() with defined time', function () { + uuid.v7({ + msecs: 1645557742000, + }); + }) .on('cycle', function (event) { console.log(event.target.toString()); }) diff --git a/examples/benchmark/package-lock.json b/examples/benchmark/package-lock.json index b061b75d..c0cda780 100644 --- a/examples/benchmark/package-lock.json +++ b/examples/benchmark/package-lock.json @@ -15,20 +15,24 @@ } }, "../../.local/uuid": { - "version": "8.3.2", + "version": "9.0.1", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", "bin": { "uuid": "dist/bin/uuid" }, "devDependencies": { - "@babel/cli": "7.18.9", - "@babel/core": "7.18.9", + "@babel/cli": "7.18.10", + "@babel/core": "7.18.10", "@babel/eslint-parser": "7.18.9", - "@babel/preset-env": "7.18.9", + "@babel/preset-env": "7.18.10", "@commitlint/cli": "17.0.3", "@commitlint/config-conventional": "17.0.3", "bundlewatch": "0.3.3", - "eslint": "8.20.0", + "eslint": "8.21.0", "eslint-config-prettier": "8.5.0", "eslint-config-standard": "17.0.0", "eslint-plugin-import": "2.26.0", @@ -42,7 +46,7 @@ "optional-dev-dependency": "2.0.1", "prettier": "2.7.1", "random-seed": "0.3.0", - "runmd": "1.3.6", + "runmd": "1.3.9", "standard-version": "9.5.0" } }, @@ -99,14 +103,14 @@ "uuid": { "version": "file:../../.local/uuid", "requires": { - "@babel/cli": "7.18.9", - "@babel/core": "7.18.9", + "@babel/cli": "7.18.10", + "@babel/core": "7.18.10", "@babel/eslint-parser": "7.18.9", - "@babel/preset-env": "7.18.9", + "@babel/preset-env": "7.18.10", "@commitlint/cli": "17.0.3", "@commitlint/config-conventional": "17.0.3", "bundlewatch": "0.3.3", - "eslint": "8.20.0", + "eslint": "8.21.0", "eslint-config-prettier": "8.5.0", "eslint-config-standard": "17.0.0", "eslint-plugin-import": "2.26.0", @@ -120,7 +124,7 @@ "optional-dev-dependency": "2.0.1", "prettier": "2.7.1", "random-seed": "0.3.0", - "runmd": "1.3.6", + "runmd": "1.3.9", "standard-version": "9.5.0" } } diff --git a/examples/browser-esmodules/example.js b/examples/browser-esmodules/example.js index 7b1ac765..5cbb6e78 100644 --- a/examples/browser-esmodules/example.js +++ b/examples/browser-esmodules/example.js @@ -6,6 +6,7 @@ import { v3 as uuidv3, v4 as uuidv4, v5 as uuidv5, + v7 as uuidv7, validate as uuidValidate, version as uuidVersion, } from './node_modules/uuid/dist/esm-browser/index.js'; @@ -15,6 +16,8 @@ console.log('uuidv1()', uuidv1()); console.log('uuidv4()', uuidv4()); +console.log('uuidv7()', uuidv7()); + // ... using predefined DNS namespace (for domain names) console.log('uuidv3() DNS', uuidv3('hello.example.com', uuidv3.DNS)); @@ -52,6 +55,7 @@ console.log('Same with default export'); console.log('uuid.v1()', uuid.v1()); console.log('uuid.v4()', uuid.v4()); +console.log('uuid.v7()', uuid.v7()); console.log('uuid.v3() DNS', uuid.v3('hello.example.com', uuid.v3.DNS)); console.log('uuid.v3() URL', uuid.v3('http://example.com/hello', uuid.v3.URL)); console.log('uuid.v3() MY_NAMESPACE', uuid.v3('Hello, World!', MY_NAMESPACE)); diff --git a/examples/browser-rollup/README.md b/examples/browser-rollup/README.md index b1a3a578..68214ed4 100644 --- a/examples/browser-rollup/README.md +++ b/examples/browser-rollup/README.md @@ -7,11 +7,12 @@ npm start Then navigate to `example-*.html`. -The `example-v{1,4}.js` demonstrate that treeshaking works as expected: +The `example-v{1,4,7}.js` demonstrate that treeshaking works as expected: ``` $ du -sh dist/* 20K dist/all.js 8.0K dist/v1.js 4.0K dist/v4.js +4.0K dist/v7.js ``` diff --git a/examples/browser-rollup/example-all.js b/examples/browser-rollup/example-all.js index 69997651..07af1908 100644 --- a/examples/browser-rollup/example-all.js +++ b/examples/browser-rollup/example-all.js @@ -6,6 +6,7 @@ import { v3 as uuidv3, v4 as uuidv4, v5 as uuidv5, + v7 as uuidv7, validate as uuidValidate, version as uuidVersion, } from 'uuid'; @@ -20,6 +21,8 @@ testpage(function (addTest, done) { addTest('uuidv4()', uuidv4()); + addTest('uuidv7()', uuidv7()); + // ... using predefined DNS namespace (for domain names) addTest('uuidv3() DNS', uuidv3('hello.example.com', uuidv3.DNS)); @@ -57,6 +60,7 @@ testpage(function (addTest, done) { addTest('uuid.v1()', uuid.v1()); addTest('uuid.v4()', uuid.v4()); + addTest('uuid.v7()', uuid.v7()); addTest('uuid.v3() DNS', uuid.v3('hello.example.com', uuid.v3.DNS)); addTest('uuid.v3() URL', uuid.v3('http://example.com/hello', uuid.v3.URL)); addTest('uuid.v3() MY_NAMESPACE', uuid.v3('Hello, World!', MY_NAMESPACE)); diff --git a/examples/browser-rollup/example-v7.html b/examples/browser-rollup/example-v7.html new file mode 100644 index 00000000..6f6c75fd --- /dev/null +++ b/examples/browser-rollup/example-v7.html @@ -0,0 +1,10 @@ + + + + UUID esmodule webpack example + + + + + + diff --git a/examples/browser-rollup/example-v7.js b/examples/browser-rollup/example-v7.js new file mode 100644 index 00000000..bd807b23 --- /dev/null +++ b/examples/browser-rollup/example-v7.js @@ -0,0 +1,8 @@ +import { v7 as uuidv7 } from 'uuid'; + +import testpage from '../utils/testpage'; + +testpage(function (addTest, done) { + addTest('uuidv7()', uuidv7()); + done(); +}); diff --git a/examples/browser-rollup/example.html b/examples/browser-rollup/example.html index ca74545c..9f99126f 100644 --- a/examples/browser-rollup/example.html +++ b/examples/browser-rollup/example.html @@ -3,4 +3,5 @@

Please open the Developer Console to view output

+ diff --git a/examples/browser-rollup/rollup.config.js b/examples/browser-rollup/rollup.config.js index f6462ee5..8a914aa2 100644 --- a/examples/browser-rollup/rollup.config.js +++ b/examples/browser-rollup/rollup.config.js @@ -27,6 +27,14 @@ module.exports = [ }, plugins, }, + { + input: './example-v7.js', + output: { + file: 'dist/v7.js', + format: 'iife', + }, + plugins, + }, { input: './size-v1.js', @@ -60,4 +68,12 @@ module.exports = [ }, plugins, }, + { + input: './size-v7.js', + output: { + file: 'dist/v7-size.js', + format: 'cjs', + }, + plugins, + }, ]; diff --git a/examples/browser-rollup/size-v7.js b/examples/browser-rollup/size-v7.js new file mode 100644 index 00000000..a7e57335 --- /dev/null +++ b/examples/browser-rollup/size-v7.js @@ -0,0 +1,3 @@ +import { v7 as uuidv7 } from 'uuid'; + +uuidv7(); diff --git a/examples/browser-webpack/example-all-require.js b/examples/browser-webpack/example-all-require.js index 4e9b4feb..13fcc296 100644 --- a/examples/browser-webpack/example-all-require.js +++ b/examples/browser-webpack/example-all-require.js @@ -7,6 +7,7 @@ const { v3: uuidv3, v4: uuidv4, v5: uuidv5, + v7: uuidv7, validate: uuidValidate, version: uuidVersion, } = uuid; @@ -20,6 +21,8 @@ testpage(function (addTest, done) { addTest('uuidv4()', uuidv4()); + addTest('uuidv7()', uuidv7()); + // ... using predefined DNS namespace (for domain names) addTest('uuidv3() DNS', uuidv3('hello.example.com', uuidv3.DNS)); @@ -57,6 +60,7 @@ testpage(function (addTest, done) { addTest('uuid.v1()', uuid.v1()); addTest('uuid.v4()', uuid.v4()); + addTest('uuid.v7()', uuid.v7()); addTest('uuid.v3() DNS', uuid.v3('hello.example.com', uuid.v3.DNS)); addTest('uuid.v3() URL', uuid.v3('http://example.com/hello', uuid.v3.URL)); addTest('uuid.v3() MY_NAMESPACE', uuid.v3('Hello, World!', MY_NAMESPACE)); diff --git a/examples/browser-webpack/example-all.js b/examples/browser-webpack/example-all.js index 69997651..07af1908 100644 --- a/examples/browser-webpack/example-all.js +++ b/examples/browser-webpack/example-all.js @@ -6,6 +6,7 @@ import { v3 as uuidv3, v4 as uuidv4, v5 as uuidv5, + v7 as uuidv7, validate as uuidValidate, version as uuidVersion, } from 'uuid'; @@ -20,6 +21,8 @@ testpage(function (addTest, done) { addTest('uuidv4()', uuidv4()); + addTest('uuidv7()', uuidv7()); + // ... using predefined DNS namespace (for domain names) addTest('uuidv3() DNS', uuidv3('hello.example.com', uuidv3.DNS)); @@ -57,6 +60,7 @@ testpage(function (addTest, done) { addTest('uuid.v1()', uuid.v1()); addTest('uuid.v4()', uuid.v4()); + addTest('uuid.v7()', uuid.v7()); addTest('uuid.v3() DNS', uuid.v3('hello.example.com', uuid.v3.DNS)); addTest('uuid.v3() URL', uuid.v3('http://example.com/hello', uuid.v3.URL)); addTest('uuid.v3() MY_NAMESPACE', uuid.v3('Hello, World!', MY_NAMESPACE)); diff --git a/examples/browser-webpack/example-v7.html b/examples/browser-webpack/example-v7.html new file mode 100644 index 00000000..6f6c75fd --- /dev/null +++ b/examples/browser-webpack/example-v7.html @@ -0,0 +1,10 @@ + + + + UUID esmodule webpack example + + + + + + diff --git a/examples/browser-webpack/example-v7.js b/examples/browser-webpack/example-v7.js new file mode 100644 index 00000000..727b450b --- /dev/null +++ b/examples/browser-webpack/example-v7.js @@ -0,0 +1,9 @@ +import { v7 as uuidv7 } from 'uuid'; + +import testpage from '../utils/testpage'; + +testpage(function (addTest, done) { + addTest('uuidv7()', uuidv7()); + + done(); +}); diff --git a/examples/browser-webpack/example.html b/examples/browser-webpack/example.html index ca74545c..9f99126f 100644 --- a/examples/browser-webpack/example.html +++ b/examples/browser-webpack/example.html @@ -3,4 +3,5 @@

Please open the Developer Console to view output

+ diff --git a/examples/browser-webpack/size-v7.js b/examples/browser-webpack/size-v7.js new file mode 100644 index 00000000..a7e57335 --- /dev/null +++ b/examples/browser-webpack/size-v7.js @@ -0,0 +1,3 @@ +import { v7 as uuidv7 } from 'uuid'; + +uuidv7(); diff --git a/examples/browser-webpack/webpack.config.js b/examples/browser-webpack/webpack.config.js index f370bfcb..bb828942 100644 --- a/examples/browser-webpack/webpack.config.js +++ b/examples/browser-webpack/webpack.config.js @@ -7,11 +7,13 @@ module.exports = { allRequire: './example-all-require.js', v1: './example-v1.js', v4: './example-v4.js', + v7: './example-v7.js', 'v1-size': './size-v1.js', 'v3-size': './size-v3.js', 'v4-size': './size-v4.js', 'v5-size': './size-v5.js', + 'v7-size': './size-v7.js', }, // Webpack now produces builds that are incompatible with IE11: // https://webpack.js.org/migrate/5/#turn-off-es2015-syntax-in-runtime-code-if-necessary diff --git a/examples/node-commonjs/example.js b/examples/node-commonjs/example.js index 6a75f5a1..8d773de5 100644 --- a/examples/node-commonjs/example.js +++ b/examples/node-commonjs/example.js @@ -6,6 +6,7 @@ const { v3: uuidv3, v4: uuidv4, v5: uuidv5, + v7: uuidv7, validate: uuidValidate, version: uuidVersion, } = require('uuid'); @@ -16,6 +17,8 @@ console.log('uuidv1()', uuidv1()); console.log('uuidv4()', uuidv4()); +console.log('uuidv7()', uuidv7()); + // ... using predefined DNS namespace (for domain names) console.log('uuidv3() DNS', uuidv3('hello.example.com', uuidv3.DNS)); @@ -53,6 +56,7 @@ console.log('Same with default export'); console.log('uuid.v1()', uuid.v1()); console.log('uuid.v4()', uuid.v4()); +console.log('uuid.v7()', uuid.v7()); console.log('uuid.v3() DNS', uuid.v3('hello.example.com', uuid.v3.DNS)); console.log('uuid.v3() URL', uuid.v3('http://example.com/hello', uuid.v3.URL)); console.log('uuid.v3() MY_NAMESPACE', uuid.v3('Hello, World!', MY_NAMESPACE)); diff --git a/examples/node-esmodules/example.mjs b/examples/node-esmodules/example.mjs index c67927bb..433170d1 100644 --- a/examples/node-esmodules/example.mjs +++ b/examples/node-esmodules/example.mjs @@ -6,6 +6,7 @@ import { v3 as uuidv3, v4 as uuidv4, v5 as uuidv5, + v7 as uuidv7, validate as uuidValidate, version as uuidVersion, } from 'uuid'; @@ -15,6 +16,8 @@ console.log('uuidv1()', uuidv1()); console.log('uuidv4()', uuidv4()); +console.log('uuidv7()', uuidv7()); + // ... using predefined DNS namespace (for domain names) console.log('uuidv3() DNS', uuidv3('hello.example.com', uuidv3.DNS)); @@ -52,6 +55,7 @@ console.log('Same with default export'); console.log('uuid.v1()', uuid.v1()); console.log('uuid.v4()', uuid.v4()); +console.log('uuid.v7()', uuid.v7()); console.log('uuid.v3() DNS', uuid.v3('hello.example.com', uuid.v3.DNS)); console.log('uuid.v3() URL', uuid.v3('http://example.com/hello', uuid.v3.URL)); console.log('uuid.v3() MY_NAMESPACE', uuid.v3('Hello, World!', MY_NAMESPACE)); diff --git a/examples/node-jest/jsdom.test.js b/examples/node-jest/jsdom.test.js index 10cc7750..6773579b 100644 --- a/examples/node-jest/jsdom.test.js +++ b/examples/node-jest/jsdom.test.js @@ -6,3 +6,8 @@ test('uuidv4()', () => { const val = uuid.v4(); expect(uuid.version(val)).toBe(4); }); + +test('uuidv7()', () => { + const val = uuid.v7(); + expect(uuid.version(val)).toBe(7); +}); diff --git a/examples/node-jest/node.test.js b/examples/node-jest/node.test.js index 6626988e..02d93dc7 100644 --- a/examples/node-jest/node.test.js +++ b/examples/node-jest/node.test.js @@ -4,3 +4,8 @@ test('uuidv4()', () => { const val = uuid.v4(); expect(uuid.version(val)).toBe(4); }); + +test('uuidv7()', () => { + const val = uuid.v7(); + expect(uuid.version(val)).toBe(7); +}); diff --git a/examples/node-webpack/example-v7.js b/examples/node-webpack/example-v7.js new file mode 100644 index 00000000..2e9dc07c --- /dev/null +++ b/examples/node-webpack/example-v7.js @@ -0,0 +1,3 @@ +import { v7 as uuidv7 } from 'uuid'; + +console.log('uuidv7()', uuidv7()); diff --git a/examples/node-webpack/package.json b/examples/node-webpack/package.json index 1b45db55..5fbe12b0 100644 --- a/examples/node-webpack/package.json +++ b/examples/node-webpack/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "build": "rm -rf dist && webpack", - "test": "npm run build && node dist/v1.js && node dist/v4.js && node dist/all.js" + "test": "npm run build && node dist/v1.js && node dist/v4.js && node dist/v7.js && node dist/all.js" }, "dependencies": { "uuid": "file:../../.local/uuid" diff --git a/examples/node-webpack/webpack.config.js b/examples/node-webpack/webpack.config.js index bc006dff..c0771e43 100644 --- a/examples/node-webpack/webpack.config.js +++ b/examples/node-webpack/webpack.config.js @@ -6,6 +6,7 @@ module.exports = { all: './example-all.js', v1: './example-v1.js', v4: './example-v4.js', + v7: './example-v7.js', }, output: { filename: '[name].js', diff --git a/package.json b/package.json index eb8e540a..3ce43ec9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "uuid", "version": "9.0.1", - "description": "RFC4122 (v1, v4, and v5) UUIDs", + "description": "RFC4122 (v1, v3, v4, v5, and v7) UUIDs", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -98,6 +98,7 @@ "eslint:fix": "eslint --fix src/ test/ examples/ *.js", "pretest": "[ -n $CI ] || npm run build", "test": "BABEL_ENV=commonjsNode node --throw-deprecation node_modules/.bin/jest test/unit/", + "test:matching": "BABEL_ENV=commonjsNode node --throw-deprecation node_modules/.bin/jest test/unit/ -t", "pretest:browser": "optional-dev-dependency && npm run build && npm-run-all --parallel examples:browser:**", "test:browser": "wdio run ./wdio.conf.js", "pretest:node": "npm run build", diff --git a/src/index.js b/src/index.js index 142ce9e4..c982986c 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,7 @@ export { default as v1 } from './v1.js'; export { default as v3 } from './v3.js'; export { default as v4 } from './v4.js'; export { default as v5 } from './v5.js'; +export { default as v7 } from './v7.js'; export { default as NIL } from './nil.js'; export { default as version } from './version.js'; export { default as validate } from './validate.js'; diff --git a/src/regex.js b/src/regex.js index 92f79a1e..0e7a4590 100644 --- a/src/regex.js +++ b/src/regex.js @@ -1 +1 @@ -export default /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i; +export default /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i; diff --git a/src/uuid-bin.js b/src/uuid-bin.js index a411b193..164e6458 100644 --- a/src/uuid-bin.js +++ b/src/uuid-bin.js @@ -4,6 +4,7 @@ import v1 from './v1.js'; import v3 from './v3.js'; import v4 from './v4.js'; import v5 from './v5.js'; +import v7 from './v7.js'; function usage() { console.log('Usage:'); @@ -12,6 +13,7 @@ function usage() { console.log(' uuid v3 '); console.log(' uuid v4'); console.log(' uuid v5 '); + console.log(' uuid v7'); console.log(' uuid --help'); console.log( '\nNote: may be "URL" or "DNS" to use the corresponding UUIDs defined by RFC4122' @@ -74,6 +76,10 @@ switch (version) { break; } + case 'v7': + console.log(v7()); + break; + default: usage(); process.exit(1); diff --git a/src/v7.js b/src/v7.js new file mode 100644 index 00000000..515cbc7a --- /dev/null +++ b/src/v7.js @@ -0,0 +1,152 @@ +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 = null; +let _seqHigh = null; +let _msecs = 0; + +function v7(options, buf, offset) { + options = 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; + } + } + + // 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; + } + + // split provided seq into high/low parts + seqHigh = (seq >>> 19) & 0xfff; + seqLow = seq & 0x7ffff; + } + + // 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); + } + + // increment seq if within msecs window + if (msecs + 10000 > _msecs && seq === null) { + if (++seqLow > 0x7ffff) { + seqLow = 0; + + if (++seqHigh > 0xfff) { + seqHigh = 0; + + // 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; + } + + _seqHigh = seqHigh; + _seqLow = seqLow; + + // [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; + + // [byte 6] - set 4 bits of version (7) with first 4 bits seq_hi + b[i++] = ((seqHigh >>> 4) & 0x0f) | 0x70; + + // [byte 7] remaining 8 bits of seq_hi + b[i++] = seqHigh & 0xff; + + // [byte 8] - variant (2 bits), first 6 bits seq_low + b[i++] = ((seqLow >>> 13) & 0x3f) | 0x80; + + // [byte 9] 8 bits seq_low + b[i++] = (seqLow >>> 5) & 0xff; + + // [byte 10] remaining 5 bits seq_low, 3 bits random + b[i++] = ((seqLow << 3) & 0xff) | (rnds[10] & 0x07); + + // [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]; + + return buf || unsafeStringify(b); +} + +export default v7; diff --git a/test/unit/v7.test.js b/test/unit/v7.test.js new file mode 100644 index 00000000..aa2b5430 --- /dev/null +++ b/test/unit/v7.test.js @@ -0,0 +1,172 @@ +import assert from 'assert'; +import v7 from '../../src/v7.js'; + +/** + * fixture bit layout: + * ref: https://www.rfc-editor.org/rfc/rfc9562.html#name-example-of-a-uuidv7-value + * + * expectedBytes was calculated using this script: + * https://gist.github.com/d5382ac3a1ce4ba9ba40a90d9da8cbf1 + * + * ------------------------------- + * field bits value + * ------------------------------- + * unix_ts_ms 48 0x17F22E279B0 + * ver 4 0x7 + * rand_a 12 0xCC3 + * var 2 b10 + * rand_b 62 b01, 0x8C4DC0C0C07398F + * ------------------------------- + * total 128 + * ------------------------------- + * final: 017f22e2-79b0-7cc3-98c4-dc0c0c07398f + */ + +describe('v7', () => { + const msecsFixture = 1645557742000; + const seqFixture = 0x661b189b; + + const randomBytesFixture = [ + 0x10, 0x91, 0x56, 0xbe, 0xc4, 0xfb, 0x0c, 0xc3, 0x18, 0xc4, 0xdc, 0x0c, 0x0c, 0x07, 0x39, 0x8f, + ]; + + const expectedBytes = [1, 127, 34, 226, 121, 176, 124, 195, 152, 196, 220, 12, 12, 7, 57, 143]; + + test('subsequent UUIDs are different', () => { + const id1 = v7(); + const id2 = v7(); + assert(id1 !== id2); + }); + + test('explicit options.random and options.msecs produces expected result', () => { + const id = v7({ + random: randomBytesFixture, + msecs: msecsFixture, + seq: seqFixture, + }); + assert.strictEqual(id, '017f22e2-79b0-7cc3-98c4-dc0c0c07398f'); + }); + + test('explicit options.rng produces expected result', () => { + const id = v7({ + rng: () => randomBytesFixture, + msecs: msecsFixture, + seq: seqFixture, + }); + assert.strictEqual(id, '017f22e2-79b0-7cc3-98c4-dc0c0c07398f'); + }); + + test('explicit options.msecs produces expected result', () => { + const id = v7({ + msecs: msecsFixture, + }); + assert.strictEqual(id.indexOf('017f22e2'), 0); + }); + + test('fills one UUID into a buffer as expected', () => { + const buffer = []; + const result = v7( + { + random: randomBytesFixture, + msecs: msecsFixture, + seq: seqFixture, + }, + buffer + ); + assert.deepEqual(buffer, expectedBytes); + assert.strictEqual(buffer, result); + }); + + test('fills two UUIDs into a buffer as expected', () => { + const buffer = []; + v7( + { + random: randomBytesFixture, + msecs: msecsFixture, + seq: seqFixture, + }, + buffer, + 0 + ); + v7( + { + random: randomBytesFixture, + msecs: msecsFixture, + seq: seqFixture, + }, + buffer, + 16 + ); + assert.deepEqual(buffer, expectedBytes.concat(expectedBytes)); + }); + + // + // monotonic and lexicographical sorting tests + // + + test('lexicographical sorting is preserved', () => { + let id; + let prior; + let msecs = msecsFixture; + for (let i = 0; i < 20000; ++i) { + if (i % 1500 === 0) { + // every 1500 runs increment msecs so seq is + // reinitialized, simulating passage of time + msecs += 1; + } + + id = v7({ msecs }); + + if (i > 0) { + assert(prior < id, `${prior} < ${id}`); + } + + prior = id; + } + }); + + test('handles seq rollover', () => { + const msecs = msecsFixture; + const a = v7({ + msecs, + seq: 0x7fffffff, + }); + + v7({ msecs }); + + const c = v7({ msecs }); + + assert(a < c, `${a} < ${c}`); + }); + + test('can supply seq', () => { + let seq = 0x12345; + let uuid = v7({ + msecs: msecsFixture, + seq, + }); + + assert.strictEqual(uuid.substr(0, 25), '017f22e2-79b0-7000-891a-2'); + + seq = 0x6fffffff; + uuid = v7({ + msecs: msecsFixture, + seq, + }); + + assert.strictEqual(uuid.substr(0, 25), '017f22e2-79b0-7fff-bfff-f'); + }); + + test('internal seq is reset upon timestamp change', () => { + v7({ + msecs: msecsFixture, + seq: 0x6fffffff, + }); + + const uuid = v7({ + msecs: msecsFixture + 1, + }); + + assert(uuid.indexOf('fff') !== 15); + }); +}); diff --git a/test/unit/validate.test.js b/test/unit/validate.test.js index 7e290ff6..3357d28e 100644 --- a/test/unit/validate.test.js +++ b/test/unit/validate.test.js @@ -6,14 +6,32 @@ describe('validate', () => { test('validate uuid', () => { assert.strictEqual(validate(NIL), true); - assert.strictEqual(validate('d9428888-122b-11e1-b85c-61cd3cbb3210'), true); + // test valid UUID versions - assert.strictEqual(validate('109156be-c4fb-41ea-b1b4-efe1671c5836'), true); + // v1 + assert.strictEqual(validate('d9428888-122b-11e1-b85c-61cd3cbb3210'), true); + // v3 assert.strictEqual(validate('a981a0c2-68b1-35dc-bcfc-296e52ab01ec'), true); + // v4 + assert.strictEqual(validate('109156be-c4fb-41ea-b1b4-efe1671c5836'), true); + + // v5 assert.strictEqual(validate('90123e1c-7512-523e-bb28-76fab9f2f73d'), true); + // v7 + assert.strictEqual(validate('017f22e2-79b0-7cc3-98c4-dc0c0c07398f'), true); + + // test invalid/unsupported UUID versions + [0, 2, 6, 8, 9, 'a', 'b', 'c', 'd', 'e', 'f'].forEach((v) => { + assert.strictEqual( + validate('12300000-0000-' + v + '000-0000-000000000000'), + false, + 'version ' + v + ' should not be valid' + ); + }); + assert.strictEqual(validate(), false); assert.strictEqual(validate(''), false); diff --git a/test/unit/version.test.js b/test/unit/version.test.js index c482b4e7..f8edba8e 100644 --- a/test/unit/version.test.js +++ b/test/unit/version.test.js @@ -14,6 +14,8 @@ describe('version', () => { assert.strictEqual(version('90123e1c-7512-523e-bb28-76fab9f2f73d'), 5); + assert.strictEqual(version('017f22e2-79b0-7cc3-98c4-dc0c0c07398f'), 7); + assert.throws(() => version()); assert.throws(() => version('')); diff --git a/wrapper.mjs b/wrapper.mjs index c31e9cef..8e4a3e74 100644 --- a/wrapper.mjs +++ b/wrapper.mjs @@ -3,6 +3,7 @@ export const v1 = uuid.v1; export const v3 = uuid.v3; export const v4 = uuid.v4; export const v5 = uuid.v5; +export const v7 = uuid.v7; export const NIL = uuid.NIL; export const version = uuid.version; export const validate = uuid.validate;