@typed/id 1.0.0-beta.3 → 1.0.0-beta.5

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 (58) hide show
  1. package/README.md +171 -65
  2. package/dist/Cuid.d.ts +128 -13
  3. package/dist/Cuid.d.ts.map +1 -1
  4. package/dist/Cuid.js +135 -41
  5. package/dist/DateTimes.d.ts +66 -3
  6. package/dist/DateTimes.d.ts.map +1 -1
  7. package/dist/DateTimes.js +72 -5
  8. package/dist/Ids.d.ts +173 -30
  9. package/dist/Ids.d.ts.map +1 -1
  10. package/dist/Ids.js +162 -19
  11. package/dist/Ksuid.d.ts +51 -1
  12. package/dist/Ksuid.d.ts.map +1 -1
  13. package/dist/Ksuid.js +59 -7
  14. package/dist/NanoId.d.ts +47 -0
  15. package/dist/NanoId.d.ts.map +1 -1
  16. package/dist/NanoId.js +48 -1
  17. package/dist/RandomValues.d.ts +63 -4
  18. package/dist/RandomValues.d.ts.map +1 -1
  19. package/dist/RandomValues.js +74 -10
  20. package/dist/Ulid.d.ts +49 -1
  21. package/dist/Ulid.d.ts.map +1 -1
  22. package/dist/Ulid.js +54 -4
  23. package/dist/Uuid4.d.ts +47 -0
  24. package/dist/Uuid4.d.ts.map +1 -1
  25. package/dist/Uuid4.js +48 -1
  26. package/dist/Uuid5.d.ts +178 -6
  27. package/dist/Uuid5.d.ts.map +1 -1
  28. package/dist/Uuid5.js +152 -15
  29. package/dist/Uuid7.d.ts +121 -15
  30. package/dist/Uuid7.d.ts.map +1 -1
  31. package/dist/Uuid7.js +124 -16
  32. package/dist/__tests__/helpers.d.ts +7 -0
  33. package/dist/__tests__/helpers.d.ts.map +1 -0
  34. package/dist/__tests__/helpers.js +19 -0
  35. package/dist/__tests__/public-contract.type-test.d.ts +2 -0
  36. package/dist/__tests__/public-contract.type-test.d.ts.map +1 -0
  37. package/dist/__tests__/public-contract.type-test.js +3 -0
  38. package/dist/_sha.d.ts +32 -0
  39. package/dist/_sha.d.ts.map +1 -1
  40. package/dist/_sha.js +32 -0
  41. package/dist/_uuid-stringify.d.ts +15 -0
  42. package/dist/_uuid-stringify.d.ts.map +1 -1
  43. package/dist/_uuid-stringify.js +15 -0
  44. package/package.json +65 -11
  45. package/src/Cuid.ts +0 -124
  46. package/src/DateTimes.ts +0 -36
  47. package/src/Id.test.ts +0 -230
  48. package/src/Ids.ts +0 -128
  49. package/src/Ksuid.ts +0 -74
  50. package/src/NanoId.ts +0 -33
  51. package/src/RandomValues.ts +0 -33
  52. package/src/Ulid.ts +0 -51
  53. package/src/Uuid4.ts +0 -24
  54. package/src/Uuid5.ts +0 -71
  55. package/src/Uuid7.ts +0 -104
  56. package/src/_sha.ts +0 -11
  57. package/src/_uuid-stringify.ts +0 -30
  58. package/src/index.ts +0 -10
package/dist/Cuid.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as Effect from "effect/Effect";
2
2
  import * as Layer from "effect/Layer";
3
3
  import * as Schema from "effect/Schema";
4
- import * as ServiceMap from "effect/ServiceMap";
4
+ import * as Context from "effect/Context";
5
5
  import { sha512 } from "./_sha.js";
6
6
  import { DateTimes } from "./DateTimes.js";
7
7
  import { RandomValues } from "./RandomValues.js";
@@ -9,10 +9,57 @@ import { RandomValues } from "./RandomValues.js";
9
9
  const DEFAULT_LENGTH = 24;
10
10
  const BIG_LENGTH = 32;
11
11
  const INITIAL_COUNT_MAX = 476782367;
12
- // Schema
13
- export const Cuid = Schema.String.pipe(Schema.check(Schema.isPattern(/^[a-z][0-9a-z]+$/)), Schema.brand("@typed/id/CUID"));
12
+ /**
13
+ * Effect Schema and branded string type for 24-character CUID values.
14
+ * @remarks
15
+ * ## Why
16
+ * The schema validates transport or persisted strings before restoring the compile-time brand; generating a new client ID is not hydration identity.
17
+ * ## Ownership and lifetime
18
+ * This module-level schema value acquires no resources and is shared; no runtime freezing guarantee is implied.
19
+ * @example
20
+ * ```ts
21
+ * import { Cuid } from "@typed/id/Cuid"
22
+ * const id = Cuid.make("a00000000000000000000000")
23
+ * ```
24
+ * See [Effect Schema](https://effect.website/docs/schema/introduction/).
25
+ * @category Schemas
26
+ * @since 1.0.0
27
+ */
28
+ export const Cuid = Schema.String.pipe(Schema.check(Schema.isPattern(/^[a-z][0-9a-z]{23}$/)), Schema.brand("@typed/id/CUID"));
29
+ /**
30
+ * Tests whether a string is a valid branded CUID.
31
+ * @remarks
32
+ * ## Why
33
+ * Runtime refinement restores trust after JSON, structured clone, or other transport has erased the TypeScript brand.
34
+ * ## Ownership and lifetime
35
+ * This pure predicate acquires no resources and retains no input.
36
+ * @example
37
+ * ```ts
38
+ * import { isCuid } from "@typed/id/Cuid"
39
+ * isCuid("a00000000000000000000000")
40
+ * ```
41
+ * @category Refinements
42
+ * @since 1.0.0
43
+ */
14
44
  export const isCuid = Schema.is(Cuid);
15
- export class CuidState extends ServiceMap.Service()("@typed/id/CuidState", {
45
+ /**
46
+ * Process-local Effect service that supplies sequential CUID seeds.
47
+ * @remarks
48
+ * ## Why
49
+ * Counter state and caller-provided `envData` live in an explicit service so tests and applications choose the sharing boundary instead of relying on hidden globals.
50
+ * ## Ownership and lifetime
51
+ * Each Layer instance owns its mutable counter and captured fingerprint. A new process, worker, or Layer resets that sequence; exact SSR identity must be serialized and reused.
52
+ * @example
53
+ * ```ts
54
+ * import { cuid, CuidState } from "@typed/id/Cuid"
55
+ * import { Effect } from "effect"
56
+ * const program = Effect.provide(cuid, CuidState.Default)
57
+ * ```
58
+ * See [Effect services](https://effect.website/docs/requirements-management/services/) and [Layers](https://effect.website/docs/requirements-management/layers/).
59
+ * @category Services
60
+ * @since 1.0.0
61
+ */
62
+ export class CuidState extends Context.Service()("@typed/id/CuidState", {
16
63
  make: (envData) => Effect.gen(function* () {
17
64
  const { now } = yield* DateTimes;
18
65
  const getRandomValues = yield* RandomValues;
@@ -21,38 +68,72 @@ export class CuidState extends ServiceMap.Service()("@typed/id/CuidState", {
21
68
  (initialBytes[1] << 16) |
22
69
  (initialBytes[2] << 8) |
23
70
  initialBytes[3]) % INITIAL_COUNT_MAX;
24
- // Create fingerprint from environment data
71
+ // Derive a stable discriminator from caller-provided environment data
25
72
  const fingerprint = (yield* hash(envData)).substring(0, BIG_LENGTH);
26
73
  let counter = initialValue;
27
- return Effect.gen(function* () {
28
- const timestamp = yield* now;
29
- const random = yield* getRandomValues(32);
30
- return {
31
- timestamp,
32
- counter: counter++,
33
- random,
34
- fingerprint,
35
- };
36
- });
74
+ return {
75
+ next: Effect.gen(function* () {
76
+ const timestamp = yield* now;
77
+ const random = yield* getRandomValues(32);
78
+ return {
79
+ timestamp,
80
+ counter: counter++,
81
+ random,
82
+ fingerprint,
83
+ };
84
+ }),
85
+ };
37
86
  }),
38
87
  }) {
39
- static next = Effect.flatten(CuidState.asEffect());
40
- static Default = Layer.effect(CuidState, CuidState.make("node")).pipe(Layer.provideMerge([DateTimes.Default, RandomValues.Default]));
88
+ /**
89
+ * Reads one seed from the current CuidState service.
90
+ * @remarks
91
+ * ## Why
92
+ * The accessor exposes stateful sequencing through Effect's service channel rather than a module-global counter.
93
+ * ## Ownership and lifetime
94
+ * The Effect requires `CuidState`; its Layer owns the counter for the Layer lifetime.
95
+ * @category Services
96
+ * @since 1.0.0
97
+ */
98
+ static next = Effect.gen(function* () {
99
+ const { next } = yield* CuidState;
100
+ return yield* next;
101
+ });
102
+ /**
103
+ * Provides a default CuidState backed by system time and Web Crypto entropy.
104
+ * @remarks
105
+ * ## Why
106
+ * The explicit Layer gives production code a standard service while keeping test and environment-specific alternatives replaceable.
107
+ * ## Ownership and lifetime
108
+ * Layer acquisition creates one counter state; the surrounding Layer Scope owns it. Web Crypto must be available in the runtime.
109
+ * @category Layers
110
+ * @since 1.0.0
111
+ */
112
+ static Default = Layer.effect(CuidState, CuidState.make("node")).pipe(Layer.provide([DateTimes.Default, RandomValues.Default]));
41
113
  }
114
+ /**
115
+ * Generates one CUID from the current CuidState service.
116
+ * @remarks
117
+ * ## Why
118
+ * Generation is an Effect so sequencing and entropy dependencies remain explicit and replaceable; `envData` is only a caller discriminator, not a machine fingerprint guarantee. Missing Web Crypto or rejected SHA-512 work is a defect because the typed error channel is `never`.
119
+ * ## Ownership and lifetime
120
+ * The Effect acquires no resources itself and uses the CuidState owned by its provided Layer.
121
+ * @example
122
+ * ```ts
123
+ * import { cuid, CuidState } from "@typed/id/Cuid"
124
+ * import { Effect } from "effect"
125
+ * const id = Effect.provide(cuid, CuidState.Default)
126
+ * ```
127
+ * @category Generators
128
+ * @since 1.0.0
129
+ */
42
130
  export const cuid = Effect.flatMap(CuidState.next, cuidFromSeed);
43
131
  // Utilities
44
- const ALPHABET = Array.from({ length: 26 }, (_, i) => String.fromCharCode(i + 97));
132
+ const LETTER_ALPHABET = "abcdefghijklmnopqrstuvwxyz";
133
+ const BODY_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";
134
+ const LETTER_DOMAIN = "@typed/id/cuid/letter";
135
+ const BODY_DOMAIN = "@typed/id/cuid/body";
45
136
  const encoder = new TextEncoder();
46
- function createEntropy(length, random) {
47
- let entropy = "";
48
- let offset = 0;
49
- while (entropy.length < length) {
50
- const value = random[offset];
51
- entropy += Math.floor(value % 36).toString(36);
52
- offset = (offset + 1) % random.length;
53
- }
54
- return entropy;
55
- }
56
137
  function hash(input) {
57
138
  return Effect.map(sha512(encoder.encode(input)), (buffer) => {
58
139
  const view = new Uint8Array(buffer);
@@ -64,20 +145,33 @@ function hash(input) {
64
145
  return value.toString(36).slice(1);
65
146
  });
66
147
  }
148
+ function sample(domain, alphabet, length, canonicalInput) {
149
+ return Effect.gen(function* () {
150
+ const limit = Math.floor(256 / alphabet.length) * alphabet.length;
151
+ let value = "";
152
+ for (let block = 0; value.length < length; block++) {
153
+ const prefix = encoder.encode(`${domain}\0${block.toString(10)}\0`);
154
+ const input = new Uint8Array(prefix.length + canonicalInput.length);
155
+ input.set(prefix);
156
+ input.set(canonicalInput, prefix.length);
157
+ const digest = new Uint8Array(yield* sha512(input));
158
+ for (const byte of digest) {
159
+ if (byte >= limit)
160
+ continue;
161
+ value += alphabet[byte % alphabet.length];
162
+ if (value.length === length)
163
+ break;
164
+ }
165
+ }
166
+ return value;
167
+ });
168
+ }
67
169
  function cuidFromSeed({ counter, fingerprint, random, timestamp }) {
68
170
  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* hash(hashInput);
79
- // Construct the final CUID
80
- const id = `${firstLetter}${hashed.substring(0, DEFAULT_LENGTH - 1)}`;
81
- return Cuid.makeUnsafe(id);
171
+ const randomHex = Array.from(random, (byte) => byte.toString(16).padStart(2, "0")).join("");
172
+ const canonicalInput = encoder.encode([timestamp.toString(36), counter.toString(36), fingerprint, randomHex].join("\0"));
173
+ const firstLetter = yield* sample(LETTER_DOMAIN, LETTER_ALPHABET, 1, canonicalInput);
174
+ const body = yield* sample(BODY_DOMAIN, BODY_ALPHABET, DEFAULT_LENGTH - 1, canonicalInput);
175
+ return Cuid.make(firstLetter + body);
82
176
  });
83
177
  }
@@ -1,7 +1,8 @@
1
+ import * as Cause from "effect/Cause";
1
2
  import * as Effect from "effect/Effect";
2
3
  import * as Layer from "effect/Layer";
3
- import * as ServiceMap from "effect/ServiceMap";
4
- declare const DateTimes_base: ServiceMap.ServiceClass<DateTimes, "@typed/id/DateTimes", {
4
+ import * as Context from "effect/Context";
5
+ declare const DateTimes_base: Context.ServiceClass<DateTimes, "@typed/id/DateTimes", {
5
6
  now: Effect.Effect<number, never, never>;
6
7
  date: Effect.Effect<Date, never, never>;
7
8
  }> & {
@@ -10,11 +11,73 @@ declare const DateTimes_base: ServiceMap.ServiceClass<DateTimes, "@typed/id/Date
10
11
  date: Effect.Effect<Date, never, never>;
11
12
  }, never, never>;
12
13
  };
14
+ /**
15
+ * Effect service for current epoch milliseconds and Date values.
16
+ * @remarks
17
+ * ## Why
18
+ * Making time a service keeps time-based ID generators deterministic under tests and explicit under server or browser runtimes.
19
+ * ## Ownership and lifetime
20
+ * A DateTimes Layer owns its time source for the Layer lifetime; individual reads acquire no resources.
21
+ * @example
22
+ * ```ts
23
+ * import { DateTimes } from "@typed/id/DateTimes"
24
+ * import { Effect } from "effect"
25
+ * const now = Effect.provide(DateTimes.now, DateTimes.Default)
26
+ * ```
27
+ * See [Effect Clock](https://effect.website/docs/testing/testclock/).
28
+ * @category Services
29
+ * @since 1.0.0
30
+ */
13
31
  export declare class DateTimes extends DateTimes_base {
32
+ /**
33
+ * Reads epoch milliseconds from the current DateTimes service.
34
+ * @remarks
35
+ * ## Why
36
+ * A service-backed read avoids hard-wiring `Date.now` into generators and tests.
37
+ * ## Ownership and lifetime
38
+ * This Effect acquires no resources and uses the DateTimes service for one invocation.
39
+ * @category Services
40
+ * @since 1.0.0
41
+ */
14
42
  static readonly now: Effect.Effect<number, never, DateTimes>;
43
+ /**
44
+ * Reads a Date from the current DateTimes service.
45
+ * @remarks
46
+ * ## Why
47
+ * The Date view stays aligned with the same replaceable time source as epoch-millisecond reads.
48
+ * ## Ownership and lifetime
49
+ * This Effect acquires no resources and uses the DateTimes service for one invocation.
50
+ * @category Services
51
+ * @since 1.0.0
52
+ */
15
53
  static readonly date: Effect.Effect<Date, never, DateTimes>;
54
+ /**
55
+ * Provides DateTimes using the runtime wall clock.
56
+ * @remarks
57
+ * ## Why
58
+ * The production default is explicit and replaceable rather than hidden in each generator.
59
+ * ## Ownership and lifetime
60
+ * The surrounding Layer Scope owns the service; reads allocate only their returned Date values.
61
+ * @category Layers
62
+ * @since 1.0.0
63
+ */
16
64
  static readonly Default: Layer.Layer<DateTimes, never, never>;
17
- static readonly Fixed: (baseDate: number | string | Date) => Layer.Layer<DateTimes, never, never>;
65
+ /**
66
+ * Provides DateTimes anchored to a base date and advanced by Effect Clock.
67
+ * @remarks
68
+ * ## Why
69
+ * Clock-relative time supports deterministic tests while still allowing controlled advancement; invalid bases fail with `IllegalArgumentError`.
70
+ * ## Ownership and lifetime
71
+ * Layer acquisition captures the base and current Clock reading; the surrounding Layer Scope owns that state.
72
+ * @example
73
+ * ```ts
74
+ * import { DateTimes } from "@typed/id/DateTimes"
75
+ * const fixed = DateTimes.Fixed("2026-01-01T00:00:00Z")
76
+ * ```
77
+ * @category Layers
78
+ * @since 1.0.0
79
+ */
80
+ static readonly Fixed: (baseDate: number | string | Date) => Layer.Layer<DateTimes, Cause.IllegalArgumentError, never>;
18
81
  }
19
82
  export {};
20
83
  //# sourceMappingURL=DateTimes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"DateTimes.d.ts","sourceRoot":"","sources":["../src/DateTimes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,UAAU,MAAM,mBAAmB,CAAC;;;;;;;;;;AAEhD,qBAAa,SAAU,SAAQ,cAK7B;IACA,MAAM,CAAC,QAAQ,CAAC,GAAG,0CAA0D;IAC7E,MAAM,CAAC,QAAQ,CAAC,IAAI,wCAA4D;IAEhF,MAAM,CAAC,QAAQ,CAAC,OAAO,uCAA2C;IAElE,MAAM,CAAC,QAAQ,CAAC,KAAK,GAAI,UAAU,MAAM,GAAG,MAAM,GAAG,IAAI,0CAkBrD;CACL"}
1
+ {"version":3,"file":"DateTimes.d.ts","sourceRoot":"","sources":["../src/DateTimes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAC;;;;;;;;;;AAE1C;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,SAAU,SAAQ,cAK7B;IACA;;;;;;;;;OASG;IACH,MAAM,CAAC,QAAQ,CAAC,GAAG,0CAA+C;IAClE;;;;;;;;;OASG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,wCAAiD;IAErE;;;;;;;;;OASG;IACH,MAAM,CAAC,QAAQ,CAAC,OAAO,uCAA2C;IAElE;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,QAAQ,CAAC,KAAK,aAAc,MAAM,GAAG,MAAM,GAAG,IAAI,+DAsBrD;CACL"}
package/dist/DateTimes.js CHANGED
@@ -1,20 +1,87 @@
1
1
  import * as Clock from "effect/Clock";
2
+ import * as Cause from "effect/Cause";
2
3
  import * as Effect from "effect/Effect";
3
4
  import * as Layer from "effect/Layer";
4
- import * as ServiceMap from "effect/ServiceMap";
5
- export class DateTimes extends ServiceMap.Service()("@typed/id/DateTimes", {
5
+ import * as Context from "effect/Context";
6
+ /**
7
+ * Effect service for current epoch milliseconds and Date values.
8
+ * @remarks
9
+ * ## Why
10
+ * Making time a service keeps time-based ID generators deterministic under tests and explicit under server or browser runtimes.
11
+ * ## Ownership and lifetime
12
+ * A DateTimes Layer owns its time source for the Layer lifetime; individual reads acquire no resources.
13
+ * @example
14
+ * ```ts
15
+ * import { DateTimes } from "@typed/id/DateTimes"
16
+ * import { Effect } from "effect"
17
+ * const now = Effect.provide(DateTimes.now, DateTimes.Default)
18
+ * ```
19
+ * See [Effect Clock](https://effect.website/docs/testing/testclock/).
20
+ * @category Services
21
+ * @since 1.0.0
22
+ */
23
+ export class DateTimes extends Context.Service()("@typed/id/DateTimes", {
6
24
  make: Effect.succeed({
7
25
  now: Effect.sync(() => Date.now()),
8
26
  date: Effect.sync(() => new Date()),
9
27
  }),
10
28
  }) {
11
- static now = Effect.flatMap(DateTimes.asEffect(), ({ now }) => now);
12
- static date = Effect.flatMap(DateTimes.asEffect(), ({ date }) => date);
29
+ /**
30
+ * Reads epoch milliseconds from the current DateTimes service.
31
+ * @remarks
32
+ * ## Why
33
+ * A service-backed read avoids hard-wiring `Date.now` into generators and tests.
34
+ * ## Ownership and lifetime
35
+ * This Effect acquires no resources and uses the DateTimes service for one invocation.
36
+ * @category Services
37
+ * @since 1.0.0
38
+ */
39
+ static now = Effect.flatMap(DateTimes, ({ now }) => now);
40
+ /**
41
+ * Reads a Date from the current DateTimes service.
42
+ * @remarks
43
+ * ## Why
44
+ * The Date view stays aligned with the same replaceable time source as epoch-millisecond reads.
45
+ * ## Ownership and lifetime
46
+ * This Effect acquires no resources and uses the DateTimes service for one invocation.
47
+ * @category Services
48
+ * @since 1.0.0
49
+ */
50
+ static date = Effect.flatMap(DateTimes, ({ date }) => date);
51
+ /**
52
+ * Provides DateTimes using the runtime wall clock.
53
+ * @remarks
54
+ * ## Why
55
+ * The production default is explicit and replaceable rather than hidden in each generator.
56
+ * ## Ownership and lifetime
57
+ * The surrounding Layer Scope owns the service; reads allocate only their returned Date values.
58
+ * @category Layers
59
+ * @since 1.0.0
60
+ */
13
61
  static Default = Layer.effect(DateTimes, DateTimes.make);
62
+ /**
63
+ * Provides DateTimes anchored to a base date and advanced by Effect Clock.
64
+ * @remarks
65
+ * ## Why
66
+ * Clock-relative time supports deterministic tests while still allowing controlled advancement; invalid bases fail with `IllegalArgumentError`.
67
+ * ## Ownership and lifetime
68
+ * Layer acquisition captures the base and current Clock reading; the surrounding Layer Scope owns that state.
69
+ * @example
70
+ * ```ts
71
+ * import { DateTimes } from "@typed/id/DateTimes"
72
+ * const fixed = DateTimes.Fixed("2026-01-01T00:00:00Z")
73
+ * ```
74
+ * @category Layers
75
+ * @since 1.0.0
76
+ */
14
77
  static Fixed = (baseDate) => Layer.effect(DateTimes, Effect.gen(function* () {
15
78
  const clock = yield* Clock.Clock;
16
79
  const base = new Date(baseDate);
17
- const baseN = BigInt(base.getTime());
80
+ const baseMillis = base.getTime();
81
+ if (!Number.isFinite(baseMillis)) {
82
+ return yield* new Cause.IllegalArgumentError(`Invalid base date: ${String(baseDate)}`);
83
+ }
84
+ const baseN = BigInt(baseMillis);
18
85
  const startMillis = yield* clock.currentTimeMillis;
19
86
  const now = clock.currentTimeMillis.pipe(Effect.map((millis) =>
20
87
  // Use BigInt to avoid floating point precision issues which can break deterministic testing
package/dist/Ids.d.ts CHANGED
@@ -1,6 +1,7 @@
1
+ import * as Cause from "effect/Cause";
1
2
  import * as Effect from "effect/Effect";
2
3
  import * as Layer from "effect/Layer";
3
- import * as ServiceMap from "effect/ServiceMap";
4
+ import * as Context from "effect/Context";
4
5
  import type { Cuid } from "./Cuid.js";
5
6
  import { CuidState } from "./Cuid.js";
6
7
  import { DateTimes } from "./DateTimes.js";
@@ -13,59 +14,201 @@ import { type Uuid5, Uuid5Namespace } from "./Uuid5.js";
13
14
  import type { Uuid7 } from "./Uuid7.js";
14
15
  import { Uuid7State } from "./Uuid7.js";
15
16
  import { TestClock } from "effect/testing";
16
- declare const Ids_base: ServiceMap.ServiceClass<Ids, "@typed/id/Ids", {
17
+ declare const Ids_base: Context.ServiceClass<Ids, "@typed/id/Ids", {
17
18
  cuid: Effect.Effect<string & import("effect/Brand").Brand<"@typed/id/CUID">, never, never>;
18
- ksuid: Effect.Effect<string & import("effect/Brand").Brand<"@typed/id/KSUID">, never, never>;
19
+ ksuid: Effect.Effect<string & import("effect/Brand").Brand<"@typed/id/KSUID">, Cause.IllegalArgumentError, never>;
19
20
  nanoId: Effect.Effect<string & import("effect/Brand").Brand<"@typed/id/NanoId">, never, never>;
20
- ulid: Effect.Effect<string & import("effect/Brand").Brand<"@typed/id/ULID">, never, never>;
21
+ ulid: Effect.Effect<string & import("effect/Brand").Brand<"@typed/id/ULID">, Cause.IllegalArgumentError, never>;
21
22
  uuid4: Effect.Effect<string & import("effect/Brand").Brand<"@typed/id/UUID4">, never, never>;
22
23
  uuid5: {
23
- (namespace: Uuid5Namespace): (name: string) => Effect.Effect<Uuid5>;
24
- (name: string, namespace: Uuid5Namespace): Effect.Effect<Uuid5>;
25
- readonly dns: (name: string) => Effect.Effect<Uuid5>;
26
- readonly url: (name: string) => Effect.Effect<Uuid5>;
27
- readonly oid: (name: string) => Effect.Effect<Uuid5>;
28
- readonly x500: (name: string) => Effect.Effect<Uuid5>;
24
+ (namespace: Uuid5Namespace): (name: string) => Effect.Effect<Uuid5, Cause.IllegalArgumentError>;
25
+ (name: string, namespace: Uuid5Namespace): Effect.Effect<Uuid5, Cause.IllegalArgumentError>;
26
+ readonly dns: (name: string) => Effect.Effect<Uuid5, Cause.IllegalArgumentError>;
27
+ readonly url: (name: string) => Effect.Effect<Uuid5, Cause.IllegalArgumentError>;
28
+ readonly oid: (name: string) => Effect.Effect<Uuid5, Cause.IllegalArgumentError>;
29
+ readonly x500: (name: string) => Effect.Effect<Uuid5, Cause.IllegalArgumentError>;
29
30
  };
30
- uuid7: Effect.Effect<string & import("effect/Brand").Brand<"@typed/id/UUID7">, never, never>;
31
+ uuid7: Effect.Effect<string & import("effect/Brand").Brand<"@typed/id/UUID7">, Cause.IllegalArgumentError, never>;
31
32
  }> & {
32
33
  readonly make: Effect.Effect<{
33
34
  cuid: Effect.Effect<string & import("effect/Brand").Brand<"@typed/id/CUID">, never, never>;
34
- ksuid: Effect.Effect<string & import("effect/Brand").Brand<"@typed/id/KSUID">, never, never>;
35
+ ksuid: Effect.Effect<string & import("effect/Brand").Brand<"@typed/id/KSUID">, Cause.IllegalArgumentError, never>;
35
36
  nanoId: Effect.Effect<string & import("effect/Brand").Brand<"@typed/id/NanoId">, never, never>;
36
- ulid: Effect.Effect<string & import("effect/Brand").Brand<"@typed/id/ULID">, never, never>;
37
+ ulid: Effect.Effect<string & import("effect/Brand").Brand<"@typed/id/ULID">, Cause.IllegalArgumentError, never>;
37
38
  uuid4: Effect.Effect<string & import("effect/Brand").Brand<"@typed/id/UUID4">, never, never>;
38
39
  uuid5: {
39
- (namespace: Uuid5Namespace): (name: string) => Effect.Effect<Uuid5>;
40
- (name: string, namespace: Uuid5Namespace): Effect.Effect<Uuid5>;
41
- readonly dns: (name: string) => Effect.Effect<Uuid5>;
42
- readonly url: (name: string) => Effect.Effect<Uuid5>;
43
- readonly oid: (name: string) => Effect.Effect<Uuid5>;
44
- readonly x500: (name: string) => Effect.Effect<Uuid5>;
40
+ (namespace: Uuid5Namespace): (name: string) => Effect.Effect<Uuid5, Cause.IllegalArgumentError>;
41
+ (name: string, namespace: Uuid5Namespace): Effect.Effect<Uuid5, Cause.IllegalArgumentError>;
42
+ readonly dns: (name: string) => Effect.Effect<Uuid5, Cause.IllegalArgumentError>;
43
+ readonly url: (name: string) => Effect.Effect<Uuid5, Cause.IllegalArgumentError>;
44
+ readonly oid: (name: string) => Effect.Effect<Uuid5, Cause.IllegalArgumentError>;
45
+ readonly x500: (name: string) => Effect.Effect<Uuid5, Cause.IllegalArgumentError>;
45
46
  };
46
- uuid7: Effect.Effect<string & import("effect/Brand").Brand<"@typed/id/UUID7">, never, never>;
47
+ uuid7: Effect.Effect<string & import("effect/Brand").Brand<"@typed/id/UUID7">, Cause.IllegalArgumentError, never>;
47
48
  }, never, CuidState | DateTimes | RandomValues | Uuid7State>;
48
49
  };
50
+ /**
51
+ * Unified Effect service for every Typed ID generator.
52
+ * @remarks
53
+ * ## Why
54
+ * One facade captures time, entropy, and state services once, so application code composes generators through a single explicit dependency without hiding their behavior.
55
+ * ## Ownership and lifetime
56
+ * Each Ids Layer owns its captured services plus lazy CUID and UUIDv7 state. Recreating the Layer resets process-local sequences; serialize server IDs when identity must survive hydration.
57
+ * @example
58
+ * ```ts
59
+ * import { Ids } from "@typed/id/Ids"
60
+ * import { Effect } from "effect"
61
+ * const program = Effect.gen(function* () { return yield* Ids.uuid7 }).pipe(Effect.provide(Ids.Default))
62
+ * ```
63
+ * See [Effect services](https://effect.website/docs/requirements-management/services/) and [Layers](https://effect.website/docs/requirements-management/layers/).
64
+ * @category Services
65
+ * @since 1.0.0
66
+ */
49
67
  export declare class Ids extends Ids_base {
68
+ /**
69
+ * Generates a CUID using the current Ids service.
70
+ * @remarks
71
+ * ## Why
72
+ * The facade shares the Ids-owned CuidState instead of allocating a new sequence for every call.
73
+ * ## Ownership and lifetime
74
+ * This Effect acquires no resources and uses state owned by the provided Ids Layer.
75
+ * @category Generators
76
+ * @since 1.0.0
77
+ */
50
78
  static readonly cuid: Effect.Effect<Cuid, never, Ids>;
51
- static readonly ksuid: Effect.Effect<Ksuid, never, Ids>;
79
+ /**
80
+ * Generates a KSUID using the current Ids service.
81
+ * @remarks
82
+ * ## Why
83
+ * The facade reuses captured time and entropy while retaining `IllegalArgumentError` for invalid KSUID timestamps.
84
+ * ## Ownership and lifetime
85
+ * This Effect acquires no persistent resource and uses services owned by the provided Ids Layer.
86
+ * @category Generators
87
+ * @since 1.0.0
88
+ */
89
+ static readonly ksuid: Effect.Effect<Ksuid, Cause.IllegalArgumentError, Ids>;
90
+ /**
91
+ * Generates a NanoId using the current Ids service.
92
+ * @remarks
93
+ * ## Why
94
+ * The facade makes the selected entropy implementation available through one application dependency.
95
+ * ## Ownership and lifetime
96
+ * This Effect acquires no persistent resource and uses entropy owned by the provided Ids Layer.
97
+ * @category Generators
98
+ * @since 1.0.0
99
+ */
52
100
  static readonly nanoId: Effect.Effect<NanoId, never, Ids>;
53
- static readonly ulid: Effect.Effect<Ulid, never, Ids>;
101
+ /**
102
+ * Generates a ULID using the current Ids service.
103
+ * @remarks
104
+ * ## Why
105
+ * The facade reuses captured time and entropy while retaining `IllegalArgumentError` for invalid 48-bit timestamps.
106
+ * ## Ownership and lifetime
107
+ * This Effect acquires no persistent resource and uses services owned by the provided Ids Layer.
108
+ * @category Generators
109
+ * @since 1.0.0
110
+ */
111
+ static readonly ulid: Effect.Effect<Ulid, Cause.IllegalArgumentError, Ids>;
112
+ /**
113
+ * Generates a random UUID version 4 using the current Ids service.
114
+ * @remarks
115
+ * ## Why
116
+ * The facade keeps entropy selection replaceable while preserving UUID version and variant semantics.
117
+ * ## Ownership and lifetime
118
+ * This Effect acquires no persistent resource and uses entropy owned by the provided Ids Layer.
119
+ * @category Generators
120
+ * @since 1.0.0
121
+ */
54
122
  static readonly uuid4: Effect.Effect<Uuid4, never, Ids>;
123
+ /**
124
+ * Derives deterministic UUID version 5 values through the current Ids service, with DNS, URL, OID, and X.500 helpers.
125
+ * @remarks
126
+ * ## Why
127
+ * The facade supports data-first and namespace-first calls while keeping namespace choice and `IllegalArgumentError` explicit.
128
+ * ## Ownership and lifetime
129
+ * Each Effect acquires no persistent resources and reads the Ids service for one invocation; helper namespaces are stable captured copies.
130
+ * @example
131
+ * ```ts
132
+ * import { Ids } from "@typed/id/Ids"
133
+ * import { Effect } from "effect"
134
+ * const program = Ids.uuid5.dns("example.com").pipe(Effect.provide(Ids.Default))
135
+ * ```
136
+ * @category Generators
137
+ * @since 1.0.0
138
+ */
55
139
  static readonly uuid5: {
56
- (namespace: Uuid5Namespace): (name: string) => Effect.Effect<Uuid5, never, Ids>;
57
- (name: string, namespace: Uuid5Namespace): Effect.Effect<Uuid5, never, Ids>;
58
- readonly dns: (name: string) => Effect.Effect<Uuid5, never, Ids>;
59
- readonly url: (name: string) => Effect.Effect<Uuid5, never, Ids>;
60
- readonly oid: (name: string) => Effect.Effect<Uuid5, never, Ids>;
61
- readonly x500: (name: string) => Effect.Effect<Uuid5, never, Ids>;
140
+ /** Binds a namespace first, then derives UUIDv5 values for names. @since 1.0.0 */
141
+ (namespace: Uuid5Namespace): (name: string) => Effect.Effect<Uuid5, Cause.IllegalArgumentError, Ids>;
142
+ /** Derives a UUIDv5 from a name and namespace through the current Ids service. @since 1.0.0 */
143
+ (name: string, namespace: Uuid5Namespace): Effect.Effect<Uuid5, Cause.IllegalArgumentError, Ids>;
144
+ /** Derives a UUIDv5 in the standard DNS namespace. @since 1.0.0 */
145
+ readonly dns: (name: string) => Effect.Effect<Uuid5, Cause.IllegalArgumentError, Ids>;
146
+ /** Derives a UUIDv5 in the standard URL namespace. @since 1.0.0 */
147
+ readonly url: (name: string) => Effect.Effect<Uuid5, Cause.IllegalArgumentError, Ids>;
148
+ /** Derives a UUIDv5 in the standard OID namespace. @since 1.0.0 */
149
+ readonly oid: (name: string) => Effect.Effect<Uuid5, Cause.IllegalArgumentError, Ids>;
150
+ /** Derives a UUIDv5 in the standard X.500 namespace. @since 1.0.0 */
151
+ readonly x500: (name: string) => Effect.Effect<Uuid5, Cause.IllegalArgumentError, Ids>;
62
152
  };
63
- static readonly uuid7: Effect.Effect<Uuid7, never, Ids>;
153
+ /**
154
+ * Generates a UUID version 7 using the current Ids service.
155
+ * @remarks
156
+ * ## Why
157
+ * The facade shares one lazy Uuid7State per Ids Layer, preserving local monotonicity and typed timestamp errors.
158
+ * ## Ownership and lifetime
159
+ * This Effect acquires no resources and uses sequence state owned by the provided Ids Layer.
160
+ * @category Generators
161
+ * @since 1.0.0
162
+ */
163
+ static readonly uuid7: Effect.Effect<Uuid7, Cause.IllegalArgumentError, Ids>;
164
+ /**
165
+ * Provides production Ids, DateTimes, and RandomValues services.
166
+ * @remarks
167
+ * ## Why
168
+ * The standard Layer uses system time and Web Crypto while lazily creating sequence state only when CUID or UUIDv7 is first requested.
169
+ * ## Ownership and lifetime
170
+ * The surrounding Layer Scope owns captured services and lazy sequence state; Web Crypto must exist in the runtime.
171
+ * @category Layers
172
+ * @since 1.0.0
173
+ */
64
174
  static readonly Default: Layer.Layer<Ids | DateTimes | RandomValues, never, never>;
65
- static readonly Test: (options?: TestOptions) => Layer.Layer<Ids | DateTimes | RandomValues | TestClock.TestClock>;
175
+ /**
176
+ * Provides deterministic Ids, time, entropy, and TestClock services.
177
+ * @remarks
178
+ * ## Why
179
+ * Reproducible generators make exact sequences testable; the deterministic entropy is for tests and simulations, not security.
180
+ * ## Ownership and lifetime
181
+ * Each Layer construction owns an independent clock, random sequence, and lazy CUID/UUIDv7 state for its Scope.
182
+ * @example
183
+ * ```ts
184
+ * import { Ids } from "@typed/id/Ids"
185
+ * import { Effect } from "effect"
186
+ * const deterministic = Ids.uuid7.pipe(Effect.provide(Ids.Test({ currentTime: 0 })))
187
+ * ```
188
+ * @category Testing
189
+ * @since 1.0.0
190
+ */
191
+ static readonly Test: (options?: TestOptions) => Layer.Layer<Ids | DateTimes | RandomValues | TestClock.TestClock, Cause.IllegalArgumentError>;
66
192
  }
193
+ /**
194
+ * Configuration for the deterministic Ids test Layer.
195
+ * @remarks
196
+ * ## Why
197
+ * Explicit initial time and CUID environment data let tests reproduce generator sequences without ambient process state.
198
+ * ## Ownership and lifetime
199
+ * This plain configuration acquires no resources and is read only by Layer acquisition; TypeScript `readonly` does not freeze it at runtime.
200
+ * @example
201
+ * ```ts
202
+ * import type { TestOptions } from "@typed/id/Ids"
203
+ * const options: TestOptions = { currentTime: "2026-01-01T00:00:00Z", envData: "test-worker" }
204
+ * ```
205
+ * @category Testing
206
+ * @since 1.0.0
207
+ */
67
208
  export type TestOptions = {
209
+ /** Base time passed to `DateTimes.Fixed`; invalid dates fail Layer acquisition. @since 1.0.0 */
68
210
  readonly currentTime?: number | string | Date;
211
+ /** CUID caller discriminator used when the test CuidState is created. @since 1.0.0 */
69
212
  readonly envData?: string;
70
213
  };
71
214
  export {};