@typed/id 1.0.0-beta.0 → 1.0.0-beta.10

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 (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +194 -86
  3. package/dist/Cuid.d.ts +128 -13
  4. package/dist/Cuid.d.ts.map +1 -1
  5. package/dist/Cuid.js +135 -41
  6. package/dist/DateTimes.d.ts +66 -3
  7. package/dist/DateTimes.d.ts.map +1 -1
  8. package/dist/DateTimes.js +72 -5
  9. package/dist/Ids.d.ts +139 -34
  10. package/dist/Ids.d.ts.map +1 -1
  11. package/dist/Ids.js +121 -21
  12. package/dist/IdsTest.d.ts +34 -0
  13. package/dist/IdsTest.d.ts.map +1 -0
  14. package/dist/IdsTest.js +44 -0
  15. package/dist/Ksuid.d.ts +51 -1
  16. package/dist/Ksuid.d.ts.map +1 -1
  17. package/dist/Ksuid.js +60 -8
  18. package/dist/NanoId.d.ts +47 -0
  19. package/dist/NanoId.d.ts.map +1 -1
  20. package/dist/NanoId.js +48 -1
  21. package/dist/RandomValues.d.ts +63 -4
  22. package/dist/RandomValues.d.ts.map +1 -1
  23. package/dist/RandomValues.js +74 -10
  24. package/dist/Ulid.d.ts +49 -1
  25. package/dist/Ulid.d.ts.map +1 -1
  26. package/dist/Ulid.js +54 -4
  27. package/dist/Uuid4.d.ts +47 -0
  28. package/dist/Uuid4.d.ts.map +1 -1
  29. package/dist/Uuid4.js +48 -1
  30. package/dist/Uuid5.d.ts +178 -6
  31. package/dist/Uuid5.d.ts.map +1 -1
  32. package/dist/Uuid5.js +152 -15
  33. package/dist/Uuid7.d.ts +121 -15
  34. package/dist/Uuid7.d.ts.map +1 -1
  35. package/dist/Uuid7.js +124 -16
  36. package/dist/__tests__/helpers.d.ts +7 -0
  37. package/dist/__tests__/helpers.d.ts.map +1 -0
  38. package/dist/__tests__/helpers.js +19 -0
  39. package/dist/__tests__/public-contract.type-test.d.ts +2 -0
  40. package/dist/__tests__/public-contract.type-test.d.ts.map +1 -0
  41. package/dist/__tests__/public-contract.type-test.js +3 -0
  42. package/dist/_sha.d.ts +32 -0
  43. package/dist/_sha.d.ts.map +1 -1
  44. package/dist/_sha.js +32 -0
  45. package/dist/_uuid-stringify.d.ts +15 -0
  46. package/dist/_uuid-stringify.d.ts.map +1 -1
  47. package/dist/_uuid-stringify.js +15 -0
  48. package/dist/internal/Ids.d.ts +23 -0
  49. package/dist/internal/Ids.d.ts.map +1 -0
  50. package/dist/internal/Ids.js +29 -0
  51. package/package.json +73 -11
  52. package/dist/Id.test.d.ts +0 -2
  53. package/dist/Id.test.d.ts.map +0 -1
  54. package/dist/Id.test.js +0 -190
  55. package/src/Cuid.ts +0 -124
  56. package/src/DateTimes.ts +0 -36
  57. package/src/Id.test.ts +0 -217
  58. package/src/Ids.ts +0 -124
  59. package/src/Ksuid.ts +0 -74
  60. package/src/NanoId.ts +0 -33
  61. package/src/RandomValues.ts +0 -33
  62. package/src/Ulid.ts +0 -51
  63. package/src/Uuid4.ts +0 -24
  64. package/src/Uuid5.ts +0 -71
  65. package/src/Uuid7.ts +0 -104
  66. package/src/_sha.ts +0 -11
  67. package/src/_uuid-stringify.ts +0 -30
  68. package/src/index.ts +0 -10
  69. package/tsconfig.json +0 -6
  70. package/vitest.config.ts +0 -8
@@ -1,2 +1,17 @@
1
+ /**
2
+ * Formats the first 16 bytes of a seed as a lowercase hyphenated UUID string.
3
+ * @remarks
4
+ * ## Why
5
+ * This published low-level helper centralizes UUID byte layout formatting but deliberately does not validate length, version, or variant bits.
6
+ * ## Ownership and lifetime
7
+ * This pure function acquires no resources, does not mutate or retain the seed, and returns a new string.
8
+ * @example
9
+ * ```ts
10
+ * import { uuidStringify } from "@typed/id/_uuid-stringify"
11
+ * const text = uuidStringify(new Uint8Array(16))
12
+ * ```
13
+ * @category Formatting
14
+ * @since 1.0.0
15
+ */
1
16
  export declare function uuidStringify(seed: Uint8Array): string;
2
17
  //# sourceMappingURL=_uuid-stringify.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"_uuid-stringify.d.ts","sourceRoot":"","sources":["../src/_uuid-stringify.ts"],"names":[],"mappings":"AAMA,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAuBtD"}
1
+ {"version":3,"file":"_uuid-stringify.d.ts","sourceRoot":"","sources":["../src/_uuid-stringify.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAuBtD"}
@@ -2,6 +2,21 @@ const byteToHex = [];
2
2
  for (let i = 0; i < 256; ++i) {
3
3
  byteToHex.push((i + 0x100).toString(16).slice(1));
4
4
  }
5
+ /**
6
+ * Formats the first 16 bytes of a seed as a lowercase hyphenated UUID string.
7
+ * @remarks
8
+ * ## Why
9
+ * This published low-level helper centralizes UUID byte layout formatting but deliberately does not validate length, version, or variant bits.
10
+ * ## Ownership and lifetime
11
+ * This pure function acquires no resources, does not mutate or retain the seed, and returns a new string.
12
+ * @example
13
+ * ```ts
14
+ * import { uuidStringify } from "@typed/id/_uuid-stringify"
15
+ * const text = uuidStringify(new Uint8Array(16))
16
+ * ```
17
+ * @category Formatting
18
+ * @since 1.0.0
19
+ */
5
20
  export function uuidStringify(seed) {
6
21
  return [
7
22
  byteToHex[seed[0]],
@@ -0,0 +1,23 @@
1
+ import * as Cause from "effect/Cause";
2
+ import * as Effect from "effect/Effect";
3
+ import type { Uuid5 } from "../Uuid5.js";
4
+ import { Uuid5Namespace } from "../Uuid5.js";
5
+ import { DateTimes } from "../DateTimes.js";
6
+ import { RandomValues } from "../RandomValues.js";
7
+ export declare const makeLazyIds: (envData: string) => Effect.Effect<{
8
+ cuid: Effect.Effect<string & import("effect/Brand").Brand<"@typed/id/CUID">, never, never>;
9
+ ksuid: Effect.Effect<string & import("effect/Brand").Brand<"@typed/id/KSUID">, Cause.IllegalArgumentError, never>;
10
+ nanoId: Effect.Effect<string & import("effect/Brand").Brand<"@typed/id/NanoId">, never, never>;
11
+ ulid: Effect.Effect<string & import("effect/Brand").Brand<"@typed/id/ULID">, Cause.IllegalArgumentError, never>;
12
+ uuid4: Effect.Effect<string & import("effect/Brand").Brand<"@typed/id/UUID4">, never, never>;
13
+ uuid5: {
14
+ (namespace: Uuid5Namespace): (name: string) => Effect.Effect<Uuid5, Cause.IllegalArgumentError>;
15
+ (name: string, namespace: Uuid5Namespace): Effect.Effect<Uuid5, Cause.IllegalArgumentError>;
16
+ readonly dns: (name: string) => Effect.Effect<Uuid5, Cause.IllegalArgumentError>;
17
+ readonly url: (name: string) => Effect.Effect<Uuid5, Cause.IllegalArgumentError>;
18
+ readonly oid: (name: string) => Effect.Effect<Uuid5, Cause.IllegalArgumentError>;
19
+ readonly x500: (name: string) => Effect.Effect<Uuid5, Cause.IllegalArgumentError>;
20
+ };
21
+ uuid7: Effect.Effect<string & import("effect/Brand").Brand<"@typed/id/UUID7">, Cause.IllegalArgumentError, never>;
22
+ }, never, DateTimes | RandomValues>;
23
+ //# sourceMappingURL=Ids.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Ids.d.ts","sourceRoot":"","sources":["../../src/internal/Ids.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,cAAc,EAAS,MAAM,aAAa,CAAC;AAEpD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAG5C,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAKlD,eAAO,MAAM,WAAW,YAAa,MAAM;;;;;;;oBAQxB,cAAc,GACxB,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,oBAAoB,CAAC;eAC9D,MAAM,aAAa,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,oBAAoB,CAAC;sBAC7E,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,oBAAoB,CAAC;sBAClE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,oBAAoB,CAAC;sBAClE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,oBAAoB,CAAC;uBACjE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,oBAAoB,CAAC;;;mCAwBnF,CAAC"}
@@ -0,0 +1,29 @@
1
+ import * as Effect from "effect/Effect";
2
+ import { dual } from "effect/Function";
3
+ import { Uuid5Namespace, uuid5 } from "../Uuid5.js";
4
+ import { CuidState, cuid } from "../Cuid.js";
5
+ import { ksuid } from "../Ksuid.js";
6
+ import { nanoId } from "../NanoId.js";
7
+ import { ulid } from "../Ulid.js";
8
+ import { uuid4 } from "../Uuid4.js";
9
+ import { Uuid7State, uuid7 } from "../Uuid7.js";
10
+ export const makeLazyIds = (envData) => Effect.gen(function* () {
11
+ const services = yield* Effect.context();
12
+ const getCuidState = yield* Effect.cached(Effect.provide(CuidState.make(envData), services));
13
+ const getUuid7State = yield* Effect.cached(Effect.provide(Uuid7State.make, services));
14
+ const uuid5_ = Object.assign(dual(2, (name, namespace) => Effect.provide(uuid5(name, namespace), services)), {
15
+ dns: uuid5(Uuid5Namespace.DNS),
16
+ url: uuid5(Uuid5Namespace.URL),
17
+ oid: uuid5(Uuid5Namespace.OID),
18
+ x500: uuid5(Uuid5Namespace.X500),
19
+ });
20
+ return {
21
+ cuid: Effect.flatMap(getCuidState, (state) => Effect.provideService(cuid, CuidState, state)),
22
+ ksuid: Effect.provide(ksuid, services),
23
+ nanoId: Effect.provide(nanoId, services),
24
+ ulid: Effect.provide(ulid, services),
25
+ uuid4: Effect.provide(uuid4, services),
26
+ uuid5: uuid5_,
27
+ uuid7: Effect.flatMap(getUuid7State, (state) => Effect.provideService(uuid7, Uuid7State, state)),
28
+ };
29
+ });
package/package.json CHANGED
@@ -1,29 +1,91 @@
1
1
  {
2
2
  "name": "@typed/id",
3
- "version": "1.0.0-beta.0",
4
- "publishConfig": {
5
- "access": "public"
3
+ "version": "1.0.0-beta.10",
4
+ "description": "Branded ID generators and facades for Typed applications.",
5
+ "homepage": "https://github.com/TylorS/typed/tree/development/packages/id#readme",
6
+ "bugs": "https://github.com/TylorS/typed/issues",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/TylorS/typed.git",
11
+ "directory": "packages/id"
6
12
  },
13
+ "files": [
14
+ "dist"
15
+ ],
7
16
  "type": "module",
17
+ "sideEffects": false,
8
18
  "exports": {
9
19
  ".": {
10
20
  "types": "./dist/index.d.ts",
11
21
  "import": "./dist/index.js"
12
22
  },
13
- "./*": {
14
- "types": "./dist/*.d.ts",
15
- "import": "./dist/*.js"
23
+ "./Cuid": {
24
+ "types": "./dist/Cuid.d.ts",
25
+ "import": "./dist/Cuid.js"
26
+ },
27
+ "./DateTimes": {
28
+ "types": "./dist/DateTimes.d.ts",
29
+ "import": "./dist/DateTimes.js"
30
+ },
31
+ "./Ids": {
32
+ "types": "./dist/Ids.d.ts",
33
+ "import": "./dist/Ids.js"
34
+ },
35
+ "./IdsTest": {
36
+ "types": "./dist/IdsTest.d.ts",
37
+ "import": "./dist/IdsTest.js"
38
+ },
39
+ "./Ksuid": {
40
+ "types": "./dist/Ksuid.d.ts",
41
+ "import": "./dist/Ksuid.js"
42
+ },
43
+ "./NanoId": {
44
+ "types": "./dist/NanoId.d.ts",
45
+ "import": "./dist/NanoId.js"
46
+ },
47
+ "./RandomValues": {
48
+ "types": "./dist/RandomValues.d.ts",
49
+ "import": "./dist/RandomValues.js"
50
+ },
51
+ "./Ulid": {
52
+ "types": "./dist/Ulid.d.ts",
53
+ "import": "./dist/Ulid.js"
54
+ },
55
+ "./Uuid4": {
56
+ "types": "./dist/Uuid4.d.ts",
57
+ "import": "./dist/Uuid4.js"
58
+ },
59
+ "./Uuid5": {
60
+ "types": "./dist/Uuid5.d.ts",
61
+ "import": "./dist/Uuid5.js"
62
+ },
63
+ "./Uuid7": {
64
+ "types": "./dist/Uuid7.d.ts",
65
+ "import": "./dist/Uuid7.js"
66
+ },
67
+ "./_sha": {
68
+ "types": "./dist/_sha.d.ts",
69
+ "import": "./dist/_sha.js"
70
+ },
71
+ "./_uuid-stringify": {
72
+ "types": "./dist/_uuid-stringify.d.ts",
73
+ "import": "./dist/_uuid-stringify.js"
16
74
  }
17
75
  },
76
+ "publishConfig": {
77
+ "access": "public"
78
+ },
18
79
  "dependencies": {
19
- "effect": "4.0.0-beta.4"
80
+ "effect": "4.0.0-rc.112"
20
81
  },
21
82
  "devDependencies": {
22
- "typescript": "5.9.3",
23
- "vitest": "4.0.18"
83
+ "typescript": "7.0.2",
84
+ "vitest": "5.0.0"
24
85
  },
25
86
  "scripts": {
26
- "build": "tsc",
27
- "test": "vitest run --passWithNoTests"
87
+ "build": "node -e \"const fs=require('node:fs'); fs.rmSync('dist',{recursive:true,force:true}); fs.rmSync('tsconfig.tsbuildinfo',{force:true})\" && tsc",
88
+ "test": "vitest run",
89
+ "test:types": "tsc -p tsconfig.type-tests.json && tsc -p tsconfig.tests.json"
28
90
  }
29
91
  }
package/dist/Id.test.d.ts DELETED
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=Id.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Id.test.d.ts","sourceRoot":"","sources":["../src/Id.test.ts"],"names":[],"mappings":""}
package/dist/Id.test.js DELETED
@@ -1,190 +0,0 @@
1
- import * as Effect from "effect/Effect";
2
- import { describe, expect, it } from "vitest";
3
- import * as Cuid from "./Cuid.js";
4
- import { Ids } from "./Ids.js";
5
- import * as Ksuid from "./Ksuid.js";
6
- import * as NanoId from "./NanoId.js";
7
- import * as Ulid from "./Ulid.js";
8
- import * as Uuid4 from "./Uuid4.js";
9
- import * as Uuid5 from "./Uuid5.js";
10
- import * as Uuid7 from "./Uuid7.js";
11
- const run = (effect) => Effect.runPromise(Effect.provide(effect, Ids.Test()));
12
- describe("@typed/id", () => {
13
- describe("type guards", () => {
14
- describe("isUuid4", () => {
15
- it("accepts valid UUID v4", () => {
16
- expect(Uuid4.isUuid4("f47ac10b-58cc-4372-a567-0e02b2c3d479")).toBe(true);
17
- expect(Uuid4.isUuid4("550e8400-e29b-41d4-a716-446655440000")).toBe(true);
18
- });
19
- it("rejects invalid formats", () => {
20
- expect(Uuid4.isUuid4("")).toBe(false);
21
- expect(Uuid4.isUuid4("not-a-uuid")).toBe(false);
22
- expect(Uuid4.isUuid4("f47ac10b-58cc-4372-a567-0e02b2c3d479".replace(/-/g, ""))).toBe(false);
23
- expect(Uuid4.isUuid4("g47ac10b-58cc-4372-a567-0e02b2c3d479")).toBe(false); // invalid hex
24
- });
25
- it("rejects UUID v5 as UUID v4", () => {
26
- expect(Uuid4.isUuid4("886313e1-3b8a-5372-9b90-0c9aee199e5d")).toBe(false);
27
- });
28
- });
29
- describe("isUuid5", () => {
30
- it("accepts valid UUID v5", () => {
31
- expect(Uuid5.isUuid5("886313e1-3b8a-5372-9b90-0c9aee199e5d")).toBe(true);
32
- expect(Uuid5.isUuid5("c2ee5f2e-5b2e-5f2e-8b2e-5b2e5f2e8b2e")).toBe(true);
33
- });
34
- it("rejects invalid formats", () => {
35
- expect(Uuid5.isUuid5("")).toBe(false);
36
- expect(Uuid5.isUuid5("f47ac10b-58cc-4372-a567-0e02b2c3d479")).toBe(false); // v4
37
- });
38
- });
39
- describe("isUuid7", () => {
40
- it("accepts valid UUID v7", () => {
41
- expect(Uuid7.isUuid7("018eebb4-1f2c-7c3a-8b4d-123456789abc")).toBe(true);
42
- });
43
- it("rejects invalid formats", () => {
44
- expect(Uuid7.isUuid7("")).toBe(false);
45
- expect(Uuid7.isUuid7("f47ac10b-58cc-4372-a567-0e02b2c3d479")).toBe(false); // v4
46
- });
47
- });
48
- describe("isCuid", () => {
49
- it("accepts valid CUID-like strings (lowercase letter + base36)", () => {
50
- expect(Cuid.isCuid("c1234567890abcdefghijklmn")).toBe(true);
51
- expect(Cuid.isCuid("a0")).toBe(true);
52
- });
53
- it("rejects invalid formats", () => {
54
- expect(Cuid.isCuid("")).toBe(false);
55
- expect(Cuid.isCuid("A123")).toBe(false); // must start with lowercase
56
- expect(Cuid.isCuid("1abc")).toBe(false); // must start with letter
57
- expect(Cuid.isCuid("c123-456")).toBe(false); // no hyphen
58
- });
59
- });
60
- describe("isKsuid", () => {
61
- it("accepts 27-char base62 strings", () => {
62
- expect(Ksuid.isKsuid("0ujsszwN8NRYtYaMBZrYCVp4O1")).toBe(true);
63
- expect(Ksuid.isKsuid("0123456789ABCDEFGHIJKLMNOP")).toBe(true);
64
- });
65
- it("rejects invalid formats", () => {
66
- expect(Ksuid.isKsuid("")).toBe(false);
67
- expect(Ksuid.isKsuid("short")).toBe(false);
68
- expect(Ksuid.isKsuid("0ujsszwN8NRYtYaMBZrYCVp4O1!")).toBe(false); // 28 chars, invalid char
69
- });
70
- });
71
- describe("isNanoId", () => {
72
- it("accepts strings with only 0-9a-zA-Z_-", () => {
73
- expect(NanoId.isNanoId("V1StGXR8_Z5jdHi6B-myT")).toBe(true);
74
- expect(NanoId.isNanoId("abc123")).toBe(true);
75
- expect(NanoId.isNanoId("_-_")).toBe(true);
76
- });
77
- it("rejects invalid characters", () => {
78
- expect(NanoId.isNanoId("")).toBe(false);
79
- expect(NanoId.isNanoId("space in id")).toBe(false);
80
- expect(NanoId.isNanoId("dot.id")).toBe(false);
81
- });
82
- });
83
- describe("isUlid", () => {
84
- it("accepts valid ULIDs", () => {
85
- expect(Ulid.isUlid("01ARZ3NDEKTSV4RRFFQ69G5FAV")).toBe(true);
86
- expect(Ulid.isUlid("0123456789ABCDEFGHJKMNPQR")).toBe(true);
87
- });
88
- it("rejects invalid formats", () => {
89
- expect(Ulid.isUlid("")).toBe(false);
90
- expect(Ulid.isUlid("01ARZ3NDEKTSV4RRFFQ69G5FA")).toBe(false); // wrong length
91
- expect(Ulid.isUlid("01ARZ3NDEKTSV4RRFFQ69G5FAV0")).toBe(false); // wrong length
92
- expect(Ulid.isUlid("01ARZ3NDEKTSV4RRFFQ69G5FAI")).toBe(false); // I not in Crockford base32
93
- });
94
- });
95
- });
96
- describe("Ids service", () => {
97
- it("uuid4 produces valid UUID v4", async () => {
98
- const id = await run(Ids.uuid4);
99
- expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
100
- expect(Uuid4.isUuid4(id)).toBe(true);
101
- });
102
- it("uuid5 produces valid UUID v5", async () => {
103
- const id = await run(Ids.uuid5("hello", Uuid5.Uuid5Namespace.DNS));
104
- expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
105
- expect(Uuid5.isUuid5(id)).toBe(true);
106
- });
107
- it("uuid5 is deterministic for same name and namespace", async () => {
108
- const a = await run(Ids.uuid5("test", Uuid5.Uuid5Namespace.URL));
109
- const b = await run(Ids.uuid5("test", Uuid5.Uuid5Namespace.URL));
110
- expect(a).toBe(b);
111
- });
112
- it("uuid5 differs for different names", async () => {
113
- const a = await run(Ids.uuid5("a", Uuid5.Uuid5Namespace.DNS));
114
- const b = await run(Ids.uuid5("b", Uuid5.Uuid5Namespace.DNS));
115
- expect(a).not.toBe(b);
116
- });
117
- it("uuid5 predefined namespaces work", async () => {
118
- const dns = await run(Ids.uuid5.dns("example.com"));
119
- const url = await run(Ids.uuid5.url("https://example.com"));
120
- const oid = await run(Ids.uuid5.oid("1.2.3"));
121
- const x500 = await run(Ids.uuid5.x500("cn=test"));
122
- expect(Uuid5.isUuid5(dns)).toBe(true);
123
- expect(Uuid5.isUuid5(url)).toBe(true);
124
- expect(Uuid5.isUuid5(oid)).toBe(true);
125
- expect(Uuid5.isUuid5(x500)).toBe(true);
126
- expect(new Set([dns, url, oid, x500]).size).toBe(4);
127
- });
128
- it("uuid7 produces valid UUID v7", async () => {
129
- const id = await run(Ids.uuid7);
130
- expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
131
- expect(Uuid7.isUuid7(id)).toBe(true);
132
- });
133
- it("cuid produces valid CUID", async () => {
134
- const id = await run(Ids.cuid);
135
- expect(Cuid.isCuid(id)).toBe(true);
136
- expect(id).toMatch(/^[a-z][0-9a-z]+$/);
137
- });
138
- it("ksuid produces valid KSUID", async () => {
139
- const id = await run(Ids.ksuid);
140
- expect(Ksuid.isKsuid(id)).toBe(true);
141
- expect(id).toHaveLength(27);
142
- expect(id).toMatch(/^[0-9a-zA-Z]{27}$/);
143
- });
144
- it("nanoId produces valid NanoId", async () => {
145
- const id = await run(Ids.nanoId);
146
- expect(NanoId.isNanoId(id)).toBe(true);
147
- expect(id).toMatch(/^[0-9a-zA-Z_-]+$/);
148
- expect(id).toHaveLength(21);
149
- });
150
- it("ulid produces valid ULID", async () => {
151
- const id = await run(Ids.ulid);
152
- expect(Ulid.isUlid(id)).toBe(true);
153
- expect(id).toHaveLength(26);
154
- expect(id).toMatch(/^[0-9A-HJKMNP-TV-Z]{26}$/);
155
- });
156
- it("Ids.Test with fixed time yields deterministic time-based prefixes", async () => {
157
- const fixedTime = new Date("2025-01-15T12:00:00Z").getTime();
158
- const runFixed = (effect) => Effect.runPromise(Effect.provide(effect, Ids.Test({ currentTime: fixedTime })));
159
- const ulid1 = await runFixed(Ids.ulid);
160
- const ulid2 = await runFixed(Ids.ulid);
161
- const ksuid1 = await runFixed(Ids.ksuid);
162
- const ksuid2 = await runFixed(Ids.ksuid);
163
- expect(ulid1.slice(0, 10)).toBe(ulid2.slice(0, 10));
164
- expect(ksuid1.slice(0, 5)).toBe(ksuid2.slice(0, 5));
165
- });
166
- });
167
- describe("Ids.Default", () => {
168
- it("runs all generators with default layer", async () => {
169
- const program = Effect.gen(function* () {
170
- const ids = yield* Ids;
171
- const u4 = yield* ids.uuid4;
172
- const u5 = yield* ids.uuid5("default-test", Uuid5.Uuid5Namespace.DNS);
173
- const u7 = yield* ids.uuid7;
174
- const c = yield* ids.cuid;
175
- const k = yield* ids.ksuid;
176
- const n = yield* ids.nanoId;
177
- const u = yield* ids.ulid;
178
- return { u4, u5, u7, c, k, n, u };
179
- });
180
- const result = await Effect.runPromise(Effect.provide(program, Ids.Default));
181
- expect(Uuid4.isUuid4(result.u4)).toBe(true);
182
- expect(Uuid5.isUuid5(result.u5)).toBe(true);
183
- expect(Uuid7.isUuid7(result.u7)).toBe(true);
184
- expect(Cuid.isCuid(result.c)).toBe(true);
185
- expect(Ksuid.isKsuid(result.k)).toBe(true);
186
- expect(NanoId.isNanoId(result.n)).toBe(true);
187
- expect(Ulid.isUlid(result.u)).toBe(true);
188
- });
189
- });
190
- });
package/src/Cuid.ts DELETED
@@ -1,124 +0,0 @@
1
- import * as Effect from "effect/Effect";
2
- import * as Layer from "effect/Layer";
3
- import * as Schema from "effect/Schema";
4
- import * as ServiceMap from "effect/ServiceMap";
5
- import { sha512 } from "./_sha.js";
6
- import { DateTimes } from "./DateTimes.js";
7
- import { RandomValues } from "./RandomValues.js";
8
-
9
- // Constants
10
- const DEFAULT_LENGTH = 24;
11
- const BIG_LENGTH = 32;
12
- const INITIAL_COUNT_MAX = 476782367;
13
-
14
- // Schema
15
- export const Cuid = Schema.String.pipe(
16
- Schema.check(Schema.isPattern(/^[a-z][0-9a-z]+$/)),
17
- Schema.brand("@typed/id/CUID"),
18
- );
19
- export type Cuid = Schema.Schema.Type<typeof Cuid>;
20
-
21
- export const isCuid: (value: string) => value is Cuid = Schema.is(Cuid);
22
-
23
- // Types
24
- export type CuidSeed = {
25
- readonly timestamp: number;
26
- readonly counter: number;
27
- readonly random: Uint8Array;
28
- readonly fingerprint: string;
29
- };
30
-
31
- export class CuidState extends ServiceMap.Service<CuidState>()("@typed/id/CuidState", {
32
- make: (envData: string) =>
33
- Effect.gen(function* () {
34
- const { now } = yield* DateTimes;
35
- const getRandomValues = yield* RandomValues;
36
- const initialBytes = yield* getRandomValues(4);
37
- const initialValue =
38
- Math.abs(
39
- (initialBytes[0] << 24) |
40
- (initialBytes[1] << 16) |
41
- (initialBytes[2] << 8) |
42
- initialBytes[3],
43
- ) % INITIAL_COUNT_MAX;
44
-
45
- // Create fingerprint from environment data
46
- const fingerprint = (yield* hash(envData)).substring(0, BIG_LENGTH);
47
-
48
- let counter = initialValue;
49
-
50
- return Effect.gen(function* () {
51
- const timestamp = yield* now;
52
- const random = yield* getRandomValues(32);
53
- return {
54
- timestamp,
55
- counter: counter++,
56
- random,
57
- fingerprint,
58
- } satisfies CuidSeed;
59
- });
60
- }),
61
- }) {
62
- static readonly next = Effect.flatten(CuidState.asEffect());
63
-
64
- static readonly Default = Layer.effect(CuidState, CuidState.make("node")).pipe(
65
- Layer.provideMerge([DateTimes.Default, RandomValues.Default]),
66
- );
67
- }
68
-
69
- export const cuid: Effect.Effect<Cuid, never, CuidState> = Effect.flatMap(
70
- CuidState.next,
71
- cuidFromSeed,
72
- );
73
-
74
- // Utilities
75
- const ALPHABET = Array.from({ length: 26 }, (_, i) => String.fromCharCode(i + 97));
76
- const encoder = new TextEncoder();
77
-
78
- function createEntropy(length: number, random: Uint8Array): string {
79
- let entropy = "";
80
- let offset = 0;
81
-
82
- while (entropy.length < length) {
83
- const value = random[offset];
84
- entropy += Math.floor(value % 36).toString(36);
85
- offset = (offset + 1) % random.length;
86
- }
87
-
88
- return entropy;
89
- }
90
-
91
- function hash(input: string): Effect.Effect<string> {
92
- return Effect.map(sha512(encoder.encode(input)), (buffer) => {
93
- const view = new Uint8Array(buffer);
94
- let value = 0n;
95
- for (const byte of view) {
96
- value = (value << 8n) + BigInt(byte);
97
- }
98
- // Drop the first character because it will bias the histogram to the left
99
- return value.toString(36).slice(1);
100
- });
101
- }
102
-
103
- function cuidFromSeed({ counter, fingerprint, random, timestamp }: CuidSeed): Effect.Effect<Cuid> {
104
- return Effect.gen(function* () {
105
- // First letter is always a random lowercase letter from the seed
106
- const firstLetter = ALPHABET[random[0] % ALPHABET.length];
107
-
108
- // Convert components to base36
109
- const time = timestamp.toString(36);
110
- const count = counter.toString(36);
111
-
112
- // Create entropy from remaining random bytes
113
- const salt = createEntropy(4, random.slice(1));
114
-
115
- // Hash all components together
116
- const hashInput = `${time}${salt}${count}${fingerprint}`;
117
- const hashed = yield* hash(hashInput);
118
-
119
- // Construct the final CUID
120
- const id = `${firstLetter}${hashed.substring(0, DEFAULT_LENGTH - 1)}`;
121
-
122
- return Cuid.makeUnsafe(id);
123
- });
124
- }
package/src/DateTimes.ts DELETED
@@ -1,36 +0,0 @@
1
- import * as Clock from "effect/Clock";
2
- import * as Effect from "effect/Effect";
3
- import * as Layer from "effect/Layer";
4
- import * as ServiceMap from "effect/ServiceMap";
5
-
6
- export class DateTimes extends ServiceMap.Service<DateTimes>()("@typed/id/DateTimes", {
7
- make: Effect.succeed({
8
- now: Effect.sync(() => Date.now()),
9
- date: Effect.sync(() => new Date()),
10
- }),
11
- }) {
12
- static readonly now = Effect.flatMap(DateTimes.asEffect(), ({ now }) => now);
13
- static readonly date = Effect.flatMap(DateTimes.asEffect(), ({ date }) => date);
14
-
15
- static readonly Default = Layer.effect(DateTimes, DateTimes.make);
16
-
17
- static readonly Fixed = (baseDate: number | string | Date) =>
18
- Layer.effect(
19
- DateTimes,
20
- Effect.gen(function* () {
21
- const clock = yield* Clock.Clock;
22
- const base = new Date(baseDate);
23
- const baseN = BigInt(base.getTime());
24
- const startMillis = yield* clock.currentTimeMillis;
25
- const now = clock.currentTimeMillis.pipe(
26
- Effect.map((millis) =>
27
- // Use BigInt to avoid floating point precision issues which can break deterministic testing
28
- Number(baseN + BigInt(millis) - BigInt(startMillis)),
29
- ),
30
- );
31
- const date = now.pipe(Effect.map((millis) => new Date(millis)));
32
-
33
- return DateTimes.of({ now, date });
34
- }),
35
- );
36
- }