@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.
Files changed (77) hide show
  1. package/.nvmrc +1 -0
  2. package/biome.json +36 -0
  3. package/dist/DateTimes.d.ts +19 -0
  4. package/dist/DateTimes.js +20 -0
  5. package/dist/DateTimes.js.map +1 -0
  6. package/dist/GetRandomValues.d.ts +11 -0
  7. package/dist/GetRandomValues.js +17 -0
  8. package/dist/GetRandomValues.js.map +1 -0
  9. package/dist/{dts/NanoId.d.ts → NanoId.d.ts} +5 -30
  10. package/dist/NanoId.js +27 -0
  11. package/dist/NanoId.js.map +1 -0
  12. package/dist/Ulid.d.ts +31 -0
  13. package/dist/Ulid.js +39 -0
  14. package/dist/Ulid.js.map +1 -0
  15. package/dist/{dts/Uuid.d.ts → Uuid.d.ts} +5 -31
  16. package/dist/{esm/Uuid.js → Uuid.js} +13 -31
  17. package/dist/Uuid.js.map +1 -0
  18. package/dist/Uuid4.d.ts +7 -0
  19. package/dist/Uuid4.js +14 -0
  20. package/dist/Uuid4.js.map +1 -0
  21. package/dist/Uuid7.d.ts +26 -0
  22. package/dist/Uuid7.js +71 -0
  23. package/dist/Uuid7.js.map +1 -0
  24. package/dist/UuidStringify.d.ts +1 -0
  25. package/dist/UuidStringify.js +29 -0
  26. package/dist/UuidStringify.js.map +1 -0
  27. package/dist/index.d.ts +6 -0
  28. package/dist/index.js +7 -0
  29. package/dist/index.js.map +1 -0
  30. package/package.json +34 -58
  31. package/readme.md +86 -0
  32. package/src/DateTimes.ts +37 -0
  33. package/src/GetRandomValues.ts +28 -77
  34. package/src/NanoId.ts +16 -40
  35. package/src/Ulid.ts +82 -0
  36. package/src/Uuid4.ts +22 -0
  37. package/src/Uuid7.ts +104 -0
  38. package/src/UuidStringify.ts +31 -0
  39. package/src/id.test.ts +73 -0
  40. package/src/index.ts +6 -16
  41. package/tsconfig.json +26 -0
  42. package/GetRandomValues/package.json +0 -6
  43. package/LICENSE +0 -21
  44. package/NanoId/package.json +0 -6
  45. package/README.md +0 -5
  46. package/Schema/package.json +0 -6
  47. package/Uuid/package.json +0 -6
  48. package/dist/cjs/GetRandomValues.js +0 -65
  49. package/dist/cjs/GetRandomValues.js.map +0 -1
  50. package/dist/cjs/NanoId.js +0 -53
  51. package/dist/cjs/NanoId.js.map +0 -1
  52. package/dist/cjs/Schema.js +0 -31
  53. package/dist/cjs/Schema.js.map +0 -1
  54. package/dist/cjs/Uuid.js +0 -55
  55. package/dist/cjs/Uuid.js.map +0 -1
  56. package/dist/cjs/index.js +0 -39
  57. package/dist/cjs/index.js.map +0 -1
  58. package/dist/dts/GetRandomValues.d.ts +0 -38
  59. package/dist/dts/GetRandomValues.d.ts.map +0 -1
  60. package/dist/dts/NanoId.d.ts.map +0 -1
  61. package/dist/dts/Schema.d.ts +0 -15
  62. package/dist/dts/Schema.d.ts.map +0 -1
  63. package/dist/dts/Uuid.d.ts.map +0 -1
  64. package/dist/dts/index.d.ts +0 -16
  65. package/dist/dts/index.d.ts.map +0 -1
  66. package/dist/esm/GetRandomValues.js +0 -57
  67. package/dist/esm/GetRandomValues.js.map +0 -1
  68. package/dist/esm/NanoId.js +0 -45
  69. package/dist/esm/NanoId.js.map +0 -1
  70. package/dist/esm/Schema.js +0 -16
  71. package/dist/esm/Schema.js.map +0 -1
  72. package/dist/esm/Uuid.js.map +0 -1
  73. package/dist/esm/index.js +0 -16
  74. package/dist/esm/index.js.map +0 -1
  75. package/dist/esm/package.json +0 -4
  76. package/src/Schema.ts +0 -29
  77. package/src/Uuid.ts +0 -99
package/package.json CHANGED
@@ -1,66 +1,42 @@
1
1
  {
2
2
  "name": "@typed/id",
3
- "version": "0.11.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
- "@effect/schema": "^0.74.1",
15
- "effect": "^3.8.4",
16
- "fast-check": "^3.22.0",
17
- "@typed/context": "0.30.0"
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
- "main": "./dist/cjs/index.js",
20
- "module": "./dist/esm/index.js",
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
- "typesVersions": {
51
- "*": {
52
- "GetRandomValues": [
53
- "./dist/dts/GetRandomValues.d.ts"
54
- ],
55
- "NanoId": [
56
- "./dist/dts/NanoId.d.ts"
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
@@ -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
+ }
@@ -1,78 +1,29 @@
1
- /**
2
- * @since 1.0.0
3
- */
4
-
5
- import * as Context from "@typed/context"
6
- import * as Effect from "effect/Effect"
7
- import type * as Layer from "effect/Layer"
8
- import * as Random from "effect/Random"
9
-
10
- /**
11
- * @since 1.0.0
12
- */
13
- export const GetRandomValues = Context.Fn<(length: number) => Effect.Effect<Uint8Array>>()(
14
- (_) => (class GetRandomValues extends _("@typed/id/GetRandomValues") {})
15
- )
16
- /**
17
- * @since 1.0.0
18
- */
19
- export interface GetRandomValues extends Context.Fn.Identifier<typeof GetRandomValues> {}
20
-
21
- const getRandomValuesWeb = (crypto: Crypto, length: number) => crypto.getRandomValues(new Uint8Array(length))
22
-
23
- /**
24
- * @since 1.0.0
25
- */
26
- export const webCrypto = (crypto: Crypto): Layer.Layer<GetRandomValues> =>
27
- GetRandomValues.implement((length) => Effect.sync(() => getRandomValuesWeb(crypto, length)))
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
- * @since 1.0.0
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
- * @since 1.0.0
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
- * @since 1.0.0
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
- * @since 1.0.0
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
- * @since 1.0.0
83
- */
84
- export const makeNanoId: Effect.Effect<NanoId, never, GetRandomValues> = Effect.map(makeNanoIdSeed, nanoId)
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
+ }