Skip to content

Commit

Permalink
release: version 0.1.0 (first version)
Browse files Browse the repository at this point in the history
  • Loading branch information
babiabeo committed Aug 7, 2024
0 parents commit 3575f42
Show file tree
Hide file tree
Showing 12 changed files with 359 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @babiabeo
52 changes: 52 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: ci

on:
push:
branches: ["main"]
pull_request:
branches: ["main"]

permissions:
contents: read

jobs:
test:
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
deno: [canary]
os:
- ubuntu-22.04
- windows-2022
- macOS-12

steps:
- name: Setup repo
uses: actions/checkout@v4

- name: Setup Deno
uses: denoland/setup-deno@v1
with:
deno-version: ${{ matrix.deno }}

- name: Install packages
run: deno add @std/assert

- name: Run tests
run: deno task test

lint:
runs-on: ubuntu-22.04
steps:
- name: Setup repo
uses: actions/checkout@v4

- name: Setup Deno
uses: denoland/setup-deno@v1
with:
deno-version: canary

- name: Check linting
run: deno task lint
16 changes: 16 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Publish

on:
push:
branches:
- main

jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # The OIDC ID token is used for authentication with JSR.
steps:
- uses: actions/checkout@v4
- run: npx jsr publish
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.vscode
deno.lock
20 changes: 20 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
MIT License

Copyright (c) 2024 David (babiabeo)

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# uuid-v7

[![JSR](https://jsr.io/badges/@babia/uuid-v7)][jsr]
[![JSR Score](https://jsr.io/badges/@babia/uuid-v7/score)][jsr]
[![CI](https://github.com/babiabeo/uuid-v7/actions/workflows/ci.yml/badge.svg)](https://github.com/babiabeo/uuid-v7/actions/workflows/ci.yml)

The `uuid-v7` package provides UUIDv7 generator and validator based on [RFC 9562][rfc].

UUIDv7 features a time-ordered value field derived from the widely implemented
and well-known Unix Epoch timestamp source, the number of milliseconds since
midnight 1 Jan 1970 UTC, leap seconds excluded.

## Quick start

#### Install

This package is available on [jsr.io][jsr].

```sh
# Deno
deno add @babia/uuid-v7

# npm
npx jsr add @babia/uuid-v7

# yarn
yarn dlx jsr add @babia/uuid-v7

# pnpm
pnpm dlx jsr add @babia/uuid-v7

# Bun
bunx jsr add @babia/uuid-v7
```

#### Example: Generate a new uuid

```ts
import { generate } from "@babia/uuid-v7";

generate() // => 01912d68-783e-7a03-8467-5661c1243ad4
```

## Documentation

The documentation for the package can be found here:

https://jsr.io/@babia/uuid-v7/doc

## License

This package is licensed for use under [MIT License](./LICENSE).

[jsr]: https://jsr.io/@babia/uuid-v7
[rfc]: https://datatracker.ietf.org/doc/html/rfc9562#name-uuid-version-7

---

###### Copyright © 2024 David (babiabeo)
20 changes: 20 additions & 0 deletions deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@babia/uuid-v7",
"version": "0.1.0",
"exports": {
".": "./mod.ts",
"./validate": "./src/validate.ts",
"./generate": "./src/generate.ts"
},
"imports": {
"@std/assert": "jsr:@std/assert@^1.0.2"
},
"exclude": [".vscode", "docs", ".github", "README.md", "LICENSE"],
"tasks": {
"check": "deno publish --dry-run --allow-dirty",
"lint": "deno fmt --check && deno lint",
"test": "deno test --trace-leaks --parallel --clean",
"ok": "deno task lint && deno task test && deno task check ",
"doc:view": "deno doc --html --name=\"@babia/uuid-v7\" ./mod.ts"
}
}
13 changes: 13 additions & 0 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* The `uuid-v7` package provides UUIDv7 generator and validator based on
* {@link https://datatracker.ietf.org/doc/html/rfc9562#name-uuid-version-7 | RFC 9562}.
*
* UUIDv7 features a time-ordered value field derived from the widely implemented
* and well-known Unix Epoch timestamp source, the number of milliseconds since
* midnight 1 Jan 1970 UTC, leap seconds excluded
*
* @module
*/

export { generate } from "./src/generate.ts";
export { validate } from "./src/validate.ts";
33 changes: 33 additions & 0 deletions src/_stringify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Based on: https://github.com/uuidjs/uuid/blob/1e0f9870db864ca93f7a69db0d468b5e1b7605e7/src/stringify.ts

const byteToHex: string[] = [];

for (let i = 0; i < 256; ++i) {
byteToHex.push((i + 0x100).toString(16).slice(1));
}

/** Converts uuid from bytes to string. */
export function stringify(buf: Uint8Array): string {
return (
byteToHex[buf[0]] +
byteToHex[buf[1]] +
byteToHex[buf[2]] +
byteToHex[buf[3]] +
"-" +
byteToHex[buf[4]] +
byteToHex[buf[5]] +
"-" +
byteToHex[buf[6]] +
byteToHex[buf[7]] +
"-" +
byteToHex[buf[8]] +
byteToHex[buf[9]] +
"-" +
byteToHex[buf[10]] +
byteToHex[buf[11]] +
byteToHex[buf[12]] +
byteToHex[buf[13]] +
byteToHex[buf[14]] +
byteToHex[buf[15]]
).toLowerCase();
}
73 changes: 73 additions & 0 deletions src/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { stringify } from "./_stringify.ts";

/**
* Generates an {@link https://datatracker.ietf.org/doc/html/rfc9562#name-uuid-version-7 | UUID version 7}
* based on Unix timestamp.
*
* @param timestamp The custom timestamp to generate the UUID.
* @returns A verion 7 UUID
*
* @example Usage
* ```ts
* import { generate } from "@babia/uuid-v7";
*
* const u1 = generate(); // Using the current timestamp
* const u2 = generate(123); // Using the custom timestamp
* ```
*/
export function generate(timestamp?: number): string {
const uuid = new Uint8Array(16);
const tm = timestamp ?? Date.now();
const rand = crypto.getRandomValues(new Uint8Array(9));
const seq = getSeq(tm, rand);

// [octets 0-5]: timestamp (48 bits)
uuid[0] = (tm / 0x10000000000) & 0xff;
uuid[1] = (tm / 0x100000000) & 0xff;
uuid[2] = (tm / 0x1000000) & 0xff;
uuid[3] = (tm / 0x10000) & 0xff;
uuid[4] = (tm / 0x100) & 0xff;
uuid[5] = tm & 0xff;

// [octet 6]: ver (4 bits) | seq bits 14-17 (4 bits)
uuid[6] = 0x70 | ((seq >>> 14) & 0x0f);

// [octet 7]: seq bits 6-13 (8 bits)
uuid[7] = (seq >>> 6) & 0xff;

// [octet 8]: var (2 bits) | seq bits 0-5 (6 bits)
uuid[8] = 0x80 | (seq & 0x3f);

// [octets 9-15]: random (56 bits)
uuid[9] = rand[2];
uuid[10] = rand[3];
uuid[11] = rand[4];
uuid[12] = rand[5];
uuid[13] = rand[6];
uuid[14] = rand[7];
uuid[15] = rand[8];

return stringify(uuid);
}

// The last time the function is called
let _lastTime: number = -Infinity;
// The sequence number (18 bits)
let _seq: number | null = null;

function getSeq(now: number, rand: Uint8Array): number {
_seq ??= ((now & 0x03) << 16) | (rand[0] << 8) | rand[1];

if (now > _lastTime) {
_lastTime = now;
return _seq;
}

_seq = (_seq + 1) & 0x3ffff;

if (_seq === 0) {
++_lastTime;
}

return _seq;
}
47 changes: 47 additions & 0 deletions src/uuid_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { assert, assertEquals, assertNotEquals } from "@std/assert";
import { generate } from "./generate.ts";
import { validate } from "./validate.ts";

// https://datatracker.ietf.org/doc/html/rfc9562#name-example-of-a-uuidv7-value
const RFC_TIMESTAMP = 0x017F22E279B0;

Deno.test("Check if the version of the provided uuid is 7", () => {
assertEquals(validate(generate()), true);
assertEquals(validate(generate(RFC_TIMESTAMP)), true);
assertEquals(validate(crypto.randomUUID()), false);
assertEquals(validate("00000000-0000-0000-0000-000000000000"), false);
});

Deno.test("Each uuid is unique", () => {
assertNotEquals(generate(), generate());
});

Deno.test("Timestamp can be equal, but uuids cannot", () => {
const uuids = new Set();

for (let i = 0; i < 200; ++i) {
const uuid = generate(RFC_TIMESTAMP);
assert(!uuids.has(uuid));
uuids.add(uuid);
}
});

Deno.test("Check uuids monotonicity", () => {
let u1 = generate();

for (let i = 0; i < 10000; ++i) {
const u2 = generate();
assert(u2 > u1, `Monotonicity failed: ${u2} <= ${u1}`);
u1 = u2;
}
});

Deno.test("Check uuids monotonicity with the same timestamp", () => {
let u1 = generate(RFC_TIMESTAMP);

for (let i = 0; i < 10000; ++i) {
const u2 = generate(RFC_TIMESTAMP);
assert(u2 > u1, `Monotonicity failed: ${u2} <= ${u1}`);
u1 = u2;
}
});
23 changes: 23 additions & 0 deletions src/validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const UUID_RE =
/^[0-9a-f]{8}-[0-9a-f]{4}-[7][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;

/**
* Determines whether a string is a valid
* {@link https://datatracker.ietf.org/doc/html/rfc9562#name-uuid-version-7 | UUID v7}.
*
* @param uuid The UUID value
*
* @returns `true` if the string is a valid UUID v7, otherwise `false`.
*
* @example Usage
* ```ts
* import { validate } from "@babia/uuid-v7";
* import { assert, assertFalse } from "@std/assert";
*
* assert(validate("01912747-539e-7817-a728-739eee071268"));
* assertFalse(validate("943bb280-732e-4ae4-a4a5-c931fc67d891"));
* ```
*/
export function validate(uuid: string): boolean {
return UUID_RE.test(uuid);
}

0 comments on commit 3575f42

Please sign in to comment.