Skip to content

Commit 886c5e7

Browse files
committed
feat: initial release
0 parents  commit 886c5e7

17 files changed

+6310
-0
lines changed

.github/workflows/cicd.yml

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: CI/CD
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- "*"
10+
11+
jobs:
12+
ci:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
- uses: pnpm/action-setup@v3
17+
with:
18+
version: 9
19+
- name: Setup Node.js
20+
uses: actions/setup-node@v4
21+
with:
22+
node-version: "20"
23+
cache: "pnpm"
24+
- run: pnpm install --frozen-lockfile
25+
- run: pnpm run lint
26+
- run: pnpm run test
27+
28+
cd:
29+
if: ${{ github.ref == 'refs/heads/main' }}
30+
runs-on: ubuntu-latest
31+
needs: [ci]
32+
steps:
33+
- uses: actions/checkout@v4
34+
- uses: pnpm/action-setup@v3
35+
with:
36+
version: 9
37+
- name: Setup Node.js
38+
uses: actions/setup-node@v4
39+
with:
40+
node-version: "20"
41+
cache: "pnpm"
42+
- run: pnpm install --frozen-lockfile
43+
- run: pnpm run build
44+
- name: Release
45+
env:
46+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
47+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48+
run: pnpm run deploy

.gitignore

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# macOS
2+
.DS_Store
3+
.AppleDouble
4+
.LSOverride
5+
6+
node_modules
7+
yarn.lock
8+
dist
9+
.env*
10+
*.pem
11+
.npmrc
12+
13+
npm-debug.log*
14+
yarn-debug.log*
15+
yarn-error.log*
16+
17+
# Exclude .example files
18+
!*.example
19+
20+
# ESLint
21+
.eslintcache
22+
23+
# TypeScript stuff
24+
*.tsbuildinfo
25+
26+
/src/test.ts

.husky/commit-msg

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/usr/bin/env sh
2+
. "$(dirname -- "$0")/_/husky.sh"
3+
4+
pnpm exec commitlint --edit "${1}"

.husky/prepare-commit-msg

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/usr/bin/env sh
2+
. "$(dirname -- "$0")/_/husky.sh"
3+
4+
exec < /dev/tty && pnpm exec cz --hook || true

.prettierrc.json

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"trailingComma": "es5",
3+
"printWidth": 120,
4+
"useTabs": true,
5+
"arrowParens": "always",
6+
"tabWidth": 2,
7+
"semi": true,
8+
"singleQuote": false,
9+
"quoteProps": "consistent"
10+
}

.vscode/extensions.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
3+
}

.vscode/settings.json

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"typescript.tsdk": "node_modules/typescript/lib",
3+
"prettier.enable": true,
4+
"eslint.validate": ["typescript"],
5+
"eslint.experimental.useFlatConfig": true,
6+
"editor.codeActionsOnSave": {
7+
"source.organizeImports": "explicit",
8+
"source.fixAll.eslint": "explicit"
9+
}
10+
}

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 Edoardo Ranghieri
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# [uuidv7-js](https://github.com/TheEdoRan/uuidv7-js)
2+
3+
UUIDv7 generator library for Node.js.
4+
5+
## Installation
6+
7+
```sh
8+
npm i uuidv7-js
9+
```
10+
11+
## Usage
12+
13+
```typescript
14+
import { UUIDv7, uuidv7, encodeUUIDv7, decodeUUIDv7 } from "uuidv7-js";
15+
16+
// Initialize a new UUIDv7 generator.
17+
// You can pass a custom encoding alphabet here.
18+
const uuidv7 = new UUIDv7();
19+
20+
const id = uuidv7.gen(); // 018ef3e8-90e2-7be4-b4ea-4be3bf8803b7
21+
const encoded = uuidv7.encode(id); // CANANjseoigQthQMd1VwC
22+
const decoded = uuidv7.decode(encoded); // 018ef3e8-90e2-7be4-b4ea-4be3bf8803b7
23+
const isValid = UUIDv7.isValid(id); // true
24+
const timestamp = UUIDv7.timestamp(id); // 1713489088738
25+
const date = UUIDv7.date(id); // 2024-04-19T01:11:28.738Z
26+
27+
// You can also use convenient function aliases if you don't need to use a custom alphabet.
28+
const id = uuidv7(); // 018ef3e8-90e2-7be4-b4ea-4be3bf8803b7
29+
const encoded = encodeUUIDv7(id); // CANANjseoigQthQMd1VwC
30+
const decoded = decodeUUIDv7(encoded); // // 018ef3e8-90e2-7be4-b4ea-4be3bf8803b7
31+
```
32+
33+
## Create a new instance
34+
35+
```typescript
36+
new UUIDv7(opts?: { encodeAlphabet: string })
37+
```
38+
39+
Creates a new `UUIDv7` instance. By default it uses the [Base58](https://www.cs.utexas.edu/users/moore/acl2/manuals/current/manual/index-seo.php/BITCOIN_____A2BASE58-CHARACTERS_A2) alphabet to `encode` and `decode` UUIDs, but you can pass a custom alphabet (16-64 characters).
40+
41+
### Instance methods
42+
43+
#### `gen`
44+
45+
```typescript
46+
gen() => string
47+
```
48+
49+
Generates a new UUIDv7.
50+
51+
#### `genMany`
52+
53+
```typescript
54+
genMany(amount: number) => string
55+
```
56+
57+
Generates a custom amount of UUIDv7s.
58+
59+
#### `encode`
60+
61+
```typescript
62+
encode(id: string) => string
63+
```
64+
65+
Encodes a UUIDv7 using the alphabet passed to the constructor or the default one.
66+
67+
#### `decode`
68+
69+
```typescript
70+
decode(encodedId: string) => string | null
71+
```
72+
73+
Decodes an encoded UUIDv7 using the alphabet passed to the constuctor or the default one. If the UUIDv7 is not valid, `null` is returned.
74+
75+
#### `decodeOrThrow`
76+
77+
```typescript
78+
decodeOrThrow(encodedId: string) => string
79+
```
80+
81+
Decodes an encoded UUIDv7 using the alphabet passed to the constuctor or the default one. If the UUIDv7 is not valid, an error is thrown.
82+
83+
### Static methods
84+
85+
#### `UUIDv7.isValid`
86+
87+
```typescript
88+
UUIDv7.isValid(id: string) => boolean
89+
```
90+
91+
Checks if the UUIDv7 is valid.
92+
93+
#### `UUIDv7.timestamp`
94+
95+
```typescript
96+
UUIDv7.timestamp(id: string) => number | null
97+
```
98+
99+
Returns the timestamp part of the UUIDv7. If the UUIDv7 is not valid, `null` is returned.
100+
101+
#### `UUIDv7.date`
102+
103+
```typescript
104+
UUIDv7.date(id: string) => Date | null
105+
```
106+
107+
Returns the timestamp part of the UUIDv7 converted to `Date`. If the UUIDv7 is not valid, `null` is returned.
108+
109+
### Function aliases
110+
111+
The library provides a few function aliases for convenience. You can use them without creating a new `UUIDv7` instance:
112+
113+
| Function name | Instance method | Description |
114+
| --------------------- | --------------- | --------------------------------------------------------------------------------------------- |
115+
| `uuidv7` | `gen` | Generates a new UUIDv7. |
116+
| `encodeUUIDv7` | `encode` | Encodes a UUIDv7 with the default Base58 alphabet. |
117+
| `decodeUUIDv7` | `decode` | Decodes an encoded UUIDv7 from Base58 alphabet. Returns null if the encoded ID is invalid. |
118+
| `decodeOrThrowUUIDv7` | `decodeOrThrow` | Decodes an encoded UUIDv7 from Base58 alphabet. Throws an error if the encoded ID is invalid. |
119+
120+
## Implementation details
121+
122+
This library follows the [draft-ietf-uuidrev-rfc4122bis-11](https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-11.html#name-uuid-version-7) draft to generate UUIDv7s:
123+
124+
- if the current timestamp is ahead of the last stored one, it generates new [rand_a] and [rand_b] parts;
125+
- if the current timestamp is behind the last stored one, it waits for the next valid timestamp to return a UUIDv7 with newly generated random parts;
126+
- if the current timestamp is the same as the last stored one:
127+
- it uses `rand_b` and then `rand_a` as randomly seeded counters, in that order. `rand_b` is the primary counter, and `rand_a` is used as the secondary one, when `rand_b` overflows its 62 bits (rare case). When used as a counter, `rand_b` increments its previous random value by a random integer between 2^6 (64) and 2^16 - 1 (65535), and `rand_a` increments its previous random value by 1, while generating a new `rand_b` part.
128+
- if both counters overflow their bit sizes, the generation function waits for the next millisecond to return a UUIDv7 with newly generated random parts.
129+
130+
This approach follows the [method 2](https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-11.html#monotonicity_counters) of the "Monotonicity and Counters" section of the draft. It guarantees monotonicity and uniqueness per instance, and always keeps timestamp the same as `Date.now()` value.
131+
132+
## Field and Bit Layout
133+
134+
This is the UUIDv7 Field and Bit Layout, took from the draft linked above:
135+
136+
```
137+
0 1 2 3
138+
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
139+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
140+
| unix_ts_ms |
141+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
142+
| unix_ts_ms | ver | rand_a |
143+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
144+
|var| rand_b |
145+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
146+
| rand_b |
147+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
148+
```
149+
150+
### Description
151+
152+
#### unix_ts_ms
153+
154+
48 bit big-endian unsigned number of Unix epoch timestamp in milliseconds as per [Section 6.1](https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-11.html#timestamp_considerations). Occupies bits 0 through 47 (octets 0-5).
155+
156+
#### ver
157+
158+
The 4 bit version field as defined by [Section 4.2](https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-11.html#version_field), set to 0b0111 (7). Occupies bits 48 through 51 of octet 6.
159+
160+
#### rand_a
161+
162+
12 bits pseudo-random data to provide uniqueness as per [Section 6.8](https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-11.html#unguessability) and/or optional constructs to guarantee additional monotonicity as per [Section 6.2](https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-11.html#monotonicity_counters). Occupies bits 52 through 63 (octets 6-7).
163+
164+
#### var
165+
166+
The 2 bit variant field as defined by [Section 4.1](https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-11.html#variant_field), set to 0b10. Occupies bits 64 and 65 of octet 8.
167+
168+
#### rand_b
169+
170+
The final 62 bits of pseudo-random data to provide uniqueness as per [Section 6.8](https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-11.html#unguessability) and/or an optional counter to guarantee additional monotonicity as per [Section 6.2](https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-11.html#monotonicity_counters). Occupies bits 66 through 127 (octets 8-15).
171+
172+
## Feedback
173+
174+
If you found a bug in the implementation, please open a new [issue](https://github.com/TheEdoRan/uuidv7/issues/new).
175+
176+
## Alternatives
177+
178+
- [uuidv7](https://www.npmjs.com/package/uuidv7) by [LiosK](https://github.com/LiosK)
179+
180+
## License
181+
182+
This project is licensed under the MIT License.

commitlint.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = { extends: ["@commitlint/config-conventional"] };

eslint.config.mjs

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// @ts-check
2+
3+
import eslint from "@eslint/js";
4+
import prettierConfig from "eslint-config-prettier";
5+
import tseslint from "typescript-eslint";
6+
7+
export default tseslint.config(
8+
{
9+
ignores: ["**/*.js", "**/*.mjs", "**/*.cjs", "dist/**"],
10+
},
11+
eslint.configs.recommended,
12+
...tseslint.configs.recommendedTypeChecked,
13+
{
14+
languageOptions: {
15+
parserOptions: {
16+
project: true,
17+
tsconfigRootDir: import.meta.dirname,
18+
},
19+
},
20+
},
21+
{
22+
rules: {
23+
"@typescript-eslint/consistent-type-imports": "error",
24+
"@typescript-eslint/consistent-type-exports": "error",
25+
},
26+
},
27+
prettierConfig
28+
);

0 commit comments

Comments
 (0)