@typed/id 0.14.0 → 0.16.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/dist/Cuid.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { Effect, Layer, Schema } from 'effect';
2
+ import { DateTimes } from './DateTimes.js';
3
+ import { GetRandomValues } from './GetRandomValues.js';
4
+ export declare const Cuid: Schema.brand<Schema.filter<Schema.Schema<string, string, never>>, "@typed/id/CUID">;
5
+ export type Cuid = Schema.Schema.Type<typeof Cuid>;
6
+ export declare const isCuid: (value: string) => value is Cuid;
7
+ export type CuidSeed = {
8
+ readonly timestamp: number;
9
+ readonly counter: number;
10
+ readonly random: Uint8Array;
11
+ readonly fingerprint: string;
12
+ };
13
+ declare const CuidState_base: import("effect/Context").TagClass<CuidState, "CuidState", {
14
+ readonly next: Effect.Effect<CuidSeed>;
15
+ }> & Effect.Tag.Proxy<CuidState, {
16
+ readonly next: Effect.Effect<CuidSeed>;
17
+ }> & {
18
+ use: <X>(body: (_: {
19
+ readonly next: Effect.Effect<CuidSeed>;
20
+ }) => X) => [X] extends [Effect.Effect<infer A, infer E, infer R>] ? Effect.Effect<A, E, R | CuidState> : [X] extends [PromiseLike<infer A_1>] ? Effect.Effect<A_1, import("effect/Cause").UnknownException, CuidState> : Effect.Effect<X, never, CuidState>;
21
+ };
22
+ export declare class CuidState extends CuidState_base {
23
+ static readonly layer: (envData: string) => Layer.Layer<CuidState, never, DateTimes | GetRandomValues>;
24
+ static readonly Default: Layer.Layer<CuidState, never, DateTimes | GetRandomValues>;
25
+ }
26
+ export declare const makeCuid: Effect.Effect<Cuid, never, DateTimes | GetRandomValues | CuidState>;
27
+ export {};
package/dist/Cuid.js ADDED
@@ -0,0 +1,86 @@
1
+ import { Effect, Layer, Schema } from 'effect';
2
+ import { DateTimes } from './DateTimes.js';
3
+ import { GetRandomValues } from './GetRandomValues.js';
4
+ // Constants
5
+ const DEFAULT_LENGTH = 24;
6
+ const BIG_LENGTH = 32;
7
+ const INITIAL_COUNT_MAX = 476782367;
8
+ // Schema
9
+ export const Cuid = Schema.String.pipe(Schema.pattern(/^[a-z][0-9a-z]+$/), Schema.brand('@typed/id/CUID'));
10
+ export const isCuid = Schema.is(Cuid);
11
+ // Utilities
12
+ const ALPHABET = Array.from({ length: 26 }, (_, i) => String.fromCharCode(i + 97));
13
+ const encoder = new TextEncoder();
14
+ function createEntropy(length, random) {
15
+ let entropy = '';
16
+ let offset = 0;
17
+ while (entropy.length < length) {
18
+ const value = random[offset];
19
+ entropy += Math.floor(value % 36).toString(36);
20
+ offset = (offset + 1) % random.length;
21
+ }
22
+ return entropy;
23
+ }
24
+ function hash(input) {
25
+ // Convert string to bytes
26
+ const data = encoder.encode(input);
27
+ // Create a hash using the Web Crypto API
28
+ return crypto.subtle.digest('SHA-512', data).then((buffer) => {
29
+ const view = new Uint8Array(buffer);
30
+ let value = 0n;
31
+ for (const byte of view) {
32
+ value = (value << 8n) + BigInt(byte);
33
+ }
34
+ // Drop the first character because it will bias the histogram to the left
35
+ return value.toString(36).slice(1);
36
+ });
37
+ }
38
+ // State Management
39
+ export class CuidState extends Effect.Tag('CuidState')() {
40
+ static layer = (envData) => Layer.effect(this, Effect.gen(function* () {
41
+ const { now } = yield* DateTimes;
42
+ const getRandomValues = yield* GetRandomValues;
43
+ const initialBytes = yield* getRandomValues(4);
44
+ const initialValue = Math.abs((initialBytes[0] << 24) |
45
+ (initialBytes[1] << 16) |
46
+ (initialBytes[2] << 8) |
47
+ initialBytes[3]) % INITIAL_COUNT_MAX;
48
+ // Create fingerprint from environment data
49
+ const fingerprint = yield* Effect.promise(() => hash(envData).then((h) => h.substring(0, BIG_LENGTH)));
50
+ let counter = initialValue;
51
+ return {
52
+ next: Effect.gen(function* () {
53
+ const timestamp = yield* now;
54
+ const random = yield* getRandomValues(32);
55
+ return {
56
+ timestamp,
57
+ counter: counter++,
58
+ random,
59
+ fingerprint,
60
+ };
61
+ }),
62
+ };
63
+ }));
64
+ static Default = this.layer('node');
65
+ }
66
+ // Core Functions
67
+ function cuidFromSeed({ timestamp, counter, random, fingerprint }) {
68
+ return Effect.gen(function* () {
69
+ // First letter is always a random lowercase letter from the seed
70
+ const firstLetter = ALPHABET[random[0] % ALPHABET.length];
71
+ // Convert components to base36
72
+ const time = timestamp.toString(36);
73
+ const count = counter.toString(36);
74
+ // Create entropy from remaining random bytes
75
+ const salt = createEntropy(4, random.slice(1));
76
+ // Hash all components together
77
+ const hashInput = `${time}${salt}${count}${fingerprint}`;
78
+ const hashed = yield* Effect.promise(() => hash(hashInput));
79
+ // Construct the final CUID
80
+ const id = `${firstLetter}${hashed.substring(0, DEFAULT_LENGTH - 1)}`;
81
+ return Cuid.make(id);
82
+ });
83
+ }
84
+ // Public API
85
+ export const makeCuid = Effect.flatMap(CuidState.next, cuidFromSeed);
86
+ //# sourceMappingURL=Cuid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Cuid.js","sourceRoot":"","sources":["../src/Cuid.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
+ export * from './Cuid.js';
1
2
  export * from './DateTimes.js';
2
3
  export * from './GetRandomValues.js';
3
4
  export * from './NanoId.js';
4
5
  export * from './Ulid.js';
5
6
  export * from './Uuid4.js';
6
7
  export * from './Uuid5.js';
7
- export * from './Uuid6.js';
8
8
  export * from './Uuid7.js';
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
+ export * from './Cuid.js';
1
2
  export * from './DateTimes.js';
2
3
  export * from './GetRandomValues.js';
3
4
  export * from './NanoId.js';
4
5
  export * from './Ulid.js';
5
6
  export * from './Uuid4.js';
6
7
  export * from './Uuid5.js';
7
- export * from './Uuid6.js';
8
8
  export * from './Uuid7.js';
9
9
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,sBAAsB,CAAA;AACpC,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,sBAAsB,CAAA;AACpC,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typed/id",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "Common ID format generation for Effect",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/readme.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @typed/id
2
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.
3
+ A TypeScript library providing common ID format generation using [Effect](https://effect.website/). This package includes implementations for UUID, NanoID, ULID, and CUID generation with a focus on type safety and functional programming principles.
4
4
 
5
5
  ## Installation
6
6
 
@@ -17,9 +17,10 @@ yarn add @typed/id effect
17
17
  - 🎯 Type-safe ID generation
18
18
  - 🔧 Built on top of Effect
19
19
  - 🎨 Multiple ID format support:
20
- - UUID (v4, v5, v6, v7)
20
+ - UUID (v4, v5, v7)
21
21
  - NanoID
22
22
  - ULID
23
+ - CUID2
23
24
  - ⚡ Efficient and secure random value generation
24
25
  - 📦 Zero dependencies (except Effect)
25
26
 
@@ -32,14 +33,14 @@ import {
32
33
  GetRandomValues,
33
34
  makeUuid4,
34
35
  makeUuid5,
35
- makeUuid6,
36
36
  makeUuid7,
37
- Uuid6State,
38
37
  Uuid7State,
39
38
  Uuid5Namespace,
40
39
  Sha1,
41
40
  makeNanoId,
42
- makeUlid
41
+ makeUlid,
42
+ makeCuid,
43
+ CuidState,
43
44
  } from '@typed/id'
44
45
 
45
46
  // Generate a UUID v4 (random)
@@ -58,15 +59,6 @@ await makeUuid5(Uuid5Namespace.URL, 'https://example.com').pipe(
58
59
  )
59
60
  // Output: "2ed6657d-e927-568b-95e1-2665a8aea6a2"
60
61
 
61
- // Generate a UUID v6 (reordered time-based)
62
- await makeUuid6.pipe(
63
- Effect.provide(Uuid6State.Default),
64
- Effect.provide([GetRandomValues.CryptoRandom, DateTimes.Default]),
65
- Effect.flatMap(Effect.log),
66
- Effect.runPromise
67
- )
68
- // Output: "1ee6742c-8f9c-6000-b9d1-0242ac120002"
69
-
70
62
  // Generate a UUID v7 (time-sortable)
71
63
  await makeUuid7.pipe(
72
64
  Effect.provide(Uuid7State.Default),
@@ -91,6 +83,15 @@ await makeUlid.pipe(
91
83
  Effect.runPromise
92
84
  )
93
85
  // Output: "01ARZ3NDEKTSV4RRFFQ69G5FAV"
86
+
87
+ // Generate a CUID
88
+ await makeCuid.pipe(
89
+ Effect.provide(CuidState.layer('my-environment')), // Provide environment fingerprint
90
+ Effect.provide([GetRandomValues.CryptoRandom, DateTimes.Default]),
91
+ Effect.flatMap(Effect.log),
92
+ Effect.runPromise
93
+ )
94
+ // Output: "clh3aqnd900003b64zpka3df"
94
95
  ```
95
96
 
96
97
  ## API
@@ -99,7 +100,6 @@ await makeUlid.pipe(
99
100
 
100
101
  - `makeUuid4`: Generates a v4 UUID (random)
101
102
  - `makeUuid5`: Generates a v5 UUID (SHA-1 hash of namespace + name)
102
- - `makeUuid6`: Generates a v6 UUID (reordered time-based for better sorting)
103
103
  - `makeUuid7`: Generates a v7 UUID (time-sortable)
104
104
 
105
105
  ### UUID v5 Namespaces
@@ -118,6 +118,21 @@ Pre-defined namespaces for UUID v5 generation:
118
118
 
119
119
  - `makeUlid`: Generates a ULID (Universally Unique Lexicographically Sortable Identifier)
120
120
 
121
+ ### CUID
122
+
123
+ - `makeCuid`: Generates a CUID2 (Collision-resistant Unique IDentifier)
124
+ - `CuidState.layer(envData)`: Creates a CUID state layer with environment fingerprint
125
+ - `envData`: A string identifying the environment (e.g., 'browser', 'node', 'mobile-ios')
126
+ - Used to help prevent collisions in distributed systems
127
+ - Cached and reused for efficiency
128
+ - Format: 24 characters, starting with a lowercase letter, followed by numbers and lowercase letters
129
+ - Properties:
130
+ - Sequential for database performance
131
+ - Secure from enumeration
132
+ - URL-safe
133
+ - Horizontally scalable
134
+ - Includes timestamp for time-based sorting
135
+
121
136
  ## License
122
137
 
123
138
  MIT
package/src/Cuid.ts ADDED
@@ -0,0 +1,135 @@
1
+ import { Effect, Layer, Schema } from 'effect'
2
+ import { DateTimes } from './DateTimes.js'
3
+ import { GetRandomValues } from './GetRandomValues.js'
4
+
5
+ // Constants
6
+ const DEFAULT_LENGTH = 24
7
+ const BIG_LENGTH = 32
8
+ const INITIAL_COUNT_MAX = 476782367
9
+
10
+ // Schema
11
+ export const Cuid = Schema.String.pipe(
12
+ Schema.pattern(/^[a-z][0-9a-z]+$/),
13
+ Schema.brand('@typed/id/CUID'),
14
+ )
15
+ export type Cuid = Schema.Schema.Type<typeof Cuid>
16
+
17
+ export const isCuid: (value: string) => value is Cuid = Schema.is(Cuid)
18
+
19
+ // Types
20
+ export type CuidSeed = {
21
+ readonly timestamp: number
22
+ readonly counter: number
23
+ readonly random: Uint8Array
24
+ readonly fingerprint: string
25
+ }
26
+
27
+ // Utilities
28
+ const ALPHABET = Array.from({ length: 26 }, (_, i) => String.fromCharCode(i + 97))
29
+ const encoder = new TextEncoder()
30
+
31
+ function createEntropy(length: number, random: Uint8Array): string {
32
+ let entropy = ''
33
+ let offset = 0
34
+
35
+ while (entropy.length < length) {
36
+ const value = random[offset]
37
+ entropy += Math.floor(value % 36).toString(36)
38
+ offset = (offset + 1) % random.length
39
+ }
40
+
41
+ return entropy
42
+ }
43
+
44
+ function hash(input: string): Promise<string> {
45
+ // Convert string to bytes
46
+ const data = encoder.encode(input)
47
+
48
+ // Create a hash using the Web Crypto API
49
+ return crypto.subtle.digest('SHA-512', data).then((buffer) => {
50
+ const view = new Uint8Array(buffer)
51
+ let value = 0n
52
+ for (const byte of view) {
53
+ value = (value << 8n) + BigInt(byte)
54
+ }
55
+ // Drop the first character because it will bias the histogram to the left
56
+ return value.toString(36).slice(1)
57
+ })
58
+ }
59
+
60
+ // State Management
61
+ export class CuidState extends Effect.Tag('CuidState')<
62
+ CuidState,
63
+ {
64
+ readonly next: Effect.Effect<CuidSeed>
65
+ }
66
+ >() {
67
+ static readonly layer = (
68
+ envData: string,
69
+ ): Layer.Layer<CuidState, never, DateTimes | GetRandomValues> =>
70
+ Layer.effect(
71
+ this,
72
+ Effect.gen(function* () {
73
+ const { now } = yield* DateTimes
74
+ const getRandomValues = yield* GetRandomValues
75
+ const initialBytes = yield* getRandomValues(4)
76
+ const initialValue =
77
+ Math.abs(
78
+ (initialBytes[0] << 24) |
79
+ (initialBytes[1] << 16) |
80
+ (initialBytes[2] << 8) |
81
+ initialBytes[3],
82
+ ) % INITIAL_COUNT_MAX
83
+
84
+ // Create fingerprint from environment data
85
+ const fingerprint = yield* Effect.promise(() =>
86
+ hash(envData).then((h) => h.substring(0, BIG_LENGTH)),
87
+ )
88
+
89
+ let counter = initialValue
90
+
91
+ return {
92
+ next: Effect.gen(function* () {
93
+ const timestamp = yield* now
94
+ const random = yield* getRandomValues(32)
95
+ return {
96
+ timestamp,
97
+ counter: counter++,
98
+ random,
99
+ fingerprint,
100
+ }
101
+ }),
102
+ }
103
+ }),
104
+ )
105
+
106
+ static readonly Default = this.layer('node')
107
+ }
108
+
109
+ // Core Functions
110
+ function cuidFromSeed({ timestamp, counter, random, fingerprint }: CuidSeed): Effect.Effect<Cuid> {
111
+ return Effect.gen(function* () {
112
+ // First letter is always a random lowercase letter from the seed
113
+ const firstLetter = ALPHABET[random[0] % ALPHABET.length]
114
+
115
+ // Convert components to base36
116
+ const time = timestamp.toString(36)
117
+ const count = counter.toString(36)
118
+
119
+ // Create entropy from remaining random bytes
120
+ const salt = createEntropy(4, random.slice(1))
121
+
122
+ // Hash all components together
123
+ const hashInput = `${time}${salt}${count}${fingerprint}`
124
+ const hashed = yield* Effect.promise(() => hash(hashInput))
125
+
126
+ // Construct the final CUID
127
+ const id = `${firstLetter}${hashed.substring(0, DEFAULT_LENGTH - 1)}`
128
+
129
+ return Cuid.make(id)
130
+ })
131
+ }
132
+
133
+ // Public API
134
+ export const makeCuid: Effect.Effect<Cuid, never, DateTimes | GetRandomValues | CuidState> =
135
+ Effect.flatMap(CuidState.next, cuidFromSeed)
package/src/id.test.ts CHANGED
@@ -5,18 +5,18 @@ import {
5
5
  GetRandomValues,
6
6
  isUuid4,
7
7
  isUuid5,
8
- isUuid6,
9
8
  isUuid7,
10
9
  makeNanoId,
11
10
  makeUlid,
12
11
  makeUuid4,
13
12
  makeUuid5,
14
- makeUuid6,
15
13
  makeUuid7,
16
14
  Sha1,
17
15
  Uuid5Namespace,
18
- Uuid6State,
19
16
  Uuid7State,
17
+ makeCuid,
18
+ isCuid,
19
+ CuidState,
20
20
  } from './index.js'
21
21
 
22
22
  const makeTestValues = (length: number) => {
@@ -28,19 +28,18 @@ const makeTestValues = (length: number) => {
28
28
  }
29
29
 
30
30
  const provideTestValues = flow(
31
- Effect.provide([Uuid6State.Default, Uuid7State.Default]),
31
+ Effect.provide(CuidState.layer('test')),
32
+ Effect.provide(Uuid7State.Default),
32
33
  Effect.provide(Sha1.Default),
33
- Effect.provide([
34
- GetRandomValues.layer((length) => Effect.succeed(makeTestValues(length))),
35
- DateTimes.Fixed(new Date(0)),
36
- ]),
34
+ Effect.provide(GetRandomValues.layer((length) => Effect.succeed(makeTestValues(length)))),
35
+ Effect.provide(DateTimes.Fixed(new Date(0))),
37
36
  )
38
37
 
39
38
  describe(__filename, () => {
40
39
  describe('Uuid4', () => {
41
40
  it.effect('generates a UUID v4', () =>
42
- Effect.gen(function* (_) {
43
- const id = yield* _(makeUuid4)
41
+ Effect.gen(function* () {
42
+ const id = yield* makeUuid4
44
43
  expect(id).toMatchInlineSnapshot(`"00010203-0405-4607-8809-0a0b0c0d0e0f"`)
45
44
  expect(id.length).toEqual(36)
46
45
  expect(isUuid4(id)).toEqual(true)
@@ -50,8 +49,8 @@ describe(__filename, () => {
50
49
 
51
50
  describe('Uuid5', () => {
52
51
  it.effect('generates a UUID v5', () =>
53
- Effect.gen(function* (_) {
54
- const id = yield* _(makeUuid5(Uuid5Namespace.DNS, 'example.com'))
52
+ Effect.gen(function* () {
53
+ const id = yield* makeUuid5(Uuid5Namespace.DNS, 'example.com')
55
54
  expect(id).toMatchInlineSnapshot(`"cfbff0d1-9375-5685-968c-48ce8b15ae17"`)
56
55
  expect(id.length).toEqual(36)
57
56
  expect(isUuid5(id)).toEqual(true)
@@ -59,21 +58,10 @@ describe(__filename, () => {
59
58
  )
60
59
  })
61
60
 
62
- describe('Uuid6', () => {
63
- it.effect('generates a UUID v6', () =>
64
- Effect.gen(function* (_) {
65
- const id = yield* _(makeUuid6)
66
- expect(id).toMatchInlineSnapshot(`"1b21dd21-3814-6000-8809-0b0b0c0d0e0f"`)
67
- expect(id.length).toEqual(36)
68
- expect(isUuid6(id)).toEqual(true)
69
- }).pipe(provideTestValues),
70
- )
71
- })
72
-
73
61
  describe('Uuid7', () => {
74
62
  it.effect('generates a UUID v7', () =>
75
- Effect.gen(function* (_) {
76
- const id = yield* _(makeUuid7)
63
+ Effect.gen(function* () {
64
+ const id = yield* makeUuid7
77
65
  expect(id).toMatchInlineSnapshot(`"00000000-0000-7030-9c20-260b0c0d0e0f"`)
78
66
  expect(id.length).toEqual(36)
79
67
  expect(isUuid7(id)).toEqual(true)
@@ -83,8 +71,8 @@ describe(__filename, () => {
83
71
 
84
72
  describe('NanoId', () => {
85
73
  it.effect('generates a NanoId', () =>
86
- Effect.gen(function* (_) {
87
- const id = yield* _(makeNanoId)
74
+ Effect.gen(function* () {
75
+ const id = yield* makeNanoId
88
76
  expect(id).toMatchInlineSnapshot(`"0123456789abcdefghijk"`)
89
77
  expect(id.length).toEqual(21)
90
78
  }).pipe(provideTestValues),
@@ -93,11 +81,28 @@ describe(__filename, () => {
93
81
 
94
82
  describe('Ulid', () => {
95
83
  it.effect('generates a Ulid', () =>
96
- Effect.gen(function* (_) {
97
- const id = yield* _(makeUlid)
84
+ Effect.gen(function* () {
85
+ const id = yield* makeUlid
98
86
  expect(id).toMatchInlineSnapshot(`"00000000000123456789ABCDEF"`)
99
87
  expect(id.length).toEqual(26)
100
88
  }).pipe(provideTestValues),
101
89
  )
102
90
  })
91
+
92
+ describe('Cuid', () => {
93
+ it.effect('generates a CUID', () =>
94
+ Effect.gen(function* () {
95
+ const id = yield* makeCuid
96
+ expect(id.length).toEqual(24)
97
+ expect(isCuid(id)).toEqual(true)
98
+ expect(id).toMatchInlineSnapshot(`"ai17q5mkkp8w5f2cey3lyzu5"`)
99
+
100
+ // Generate another to ensure uniqueness
101
+ const id2 = yield* makeCuid
102
+ expect(id2).not.toEqual(id)
103
+ expect(isCuid(id2)).toEqual(true)
104
+ expect(id2).toMatchInlineSnapshot(`"abeo5wmlmnjxjrnjiidlfvzp"`)
105
+ }).pipe(provideTestValues),
106
+ )
107
+ })
103
108
  })
package/src/index.ts CHANGED
@@ -1,8 +1,8 @@
1
+ export * from './Cuid.js'
1
2
  export * from './DateTimes.js'
2
3
  export * from './GetRandomValues.js'
3
4
  export * from './NanoId.js'
4
5
  export * from './Ulid.js'
5
6
  export * from './Uuid4.js'
6
7
  export * from './Uuid5.js'
7
- export * from './Uuid6.js'
8
8
  export * from './Uuid7.js'
package/src/Uuid6.ts DELETED
@@ -1,141 +0,0 @@
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 Uuid6 = Schema.UUID.pipe(Schema.brand('@typed/id/UUID6'))
8
- export type Uuid6 = Schema.Schema.Type<typeof Uuid6>
9
-
10
- export const isUuid6: (value: string) => value is Uuid6 = Schema.is(Uuid6)
11
-
12
- export type Uuid6Seed = {
13
- readonly msecs: number
14
- readonly nsecs: number
15
- readonly clockSeq: number
16
- readonly nodeId: Uint8Array
17
- }
18
-
19
- export class Uuid6State extends Effect.Tag('Uuid6State')<
20
- Uuid6State,
21
- {
22
- readonly next: Effect.Effect<Uuid6Seed>
23
- }
24
- >() {
25
- static Default: Layer.Layer<Uuid6State, never, GetRandomValues | DateTimes> = Layer.effect(
26
- this,
27
- Effect.gen(function* () {
28
- const { now } = yield* DateTimes
29
- const getRandomValues = yield* GetRandomValues
30
-
31
- const state = {
32
- msecs: Number.NEGATIVE_INFINITY,
33
- nsecs: 0,
34
- clockSeq: -1,
35
- nodeId: undefined as Uint8Array | undefined,
36
- }
37
-
38
- function updateState(currentTime: number, randomBytes: Uint8Array) {
39
- if (currentTime === state.msecs) {
40
- // Same msec-interval = simulate higher clock resolution by bumping `nsecs`
41
- state.nsecs++
42
-
43
- // Check for `nsecs` overflow (nsecs is capped at 10K intervals / msec)
44
- if (state.nsecs >= 10000) {
45
- state.nodeId = undefined
46
- state.nsecs = 0
47
- }
48
- } else if (currentTime > state.msecs) {
49
- // Reset nsec counter when clock advances to a new msec interval
50
- state.nsecs = 0
51
- } else if (currentTime < state.msecs) {
52
- // Handle clock regression
53
- state.nodeId = undefined
54
- }
55
-
56
- // Init node and clock sequence if needed
57
- if (!state.nodeId) {
58
- state.nodeId = randomBytes.slice(10, 16)
59
- // Set multicast bit
60
- state.nodeId[0] |= 0x01
61
-
62
- // Clock sequence must be randomized
63
- state.clockSeq = ((randomBytes[8] << 8) | randomBytes[9]) & 0x3fff
64
- }
65
-
66
- state.msecs = currentTime
67
-
68
- return {
69
- msecs: state.msecs,
70
- nsecs: state.nsecs,
71
- clockSeq: state.clockSeq,
72
- nodeId: state.nodeId,
73
- }
74
- }
75
-
76
- return {
77
- next: Effect.gen(function* () {
78
- const timestamp = yield* now
79
- const randomBytes = yield* getRandomValues(16)
80
- return updateState(timestamp, randomBytes)
81
- }),
82
- }
83
- }),
84
- )
85
- }
86
-
87
- export const makeUuid6: Effect.Effect<Uuid6, never, Uuid6State> = Effect.map(
88
- Uuid6State.next,
89
- uuid6FromSeed,
90
- )
91
-
92
- function uuid6FromSeed({ msecs, nsecs, clockSeq, nodeId }: Uuid6Seed): Uuid6 {
93
- // First generate the fields as they would be in a v1 UUID
94
- const result = new Uint8Array(16)
95
-
96
- // Offset to Gregorian epoch
97
- msecs += 12219292800000
98
-
99
- // `time_low`
100
- const tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000
101
- const time_low_bytes = new Uint8Array([
102
- (tl >>> 24) & 0xff,
103
- (tl >>> 16) & 0xff,
104
- (tl >>> 8) & 0xff,
105
- tl & 0xff,
106
- ])
107
-
108
- // `time_mid` and `time_high`
109
- const tmh = ((msecs / 0x100000000) * 10000) & 0xfffffff
110
- const time_mid_high_bytes = new Uint8Array([
111
- (tmh >>> 8) & 0xff,
112
- tmh & 0xff,
113
- ((tmh >>> 24) & 0xf) | 0x10, // include version 1
114
- (tmh >>> 16) & 0xff,
115
- ])
116
-
117
- result[0] = ((time_mid_high_bytes[2] & 0x0f) << 4) | ((time_mid_high_bytes[3] >> 4) & 0x0f)
118
- result[1] = ((time_mid_high_bytes[3] & 0x0f) << 4) | ((time_mid_high_bytes[0] & 0xf0) >> 4)
119
- result[2] = ((time_mid_high_bytes[0] & 0x0f) << 4) | ((time_mid_high_bytes[1] & 0xf0) >> 4)
120
- result[3] = ((time_mid_high_bytes[1] & 0x0f) << 4) | ((time_low_bytes[0] & 0xf0) >> 4)
121
- result[4] = ((time_low_bytes[0] & 0x0f) << 4) | ((time_low_bytes[1] & 0xf0) >> 4)
122
- result[5] = ((time_low_bytes[1] & 0x0f) << 4) | ((time_low_bytes[2] & 0xf0) >> 4)
123
- result[6] = 0x60 | (time_low_bytes[2] & 0x0f)
124
- result[7] = time_low_bytes[3]
125
-
126
- // clock_seq_hi_and_reserved
127
- result[8] = (clockSeq >>> 8) | 0x80 // variant bits
128
-
129
- // clock_seq_low
130
- result[9] = clockSeq & 0xff
131
-
132
- // node
133
- result[10] = nodeId[0]
134
- result[11] = nodeId[1]
135
- result[12] = nodeId[2]
136
- result[13] = nodeId[3]
137
- result[14] = nodeId[4]
138
- result[15] = nodeId[5]
139
-
140
- return uuidStringify(result) as Uuid6
141
- }