@typed/id 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.nvmrc +1 -0
- package/biome.json +36 -0
- package/dist/DateTimes.d.ts +19 -0
- package/dist/DateTimes.js +20 -0
- package/dist/DateTimes.js.map +1 -0
- package/dist/GetRandomValues.d.ts +11 -0
- package/dist/GetRandomValues.js +17 -0
- package/dist/GetRandomValues.js.map +1 -0
- package/dist/{dts/NanoId.d.ts → NanoId.d.ts} +5 -30
- package/dist/NanoId.js +27 -0
- package/dist/NanoId.js.map +1 -0
- package/dist/Ulid.d.ts +31 -0
- package/dist/Ulid.js +39 -0
- package/dist/Ulid.js.map +1 -0
- package/dist/{dts/Uuid.d.ts → Uuid.d.ts} +5 -31
- package/dist/{esm/Uuid.js → Uuid.js} +13 -31
- package/dist/Uuid.js.map +1 -0
- package/dist/Uuid4.d.ts +7 -0
- package/dist/Uuid4.js +14 -0
- package/dist/Uuid4.js.map +1 -0
- package/dist/Uuid7.d.ts +26 -0
- package/dist/Uuid7.js +71 -0
- package/dist/Uuid7.js.map +1 -0
- package/dist/UuidStringify.d.ts +1 -0
- package/dist/UuidStringify.js +29 -0
- package/dist/UuidStringify.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/package.json +34 -58
- package/readme.md +86 -0
- package/src/DateTimes.ts +37 -0
- package/src/GetRandomValues.ts +28 -77
- package/src/NanoId.ts +16 -40
- package/src/Ulid.ts +82 -0
- package/src/Uuid4.ts +22 -0
- package/src/Uuid7.ts +104 -0
- package/src/UuidStringify.ts +31 -0
- package/src/id.test.ts +73 -0
- package/src/index.ts +6 -16
- package/tsconfig.json +26 -0
- package/GetRandomValues/package.json +0 -6
- package/LICENSE +0 -21
- package/NanoId/package.json +0 -6
- package/README.md +0 -5
- package/Schema/package.json +0 -6
- package/Uuid/package.json +0 -6
- package/dist/cjs/GetRandomValues.js +0 -65
- package/dist/cjs/GetRandomValues.js.map +0 -1
- package/dist/cjs/NanoId.js +0 -53
- package/dist/cjs/NanoId.js.map +0 -1
- package/dist/cjs/Schema.js +0 -31
- package/dist/cjs/Schema.js.map +0 -1
- package/dist/cjs/Uuid.js +0 -55
- package/dist/cjs/Uuid.js.map +0 -1
- package/dist/cjs/index.js +0 -39
- package/dist/cjs/index.js.map +0 -1
- package/dist/dts/GetRandomValues.d.ts +0 -38
- package/dist/dts/GetRandomValues.d.ts.map +0 -1
- package/dist/dts/NanoId.d.ts.map +0 -1
- package/dist/dts/Schema.d.ts +0 -15
- package/dist/dts/Schema.d.ts.map +0 -1
- package/dist/dts/Uuid.d.ts.map +0 -1
- package/dist/dts/index.d.ts +0 -16
- package/dist/dts/index.d.ts.map +0 -1
- package/dist/esm/GetRandomValues.js +0 -57
- package/dist/esm/GetRandomValues.js.map +0 -1
- package/dist/esm/NanoId.js +0 -45
- package/dist/esm/NanoId.js.map +0 -1
- package/dist/esm/Schema.js +0 -16
- package/dist/esm/Schema.js.map +0 -1
- package/dist/esm/Uuid.js.map +0 -1
- package/dist/esm/index.js +0 -16
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/package.json +0 -4
- package/src/Schema.ts +0 -29
- package/src/Uuid.ts +0 -99
package/package.json
CHANGED
|
@@ -1,66 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typed/id",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "",
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"description": "Common ID format generation for Effect",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"effect",
|
|
10
|
+
"ID",
|
|
11
|
+
"UUID",
|
|
12
|
+
"NanoId",
|
|
13
|
+
"ULID",
|
|
14
|
+
"typescript"
|
|
15
|
+
],
|
|
16
|
+
"author": "Tylor Steinberger <tlsteinberger167@gmail.com>",
|
|
5
17
|
"license": "MIT",
|
|
6
|
-
"repository": {
|
|
7
|
-
"type": "git",
|
|
8
|
-
"url": "https://github.com/tylors/typed.git"
|
|
9
|
-
},
|
|
10
|
-
"sideEffects": [],
|
|
11
|
-
"author": "Typed contributors",
|
|
12
|
-
"homepage": "https://github.com/tylors/typed",
|
|
13
18
|
"dependencies": {
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
19
|
+
"effect": "^3.11.9"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"effect": "^3.11.9"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@biomejs/biome": "^1.9.4",
|
|
26
|
+
"@effect/vitest": "^0.16.0",
|
|
27
|
+
"@types/node": "^22.10.2",
|
|
28
|
+
"typescript": "^5.4.2",
|
|
29
|
+
"vitest": "^2.1.8"
|
|
18
30
|
},
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
"types": "./dist/dts/index.d.ts",
|
|
22
|
-
"exports": {
|
|
23
|
-
"./package.json": "./package.json",
|
|
24
|
-
".": {
|
|
25
|
-
"types": "./dist/dts/index.d.ts",
|
|
26
|
-
"import": "./dist/esm/index.js",
|
|
27
|
-
"default": "./dist/cjs/index.js"
|
|
28
|
-
},
|
|
29
|
-
"./GetRandomValues": {
|
|
30
|
-
"types": "./dist/dts/GetRandomValues.d.ts",
|
|
31
|
-
"import": "./dist/esm/GetRandomValues.js",
|
|
32
|
-
"default": "./dist/cjs/GetRandomValues.js"
|
|
33
|
-
},
|
|
34
|
-
"./NanoId": {
|
|
35
|
-
"types": "./dist/dts/NanoId.d.ts",
|
|
36
|
-
"import": "./dist/esm/NanoId.js",
|
|
37
|
-
"default": "./dist/cjs/NanoId.js"
|
|
38
|
-
},
|
|
39
|
-
"./Schema": {
|
|
40
|
-
"types": "./dist/dts/Schema.d.ts",
|
|
41
|
-
"import": "./dist/esm/Schema.js",
|
|
42
|
-
"default": "./dist/cjs/Schema.js"
|
|
43
|
-
},
|
|
44
|
-
"./Uuid": {
|
|
45
|
-
"types": "./dist/dts/Uuid.d.ts",
|
|
46
|
-
"import": "./dist/esm/Uuid.js",
|
|
47
|
-
"default": "./dist/cjs/Uuid.js"
|
|
48
|
-
}
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
49
33
|
},
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
],
|
|
58
|
-
"Schema": [
|
|
59
|
-
"./dist/dts/Schema.d.ts"
|
|
60
|
-
],
|
|
61
|
-
"Uuid": [
|
|
62
|
-
"./dist/dts/Uuid.d.ts"
|
|
63
|
-
]
|
|
64
|
-
}
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc",
|
|
36
|
+
"dev": "tsc --watch",
|
|
37
|
+
"lint": "biome lint --write",
|
|
38
|
+
"test:watch": "vitest --typecheck",
|
|
39
|
+
"test": "vitest run --typecheck",
|
|
40
|
+
"typecheck": "tsc --noEmit"
|
|
65
41
|
}
|
|
66
42
|
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# @typed/id
|
|
2
|
+
|
|
3
|
+
A TypeScript library providing common ID format generation using [Effect](https://effect.website/). This package includes implementations for UUID, NanoID, and ULID generation with a focus on type safety and functional programming principles.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @typed/id effect
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @typed/id effect
|
|
11
|
+
# or
|
|
12
|
+
yarn add @typed/id effect
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- 🎯 Type-safe ID generation
|
|
18
|
+
- 🔧 Built on top of Effect
|
|
19
|
+
- 🎨 Multiple ID format support:
|
|
20
|
+
- UUID (v4)
|
|
21
|
+
- NanoID
|
|
22
|
+
- ULID
|
|
23
|
+
- ⚡ Efficient and secure random value generation
|
|
24
|
+
- 📦 Zero dependencies (except Effect)
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { Effect } from 'effect'
|
|
30
|
+
import {
|
|
31
|
+
DateTimes,
|
|
32
|
+
GetRandomValues,
|
|
33
|
+
makeUuid,
|
|
34
|
+
makeNanoId,
|
|
35
|
+
makeUlid
|
|
36
|
+
} from '@typed/id'
|
|
37
|
+
|
|
38
|
+
// Generate a UUID
|
|
39
|
+
await makeUuid.pipe(
|
|
40
|
+
Effect.provide(GetRandomValues.CryptoRandom),
|
|
41
|
+
// Effect.provide(GetRandomValues.PseudoRandom),
|
|
42
|
+
Effect.flatMap(Effect.log),
|
|
43
|
+
Effect.runPromise
|
|
44
|
+
)
|
|
45
|
+
// Output: "550e8400-e29b-41d4-a716-446655440000"
|
|
46
|
+
|
|
47
|
+
// Generate a NanoID
|
|
48
|
+
await makeNanoId.pipe(
|
|
49
|
+
Effect.provide(GetRandomValues.CryptoRandom),
|
|
50
|
+
// Effect.provide(GetRandomValues.PseudoRandom),
|
|
51
|
+
Effect.flatMap(Effect.log),
|
|
52
|
+
Effect.runPromise
|
|
53
|
+
)
|
|
54
|
+
// Output: "V1StGXR8_Z5jdHi6B-myT"
|
|
55
|
+
|
|
56
|
+
// Generate a ULID
|
|
57
|
+
await makeUlid.pipe(
|
|
58
|
+
Effect.provide([
|
|
59
|
+
GetRandomValues.CryptoRandom,
|
|
60
|
+
// GetRandomValues.PseudoRandom,
|
|
61
|
+
DateTimes.Default
|
|
62
|
+
// DateTimes.Fixed(new Date(0))
|
|
63
|
+
]),
|
|
64
|
+
Effect.flatMap(Effect.log),
|
|
65
|
+
Effect.runPromise
|
|
66
|
+
)
|
|
67
|
+
// Output: "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## API
|
|
71
|
+
|
|
72
|
+
### UUID
|
|
73
|
+
|
|
74
|
+
- `makeUuid`: Generates a v4 UUID
|
|
75
|
+
|
|
76
|
+
### NanoID
|
|
77
|
+
|
|
78
|
+
- `makeNanoId`: Generates a NanoID with a default length of 21 characters
|
|
79
|
+
|
|
80
|
+
### ULID
|
|
81
|
+
|
|
82
|
+
- `makeUlid`: Generates a ULID (Universally Unique Lexicographically Sortable Identifier)
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT
|
package/src/DateTimes.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Effect, Layer, Option, TestClock } from 'effect'
|
|
2
|
+
|
|
3
|
+
export class DateTimes extends Effect.Tag('DateTimes')<
|
|
4
|
+
DateTimes,
|
|
5
|
+
{
|
|
6
|
+
readonly now: Effect.Effect<number>
|
|
7
|
+
readonly date: Effect.Effect<Date>
|
|
8
|
+
}
|
|
9
|
+
>() {
|
|
10
|
+
static readonly make = (now: Effect.Effect<number>): Layer.Layer<DateTimes> =>
|
|
11
|
+
Layer.succeed(this, {
|
|
12
|
+
now,
|
|
13
|
+
date: now.pipe(Effect.map((millis) => new Date(millis))),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
static readonly Default: Layer.Layer<DateTimes> = this.make(Effect.clockWith((clock) => clock.currentTimeMillis))
|
|
17
|
+
|
|
18
|
+
static readonly Fixed = (base: Date): Layer.Layer<DateTimes> =>
|
|
19
|
+
Layer.effect(
|
|
20
|
+
DateTimes,
|
|
21
|
+
Effect.gen(function* () {
|
|
22
|
+
const clock = yield* Effect.clock
|
|
23
|
+
const startMillis = yield* clock.currentTimeMillis
|
|
24
|
+
const now = clock.currentTimeMillis.pipe(
|
|
25
|
+
Effect.map((millis) =>
|
|
26
|
+
// Use BigInt to avoid floating point precision issues which can break deterministic testing
|
|
27
|
+
Number(BigInt(base.getTime()) + BigInt(millis) - BigInt(startMillis)),
|
|
28
|
+
),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
now,
|
|
33
|
+
date: now.pipe(Effect.map((millis) => new Date(millis))),
|
|
34
|
+
}
|
|
35
|
+
}),
|
|
36
|
+
)
|
|
37
|
+
}
|
package/src/GetRandomValues.ts
CHANGED
|
@@ -1,78 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* @since 1.0.0
|
|
31
|
-
*/
|
|
32
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
33
|
-
export const getRandomValuesNode = (crypto: typeof import("node:crypto"), length: number) => {
|
|
34
|
-
const bytes = crypto.randomBytes(length)
|
|
35
|
-
const view = new Uint8Array(length)
|
|
36
|
-
for (let i = 0; i < bytes.length; ++i) {
|
|
37
|
-
view[i] = bytes[i]
|
|
38
|
-
}
|
|
39
|
-
return view
|
|
1
|
+
import * as Context from 'effect/Context'
|
|
2
|
+
import * as Effect from 'effect/Effect'
|
|
3
|
+
import * as Layer from 'effect/Layer'
|
|
4
|
+
import * as Random from 'effect/Random'
|
|
5
|
+
|
|
6
|
+
export class GetRandomValues extends Context.Tag('GetRandomValues')<
|
|
7
|
+
GetRandomValues,
|
|
8
|
+
(length: number) => Effect.Effect<Uint8Array>
|
|
9
|
+
>() {
|
|
10
|
+
static readonly layer = (f: (length: number) => Effect.Effect<Uint8Array>) =>
|
|
11
|
+
Layer.succeed(GetRandomValues, f)
|
|
12
|
+
|
|
13
|
+
static readonly apply = (length: number) => Effect.flatMap(GetRandomValues, (f) => f(length))
|
|
14
|
+
|
|
15
|
+
static readonly PseudoRandom = this.layer((length) =>
|
|
16
|
+
Effect.gen(function* () {
|
|
17
|
+
const view = new Uint8Array(length)
|
|
18
|
+
for (let i = 0; i < length; ++i) {
|
|
19
|
+
view[i] = yield* Random.nextInt
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return view
|
|
23
|
+
}),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
static readonly CryptoRandom = this.layer((length) =>
|
|
27
|
+
Effect.sync(() => crypto.getRandomValues(new Uint8Array(length))),
|
|
28
|
+
)
|
|
40
29
|
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* @since 1.0.0
|
|
44
|
-
*/
|
|
45
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
46
|
-
export const nodeCrypto = (crypto: typeof import("node:crypto")): Layer.Layer<GetRandomValues> =>
|
|
47
|
-
GetRandomValues.implement((length) => Effect.sync(() => getRandomValuesNode(crypto, length)))
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* @since 1.0.0
|
|
51
|
-
*/
|
|
52
|
-
export const pseudoRandom: Layer.Layer<GetRandomValues> = GetRandomValues.implement((length) =>
|
|
53
|
-
Effect.gen(function*() {
|
|
54
|
-
const view = new Uint8Array(length)
|
|
55
|
-
|
|
56
|
-
for (let i = 0; i < length; ++i) {
|
|
57
|
-
view[i] = yield* Random.nextInt
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return view
|
|
61
|
-
})
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* @since 1.0.0
|
|
66
|
-
*/
|
|
67
|
-
export const getRandomValues: Layer.Layer<GetRandomValues> = GetRandomValues.layer(
|
|
68
|
-
Effect.gen(function*() {
|
|
69
|
-
if (typeof crypto === "undefined") {
|
|
70
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
71
|
-
const crypto: typeof import("node:crypto") = yield* Effect.promise(() => import("node:crypto"))
|
|
72
|
-
|
|
73
|
-
return (length: number) => Effect.sync(() => getRandomValuesNode(crypto, length))
|
|
74
|
-
} else {
|
|
75
|
-
return (length: number) => Effect.sync(() => getRandomValuesWeb(crypto, length))
|
|
76
|
-
}
|
|
77
|
-
})
|
|
78
|
-
)
|
package/src/NanoId.ts
CHANGED
|
@@ -1,34 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import * as Brand from "effect/Brand"
|
|
6
|
-
import * as Effect from "effect/Effect"
|
|
7
|
-
import { GetRandomValues } from "./GetRandomValues.js"
|
|
1
|
+
import { Schema } from 'effect'
|
|
2
|
+
import * as Effect from 'effect/Effect'
|
|
3
|
+
import { GetRandomValues } from './GetRandomValues.js'
|
|
8
4
|
|
|
9
5
|
const nanoIdPattern = /[0-9a-zA-Z_-]/
|
|
10
6
|
|
|
11
|
-
/**
|
|
12
|
-
* @since 1.0.0
|
|
13
|
-
*/
|
|
14
7
|
export const isNanoId = (id: string): id is NanoId => nanoIdPattern.test(id)
|
|
15
8
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*/
|
|
19
|
-
export type NanoId = string & Brand.Brand<"@typed/id/NanoId">
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @since 1.0.0
|
|
23
|
-
*/
|
|
24
|
-
export const NanoId = Brand.refined<NanoId>(
|
|
25
|
-
isNanoId,
|
|
26
|
-
(input) => Brand.error(`Expected a NanoID but received ${input}.`)
|
|
27
|
-
)
|
|
9
|
+
export const NanoId = Schema.String.pipe(Schema.brand('@typed/id/NanoId'))
|
|
10
|
+
export type NanoId = Schema.Schema.Type<typeof NanoId>
|
|
28
11
|
|
|
29
|
-
/**
|
|
30
|
-
* @since 1.0.0
|
|
31
|
-
*/
|
|
32
12
|
export type NanoIdSeed = readonly [
|
|
33
13
|
zero: number,
|
|
34
14
|
one: number,
|
|
@@ -50,7 +30,7 @@ export type NanoIdSeed = readonly [
|
|
|
50
30
|
seventeen: number,
|
|
51
31
|
eighteen: number,
|
|
52
32
|
nineteen: number,
|
|
53
|
-
twenty: number
|
|
33
|
+
twenty: number,
|
|
54
34
|
]
|
|
55
35
|
|
|
56
36
|
const numToCharacter = (byte: number): string => {
|
|
@@ -62,23 +42,19 @@ const numToCharacter = (byte: number): string => {
|
|
|
62
42
|
// `A-Z`
|
|
63
43
|
return (byte - 26).toString(36).toUpperCase()
|
|
64
44
|
} else if (byte > 62) {
|
|
65
|
-
return
|
|
45
|
+
return '-'
|
|
66
46
|
} else {
|
|
67
|
-
return
|
|
47
|
+
return '_'
|
|
68
48
|
}
|
|
69
49
|
}
|
|
70
50
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
*/
|
|
74
|
-
export const nanoId = (seed: NanoIdSeed): NanoId => NanoId(seed.reduce((id, x) => id + numToCharacter(x), ""))
|
|
51
|
+
export const nanoId = (seed: NanoIdSeed): NanoId =>
|
|
52
|
+
NanoId.make(seed.reduce((id, x) => id + numToCharacter(x), ''))
|
|
75
53
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
*/
|
|
79
|
-
export const makeNanoIdSeed: Effect.Effect<NanoIdSeed, never, GetRandomValues> = GetRandomValues(21) as any
|
|
54
|
+
export const makeNanoIdSeed: Effect.Effect<NanoIdSeed, never, GetRandomValues> =
|
|
55
|
+
GetRandomValues.apply(21) as any
|
|
80
56
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
57
|
+
export const makeNanoId: Effect.Effect<NanoId, never, GetRandomValues> = Effect.map(
|
|
58
|
+
makeNanoIdSeed,
|
|
59
|
+
nanoId,
|
|
60
|
+
)
|
package/src/Ulid.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as Effect from 'effect/Effect'
|
|
2
|
+
import { GetRandomValues } from './GetRandomValues.js'
|
|
3
|
+
import { Schema } from 'effect'
|
|
4
|
+
import { DateTimes } from './DateTimes.js'
|
|
5
|
+
|
|
6
|
+
export const isUlid: (value: string) => value is Ulid = Schema.is(Schema.ULID) as any
|
|
7
|
+
|
|
8
|
+
export const Ulid = Schema.ULID.pipe(Schema.brand('@typed/id/ULID'))
|
|
9
|
+
export type Ulid = Schema.Schema.Type<typeof Ulid>
|
|
10
|
+
|
|
11
|
+
export type UlidSeed = {
|
|
12
|
+
readonly seed: readonly [
|
|
13
|
+
zero: number,
|
|
14
|
+
one: number,
|
|
15
|
+
two: number,
|
|
16
|
+
three: number,
|
|
17
|
+
four: number,
|
|
18
|
+
five: number,
|
|
19
|
+
six: number,
|
|
20
|
+
seven: number,
|
|
21
|
+
eight: number,
|
|
22
|
+
nine: number,
|
|
23
|
+
ten: number,
|
|
24
|
+
eleven: number,
|
|
25
|
+
twelve: number,
|
|
26
|
+
thirteen: number,
|
|
27
|
+
fourteen: number,
|
|
28
|
+
fifteen: number,
|
|
29
|
+
]
|
|
30
|
+
readonly now: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Crockford's Base32
|
|
34
|
+
const ENCODING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
|
|
35
|
+
const ENCODING_LEN = ENCODING.length
|
|
36
|
+
const TIME_MAX = 2 ** 48 - 1
|
|
37
|
+
const TIME_LEN = 10
|
|
38
|
+
const RANDOM_LEN = 16
|
|
39
|
+
|
|
40
|
+
export const makeUlidSeed: Effect.Effect<UlidSeed, never, GetRandomValues | DateTimes> =
|
|
41
|
+
DateTimes.now.pipe(
|
|
42
|
+
Effect.bindTo('now'),
|
|
43
|
+
Effect.bind(
|
|
44
|
+
'seed',
|
|
45
|
+
() =>
|
|
46
|
+
GetRandomValues.apply(16) as any as Effect.Effect<UlidSeed['seed'], never, GetRandomValues>,
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
export const makeUlid: Effect.Effect<Ulid, never, GetRandomValues | DateTimes> = Effect.map(
|
|
51
|
+
makeUlidSeed,
|
|
52
|
+
({ seed, now }) => ulid(seed, now),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
function encodeTime(now: number, len: number): string {
|
|
56
|
+
let str = ''
|
|
57
|
+
for (let i = len - 1; i >= 0; i--) {
|
|
58
|
+
const mod = now % ENCODING_LEN
|
|
59
|
+
str = ENCODING.charAt(mod) + str
|
|
60
|
+
now = (now - mod) / ENCODING_LEN
|
|
61
|
+
}
|
|
62
|
+
return str
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function encodeRandom(seed: UlidSeed['seed']): string {
|
|
66
|
+
let str = ''
|
|
67
|
+
for (let i = 0; i < RANDOM_LEN; i++) {
|
|
68
|
+
str = str + ENCODING.charAt(seed[i] % ENCODING_LEN)
|
|
69
|
+
}
|
|
70
|
+
return str
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function ulid(seed: UlidSeed['seed'], now: UlidSeed['now']): Ulid {
|
|
74
|
+
if (now > TIME_MAX) {
|
|
75
|
+
throw new Error('Cannot generate ULID due to timestamp overflow')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const time = encodeTime(now, TIME_LEN)
|
|
79
|
+
const random = encodeRandom(seed)
|
|
80
|
+
|
|
81
|
+
return Ulid.make(time + random)
|
|
82
|
+
}
|
package/src/Uuid4.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as Effect from 'effect/Effect'
|
|
2
|
+
import { GetRandomValues } from './GetRandomValues.js'
|
|
3
|
+
import { Schema } from 'effect'
|
|
4
|
+
import { uuidStringify } from './UuidStringify.js'
|
|
5
|
+
|
|
6
|
+
export const Uuid4 = Schema.UUID.pipe(Schema.brand('@typed/id/UUID4'))
|
|
7
|
+
export type Uuid4 = Schema.Schema.Type<typeof Uuid4>
|
|
8
|
+
|
|
9
|
+
export const isUuid4: (value: string) => value is Uuid4 = Schema.is(Uuid4)
|
|
10
|
+
|
|
11
|
+
export const makeUuid4: Effect.Effect<Uuid4, never, GetRandomValues> = Effect.map(
|
|
12
|
+
GetRandomValues.apply(16),
|
|
13
|
+
uuid4FromSeed,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
function uuid4FromSeed(seed: Uint8Array): Uuid4 {
|
|
17
|
+
// Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
|
|
18
|
+
seed[6] = (seed[6] & 0x0f) | 0x40
|
|
19
|
+
seed[8] = (seed[8] & 0x3f) | 0x80
|
|
20
|
+
|
|
21
|
+
return uuidStringify(seed) as Uuid4
|
|
22
|
+
}
|
package/src/Uuid7.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import * as Effect from 'effect/Effect'
|
|
2
|
+
import { GetRandomValues } from './GetRandomValues.js'
|
|
3
|
+
import { Layer, Schema } from 'effect'
|
|
4
|
+
import { DateTimes } from './DateTimes.js'
|
|
5
|
+
import { uuidStringify } from './UuidStringify.js'
|
|
6
|
+
|
|
7
|
+
export const Uuid7 = Schema.UUID.pipe(Schema.brand('@typed/id/UUID7'))
|
|
8
|
+
export type Uuid7 = Schema.Schema.Type<typeof Uuid7>
|
|
9
|
+
|
|
10
|
+
export const isUuid7: (value: string) => value is Uuid7 = Schema.is(Uuid7)
|
|
11
|
+
|
|
12
|
+
export type Uuid7Seed = {
|
|
13
|
+
readonly timestamp: number
|
|
14
|
+
readonly seq: number
|
|
15
|
+
readonly randomBytes: Uint8Array
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class Uuid7State extends Effect.Tag('Uuid7State')<
|
|
19
|
+
Uuid7State,
|
|
20
|
+
{
|
|
21
|
+
readonly next: Effect.Effect<Uuid7Seed>
|
|
22
|
+
}
|
|
23
|
+
>() {
|
|
24
|
+
static Default: Layer.Layer<Uuid7State, never, GetRandomValues | DateTimes> = Layer.effect(
|
|
25
|
+
this,
|
|
26
|
+
Effect.gen(function* () {
|
|
27
|
+
const { now } = yield* DateTimes
|
|
28
|
+
const getRandomValues = yield* GetRandomValues
|
|
29
|
+
|
|
30
|
+
const state = {
|
|
31
|
+
msecs: Number.NEGATIVE_INFINITY,
|
|
32
|
+
seq: 0,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function updateV7State(now: number, randomBytes: Uint8Array) {
|
|
36
|
+
if (now > state.msecs) {
|
|
37
|
+
// Time has moved on! Pick a new random sequence number
|
|
38
|
+
state.seq =
|
|
39
|
+
(randomBytes[6] << 23) | (randomBytes[7] << 16) | (randomBytes[8] << 8) | randomBytes[9]
|
|
40
|
+
state.msecs = now
|
|
41
|
+
} else {
|
|
42
|
+
// Bump sequence counter w/ 32-bit rollover
|
|
43
|
+
state.seq = (state.seq + 1) | 0
|
|
44
|
+
|
|
45
|
+
// In case of rollover, bump timestamp to preserve monotonicity. This is
|
|
46
|
+
// allowed by the RFC and should self-correct as the system clock catches
|
|
47
|
+
// up. See https://www.rfc-editor.org/rfc/rfc9562.html#section-6.2-9.4
|
|
48
|
+
if (state.seq === 0) {
|
|
49
|
+
state.msecs++
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
next: Effect.gen(function* () {
|
|
56
|
+
const randomBytes = yield* getRandomValues(16)
|
|
57
|
+
updateV7State(yield* now, randomBytes)
|
|
58
|
+
return { timestamp: state.msecs, seq: state.seq, randomBytes }
|
|
59
|
+
}),
|
|
60
|
+
}
|
|
61
|
+
}),
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const makeUuid7: Effect.Effect<Uuid7, never, Uuid7State> = Effect.map(
|
|
66
|
+
Uuid7State.next,
|
|
67
|
+
uuid7FromSeed,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
function uuid7FromSeed({ timestamp, seq, randomBytes }: Uuid7Seed): Uuid7 {
|
|
71
|
+
const result = new Uint8Array(16)
|
|
72
|
+
|
|
73
|
+
// byte 0-5: timestamp (48 bits)
|
|
74
|
+
result[0] = (timestamp / 0x10000000000) & 0xff
|
|
75
|
+
result[1] = (timestamp / 0x100000000) & 0xff
|
|
76
|
+
result[2] = (timestamp / 0x1000000) & 0xff
|
|
77
|
+
result[3] = (timestamp / 0x10000) & 0xff
|
|
78
|
+
result[4] = (timestamp / 0x100) & 0xff
|
|
79
|
+
result[5] = timestamp & 0xff
|
|
80
|
+
|
|
81
|
+
// byte 6: `version` (4 bits) | sequence bits 28-31 (4 bits)
|
|
82
|
+
result[6] = 0x70 | ((seq >>> 28) & 0x0f)
|
|
83
|
+
|
|
84
|
+
// byte 7: sequence bits 20-27 (8 bits)
|
|
85
|
+
result[7] = (seq >>> 20) & 0xff
|
|
86
|
+
|
|
87
|
+
// byte 8: `variant` (2 bits) | sequence bits 14-19 (6 bits)
|
|
88
|
+
result[8] = 0x80 | ((seq >>> 14) & 0x3f)
|
|
89
|
+
|
|
90
|
+
// byte 9: sequence bits 6-13 (8 bits)
|
|
91
|
+
result[9] = (seq >>> 6) & 0xff
|
|
92
|
+
|
|
93
|
+
// byte 10: sequence bits 0-5 (6 bits) | random (2 bits)
|
|
94
|
+
result[10] = ((seq << 2) & 0xff) | (randomBytes[10] & 0x03)
|
|
95
|
+
|
|
96
|
+
// bytes 11-15: random (40 bits)
|
|
97
|
+
result[11] = randomBytes[11]
|
|
98
|
+
result[12] = randomBytes[12]
|
|
99
|
+
result[13] = randomBytes[13]
|
|
100
|
+
result[14] = randomBytes[14]
|
|
101
|
+
result[15] = randomBytes[15]
|
|
102
|
+
|
|
103
|
+
return uuidStringify(result) as Uuid7
|
|
104
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const byteToHex: Array<string> = []
|
|
2
|
+
|
|
3
|
+
for (let i = 0; i < 256; ++i) {
|
|
4
|
+
byteToHex.push((i + 0x100).toString(16).slice(1))
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function uuidStringify(seed: Uint8Array): string {
|
|
8
|
+
return (
|
|
9
|
+
// biome-ignore lint/style/useTemplate: Faster than template literals
|
|
10
|
+
byteToHex[seed[0]] +
|
|
11
|
+
byteToHex[seed[1]] +
|
|
12
|
+
byteToHex[seed[2]] +
|
|
13
|
+
byteToHex[seed[3]] +
|
|
14
|
+
'-' +
|
|
15
|
+
byteToHex[seed[4]] +
|
|
16
|
+
byteToHex[seed[5]] +
|
|
17
|
+
'-' +
|
|
18
|
+
byteToHex[seed[6]] +
|
|
19
|
+
byteToHex[seed[7]] +
|
|
20
|
+
'-' +
|
|
21
|
+
byteToHex[seed[8]] +
|
|
22
|
+
byteToHex[seed[9]] +
|
|
23
|
+
'-' +
|
|
24
|
+
byteToHex[seed[10]] +
|
|
25
|
+
byteToHex[seed[11]] +
|
|
26
|
+
byteToHex[seed[12]] +
|
|
27
|
+
byteToHex[seed[13]] +
|
|
28
|
+
byteToHex[seed[14]] +
|
|
29
|
+
byteToHex[seed[15]]
|
|
30
|
+
)
|
|
31
|
+
}
|