@typed/id 1.0.0-beta.4 → 1.0.0-beta.6

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 +127 -12
  3. package/dist/Cuid.d.ts.map +1 -1
  4. package/dist/Cuid.js +133 -39
  5. package/dist/DateTimes.d.ts +64 -1
  6. package/dist/DateTimes.d.ts.map +1 -1
  7. package/dist/DateTimes.js +70 -3
  8. package/dist/Ids.d.ts +171 -28
  9. package/dist/Ids.d.ts.map +1 -1
  10. package/dist/Ids.js +159 -16
  11. package/dist/Ksuid.d.ts +51 -1
  12. package/dist/Ksuid.d.ts.map +1 -1
  13. package/dist/Ksuid.js +58 -6
  14. package/dist/NanoId.d.ts +47 -0
  15. package/dist/NanoId.d.ts.map +1 -1
  16. package/dist/NanoId.js +47 -0
  17. package/dist/RandomValues.d.ts +62 -3
  18. package/dist/RandomValues.d.ts.map +1 -1
  19. package/dist/RandomValues.js +72 -8
  20. package/dist/Ulid.d.ts +49 -1
  21. package/dist/Ulid.d.ts.map +1 -1
  22. package/dist/Ulid.js +53 -3
  23. package/dist/Uuid4.d.ts +47 -0
  24. package/dist/Uuid4.d.ts.map +1 -1
  25. package/dist/Uuid4.js +47 -0
  26. package/dist/Uuid5.d.ts +178 -6
  27. package/dist/Uuid5.d.ts.map +1 -1
  28. package/dist/Uuid5.js +151 -14
  29. package/dist/Uuid7.d.ts +120 -14
  30. package/dist/Uuid7.d.ts.map +1 -1
  31. package/dist/Uuid7.js +121 -13
  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 +68 -14
  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/Ids.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as Effect from "effect/Effect";
2
2
  import { dual } from "effect/Function";
3
3
  import * as Layer from "effect/Layer";
4
+ import * as Random from "effect/Random";
4
5
  import * as Context from "effect/Context";
5
6
  import { cuid, CuidState } from "./Cuid.js";
6
7
  import { DateTimes } from "./DateTimes.js";
@@ -12,6 +13,24 @@ import { uuid4 } from "./Uuid4.js";
12
13
  import { uuid5, Uuid5Namespace } from "./Uuid5.js";
13
14
  import { uuid7, Uuid7State } from "./Uuid7.js";
14
15
  import { TestClock } from "effect/testing";
16
+ const testRandomValues = () => Layer.effect(RandomValues, RandomValues.pipe(Effect.provide(RandomValues.Random), Random.withSeed("@typed/id/Ids.Test")));
17
+ /**
18
+ * Unified Effect service for every Typed ID generator.
19
+ * @remarks
20
+ * ## Why
21
+ * One facade captures time, entropy, and state services once, so application code composes generators through a single explicit dependency without hiding their behavior.
22
+ * ## Ownership and lifetime
23
+ * 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.
24
+ * @example
25
+ * ```ts
26
+ * import { Ids } from "@typed/id/Ids"
27
+ * import { Effect } from "effect"
28
+ * const program = Effect.gen(function* () { return yield* Ids.uuid7 }).pipe(Effect.provide(Ids.Default))
29
+ * ```
30
+ * See [Effect services](https://effect.website/docs/requirements-management/services/) and [Layers](https://effect.website/docs/requirements-management/layers/).
31
+ * @category Services
32
+ * @since 1.0.0
33
+ */
15
34
  export class Ids extends Context.Service()("@typed/id/Ids", {
16
35
  make: Effect.gen(function* () {
17
36
  const services = yield* Effect.context();
@@ -32,21 +51,145 @@ export class Ids extends Context.Service()("@typed/id/Ids", {
32
51
  };
33
52
  }),
34
53
  }) {
35
- static cuid = Effect.flatMap(Ids.asEffect(), ({ cuid }) => cuid);
36
- static ksuid = Effect.flatMap(Ids.asEffect(), ({ ksuid }) => ksuid);
37
- static nanoId = Effect.flatMap(Ids.asEffect(), ({ nanoId }) => nanoId);
38
- static ulid = Effect.flatMap(Ids.asEffect(), ({ ulid }) => ulid);
39
- static uuid4 = Effect.flatMap(Ids.asEffect(), ({ uuid4 }) => uuid4);
40
- static uuid5 = Object.assign(dual(2, (name, namespace) => Effect.flatMap(Ids.asEffect(), ({ uuid5 }) => uuid5(name, namespace))), {
41
- dns: (name) => Effect.flatMap(Ids.asEffect(), ({ uuid5 }) => uuid5.dns(name)),
42
- url: (name) => Effect.flatMap(Ids.asEffect(), ({ uuid5 }) => uuid5.url(name)),
43
- oid: (name) => Effect.flatMap(Ids.asEffect(), ({ uuid5 }) => uuid5.oid(name)),
44
- x500: (name) => Effect.flatMap(Ids.asEffect(), ({ uuid5 }) => uuid5.x500(name)),
54
+ /**
55
+ * Generates a CUID using the current Ids service.
56
+ * @remarks
57
+ * ## Why
58
+ * The facade shares the Ids-owned CuidState instead of allocating a new sequence for every call.
59
+ * ## Ownership and lifetime
60
+ * This Effect acquires no resources and uses state owned by the provided Ids Layer.
61
+ * @category Generators
62
+ * @since 1.0.0
63
+ */
64
+ static cuid = Effect.flatMap(Ids, ({ cuid }) => cuid);
65
+ /**
66
+ * Generates a KSUID using the current Ids service.
67
+ * @remarks
68
+ * ## Why
69
+ * The facade reuses captured time and entropy while retaining `IllegalArgumentError` for invalid KSUID timestamps.
70
+ * ## Ownership and lifetime
71
+ * This Effect acquires no persistent resource and uses services owned by the provided Ids Layer.
72
+ * @category Generators
73
+ * @since 1.0.0
74
+ */
75
+ static ksuid = Effect.flatMap(Ids, ({ ksuid }) => ksuid);
76
+ /**
77
+ * Generates a NanoId using the current Ids service.
78
+ * @remarks
79
+ * ## Why
80
+ * The facade makes the selected entropy implementation available through one application dependency.
81
+ * ## Ownership and lifetime
82
+ * This Effect acquires no persistent resource and uses entropy owned by the provided Ids Layer.
83
+ * @category Generators
84
+ * @since 1.0.0
85
+ */
86
+ static nanoId = Effect.flatMap(Ids, ({ nanoId }) => nanoId);
87
+ /**
88
+ * Generates a ULID using the current Ids service.
89
+ * @remarks
90
+ * ## Why
91
+ * The facade reuses captured time and entropy while retaining `IllegalArgumentError` for invalid 48-bit timestamps.
92
+ * ## Ownership and lifetime
93
+ * This Effect acquires no persistent resource and uses services owned by the provided Ids Layer.
94
+ * @category Generators
95
+ * @since 1.0.0
96
+ */
97
+ static ulid = Effect.flatMap(Ids, ({ ulid }) => ulid);
98
+ /**
99
+ * Generates a random UUID version 4 using the current Ids service.
100
+ * @remarks
101
+ * ## Why
102
+ * The facade keeps entropy selection replaceable while preserving UUID version and variant semantics.
103
+ * ## Ownership and lifetime
104
+ * This Effect acquires no persistent resource and uses entropy owned by the provided Ids Layer.
105
+ * @category Generators
106
+ * @since 1.0.0
107
+ */
108
+ static uuid4 = Effect.flatMap(Ids, ({ uuid4 }) => uuid4);
109
+ /**
110
+ * Derives deterministic UUID version 5 values through the current Ids service, with DNS, URL, OID, and X.500 helpers.
111
+ * @remarks
112
+ * ## Why
113
+ * The facade supports data-first and namespace-first calls while keeping namespace choice and `IllegalArgumentError` explicit.
114
+ * ## Ownership and lifetime
115
+ * Each Effect acquires no persistent resources and reads the Ids service for one invocation; helper namespaces are stable captured copies.
116
+ * @example
117
+ * ```ts
118
+ * import { Ids } from "@typed/id/Ids"
119
+ * import { Effect } from "effect"
120
+ * const program = Ids.uuid5.dns("example.com").pipe(Effect.provide(Ids.Default))
121
+ * ```
122
+ * @category Generators
123
+ * @since 1.0.0
124
+ */
125
+ static uuid5 = Object.assign(dual(2, (name, namespace) => Effect.flatMap(Ids, ({ uuid5 }) => uuid5(name, namespace))), {
126
+ dns: (name) => Effect.flatMap(Ids, ({ uuid5 }) => uuid5.dns(name)),
127
+ url: (name) => Effect.flatMap(Ids, ({ uuid5 }) => uuid5.url(name)),
128
+ oid: (name) => Effect.flatMap(Ids, ({ uuid5 }) => uuid5.oid(name)),
129
+ x500: (name) => Effect.flatMap(Ids, ({ uuid5 }) => uuid5.x500(name)),
130
+ });
131
+ /**
132
+ * Generates a UUID version 7 using the current Ids service.
133
+ * @remarks
134
+ * ## Why
135
+ * The facade shares one lazy Uuid7State per Ids Layer, preserving local monotonicity and typed timestamp errors.
136
+ * ## Ownership and lifetime
137
+ * This Effect acquires no resources and uses sequence state owned by the provided Ids Layer.
138
+ * @category Generators
139
+ * @since 1.0.0
140
+ */
141
+ static uuid7 = Effect.flatMap(Ids, ({ uuid7 }) => uuid7);
142
+ /**
143
+ * Provides production Ids, DateTimes, and RandomValues services.
144
+ * @remarks
145
+ * ## Why
146
+ * The standard Layer uses system time and Web Crypto while lazily creating sequence state only when CUID or UUIDv7 is first requested.
147
+ * ## Ownership and lifetime
148
+ * The surrounding Layer Scope owns captured services and lazy sequence state; Web Crypto must exist in the runtime.
149
+ * @category Layers
150
+ * @since 1.0.0
151
+ */
152
+ static Default = Layer.effect(Ids, makeLazyIds("node")).pipe(Layer.provideMerge([DateTimes.Default, RandomValues.Default]));
153
+ /**
154
+ * Provides deterministic Ids, time, entropy, and TestClock services.
155
+ * @remarks
156
+ * ## Why
157
+ * Reproducible generators make exact sequences testable; the deterministic entropy is for tests and simulations, not security.
158
+ * ## Ownership and lifetime
159
+ * Each Layer construction owns an independent clock, random sequence, and lazy CUID/UUIDv7 state for its Scope.
160
+ * @example
161
+ * ```ts
162
+ * import { Ids } from "@typed/id/Ids"
163
+ * import { Effect } from "effect"
164
+ * const deterministic = Ids.uuid7.pipe(Effect.provide(Ids.Test({ currentTime: 0 })))
165
+ * ```
166
+ * @category Testing
167
+ * @since 1.0.0
168
+ */
169
+ static Test = (options = {}) => {
170
+ const services = Layer.mergeAll(DateTimes.Fixed(options.currentTime ?? 1400000000000), testRandomValues());
171
+ return Layer.effect(Ids, makeLazyIds(options.envData ?? "node")).pipe(Layer.provide(services), Layer.provideMerge(services), Layer.provideMerge(TestClock.layer({})));
172
+ };
173
+ }
174
+ function makeLazyIds(envData) {
175
+ return Effect.gen(function* () {
176
+ const services = yield* Effect.context();
177
+ const getCuidState = yield* Effect.cached(Effect.provide(CuidState.make(envData), services));
178
+ const getUuid7State = yield* Effect.cached(Effect.provide(Uuid7State.make, services));
179
+ const uuid5_ = Object.assign(dual(2, (name, namespace) => Effect.provide(uuid5(name, namespace), services)), {
180
+ dns: uuid5(Uuid5Namespace.DNS),
181
+ url: uuid5(Uuid5Namespace.URL),
182
+ oid: uuid5(Uuid5Namespace.OID),
183
+ x500: uuid5(Uuid5Namespace.X500),
184
+ });
185
+ return {
186
+ cuid: Effect.flatMap(getCuidState, (state) => Effect.provideService(cuid, CuidState, state)),
187
+ ksuid: Effect.provide(ksuid, services),
188
+ nanoId: Effect.provide(nanoId, services),
189
+ ulid: Effect.provide(ulid, services),
190
+ uuid4: Effect.provide(uuid4, services),
191
+ uuid5: uuid5_,
192
+ uuid7: Effect.flatMap(getUuid7State, (state) => Effect.provideService(uuid7, Uuid7State, state)),
193
+ };
45
194
  });
46
- static uuid7 = Effect.flatMap(Ids.asEffect(), ({ uuid7 }) => uuid7);
47
- static Default = Layer.effect(Ids, Ids.make).pipe(Layer.provide([CuidState.Default, Uuid7State.Default]), Layer.provideMerge([DateTimes.Default, RandomValues.Default]));
48
- static Test = (options) => Layer.effect(Ids, Ids.make).pipe(Layer.provide([
49
- Layer.effect(CuidState, CuidState.make(options?.envData ?? "node")),
50
- Uuid7State.Default,
51
- ]), Layer.provideMerge([DateTimes.Fixed(options?.currentTime ?? 0), RandomValues.Random]), Layer.provideMerge(TestClock.layer({})));
52
195
  }
package/dist/Ksuid.d.ts CHANGED
@@ -1,9 +1,59 @@
1
+ import * as Cause from "effect/Cause";
1
2
  import * as Effect from "effect/Effect";
2
3
  import * as Schema from "effect/Schema";
3
4
  import { DateTimes } from "./DateTimes.js";
4
5
  import { RandomValues } from "./RandomValues.js";
6
+ /**
7
+ * Effect Schema and branded string type for 27-character KSUID values.
8
+ * @remarks
9
+ * ## Why
10
+ * The schema validates transported values before restoring the compile-time brand; lexical time ordering is not a global generation-order guarantee.
11
+ * ## Ownership and lifetime
12
+ * This module-level schema value acquires no resources and is shared; no runtime freezing guarantee is implied.
13
+ * @example
14
+ * ```ts
15
+ * import { Ksuid } from "@typed/id/Ksuid"
16
+ * import { Schema } from "effect"
17
+ * const id = Schema.decodeUnknownSync(Ksuid)("0ujtsYcgvSTl8PAuAdqWYSMnLOv")
18
+ * ```
19
+ * @category Schemas
20
+ * @since 1.0.0
21
+ */
5
22
  export declare const Ksuid: Schema.brand<Schema.String, "@typed/id/KSUID">;
6
23
  export type Ksuid = typeof Ksuid.Type;
24
+ /**
25
+ * Tests whether a string has the KSUID encoding shape.
26
+ * @remarks
27
+ * ## Why
28
+ * Runtime refinement restores trust after serialization has erased the TypeScript brand.
29
+ * ## Ownership and lifetime
30
+ * This pure predicate acquires no resources and retains no input.
31
+ * @example
32
+ * ```ts
33
+ * import { isKsuid } from "@typed/id/Ksuid"
34
+ * isKsuid("0ujtsYcgvSTl8PAuAdqWYSMnLOv")
35
+ * ```
36
+ * @category Refinements
37
+ * @since 1.0.0
38
+ */
7
39
  export declare const isKsuid: (value: string) => value is Ksuid;
8
- export declare const ksuid: Effect.Effect<Ksuid, never, DateTimes | RandomValues>;
40
+ /**
41
+ * Generates a KSUID from the current time and 16 random bytes.
42
+ * @remarks
43
+ * ## Why
44
+ * Time and entropy remain explicit Effect services; timestamps outside KSUID's 32-bit seconds field fail with `IllegalArgumentError`.
45
+ * ## Ownership and lifetime
46
+ * The Effect acquires no persistent resources and uses DateTimes and RandomValues only for the invocation.
47
+ * @example
48
+ * ```ts
49
+ * import { ksuid } from "@typed/id/Ksuid"
50
+ * import { DateTimes } from "@typed/id/DateTimes"
51
+ * import { RandomValues } from "@typed/id/RandomValues"
52
+ * import { Effect, Layer } from "effect"
53
+ * const id = Effect.provide(ksuid, Layer.merge(DateTimes.Default, RandomValues.Default))
54
+ * ```
55
+ * @category Generators
56
+ * @since 1.0.0
57
+ */
58
+ export declare const ksuid: Effect.Effect<Ksuid, Cause.IllegalArgumentError, DateTimes | RandomValues>;
9
59
  //# sourceMappingURL=Ksuid.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Ksuid.d.ts","sourceRoot":"","sources":["../src/Ksuid.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAUjD,eAAO,MAAM,KAAK,gDAGjB,CAAC;AACF,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAC;AAEtC,eAAO,MAAM,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,KAAK,IAAI,KAAwB,CAAC;AAM3E,eAAO,MAAM,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,GAAG,YAAY,CAyBvE,CAAC"}
1
+ {"version":3,"file":"Ksuid.d.ts","sourceRoot":"","sources":["../src/Ksuid.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAWjD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,KAAK,gDAGjB,CAAC;AACF,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAC;AAEtC;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,KAAK,IAAI,KAAwB,CAAC;AAM3E;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,oBAAoB,EAAE,SAAS,GAAG,YAAY,CAyBzF,CAAC"}
package/dist/Ksuid.js CHANGED
@@ -1,24 +1,76 @@
1
+ import * as Cause from "effect/Cause";
1
2
  import * as Effect from "effect/Effect";
2
3
  import * as Schema from "effect/Schema";
3
4
  import { DateTimes } from "./DateTimes.js";
4
5
  import { RandomValues } from "./RandomValues.js";
5
6
  // Constants
6
- const EPOCH = 14e11; // 2014-03-01T00:00:00Z
7
+ const EPOCH = 14e11; // 2014-05-13T16:53:20Z
8
+ const TIMESTAMP_MAX = EPOCH + 2 ** 32 * 1000 - 1;
7
9
  const TIMESTAMP_BYTES = 4;
8
10
  const PAYLOAD_BYTES = 16;
9
11
  const TOTAL_BYTES = TIMESTAMP_BYTES + PAYLOAD_BYTES;
10
12
  const STRING_LENGTH = 27;
11
13
  // Schema
14
+ /**
15
+ * Effect Schema and branded string type for 27-character KSUID values.
16
+ * @remarks
17
+ * ## Why
18
+ * The schema validates transported values before restoring the compile-time brand; lexical time ordering is not a global generation-order guarantee.
19
+ * ## Ownership and lifetime
20
+ * This module-level schema value acquires no resources and is shared; no runtime freezing guarantee is implied.
21
+ * @example
22
+ * ```ts
23
+ * import { Ksuid } from "@typed/id/Ksuid"
24
+ * import { Schema } from "effect"
25
+ * const id = Schema.decodeUnknownSync(Ksuid)("0ujtsYcgvSTl8PAuAdqWYSMnLOv")
26
+ * ```
27
+ * @category Schemas
28
+ * @since 1.0.0
29
+ */
12
30
  export const Ksuid = Schema.String.pipe(Schema.check(Schema.isPattern(/^[0-9A-Za-z]{27}$/)), Schema.brand("@typed/id/KSUID"));
31
+ /**
32
+ * Tests whether a string has the KSUID encoding shape.
33
+ * @remarks
34
+ * ## Why
35
+ * Runtime refinement restores trust after serialization has erased the TypeScript brand.
36
+ * ## Ownership and lifetime
37
+ * This pure predicate acquires no resources and retains no input.
38
+ * @example
39
+ * ```ts
40
+ * import { isKsuid } from "@typed/id/Ksuid"
41
+ * isKsuid("0ujtsYcgvSTl8PAuAdqWYSMnLOv")
42
+ * ```
43
+ * @category Refinements
44
+ * @since 1.0.0
45
+ */
13
46
  export const isKsuid = Schema.is(Ksuid);
14
47
  // Public API
15
- export const ksuid = Effect.zipWith(DateTimes.now, RandomValues.call(PAYLOAD_BYTES), (timestamp, payload) => {
48
+ /**
49
+ * Generates a KSUID from the current time and 16 random bytes.
50
+ * @remarks
51
+ * ## Why
52
+ * Time and entropy remain explicit Effect services; timestamps outside KSUID's 32-bit seconds field fail with `IllegalArgumentError`.
53
+ * ## Ownership and lifetime
54
+ * The Effect acquires no persistent resources and uses DateTimes and RandomValues only for the invocation.
55
+ * @example
56
+ * ```ts
57
+ * import { ksuid } from "@typed/id/Ksuid"
58
+ * import { DateTimes } from "@typed/id/DateTimes"
59
+ * import { RandomValues } from "@typed/id/RandomValues"
60
+ * import { Effect, Layer } from "effect"
61
+ * const id = Effect.provide(ksuid, Layer.merge(DateTimes.Default, RandomValues.Default))
62
+ * ```
63
+ * @category Generators
64
+ * @since 1.0.0
65
+ */
66
+ export const ksuid = Effect.gen(function* () {
67
+ const timestamp = yield* DateTimes.now;
68
+ if (!Number.isSafeInteger(timestamp) || timestamp < EPOCH || timestamp > TIMESTAMP_MAX) {
69
+ return yield* new Cause.IllegalArgumentError(`KSUID timestamp must be a safe integer between ${EPOCH} and ${TIMESTAMP_MAX}, received ${timestamp}`);
70
+ }
71
+ const payload = yield* RandomValues.call(PAYLOAD_BYTES);
16
72
  // Create the combined bytes
17
73
  const bytes = new Uint8Array(TOTAL_BYTES);
18
- // Support for timestamps before the epoch, usually for testing
19
- if (timestamp < EPOCH) {
20
- timestamp += EPOCH;
21
- }
22
74
  // Write timestamp (4 bytes, big-endian)
23
75
  const seconds = Math.floor((timestamp - EPOCH) / 1000);
24
76
  bytes[0] = (seconds >>> 24) & 0xff;
package/dist/NanoId.d.ts CHANGED
@@ -1,8 +1,55 @@
1
1
  import * as Effect from "effect/Effect";
2
2
  import * as Schema from "effect/Schema";
3
3
  import { RandomValues } from "./RandomValues.js";
4
+ /**
5
+ * Effect Schema and branded string type for URL-safe Nano IDs.
6
+ * @remarks
7
+ * ## Why
8
+ * Runtime validation restores the brand after transport and prevents arbitrary strings from entering NanoId-specific APIs.
9
+ * ## Ownership and lifetime
10
+ * This module-level schema value acquires no resources and is shared; no runtime freezing guarantee is implied.
11
+ * @example
12
+ * ```ts
13
+ * import { NanoId } from "@typed/id/NanoId"
14
+ * const id = NanoId.make("V1StGXR8_Z5jdHi6B-myT")
15
+ * ```
16
+ * @category Schemas
17
+ * @since 1.0.0
18
+ */
4
19
  export declare const NanoId: Schema.brand<Schema.String, "@typed/id/NanoId">;
5
20
  export type NanoId = typeof NanoId.Type;
21
+ /**
22
+ * Tests whether a string has the NanoId alphabet and brandable shape.
23
+ * @remarks
24
+ * ## Why
25
+ * Refinement is the lightweight boundary check when full schema decoding is unnecessary.
26
+ * ## Ownership and lifetime
27
+ * This pure predicate acquires no resources and retains no input.
28
+ * @example
29
+ * ```ts
30
+ * import { isNanoId } from "@typed/id/NanoId"
31
+ * isNanoId("V1StGXR8_Z5jdHi6B-myT")
32
+ * ```
33
+ * @category Refinements
34
+ * @since 1.0.0
35
+ */
6
36
  export declare const isNanoId: (value: string) => value is NanoId;
37
+ /**
38
+ * Generates a 21-character NanoId from the current RandomValues service.
39
+ * @remarks
40
+ * ## Why
41
+ * Effectful generation exposes entropy as a service so production and deterministic test sources are interchangeable.
42
+ * ## Ownership and lifetime
43
+ * The Effect acquires no persistent resource and consumes one fresh 21-byte buffer owned by the invocation.
44
+ * @example
45
+ * ```ts
46
+ * import { nanoId } from "@typed/id/NanoId"
47
+ * import { RandomValues } from "@typed/id/RandomValues"
48
+ * import { Effect } from "effect"
49
+ * const id = Effect.provide(nanoId, RandomValues.Default)
50
+ * ```
51
+ * @category Generators
52
+ * @since 1.0.0
53
+ */
7
54
  export declare const nanoId: Effect.Effect<NanoId, never, RandomValues>;
8
55
  //# sourceMappingURL=NanoId.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"NanoId.d.ts","sourceRoot":"","sources":["../src/NanoId.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,eAAO,MAAM,MAAM,iDAGlB,CAAC;AACF,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC,IAAI,CAAC;AAExC,eAAO,MAAM,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,KAAK,IAAI,MAA0B,CAAC;AAI9E,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,YAAY,CAG7D,CAAC"}
1
+ {"version":3,"file":"NanoId.d.ts","sourceRoot":"","sources":["../src/NanoId.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,MAAM,iDAGlB,CAAC;AACF,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC,IAAI,CAAC;AAExC;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,KAAK,IAAI,MAA0B,CAAC;AAI9E;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,YAAY,CAG7D,CAAC"}
package/dist/NanoId.js CHANGED
@@ -1,8 +1,55 @@
1
1
  import * as Effect from "effect/Effect";
2
2
  import * as Schema from "effect/Schema";
3
3
  import { RandomValues } from "./RandomValues.js";
4
+ /**
5
+ * Effect Schema and branded string type for URL-safe Nano IDs.
6
+ * @remarks
7
+ * ## Why
8
+ * Runtime validation restores the brand after transport and prevents arbitrary strings from entering NanoId-specific APIs.
9
+ * ## Ownership and lifetime
10
+ * This module-level schema value acquires no resources and is shared; no runtime freezing guarantee is implied.
11
+ * @example
12
+ * ```ts
13
+ * import { NanoId } from "@typed/id/NanoId"
14
+ * const id = NanoId.make("V1StGXR8_Z5jdHi6B-myT")
15
+ * ```
16
+ * @category Schemas
17
+ * @since 1.0.0
18
+ */
4
19
  export const NanoId = Schema.String.pipe(Schema.check(Schema.isPattern(/^[0-9a-zA-Z_-]+$/)), Schema.brand("@typed/id/NanoId"));
20
+ /**
21
+ * Tests whether a string has the NanoId alphabet and brandable shape.
22
+ * @remarks
23
+ * ## Why
24
+ * Refinement is the lightweight boundary check when full schema decoding is unnecessary.
25
+ * ## Ownership and lifetime
26
+ * This pure predicate acquires no resources and retains no input.
27
+ * @example
28
+ * ```ts
29
+ * import { isNanoId } from "@typed/id/NanoId"
30
+ * isNanoId("V1StGXR8_Z5jdHi6B-myT")
31
+ * ```
32
+ * @category Refinements
33
+ * @since 1.0.0
34
+ */
5
35
  export const isNanoId = Schema.is(NanoId);
36
+ /**
37
+ * Generates a 21-character NanoId from the current RandomValues service.
38
+ * @remarks
39
+ * ## Why
40
+ * Effectful generation exposes entropy as a service so production and deterministic test sources are interchangeable.
41
+ * ## Ownership and lifetime
42
+ * The Effect acquires no persistent resource and consumes one fresh 21-byte buffer owned by the invocation.
43
+ * @example
44
+ * ```ts
45
+ * import { nanoId } from "@typed/id/NanoId"
46
+ * import { RandomValues } from "@typed/id/RandomValues"
47
+ * import { Effect } from "effect"
48
+ * const id = Effect.provide(nanoId, RandomValues.Default)
49
+ * ```
50
+ * @category Generators
51
+ * @since 1.0.0
52
+ */
6
53
  export const nanoId = Effect.map(RandomValues.call(21), (seed) => NanoId.make(Array.from(seed, numToCharacter).join("")));
7
54
  function numToCharacter(byte) {
8
55
  byte &= 63;
@@ -1,12 +1,71 @@
1
1
  import * as Effect from "effect/Effect";
2
2
  import * as Layer from "effect/Layer";
3
3
  import * as Context from "effect/Context";
4
- declare const RandomValues_base: Context.ServiceClass<RandomValues, "@typed/id/RandomValues", <A extends Uint8Array>(length: A["length"]) => Effect.Effect<A>> & {
5
- readonly make: Effect.Effect<(<A extends Uint8Array>(length: A["length"]) => Effect.Effect<A>), never, never>;
4
+ declare const RandomValues_base: Context.ServiceClass<RandomValues, "@typed/id/RandomValues", <const N extends number>(length: N) => Effect.Effect<Uint8Array & {
5
+ readonly length: N;
6
+ }>> & {
7
+ readonly make: Effect.Effect<<const N extends number>(length: N) => Effect.Effect<Uint8Array & {
8
+ readonly length: N;
9
+ }>, never, never>;
6
10
  };
11
+ /**
12
+ * Effect service that produces fresh byte arrays of an exact requested length.
13
+ * @remarks
14
+ * ## Why
15
+ * Entropy is an explicit dependency so secure production generation and reproducible tests use the same typed generator APIs.
16
+ * `RandomValues.Default` requires `globalThis.crypto.getRandomValues`; an unavailable implementation or thrown platform error is an Effect defect because the service's typed error channel is `never`.
17
+ * ## Ownership and lifetime
18
+ * A Layer owns the entropy source; every call allocates and transfers ownership of a fresh mutable byte array to the caller.
19
+ * @example
20
+ * ```ts
21
+ * import { RandomValues } from "@typed/id/RandomValues"
22
+ * import { Effect } from "effect"
23
+ * const bytes = Effect.provide(RandomValues.call(16), RandomValues.Default)
24
+ * ```
25
+ * @category Services
26
+ * @since 1.0.0
27
+ */
7
28
  export declare class RandomValues extends RandomValues_base {
8
- static readonly call: <A extends Uint8Array>(length: A["length"]) => Effect.Effect<A, never, RandomValues>;
29
+ /**
30
+ * Requests a fresh byte array from the current RandomValues service.
31
+ * @remarks
32
+ * ## Why
33
+ * The static call preserves literal length in the type while keeping the entropy source in Effect's service channel.
34
+ * ## Ownership and lifetime
35
+ * Each invocation allocates a new buffer owned by the caller and acquires no persistent resource.
36
+ * @example
37
+ * ```ts
38
+ * import { RandomValues } from "@typed/id/RandomValues"
39
+ * import { Effect } from "effect"
40
+ * const bytes = RandomValues.call(32).pipe(Effect.provide(RandomValues.Default))
41
+ * ```
42
+ * @category Services
43
+ * @since 1.0.0
44
+ */
45
+ static readonly call: <const N extends number>(length: N) => Effect.Effect<Uint8Array & {
46
+ readonly length: N;
47
+ }, never, RandomValues>;
48
+ /**
49
+ * Provides cryptographic bytes from `globalThis.crypto.getRandomValues`.
50
+ * @remarks
51
+ * ## Why
52
+ * Production IDs need platform entropy. Missing `globalThis.crypto.getRandomValues` and platform exceptions are Effect defects, not typed errors, rather than silently weakening randomness.
53
+ * ## Ownership and lifetime
54
+ * The Layer owns no mutable generator state; each request returns a fresh buffer. The runtime must provide Web Crypto or the request defects.
55
+ * @category Layers
56
+ * @since 1.0.0
57
+ */
9
58
  static readonly Default: Layer.Layer<RandomValues, never, never>;
59
+ /**
60
+ * Provides reproducible bytes from Effect Random.
61
+ * @remarks
62
+ * ## Why
63
+ * Deterministic tests and simulations need replaceable entropy; this Layer is not cryptographically secure.
64
+ * ## Ownership and lifetime
65
+ * The provided Effect Random service owns sequence state for the Layer lifetime; each request returns a fresh buffer.
66
+ * @category Layers
67
+ * @since 1.0.0
68
+ */
10
69
  static readonly Random: Layer.Layer<RandomValues, never, never>;
11
70
  }
12
71
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"RandomValues.d.ts","sourceRoot":"","sources":["../src/RandomValues.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAEtC,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAC;+FAIrC,CAAC,SAAS,UAAU,UAAU,CAAC,CAAC,QAAQ,CAAC,KAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;mCAA5D,CAAC,SAAS,UAAU,UAAU,CAAC,CAAC,QAAQ,CAAC,KAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;;AAFjE,qBAAa,YAAa,SAAQ,iBAKhC;IACA,gBAAyB,IAAI,GAAI,CAAC,SAAS,UAAU,EACnD,QAAQ,CAAC,CAAC,QAAQ,CAAC,KAClB,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,YAAY,CAAC,CAC+C;IAEvF,MAAM,CAAC,QAAQ,CAAC,OAAO,0CAAiD;IAExE,MAAM,CAAC,QAAQ,CAAC,MAAM,0CAapB;CACH"}
1
+ {"version":3,"file":"RandomValues.d.ts","sourceRoot":"","sources":["../src/RandomValues.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAEtC,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAC;qGAkD/B,CAAC,SAAS,MAAM,UAAU,CAAC,KAAG,MAAM,CAAC,MAAM,CAAC,UAAU,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAA;CAAE,CAAC;wCAAhF,CAAC,SAAS,MAAM,UAAU,CAAC,KAAG,MAAM,CAAC,MAAM,CAAC,UAAU,GAAG;QAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAA;KAAE,CAAC;;AAnB3F;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,YAAa,SAAQ,iBAKhC;IACA;;;;;;;;;;;;;;;OAeG;IACH,gBAAyB,IAAI,GAAI,KAAK,CAAC,CAAC,SAAS,MAAM,UAC7C,CAAC,KACR,MAAM,CAAC,MAAM,CAAC,UAAU,GAAG;QAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAA;KAAE,EAAE,KAAK,EAAE,YAAY,CAAC,CACE;IAE5E;;;;;;;;;OASG;IACH,MAAM,CAAC,QAAQ,CAAC,OAAO,0CAAiD;IAExE;;;;;;;;;OASG;IACH,MAAM,CAAC,QAAQ,CAAC,MAAM,0CAMpB;CACH"}
@@ -2,18 +2,82 @@ import * as Effect from "effect/Effect";
2
2
  import * as Layer from "effect/Layer";
3
3
  import * as Random from "effect/Random";
4
4
  import * as Context from "effect/Context";
5
+ const allocate = (length, fill) => {
6
+ const view = new Uint8Array(length);
7
+ fill(view);
8
+ return view;
9
+ };
10
+ const fillFromWebCrypto = (view) => {
11
+ const webCrypto = globalThis.crypto;
12
+ if (typeof webCrypto?.getRandomValues !== "function") {
13
+ throw new TypeError("RandomValues.Default requires globalThis.crypto.getRandomValues. Provide RandomValues.Random or a custom RandomValues service for unsupported runtimes.");
14
+ }
15
+ void webCrypto.getRandomValues(view);
16
+ };
17
+ const fromRandom = (random) => RandomValues.of((length) => Effect.sync(() => allocate(length, (view) => {
18
+ for (let i = 0; i < length; ++i)
19
+ view[i] = random.nextIntUnsafe();
20
+ })));
21
+ /**
22
+ * Effect service that produces fresh byte arrays of an exact requested length.
23
+ * @remarks
24
+ * ## Why
25
+ * Entropy is an explicit dependency so secure production generation and reproducible tests use the same typed generator APIs.
26
+ * `RandomValues.Default` requires `globalThis.crypto.getRandomValues`; an unavailable implementation or thrown platform error is an Effect defect because the service's typed error channel is `never`.
27
+ * ## Ownership and lifetime
28
+ * A Layer owns the entropy source; every call allocates and transfers ownership of a fresh mutable byte array to the caller.
29
+ * @example
30
+ * ```ts
31
+ * import { RandomValues } from "@typed/id/RandomValues"
32
+ * import { Effect } from "effect"
33
+ * const bytes = Effect.provide(RandomValues.call(16), RandomValues.Default)
34
+ * ```
35
+ * @category Services
36
+ * @since 1.0.0
37
+ */
5
38
  export class RandomValues extends Context.Service()("@typed/id/RandomValues", {
6
- make: Effect.succeed((length) => Effect.sync(() => crypto.getRandomValues(new Uint8Array(length)))),
39
+ make: Effect.succeed((length) => Effect.sync(() => allocate(length, fillFromWebCrypto))),
7
40
  }) {
8
- static call = (length) => RandomValues.asEffect().pipe(Effect.flatMap((randomValues) => randomValues(length)));
41
+ /**
42
+ * Requests a fresh byte array from the current RandomValues service.
43
+ * @remarks
44
+ * ## Why
45
+ * The static call preserves literal length in the type while keeping the entropy source in Effect's service channel.
46
+ * ## Ownership and lifetime
47
+ * Each invocation allocates a new buffer owned by the caller and acquires no persistent resource.
48
+ * @example
49
+ * ```ts
50
+ * import { RandomValues } from "@typed/id/RandomValues"
51
+ * import { Effect } from "effect"
52
+ * const bytes = RandomValues.call(32).pipe(Effect.provide(RandomValues.Default))
53
+ * ```
54
+ * @category Services
55
+ * @since 1.0.0
56
+ */
57
+ static call = (length) => RandomValues.pipe(Effect.flatMap((randomValues) => randomValues(length)));
58
+ /**
59
+ * Provides cryptographic bytes from `globalThis.crypto.getRandomValues`.
60
+ * @remarks
61
+ * ## Why
62
+ * Production IDs need platform entropy. Missing `globalThis.crypto.getRandomValues` and platform exceptions are Effect defects, not typed errors, rather than silently weakening randomness.
63
+ * ## Ownership and lifetime
64
+ * The Layer owns no mutable generator state; each request returns a fresh buffer. The runtime must provide Web Crypto or the request defects.
65
+ * @category Layers
66
+ * @since 1.0.0
67
+ */
9
68
  static Default = Layer.effect(RandomValues, RandomValues.make);
69
+ /**
70
+ * Provides reproducible bytes from Effect Random.
71
+ * @remarks
72
+ * ## Why
73
+ * Deterministic tests and simulations need replaceable entropy; this Layer is not cryptographically secure.
74
+ * ## Ownership and lifetime
75
+ * The provided Effect Random service owns sequence state for the Layer lifetime; each request returns a fresh buffer.
76
+ * @category Layers
77
+ * @since 1.0.0
78
+ */
10
79
  static Random = Layer.effect(RandomValues, Effect.gen(function* () {
11
80
  const random = yield* Random.Random;
12
- return RandomValues.of((length) => Effect.sync(() => {
13
- const view = new Uint8Array(length);
14
- for (let i = 0; i < length; ++i)
15
- view[i] = random.nextIntUnsafe();
16
- return view;
17
- }));
81
+ return fromRandom(random);
18
82
  }));
19
83
  }