@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 +27 -0
- package/dist/Cuid.js +86 -0
- package/dist/Cuid.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/readme.md +30 -15
- package/src/Cuid.ts +135 -0
- package/src/id.test.ts +34 -29
- package/src/index.ts +1 -1
- package/src/Uuid6.ts +0 -141
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
|
package/dist/Cuid.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Cuid.js","sourceRoot":"","sources":["../src/Cuid.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAEtD,YAAY;AACZ,MAAM,cAAc,GAAG,EAAE,CAAA;AACzB,MAAM,UAAU,GAAG,EAAE,CAAA;AACrB,MAAM,iBAAiB,GAAG,SAAS,CAAA;AAEnC,SAAS;AACT,MAAM,CAAC,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CACpC,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,EAClC,MAAM,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAC/B,CAAA;AAGD,MAAM,CAAC,MAAM,MAAM,GAAqC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAA;AAUvE,YAAY;AACZ,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;AAClF,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAA;AAEjC,SAAS,aAAa,CAAC,MAAc,EAAE,MAAkB;IACvD,IAAI,OAAO,GAAG,EAAE,CAAA;IAChB,IAAI,MAAM,GAAG,CAAC,CAAA;IAEd,OAAO,OAAO,CAAC,MAAM,GAAG,MAAM,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,CAAA;QAC5B,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;QAC9C,MAAM,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,CAAA;IACvC,CAAC;IAED,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,SAAS,IAAI,CAAC,KAAa;IACzB,0BAA0B;IAC1B,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IAElC,yCAAyC;IACzC,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;QAC3D,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,CAAA;QACnC,IAAI,KAAK,GAAG,EAAE,CAAA;QACd,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;YACxB,KAAK,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,CAAA;QACtC,CAAC;QACD,0EAA0E;QAC1E,OAAO,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IACpC,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,mBAAmB;AACnB,MAAM,OAAO,SAAU,SAAQ,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,EAKnD;IACD,MAAM,CAAU,KAAK,GAAG,CACtB,OAAe,EAC6C,EAAE,CAC9D,KAAK,CAAC,MAAM,CACV,IAAI,EACJ,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAClB,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC,SAAS,CAAA;QAChC,MAAM,eAAe,GAAG,KAAK,CAAC,CAAC,eAAe,CAAA;QAC9C,MAAM,YAAY,GAAG,KAAK,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAA;QAC9C,MAAM,YAAY,GAChB,IAAI,CAAC,GAAG,CACN,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YACrB,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YACvB,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YACtB,YAAY,CAAC,CAAC,CAAC,CAClB,GAAG,iBAAiB,CAAA;QAEvB,2CAA2C;QAC3C,MAAM,WAAW,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAC7C,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CACtD,CAAA;QAED,IAAI,OAAO,GAAG,YAAY,CAAA;QAE1B,OAAO;YACL,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;gBACxB,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,GAAG,CAAA;gBAC5B,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,eAAe,CAAC,EAAE,CAAC,CAAA;gBACzC,OAAO;oBACL,SAAS;oBACT,OAAO,EAAE,OAAO,EAAE;oBAClB,MAAM;oBACN,WAAW;iBACZ,CAAA;YACH,CAAC,CAAC;SACH,CAAA;IACH,CAAC,CAAC,CACH,CAAA;IAEH,MAAM,CAAU,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;;AAG9C,iBAAiB;AACjB,SAAS,YAAY,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAY;IACzE,OAAO,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACzB,iEAAiE;QACjE,MAAM,WAAW,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAA;QAEzD,+BAA+B;QAC/B,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;QACnC,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;QAElC,6CAA6C;QAC7C,MAAM,IAAI,GAAG,aAAa,CAAC,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;QAE9C,+BAA+B;QAC/B,MAAM,SAAS,GAAG,GAAG,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG,WAAW,EAAE,CAAA;QACxD,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAA;QAE3D,2BAA2B;QAC3B,MAAM,EAAE,GAAG,GAAG,WAAW,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,cAAc,GAAG,CAAC,CAAC,EAAE,CAAA;QAErE,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACtB,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,aAAa;AACb,MAAM,CAAC,MAAM,QAAQ,GACnB,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,EAAE,YAAY,CAAC,CAAA"}
|
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
|
|
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
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
|
|
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,
|
|
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(
|
|
31
|
+
Effect.provide(CuidState.layer('test')),
|
|
32
|
+
Effect.provide(Uuid7State.Default),
|
|
32
33
|
Effect.provide(Sha1.Default),
|
|
33
|
-
Effect.provide(
|
|
34
|
-
|
|
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*
|
|
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*
|
|
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*
|
|
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*
|
|
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*
|
|
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
|
-
}
|