@evolu/common 6.0.1-preview.2 → 6.0.1-preview.20

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 (154) hide show
  1. package/dist/src/Assert.d.ts +6 -3
  2. package/dist/src/Assert.d.ts.map +1 -1
  3. package/dist/src/Assert.js +7 -4
  4. package/dist/src/Brand.d.ts +75 -0
  5. package/dist/src/Brand.d.ts.map +1 -0
  6. package/dist/src/Brand.js +1 -0
  7. package/dist/src/Buffer.d.ts +1 -1
  8. package/dist/src/Buffer.d.ts.map +1 -1
  9. package/dist/src/Buffer.js +1 -1
  10. package/dist/src/CallbackRegistry.d.ts +53 -0
  11. package/dist/src/CallbackRegistry.d.ts.map +1 -0
  12. package/dist/src/CallbackRegistry.js +25 -0
  13. package/dist/src/Console.d.ts +31 -6
  14. package/dist/src/Console.d.ts.map +1 -1
  15. package/dist/src/Console.js +72 -9
  16. package/dist/src/Crypto.d.ts +61 -34
  17. package/dist/src/Crypto.d.ts.map +1 -1
  18. package/dist/src/Crypto.js +29 -42
  19. package/dist/src/Evolu/Db.d.ts +137 -66
  20. package/dist/src/Evolu/Db.d.ts.map +1 -1
  21. package/dist/src/Evolu/Db.js +300 -705
  22. package/dist/src/Evolu/Diff.d.ts +3 -3
  23. package/dist/src/Evolu/Diff.d.ts.map +1 -1
  24. package/dist/src/Evolu/Diff.js +7 -5
  25. package/dist/src/Evolu/Evolu.d.ts +200 -115
  26. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  27. package/dist/src/Evolu/Evolu.js +287 -157
  28. package/dist/src/Evolu/Internal.d.ts +0 -2
  29. package/dist/src/Evolu/Internal.d.ts.map +1 -1
  30. package/dist/src/Evolu/Internal.js +0 -2
  31. package/dist/src/Evolu/LocalAuth.d.ts +144 -0
  32. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
  33. package/dist/src/Evolu/LocalAuth.js +171 -0
  34. package/dist/src/Evolu/Owner.d.ts +152 -127
  35. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  36. package/dist/src/Evolu/Owner.js +88 -107
  37. package/dist/src/Evolu/Platform.d.ts +9 -7
  38. package/dist/src/Evolu/Platform.d.ts.map +1 -1
  39. package/dist/src/Evolu/Protocol.d.ts +170 -186
  40. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  41. package/dist/src/Evolu/Protocol.js +487 -343
  42. package/dist/src/Evolu/Public.d.ts +6 -8
  43. package/dist/src/Evolu/Public.d.ts.map +1 -1
  44. package/dist/src/Evolu/Public.js +2 -3
  45. package/dist/src/Evolu/PublicKysely.js +3 -3
  46. package/dist/src/Evolu/Query.d.ts +2 -1
  47. package/dist/src/Evolu/Query.d.ts.map +1 -1
  48. package/dist/src/Evolu/Relay.d.ts +4 -3
  49. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  50. package/dist/src/Evolu/Relay.js +50 -12
  51. package/dist/src/Evolu/Schema.d.ts +129 -73
  52. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  53. package/dist/src/Evolu/Schema.js +169 -89
  54. package/dist/src/Evolu/Storage.d.ts +159 -14
  55. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  56. package/dist/src/Evolu/Storage.js +42 -32
  57. package/dist/src/Evolu/Sync.d.ts +77 -13
  58. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  59. package/dist/src/Evolu/Sync.js +453 -20
  60. package/dist/src/Evolu/Timestamp.d.ts +35 -27
  61. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  62. package/dist/src/Evolu/Timestamp.js +27 -18
  63. package/dist/src/ManyToManyMap.d.ts +74 -10
  64. package/dist/src/ManyToManyMap.d.ts.map +1 -1
  65. package/dist/src/ManyToManyMap.js +41 -6
  66. package/dist/src/Number.d.ts +2 -1
  67. package/dist/src/Number.d.ts.map +1 -1
  68. package/dist/src/Random.d.ts +3 -2
  69. package/dist/src/Random.d.ts.map +1 -1
  70. package/dist/src/RefCountedResourceManager.d.ts +119 -0
  71. package/dist/src/RefCountedResourceManager.d.ts.map +1 -0
  72. package/dist/src/RefCountedResourceManager.js +197 -0
  73. package/dist/src/Result.d.ts +147 -51
  74. package/dist/src/Result.d.ts.map +1 -1
  75. package/dist/src/Result.js +7 -241
  76. package/dist/src/Sqlite.d.ts +21 -4
  77. package/dist/src/Sqlite.d.ts.map +1 -1
  78. package/dist/src/Sqlite.js +50 -8
  79. package/dist/src/Task.d.ts +511 -0
  80. package/dist/src/Task.d.ts.map +1 -0
  81. package/dist/src/Task.js +410 -0
  82. package/dist/src/Time.d.ts +59 -0
  83. package/dist/src/Time.d.ts.map +1 -1
  84. package/dist/src/Time.js +87 -4
  85. package/dist/src/Type.d.ts +462 -327
  86. package/dist/src/Type.d.ts.map +1 -1
  87. package/dist/src/Type.js +490 -436
  88. package/dist/src/Types.d.ts +0 -74
  89. package/dist/src/Types.d.ts.map +1 -1
  90. package/dist/src/WebSocket.d.ts +5 -2
  91. package/dist/src/WebSocket.d.ts.map +1 -1
  92. package/dist/src/WebSocket.js +12 -13
  93. package/dist/src/Worker.d.ts +39 -11
  94. package/dist/src/Worker.d.ts.map +1 -1
  95. package/dist/src/Worker.js +22 -4
  96. package/dist/src/index.d.ts +3 -3
  97. package/dist/src/index.d.ts.map +1 -1
  98. package/dist/src/index.js +3 -3
  99. package/package.json +14 -13
  100. package/src/Assert.ts +8 -7
  101. package/src/Brand.ts +75 -0
  102. package/src/Buffer.ts +1 -1
  103. package/src/CallbackRegistry.ts +84 -0
  104. package/src/Console.ts +91 -11
  105. package/src/Crypto.ts +94 -79
  106. package/src/Evolu/Db.ts +505 -1027
  107. package/src/Evolu/Diff.ts +7 -5
  108. package/src/Evolu/Evolu.ts +682 -329
  109. package/src/Evolu/Internal.ts +0 -2
  110. package/src/Evolu/LocalAuth.ts +422 -0
  111. package/src/Evolu/Owner.ts +221 -235
  112. package/src/Evolu/Platform.ts +9 -9
  113. package/src/Evolu/Protocol.ts +681 -614
  114. package/src/Evolu/Public.ts +7 -14
  115. package/src/Evolu/PublicKysely.ts +3 -3
  116. package/src/Evolu/Query.ts +2 -1
  117. package/src/Evolu/Relay.ts +62 -16
  118. package/src/Evolu/Schema.ts +391 -191
  119. package/src/Evolu/Storage.ts +275 -55
  120. package/src/Evolu/Sync.ts +758 -37
  121. package/src/Evolu/Timestamp.ts +38 -35
  122. package/src/ManyToManyMap.ts +127 -24
  123. package/src/Number.ts +2 -6
  124. package/src/Random.ts +3 -2
  125. package/src/RefCountedResourceManager.ts +368 -0
  126. package/src/Result.ts +152 -53
  127. package/src/Sqlite.ts +59 -23
  128. package/src/Task.ts +779 -0
  129. package/src/Time.ts +168 -4
  130. package/src/Type.ts +696 -660
  131. package/src/Types.ts +0 -76
  132. package/src/WebSocket.ts +23 -17
  133. package/src/Worker.ts +72 -23
  134. package/src/index.ts +3 -3
  135. package/dist/src/Callbacks.d.ts +0 -20
  136. package/dist/src/Callbacks.d.ts.map +0 -1
  137. package/dist/src/Callbacks.js +0 -18
  138. package/dist/src/Evolu/Config.d.ts +0 -69
  139. package/dist/src/Evolu/Config.d.ts.map +0 -1
  140. package/dist/src/Evolu/Config.js +0 -9
  141. package/dist/src/Evolu/Kysely.d.ts +0 -6
  142. package/dist/src/Evolu/Kysely.d.ts.map +0 -1
  143. package/dist/src/Evolu/Kysely.js +0 -21
  144. package/dist/src/NanoId.d.ts +0 -27
  145. package/dist/src/NanoId.d.ts.map +0 -1
  146. package/dist/src/NanoId.js +0 -6
  147. package/dist/src/Promise.d.ts +0 -180
  148. package/dist/src/Promise.d.ts.map +0 -1
  149. package/dist/src/Promise.js +0 -176
  150. package/src/Callbacks.ts +0 -43
  151. package/src/Evolu/Config.ts +0 -83
  152. package/src/Evolu/Kysely.ts +0 -38
  153. package/src/NanoId.ts +0 -39
  154. package/src/Promise.ts +0 -295
package/src/Type.ts CHANGED
@@ -1,84 +1,187 @@
1
1
  /**
2
- * 🧩 Validation, Parsing, and Transformation
2
+ * 🧩 Type-safe runtime types
3
3
  *
4
- * ## Intro
4
+ * Evolu {@link Type} is like a type guard that returns typed errors (via
5
+ * {@link Result}) instead of throwing. We either get a safely typed value or a
6
+ * precise, composable error value telling us exactly why validation failed.
5
7
  *
6
- * You probably know [Zod](https://zod.dev). Evolu has {@link Type}.
8
+ * Why another validation library?
7
9
  *
8
- * Evolu Type exists because no existing validation/parsing/transformation
9
- * library fully met our needs:
10
+ * - **Result-based error handling** no exceptions for normal control flow.
11
+ * - **Typed errors with decoupled formatters** – validation logic ≠ user
12
+ * messages.
13
+ * - **Consistent constraints via {@link Brand}** – every constraint becomes part
14
+ * of the type.
15
+ * - **No user-land chaining DSL** – designed with the upcoming ES pipe operator
16
+ * in mind.
17
+ * - **Selective validation** – parent validations are skipped when already proved
18
+ * by typing.
19
+ * - **Simple, top-down implementation** – readable source code from top to bottom
20
+ * with no hidden magic; just plain functions and composition.
10
21
  *
11
- * - **Result-based error handling**: Leveraging {@link Result} instead of throwing
12
- * exceptions.
13
- * - **Consistent constraints**: Enforcing {@link Brand} for all constraints.
14
- * - **Typed errors with decoupled formatters**: Avoiding coupling error messages
15
- * with validators.
16
- * - **No user-land chaining**: Designed with ES pipe operator in mind.
17
- * - **Selective validation/transformation**: Skipping parent Type validations and
18
- * transformations when TypeScript's type system can be relied upon.
19
- * - **Bidirectional transformations**: Supporting transformations in both
20
- * directions.
21
- * - **Minimal and transparent code**: No runtime dependencies or hidden magic.
22
+ * ### Base Types Quick Start
22
23
  *
23
- * **Note**: A proper quickstart guide is on the way. In the meantime, each type
24
- * includes its own usage example, and you can (and should) check the tests for
25
- * practical demonstrations of the API. Or dang, just read the code. It's
26
- * simple.
24
+ * ```ts
25
+ * // Validate unknown values
26
+ * const value: unknown = "hello";
27
+ * const stringResult = String.fromUnknown(value);
28
+ * if (!stringResult.ok) {
29
+ * // console.error(formatStringError(stringResult.error));
30
+ * return stringResult; // inside a function returning Result<string, _>
31
+ * }
32
+ * // Safe branch: value is now string
33
+ * const upper = stringResult.value.toUpperCase();
34
+ *
35
+ * // Type guard style
36
+ * if (String.is(value)) {
37
+ * // narrowed to string
38
+ * }
39
+ *
40
+ * // Composing: arrays & objects
41
+ * const Numbers = array(Number); // ReadonlyArray<number>
42
+ * const Point = object({ x: Number, y: Number });
43
+ *
44
+ * Numbers.from([1, 2, 3]); // ok
45
+ * Point.from({ x: 1, y: 2 }); // ok
46
+ * Point.from({ x: 1, y: "2" }); // err -> nested Number error
47
+ * ```
48
+ *
49
+ * ### Branding Basics
50
+ *
51
+ * Branding adds semantic meaning & constraints while preserving the runtime
52
+ * shape:
53
+ *
54
+ * ```ts
55
+ * const CurrencyCode = brand("CurrencyCode", String, (value) =>
56
+ * /^[A-Z]{3}$/.test(value)
57
+ * ? ok(value)
58
+ * : err<CurrencyCodeError>({ type: "CurrencyCode", value }),
59
+ * );
60
+ * type CurrencyCode = typeof CurrencyCode.Type; // string & Brand<"CurrencyCode">
61
+ *
62
+ * interface CurrencyCodeError extends TypeError<"CurrencyCode"> {}
63
+ *
64
+ * const formatCurrencyCodeError =
65
+ * createTypeErrorFormatter<CurrencyCodeError>(
66
+ * (error) => `Invalid currency code: ${error.value}`,
67
+ * );
68
+ *
69
+ * const r = CurrencyCode.from("USD"); // ok("USD")
70
+ * const e = CurrencyCode.from("usd"); // err(...)
71
+ * ```
72
+ *
73
+ * See also reusable brand factories like `minLength`, `maxLength`, `trimmed`,
74
+ * `positive`, `between`, etc.
75
+ *
76
+ * ### Objects & Optional Fields
77
+ *
78
+ * ```ts
79
+ * const User = object({
80
+ * name: NonEmptyTrimmedString100,
81
+ * age: optional(PositiveInt),
82
+ * });
83
+ * type User = typeof User.Type;
27
84
  *
28
- * - Evolu `Type` is:
85
+ * User.from({ name: "Alice" }); // ok
86
+ * User.from({ name: "Alice", age: -1 }); // err(PositiveInt)
87
+ * ```
29
88
  *
30
- * - A TypeScript type with a {@link Brand} whenever it's possible.
31
- * - A function to create a value of that type, which may fail.
32
- * - A function to transform value back to its original representation, which
33
- * cannot fail.
89
+ * ### Deriving JSON String Types
34
90
  *
35
- * Types are chainable. The chain starts with a Base Type that refines an
36
- * unknown value into something and can continue with further refinements or
37
- * transformations. For example, `NonEmptyTrimmedString100` chain looks like
38
- * this:
91
+ * ```ts
92
+ * const Person = object({
93
+ * name: NonEmptyString50,
94
+ * // Did you know that JSON.stringify converts NaN (a number) into null?
95
+ * // To prevent this, use FiniteNumber.
96
+ * age: FiniteNumber,
97
+ * });
98
+ * type Person = typeof Person.Type;
39
99
  *
40
- * `Unknown` -> `String` -> `TrimmedString` -> `NonEmptyTrimmedString100`
100
+ * const [PersonJson, personToPersonJson, personJsonToPerson] = json(
101
+ * Person,
102
+ * "PersonJson",
103
+ * );
104
+ * // string & Brand<"PersonJson">
105
+ * type PersonJson = typeof PersonJson.Type;
41
106
  *
42
- * For `NonEmptyTrimmedString100`, the parent Type is `TrimmedString`. For
43
- * `TrimmedString`, the parent Type is `String`.
107
+ * const person = Person.orThrow({
108
+ * name: "Alice",
109
+ * age: 30,
110
+ * });
44
111
  *
45
- * The parent of the `String` Type is the `String` Type itself. All Base Types
46
- * `fromParent` functions are just a typed alias to `fromUnknown` to ensure that
47
- * `fromParent` and `toParent` can be called on any Type.
112
+ * const personJson = personToPersonJson(person);
113
+ * expect(personJsonToPerson(personJson)).toEqual(person);
114
+ * ```
48
115
  *
49
- * Speaking of `fromParent` and `toParent`, those functions exist to bypass
50
- * parent Types when we can rely on TypeScript types.
116
+ * ### Error Formatting
117
+ *
118
+ * Evolu separates validation logic from human-readable messages. There are two
119
+ * layers:
120
+ *
121
+ * 1. Per-type formatters (e.g. `formatStringError`) – simple, focused, already
122
+ * used earlier in the quick start example.
123
+ * 2. A unified formatter via `createFormatTypeError` – composes all built-in and
124
+ * custom errors (including nested composite types) and lets us override
125
+ * selected messages.
126
+ *
127
+ * #### 1. Per-Type Formatter (recap)
128
+ *
129
+ * ```ts
130
+ * const r = String.fromUnknown(42);
131
+ * if (!r.ok) console.error(formatStringError(r.error));
132
+ * ```
133
+ *
134
+ * #### 2. Unified Formatter with Overrides
135
+ *
136
+ * ```ts
137
+ * // Override only what we care about; fall back to built-ins for the rest.
138
+ * const formatTypeError = createFormatTypeError((error) => {
139
+ * if (error.type === "MinLength") return `Min length is ${error.min}`;
140
+ * });
141
+ *
142
+ * const User = object({ name: NonEmptyTrimmedString100 });
143
+ * const resultUser = User.from({ name: "" });
144
+ * if (!resultUser.ok) console.error(formatTypeError(resultUser.error));
145
+ *
146
+ * const badPoint = object({ x: Number, y: Number }).from({
147
+ * x: 1,
148
+ * y: "foo",
149
+ * });
150
+ * if (!badPoint.ok) console.error(formatTypeError(badPoint.error));
151
+ * ```
51
152
  *
52
- * `Type` transformations should be reversible. If you need an irreversible
53
- * transformation, such as `TrimString` (trimming is not reversible as `untrim`
54
- * can't know what has been trimmed), you can do that, but note in JSDoc that
55
- * `to` will not restore the original representation. You can also use
56
- * {@link assert}: `assert(false, "Untrim is not possible")`.
153
+ * The unified formatter walks nested structures (object / array / record /
154
+ * tuple / union) and applies overrides only where specified, greatly reducing
155
+ * boilerplate when formatting complex validation errors.
57
156
  *
58
157
  * ### Tip
59
158
  *
60
159
  * If necessary, write `globalThis.String` instead of `String` to avoid naming
61
- * clashes with Base Types.
160
+ * clashes with native types.
62
161
  *
63
- * ### Design Decision:
162
+ * ### Design Decision: No Bidirectional Transformations
64
163
  *
65
- * While the `from` function can fail, the `to` function cannot. This simplifies
66
- * the model by ensuring that every valid input has a corresponding valid
67
- * output, eliminating the risk of edge cases caused by irreversible
68
- * operations.
164
+ * Evolu Type intentionally does not support bidirectional transformations. It
165
+ * previously did, but supporting that while keeping typed error fidelity added
166
+ * complexity that hurt readability & reliability. Most persistence pipelines
167
+ * (e.g. SQLite) already require explicit mapping of query results, so implicit
168
+ * reverse transforms would not buy much. We may revisit this if we can design a
169
+ * minimal, 100% safe API that preserves simplicity.
69
170
  *
70
171
  * @module
71
172
  */
72
173
 
174
+ import { utf8ToBytes } from "@noble/ciphers/utils.js";
175
+ import { sha256 } from "@noble/hashes/sha2.js";
73
176
  import * as bip39 from "@scure/bip39";
74
- import { wordlist } from "@scure/bip39/wordlists/english";
75
- import { assert } from "./Assert.js";
76
- import { identity } from "./Function.js";
77
- import { NanoIdLibDep } from "./NanoId.js";
177
+ import { wordlist } from "@scure/bip39/wordlists/english.js";
178
+ import { pack } from "msgpackr";
179
+ import type { Brand } from "./Brand.js";
180
+ import { type RandomBytesDep } from "./Crypto.js";
78
181
  import { isPlainObject } from "./Object.js";
79
- import { Err, err, Ok, ok, Result, trySync } from "./Result.js";
182
+ import { err, getOrThrow, ok, Result, trySync } from "./Result.js";
80
183
  import { safelyStringifyUnknownValue } from "./String.js";
81
- import type { Brand, Literal, Simplify, WidenLiteral } from "./Types.js";
184
+ import type { Literal, Simplify, WidenLiteral } from "./Types.js";
82
185
  import { IntentionalNever } from "./Types.js";
83
186
 
84
187
  export interface Type<
@@ -106,41 +209,60 @@ export interface Type<
106
209
  readonly from: (value: Input) => Result<T, ParentError | Error>;
107
210
 
108
211
  /**
109
- * Creates `T` from an unknown value.
212
+ * Creates `T` from an `Input` value, throwing an error if validation fails.
110
213
  *
111
- * This is useful when a value is unknown.
112
- */
113
- readonly fromUnknown: (value: unknown) => Result<T, ParentError | Error>;
114
-
115
- /**
116
- * The opposite of `from` and `fromUnknown`.
214
+ * This is a convenience method that combines `from` with `getOrThrow`.
117
215
  *
118
- * This is useful to transform `T` back to its `Input` representation.
216
+ * **When to use:**
119
217
  *
120
- * For `refine`, it only removes the brand. For `transform`, it changes value.
121
- */
122
- readonly to: (value: T) => Input;
123
-
124
- /**
125
- * Creates `T` from `Parent` type.
218
+ * - Configuration values that are guaranteed to be valid (e.g., hardcoded
219
+ * constants)
220
+ * - Application startup where failure should crash the program
221
+ * - Test code with known valid inputs
222
+ * - Converting from trusted sources where validation failure indicates a
223
+ * programming error
126
224
  *
127
- * This function skips parent Types validations/transformations when we have
128
- * already partially validated/transformed value.
225
+ * **When NOT to use:**
129
226
  *
130
- * For example, `TrimString.from` checks whether a value is a string and trims
131
- * it. If we only want to trim a string, we can use `fromParent`.
227
+ * - User input validation - use `from` and handle errors gracefully
228
+ * - Data from external APIs or files - use `from` for proper error handling
229
+ * - Library code that should return Results rather than throw
132
230
  *
133
231
  * ### Example
134
232
  *
135
233
  * ```ts
136
- * // string & Brand<"Trimmed">
137
- * const value = TrimString.fromParent("a ").value; // as efficient as foo.trim()
234
+ * // Good: Known valid constant
235
+ * const maxRetries = PositiveInt.orThrow(3);
236
+ *
237
+ * // ✅ Good: App configuration that should crash on invalid values
238
+ * const appName = SimpleName.orThrow("MyApp");
239
+ *
240
+ * // ❌ Avoid: User input (use `from` instead)
241
+ * const userAge = PositiveInt.orThrow(userInput); // Could crash!
242
+ *
243
+ * // ✅ Better: Handle user input gracefully
244
+ * const ageResult = PositiveInt.from(userInput);
245
+ * if (!ageResult.ok) {
246
+ * // Handle validation error
247
+ * }
138
248
  * ```
139
249
  */
140
- readonly fromParent: (value: Parent) => Result<T, Error>;
250
+ readonly orThrow: (value: Input) => T;
141
251
 
142
- /** The opposite of `fromParent`. */
143
- readonly toParent: (value: T) => Parent;
252
+ /**
253
+ * Creates `T` from an unknown value.
254
+ *
255
+ * This is useful when a value is unknown.
256
+ */
257
+ readonly fromUnknown: (value: unknown) => Result<T, ParentError | Error>;
258
+
259
+ /**
260
+ * Creates `T` from `Parent` type.
261
+ *
262
+ * This function skips parent Types validations when we have already partially
263
+ * validated value.
264
+ */
265
+ readonly fromParent: (value: Parent) => Result<T, Error>;
144
266
 
145
267
  /**
146
268
  * A **type guard** that checks whether an unknown value satisfies the
@@ -261,26 +383,61 @@ export interface TypeErrorWithReason<
261
383
 
262
384
  export type AnyType = Type<any, any, any, any, any, any>;
263
385
 
386
+ /**
387
+ * Extracts the name from a {@link Type}.
388
+ *
389
+ * @category Utilities
390
+ */
264
391
  export type InferName<A extends AnyType> =
265
392
  A extends Type<infer Name, any, any, any, any, any> ? Name : never;
266
393
 
394
+ /**
395
+ * Extracts the type from a {@link Type}.
396
+ *
397
+ * @category Utilities
398
+ */
267
399
  export type InferType<A extends AnyType> =
268
400
  A extends Type<any, infer T, any, any, any, any> ? T : never;
269
401
 
402
+ /**
403
+ * Extracts the input type from a {@link Type}.
404
+ *
405
+ * @category Utilities
406
+ */
270
407
  export type InferInput<A extends AnyType> =
271
408
  A extends Type<any, any, infer Input, any, any, any> ? Input : never;
272
409
 
410
+ /**
411
+ * Extracts the specific error type from a {@link Type}.
412
+ *
413
+ * @category Utilities
414
+ */
273
415
  export type InferError<A extends AnyType> =
274
416
  A extends Type<any, any, any, infer Error, any, any> ? Error : never;
275
417
 
418
+ /**
419
+ * Extracts the parent type from a {@link Type}.
420
+ *
421
+ * @category Utilities
422
+ */
276
423
  export type InferParent<A extends AnyType> =
277
424
  A extends Type<any, any, any, any, infer Parent, any> ? Parent : never;
278
425
 
426
+ /**
427
+ * Extracts the parent error type from a {@link Type}.
428
+ *
429
+ * @category Utilities
430
+ */
279
431
  export type InferParentError<A extends AnyType> =
280
432
  A extends Type<any, any, any, any, any, infer ParentError>
281
433
  ? ParentError
282
434
  : never;
283
435
 
436
+ /**
437
+ * Extracts all error types (Error | ParentError) from a {@link Type}.
438
+ *
439
+ * @category Utilities
440
+ */
284
441
  export type InferErrors<T extends AnyType> =
285
442
  T extends Type<any, any, any, infer Error, any, infer ParentError>
286
443
  ? Error | ParentError
@@ -310,6 +467,7 @@ const createType = <
310
467
  | "name"
311
468
  | "is"
312
469
  | "from"
470
+ | "orThrow"
313
471
  | typeof EvoluTypeSymbol
314
472
  | "Type"
315
473
  | "Input"
@@ -323,6 +481,7 @@ const createType = <
323
481
  name,
324
482
  is: (value: unknown): value is T => definition.fromUnknown(value).ok,
325
483
  from: definition.fromUnknown,
484
+ orThrow: (value: Input): T => getOrThrow(definition.fromUnknown(value)),
326
485
  [EvoluTypeSymbol]: true,
327
486
  Type: undefined as unknown as T,
328
487
  Input: undefined as unknown as Input,
@@ -365,12 +524,6 @@ export type TypeErrorFormatter<Error extends TypeError> = (
365
524
  * Base {@link Type}.
366
525
  *
367
526
  * A Base Type validates that a value conforms to a specific TypeScript type.
368
- * Unlike refinements or transformations, Base Types establish the fundamental
369
- * shape of a value before any branding or transformation occurs.
370
- *
371
- * - To **refine** a Base Type further, use the {@link brand} Type Factory.
372
- * - To **transform** a Base Type into a different representation, use the
373
- * {@link transform} Type Factory.
374
527
  *
375
528
  * ### Example
376
529
  *
@@ -396,9 +549,7 @@ export const base = <Name extends TypeName, T, Error extends TypeError>(
396
549
  ): Type<Name, T, T, Error> =>
397
550
  createType(name, {
398
551
  fromUnknown,
399
- to: identity,
400
552
  fromParent: ok<T>, // `fromParent` relies on types, so it can't fail for the Base Type
401
- toParent: identity,
402
553
  });
403
554
 
404
555
  /**
@@ -607,7 +758,7 @@ export const formatIsTypeError = createTypeErrorFormatter<EvoluTypeError>(
607
758
  * The `brand` Type Factory takes the name of a new {@link Brand}, a parent Type
608
759
  * to be branded, and the optional `refine` function for additional constraint.
609
760
  *
610
- * If the `refine` function is omited, TODO:
761
+ * The `refine` function can be omitted if we only want to add a brand.
611
762
  *
612
763
  * ### Examples
613
764
  *
@@ -686,7 +837,7 @@ export const formatIsTypeError = createTypeErrorFormatter<EvoluTypeError>(
686
837
  * confirmPassword: SimplePassword,
687
838
  * });
688
839
  *
689
- * const ValidForm = brand("Valid", Form, (value) => {
840
+ * const ValidForm = brand("ValidForm", Form, (value) => {
690
841
  * if (value.password !== value.confirmPassword)
691
842
  * return err<ValidFormError>({
692
843
  * type: "ValidForm",
@@ -780,9 +931,7 @@ export function brand<
780
931
  return {
781
932
  ...createType("Brand", {
782
933
  fromUnknown,
783
- to: identity,
784
934
  fromParent: (refine ?? ok) as IntentionalNever,
785
- toParent: identity,
786
935
  }),
787
936
  brand: name,
788
937
  parentType: parent,
@@ -848,30 +997,41 @@ export const formatCurrencyCodeError =
848
997
  * ### Example
849
998
  *
850
999
  * ```ts
851
- * const result = DateIsoString.from("2023-01-01T12:00:00.000Z"); // ok
852
- * const error = DateIsoString.from("10000-01-01T00:00:00.000Z"); // err
1000
+ * const result = DateIso.from("2023-01-01T12:00:00.000Z"); // ok
1001
+ * const error = DateIso.from("10000-01-01T00:00:00.000Z"); // err
853
1002
  * ```
854
1003
  *
855
1004
  * @category String
856
1005
  */
857
- export const DateIsoString = brand("DateIso", String, (value) => {
1006
+ export const DateIso = brand("DateIso", String, (value) => {
858
1007
  if (value.length !== 24) {
859
- return err<DateIsoStringError>({ type: "DateIsoString", value });
1008
+ return err<DateIsoError>({ type: "DateIso", value });
1009
+ }
1010
+ const parsed = globalThis.Date.parse(value);
1011
+ if (isNaN(parsed)) {
1012
+ return err<DateIsoError>({ type: "DateIso", value });
860
1013
  }
861
- if (isNaN(globalThis.Date.parse(value))) {
862
- return err<DateIsoStringError>({ type: "DateIsoString", value });
1014
+ // Round-trip test: ensure the string is actually a proper ISO format
1015
+ const roundTrip = new globalThis.Date(parsed).toISOString();
1016
+ if (roundTrip !== value) {
1017
+ return err<DateIsoError>({ type: "DateIso", value });
863
1018
  }
864
1019
  return ok(value);
865
1020
  });
866
1021
 
867
- export type DateIsoString = typeof DateIsoString.Type;
1022
+ export type DateIso = typeof DateIso.Type;
868
1023
 
869
- export interface DateIsoStringError extends TypeError<"DateIsoString"> {}
1024
+ export interface DateIsoError extends TypeError<"DateIso"> {}
870
1025
 
871
- export const formatDateIsoStringError =
872
- createTypeErrorFormatter<DateIsoStringError>(
873
- (error) => `The value ${error.value} is not a valid ISO 8601 date string.`,
874
- );
1026
+ export const formatDateIsoError = createTypeErrorFormatter<DateIsoError>(
1027
+ (error) => `The value ${error.value} is not a valid ISO 8601 date string.`,
1028
+ );
1029
+
1030
+ export const dateToDateIso = (value: Date): Result<DateIso, DateIsoError> =>
1031
+ DateIso.fromParent(value.toISOString());
1032
+
1033
+ export const dateIsoToDate = (value: DateIso): Date =>
1034
+ new globalThis.Date(value);
875
1035
 
876
1036
  /**
877
1037
  * Helper type for Type Factory that creates a branded Type.
@@ -914,18 +1074,12 @@ export type BrandFactory<
914
1074
  /**
915
1075
  * Trimmed string.
916
1076
  *
917
- * This Type Factory does not transform; it only validates whether a string has
918
- * no leading or trailing whitespaces. To trim a string, use {@link trim} Type
919
- * Factory.
1077
+ * This Type Factory validates whether a string has no leading or trailing
1078
+ * whitespaces.
920
1079
  *
921
- * ### Examples
1080
+ * ### Example
922
1081
  *
923
1082
  * ```ts
924
- * // this Type already exists
925
- * const TrimmedString = trimmed(String);
926
- * type TrimmedString = typeof TrimmedString.Type;
927
- *
928
- * // we can make any branded Type trimmed:
929
1083
  * const TrimmedNonEmptyString = trimmed(minLength(1)(String));
930
1084
  * // string & Brand<"MinLength1"> & Brand<"Trimmed">
931
1085
  * type TrimmedNonEmptyString = typeof TrimmedNonEmptyString.Type;
@@ -948,64 +1102,6 @@ export const formatTrimmedError = createTypeErrorFormatter<TrimmedError>(
948
1102
  (error) => `A value ${error.value} is not trimmed`,
949
1103
  );
950
1104
 
951
- export type TransformBrandFactory<
952
- Name extends TypeName,
953
- Input,
954
- TransformError extends TypeError = never,
955
- > = <
956
- PName extends TypeName,
957
- P extends Input,
958
- PInput,
959
- PParent,
960
- PError extends TypeError = never,
961
- PParentError extends TypeError = never,
962
- >(
963
- parent: Type<PName, P, PInput, PError, PParent, PParentError>,
964
- ) => TransformType<
965
- Type<PName, P, PInput, PError, PParent, PParentError>,
966
- BrandType<
967
- Type<PName, P, PInput, PError, PParent, PParentError>,
968
- Name,
969
- never,
970
- PError | PParentError
971
- >,
972
- TransformError
973
- >;
974
-
975
- /**
976
- * Trims leading and trailing whitespace from a string.
977
- *
978
- * This Type Factory **transforms** the input string by removing whitespace from
979
- * both ends. For validation only, use {@link trimmed} Type Factory.
980
- *
981
- * ### Example
982
- *
983
- * ```ts
984
- * const TrimString = trim(String);
985
- * expect(TrimString.from("a ")).toEqual(ok("a"));
986
- * expect(TrimString.fromParent("a ").value).toEqual("a");
987
- *
988
- * const TrimNonEmptyString = trim(NonEmptyString);
989
- * expect(TrimNonEmptyString.from("a " as NonEmptyString)).toEqual(ok("a"));
990
- * expect(
991
- * TrimNonEmptyString.fromParent("a " as NonEmptyString).value,
992
- * ).toEqual("a");
993
- * ```
994
- *
995
- * **Note:** This transformation is irreversible. Calling `toParent` will not
996
- * restore the original representation.
997
- *
998
- * @category String
999
- */
1000
-
1001
- export const trim: TransformBrandFactory<"Trimmed", string> = (parent) =>
1002
- transform(
1003
- parent,
1004
- trimmed(parent) as BrandType<typeof parent, "Trimmed">,
1005
- (value) => ok(value.trim() as InferType<typeof parent> & Brand<"Trimmed">),
1006
- (value) => value,
1007
- );
1008
-
1009
1105
  /**
1010
1106
  * Trimmed string
1011
1107
  *
@@ -1017,6 +1113,9 @@ export const trim: TransformBrandFactory<"Trimmed", string> = (parent) =>
1017
1113
  export const TrimmedString = trimmed(String);
1018
1114
  export type TrimmedString = typeof TrimmedString.Type;
1019
1115
 
1116
+ export const trim = (value: string): TrimmedString =>
1117
+ value.trim() as TrimmedString;
1118
+
1020
1119
  /**
1021
1120
  * Minimum length.
1022
1121
  *
@@ -1213,9 +1312,9 @@ export const formatRegexError = createTypeErrorFormatter<RegexError>(
1213
1312
  );
1214
1313
 
1215
1314
  /**
1216
- * URL-safe Base64 string.
1315
+ * URL-safe string.
1217
1316
  *
1218
- * A `Base64Url` string uses a limited alphabet that is URL-safe:
1317
+ * A `UrlSafeString` uses a limited alphabet that is safe for URLs:
1219
1318
  *
1220
1319
  * - Uppercase letters (`A-Z`)
1221
1320
  * - Lowercase letters (`a-z`)
@@ -1223,35 +1322,116 @@ export const formatRegexError = createTypeErrorFormatter<RegexError>(
1223
1322
  * - Dash (`-`)
1224
1323
  * - Underscore (`_`)
1225
1324
  *
1325
+ * This is the same character set used by Base64Url encoding, but this type does
1326
+ * not validate that the string is actually Base64Url-encoded data.
1327
+ *
1226
1328
  * ### Example
1227
1329
  *
1228
1330
  * ```ts
1229
- * const result = Base64Url.from("abc123_-");
1331
+ * const result = UrlSafeString.from("abc123_-");
1230
1332
  * if (result.ok) {
1231
- * console.log("Valid Base64Url string:", result.value);
1333
+ * console.log("Valid URL-safe string:", result.value);
1232
1334
  * } else {
1233
- * console.error("Invalid Base64Url string:", result.error);
1335
+ * console.error("Invalid URL-safe string:", result.error);
1234
1336
  * }
1235
1337
  * ```
1236
1338
  *
1237
1339
  * @category String
1238
1340
  */
1239
- export const Base64Url = regex(
1341
+ export const UrlSafeString = regex("UrlSafeString", /^[A-Za-z0-9_-]+$/)(String);
1342
+ export type UrlSafeString = typeof UrlSafeString.Type;
1343
+ export type UrlSafeStringError = typeof UrlSafeString.Error;
1344
+
1345
+ /**
1346
+ * Base64Url without padding.
1347
+ *
1348
+ * Encode with {@link uint8ArrayToBase64Url}, decode with
1349
+ * {@link base64UrlToUint8Array}.
1350
+ *
1351
+ * @category String
1352
+ */
1353
+ export const Base64Url = brand(
1240
1354
  "Base64Url",
1241
- /^[A-Za-z0-9_-]+$/, // URL-safe Base64 alphabet
1242
- )(String);
1355
+ String,
1356
+ (value: string): Result<string, Base64UrlError> => {
1357
+ // Round-trip validation ensures consistency across different base64url
1358
+ // implementations (Node.js Buffer, native browser API, manual fallback).
1359
+ // Only strings that decode and encode identically are accepted.
1360
+ let roundTrip;
1361
+ try {
1362
+ roundTrip = uint8ArrayToBase64Url(
1363
+ base64UrlToUint8Array(value as Base64Url),
1364
+ );
1365
+ } catch {
1366
+ //
1367
+ }
1368
+ return roundTrip === value
1369
+ ? ok(value)
1370
+ : err<Base64UrlError>({ type: "Base64Url", value });
1371
+ },
1372
+ );
1243
1373
  export type Base64Url = typeof Base64Url.Type;
1244
- export type Base64UrlError = typeof Base64Url.Error;
1374
+ export interface Base64UrlError extends TypeError<"Base64Url"> {}
1375
+
1376
+ export const formatBase64UrlError = createTypeErrorFormatter<Base64UrlError>(
1377
+ (error) => `Value ${error.value} is not a valid Base64Url string.`,
1378
+ );
1379
+
1380
+ const hasNodeBuffer = typeof globalThis.Buffer !== "undefined";
1381
+ const base64UrlOptions = { alphabet: "base64url", omitPadding: true };
1382
+
1383
+ /** Encodes a Uint8Array to a {@link Base64Url} string. */
1384
+ export const uint8ArrayToBase64Url: (bytes: Uint8Array) => Base64Url =
1385
+ hasNodeBuffer
1386
+ ? (bytes: Uint8Array) =>
1387
+ globalThis.Buffer.from(bytes).toString("base64url") as Base64Url
1388
+ : // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
1389
+ typeof (globalThis.Uint8Array.prototype as any)?.toBase64 !== "undefined"
1390
+ ? (bytes: Uint8Array) =>
1391
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
1392
+ (bytes as any).toBase64(base64UrlOptions) as Base64Url
1393
+ : (bytes: Uint8Array) => {
1394
+ const binaryString = Array.from(bytes, (byte) =>
1395
+ globalThis.String.fromCodePoint(byte),
1396
+ ).join("");
1397
+ const base64 = globalThis.btoa(binaryString);
1398
+ return base64
1399
+ .replace(/\+/g, "-")
1400
+ .replace(/\//g, "_")
1401
+ .replace(/=/g, "") as Base64Url;
1402
+ };
1403
+
1404
+ /** Decodes a {@link Base64Url} string to a Uint8Array. */
1405
+ export const base64UrlToUint8Array: (str: Base64Url) => Uint8Array =
1406
+ hasNodeBuffer
1407
+ ? (str: Base64Url) => {
1408
+ const nodeBuffer = globalThis.Buffer.from(str, "base64url");
1409
+ return new globalThis.Uint8Array(nodeBuffer);
1410
+ }
1411
+ : // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
1412
+ typeof (globalThis.Uint8Array as any)?.fromBase64 !== "undefined"
1413
+ ? (str: Base64Url) =>
1414
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
1415
+ (globalThis.Uint8Array as any).fromBase64(
1416
+ str,
1417
+ base64UrlOptions,
1418
+ ) as Uint8Array
1419
+ : (str: Base64Url) => {
1420
+ let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
1421
+ while (base64.length % 4 !== 0) {
1422
+ base64 += "=";
1423
+ }
1424
+ const binaryString = globalThis.atob(base64);
1425
+ return globalThis.Uint8Array.from(binaryString, (c) =>
1426
+ c.charCodeAt(0),
1427
+ );
1428
+ };
1245
1429
 
1246
1430
  /**
1247
- * Simple alphanumeric string for naming.
1431
+ * Simple alphanumeric string for naming in file systems, URLs, and identifiers.
1248
1432
  *
1249
- * A `SimpleName` string uses a limited, safe alphabet for naming purposes:
1250
- *
1251
- * - Uppercase letters (`A-Z`)
1252
- * - Lowercase letters (`a-z`)
1253
- * - Digits (`0-9`)
1254
- * - Dash (`-`)
1433
+ * Uses the same safe alphabet as {@link UrlSafeString} (letters, digits, `-`,
1434
+ * `_`). See `UrlSafeString` for details.
1255
1435
  *
1256
1436
  * The string must be between 1 and 42 characters.
1257
1437
  *
@@ -1268,18 +1448,13 @@ export type Base64UrlError = typeof Base64Url.Error;
1268
1448
  *
1269
1449
  * @category String
1270
1450
  */
1271
- export const SimpleName = regex("SimpleName", /^[a-z0-9-]{1,42}$/i)(String);
1451
+ export const SimpleName = brand("SimpleName", UrlSafeString, (value) =>
1452
+ value.length >= 1 && value.length <= 42
1453
+ ? ok(value)
1454
+ : err<SimpleNameError>({ type: "SimpleName", value }),
1455
+ );
1272
1456
  export type SimpleName = typeof SimpleName.Type;
1273
- export type SimpleNameError = typeof SimpleName.Error;
1274
-
1275
- /**
1276
- * Default NanoId.
1277
- *
1278
- * @category String
1279
- */
1280
- export const NanoId = regex("NanoId", /^[A-Za-z0-9_-]{21}$/)(String);
1281
- export type NanoId = typeof NanoId.Type;
1282
- export type NanoIdError = typeof NanoId.Error;
1457
+ export interface SimpleNameError extends TypeError<"SimpleName"> {}
1283
1458
 
1284
1459
  /**
1285
1460
  * Trimmed string between 8 and 64 characters, branded as `SimplePassword`.
@@ -1304,17 +1479,44 @@ export const formatSimplePasswordError = (
1304
1479
  );
1305
1480
 
1306
1481
  /**
1307
- * `Id` {@link Type}.
1482
+ * Globally unique identifier.
1483
+ *
1484
+ * **Evolu Id** is 16 random bytes from a cryptographically secure random
1485
+ * generator, encoded as 22-character Base64Url string. This provides strong
1486
+ * collision resistance for distributed ID generation.
1487
+ *
1488
+ * ### Design Rationale
1489
+ *
1490
+ * Why Evolu Id over alternatives:
1308
1491
  *
1309
- * Represents a unique identifier with exactly 21 characters, using NanoID's
1310
- * standard format (`A-Za-z0-9_-`).
1492
+ * - **NanoID**: No standard binary serialization format, and uses only ~126 bits
1493
+ * of entropy (21 characters from 64-symbol alphabet) compared to Evolu Id's
1494
+ * 128 bits.
1495
+ * - **UUID (v4)**: String format is 36 characters (with hyphens) compared to
1496
+ * Evolu Id's 22 characters. While UUIDs can be stored as 16 bytes, their
1497
+ * standard string representation is verbose.
1498
+ * - **UUID v7**: Includes timestamp in the ID, which leaks information about when
1499
+ * data was created. This is a privacy concern for local-first applications
1500
+ * where creation time must remain private.
1501
+ *
1502
+ * Evolu Id provides 128 bits of entropy, compact string representation (22
1503
+ * characters), standard and native string serialization (Base64Url), and no
1504
+ * privacy leaks.
1311
1505
  *
1312
1506
  * @category String
1313
1507
  */
1314
- export const Id = regex("Id", /^[A-Za-z0-9_-]{21}$/)(String);
1508
+ export const Id = brand("Id", String, (value) =>
1509
+ value.length === 22 && Base64Url.fromParent(value).ok
1510
+ ? ok(value)
1511
+ : err<IdError>({ type: "Id", value }),
1512
+ );
1315
1513
  export type Id = typeof Id.Type;
1316
1514
 
1317
- export const idTypeValueLength = 21;
1515
+ export interface IdError extends TypeError<"Id"> {}
1516
+
1517
+ export const formatIdError = createTypeErrorFormatter<IdError>(
1518
+ (error) => `Value ${error.value} is not a valid Id.`,
1519
+ );
1318
1520
 
1319
1521
  /**
1320
1522
  * Creates an {@link Id}.
@@ -1324,13 +1526,65 @@ export const idTypeValueLength = 21;
1324
1526
  * ```ts
1325
1527
  * // string & Brand<"Id">
1326
1528
  * const id = createId(deps);
1529
+ *
1530
+ * // string & Brand<"Id"> & Brand<"Todo">
1531
+ * const todoId = createId<"Todo">(deps);
1327
1532
  * ```
1328
1533
  */
1329
- export const createId = (deps: NanoIdLibDep): Id =>
1330
- deps.nanoIdLib.nanoid() as Id;
1534
+ export const createId = <B extends string = never>(
1535
+ deps: RandomBytesDep,
1536
+ ): [B] extends [never] ? Id : Id & Brand<B> =>
1537
+ uint8ArrayToBase64Url(deps.randomBytes.create(16)) as unknown as [B] extends [
1538
+ never,
1539
+ ]
1540
+ ? Id
1541
+ : Id & Brand<B>;
1331
1542
 
1332
1543
  /**
1333
- * Type Factory to create branded {@link Id} Type for a specific table.
1544
+ * Creates an {@link Id} from a string using SHA-256.
1545
+ *
1546
+ * When integrating with external systems that use different ID formats, use
1547
+ * this function to convert external IDs into valid Evolu IDs.
1548
+ *
1549
+ * In Evolu's CRDT, the ID serves as the unique identifier for conflict
1550
+ * resolution across distributed clients. When multiple clients create records
1551
+ * with the same external identifier, they must resolve to the same Evolu ID to
1552
+ * ensure data consistency.
1553
+ *
1554
+ * ### Example
1555
+ *
1556
+ * ```ts
1557
+ * // Both clients will generate the same ID
1558
+ * const id1 = createIdFromString("user-api-123");
1559
+ * const id2 = createIdFromString("user-api-123");
1560
+ * console.log(id1 === id2); // true
1561
+ *
1562
+ * upsert("todo", {
1563
+ * id: createIdFromString("external-todo-456"),
1564
+ * title: "Synced from external system",
1565
+ * });
1566
+ * ```
1567
+ *
1568
+ * **Important**: This transformation is one-way. We cannot recover the original
1569
+ * external string from the generated {@link Id}. If we need to preserve the
1570
+ * original external ID, store it in a separate column.
1571
+ *
1572
+ * @category String
1573
+ */
1574
+ export const createIdFromString = <B extends string = never>(
1575
+ value: string,
1576
+ ): [B] extends [never] ? Id : Id & Brand<B> => {
1577
+ const hash = sha256(utf8ToBytes(value));
1578
+ // Take first 16 bytes of hash and convert to Id
1579
+ const id = idBytesToId(hash.slice(0, 16) as IdBytes);
1580
+
1581
+ return id as [B] extends [never] ? Id : Id & Brand<B>;
1582
+ };
1583
+
1584
+ /**
1585
+ * Creates a branded {@link Id} Type for a table's primary key.
1586
+ *
1587
+ * The table name becomes an additional brand for type safety.
1334
1588
  *
1335
1589
  * ### Example
1336
1590
  *
@@ -1342,53 +1596,62 @@ export const createId = (deps: NanoIdLibDep): Id =>
1342
1596
  *
1343
1597
  * @category String
1344
1598
  */
1345
- export const id = <Table extends TypeName>(table: Table): IdType<Table> => {
1599
+ export const id = <Table extends TypeName>(table: Table): TableId<Table> => {
1600
+ const fromUnknown = (value: unknown) => {
1601
+ const parentResult = String.fromUnknown(value);
1602
+ if (!parentResult.ok) return parentResult;
1603
+ return fromParent(parentResult.value);
1604
+ };
1605
+
1346
1606
  const fromParent = (value: string) => {
1347
1607
  const idResult = Id.fromParent(value);
1348
1608
  if (!idResult.ok) {
1349
- return err<IdError<Table>>({ type: "Id", value, table });
1609
+ return err<TableIdError<Table>>({ type: "TableId", value, table });
1350
1610
  }
1351
1611
  return ok(idResult.value as Id & Brand<Table>);
1352
1612
  };
1353
1613
 
1354
- const fromUnknown = (value: unknown) => {
1355
- const parentResult = String.fromUnknown(value);
1356
- if (!parentResult.ok) return parentResult;
1357
- return fromParent(parentResult.value);
1358
- };
1359
-
1360
1614
  return {
1361
- ...createType("Id", {
1362
- fromUnknown,
1363
- to: (value: Id & Brand<Table>) => value as string,
1364
- fromParent,
1365
- toParent: (value: Id & Brand<Table>) => value as string,
1366
- }),
1615
+ ...createType("Id", { fromUnknown, fromParent }),
1367
1616
  table,
1368
1617
  };
1369
1618
  };
1370
1619
 
1371
- export interface IdType<Table extends TypeName>
1620
+ export interface TableId<Table extends TypeName>
1372
1621
  extends Type<
1373
1622
  "Id",
1374
1623
  string & Brand<"Id"> & Brand<Table>,
1375
1624
  string,
1376
- IdError<Table>,
1625
+ TableIdError<Table>,
1377
1626
  string,
1378
1627
  StringError
1379
1628
  > {
1380
1629
  table: Table;
1381
1630
  }
1382
1631
 
1383
- export interface IdError<Table extends TypeName = TypeName>
1384
- extends TypeError<"Id"> {
1632
+ export interface TableIdError<Table extends TypeName = TypeName>
1633
+ extends TypeError<"TableId"> {
1385
1634
  readonly table: Table;
1386
1635
  }
1387
1636
 
1388
- export const formatIdError = createTypeErrorFormatter<IdError>(
1637
+ export const formatTableIdError = createTypeErrorFormatter<TableIdError>(
1389
1638
  (error) => `Invalid ${error.type} table Id: ${error.value}`,
1390
1639
  );
1391
1640
 
1641
+ /** Binary representation of an {@link Id}. */
1642
+ export const IdBytes = brand("IdBytes", length(16)(Uint8Array));
1643
+ export type IdBytes = typeof IdBytes.Type;
1644
+
1645
+ export const idBytesTypeValueLength = 16 as NonNegativeInt;
1646
+
1647
+ export const idToIdBytes = (id: Id): IdBytes =>
1648
+ // Id is Base64Url (validated by Id.from), cast is safe
1649
+ base64UrlToUint8Array(id as unknown as Base64Url) as IdBytes;
1650
+
1651
+ export const idBytesToId = (idBytes: IdBytes): Id =>
1652
+ // Base64Url encoding of 16 bytes always produces valid Id (22 chars)
1653
+ uint8ArrayToBase64Url(idBytes) as unknown as Id;
1654
+
1392
1655
  /**
1393
1656
  * Positive number.
1394
1657
  *
@@ -1554,6 +1817,11 @@ export type NonNegativeInt = typeof NonNegativeInt.Type;
1554
1817
  export const PositiveInt = positive(NonNegativeInt);
1555
1818
  export type PositiveInt = typeof PositiveInt.Type;
1556
1819
 
1820
+ /** Maximum safe positive integer value for practically infinite operations. */
1821
+ export const maxPositiveInt = PositiveInt.orThrow(
1822
+ globalThis.Number.MAX_SAFE_INTEGER,
1823
+ );
1824
+
1557
1825
  /** @category Number */
1558
1826
  export const NonPositiveInt = nonPositive(Int);
1559
1827
  export type NonPositiveInt = typeof NonPositiveInt.Type;
@@ -1778,10 +2046,6 @@ export const formatBetweenError = createTypeErrorFormatter<BetweenError>(
1778
2046
  `The value ${error.value} is not between ${error.min} and ${error.max}, inclusive.`,
1779
2047
  );
1780
2048
 
1781
- /** @category Number */
1782
- export const Between1And10 = between(1, 10)(Number);
1783
- export type Between1And10 = typeof Between1And10.Type;
1784
-
1785
2049
  /**
1786
2050
  * Literal {@link Type}.
1787
2051
  *
@@ -1808,12 +2072,10 @@ export const literal = <T extends Literal>(expected: T): LiteralType<T> => {
1808
2072
  return {
1809
2073
  ...createType("Literal", {
1810
2074
  fromUnknown,
1811
- to: identity as (value: T) => WidenLiteral<T>,
1812
2075
  fromParent: ok,
1813
- toParent: identity,
1814
2076
  }),
1815
2077
  expected,
1816
- };
2078
+ } as LiteralType<T>;
1817
2079
  };
1818
2080
 
1819
2081
  export interface LiteralType<T extends Literal>
@@ -1833,141 +2095,6 @@ export const formatLiteralError = createTypeErrorFormatter<LiteralError>(
1833
2095
  )}.`,
1834
2096
  );
1835
2097
 
1836
- /**
1837
- * {@link Type} that transforms values between `FromType` and `ToType`.
1838
- *
1839
- * - `fromParent`: Converts `FromType` to `ToType`, may fail.
1840
- * - `toParent`: Converts `ToType` back to `FromType`, must not fail.
1841
- *
1842
- * ### Example
1843
- *
1844
- * // TODO: Examples
1845
- *
1846
- * @category Base Factories
1847
- */
1848
- export const transform = <
1849
- FromType extends AnyType,
1850
- ToType extends AnyType,
1851
- TransformError extends TypeError = never,
1852
- >(
1853
- fromType: FromType,
1854
- toType: ToType,
1855
- fromParent: (
1856
- parentValue: InferType<FromType>,
1857
- ) => Result<InferType<ToType>, TransformError>,
1858
- toParent: (value: InferType<ToType>) => InferType<FromType>,
1859
- ): TransformType<FromType, ToType, TransformError> => {
1860
- const fromUnknown = (
1861
- value: unknown,
1862
- ): Result<InferType<ToType>, InferErrors<FromType> | TransformError> => {
1863
- const parentResult = fromType.fromUnknown(value);
1864
- if (!parentResult.ok) return parentResult;
1865
- return fromParent(parentResult.value as IntentionalNever);
1866
- };
1867
-
1868
- const to = (value: InferType<ToType>): InferInput<FromType> =>
1869
- fromType.to(toParent(value)) as IntentionalNever;
1870
-
1871
- return {
1872
- ...createType("Transform", {
1873
- fromUnknown,
1874
- to,
1875
- fromParent,
1876
- toParent,
1877
- }),
1878
- is: toType.is,
1879
- fromType,
1880
- toType,
1881
- } as TransformType<FromType, ToType, TransformError>;
1882
- };
1883
-
1884
- /**
1885
- * TransformType extends {@link Type} with additional `fromType` and `toType`
1886
- * properties for reflection.
1887
- */
1888
- export interface TransformType<
1889
- FromType extends AnyType,
1890
- ToType extends AnyType,
1891
- TransformError extends TypeError = never,
1892
- > extends Type<
1893
- "Transform",
1894
- InferType<ToType>,
1895
- InferInput<FromType>,
1896
- TransformError,
1897
- InferType<FromType>,
1898
- InferErrors<FromType>
1899
- > {
1900
- readonly fromType: FromType;
1901
- readonly toType: ToType;
1902
-
1903
- readonly fromParent: (
1904
- value: InferType<FromType>,
1905
- ) => [TransformError] extends [never]
1906
- ? Ok<InferType<ToType>>
1907
- : Result<InferType<ToType>, TransformError>;
1908
- }
1909
-
1910
- /**
1911
- * Trims leading and trailing whitespace from a string.
1912
- *
1913
- * ### Example
1914
- *
1915
- * ```ts
1916
- * expect(TrimString.from("a ")).toEqual(ok("a"));
1917
- * expect(TrimString.fromParent("a ").value).toEqual("a");
1918
- * ```
1919
- *
1920
- * @category String
1921
- */
1922
- export const TrimString = trim(String);
1923
-
1924
- /**
1925
- * Transforms a {@link Date} into a {@link DateIsoString} string and vice versa.
1926
- *
1927
- * ### Example
1928
- *
1929
- * TODO:
1930
- *
1931
- * @category String
1932
- */
1933
- export const DateIso = transform(
1934
- Date,
1935
- DateIsoString,
1936
- (value) => DateIsoString.fromParent(value.toISOString()),
1937
- (value) => new globalThis.Date(value),
1938
- );
1939
-
1940
- /**
1941
- * Transforms a {@link NonEmptyTrimmedString} into a {@link FiniteNumber}.
1942
- *
1943
- * ### Example
1944
- *
1945
- * ```ts
1946
- * NumberFromString.from("42"); // ok(42)
1947
- * NumberFromString.from("abc"); // err({ type: "NumberFromString", value: "abc" })
1948
- * ```
1949
- *
1950
- * @category Number
1951
- */
1952
- export const NumberFromString = transform(
1953
- NonEmptyTrimmedString,
1954
- FiniteNumber,
1955
- (value) => {
1956
- const result = FiniteNumber.fromParent(globalThis.Number(value));
1957
- if (!result.ok)
1958
- return err<NumberFromStringError>({ type: "NumberFromString", value });
1959
- return result;
1960
- },
1961
- (num) => num.toString() as NonEmptyTrimmedString,
1962
- );
1963
-
1964
- export interface NumberFromStringError extends TypeError<"NumberFromString"> {}
1965
-
1966
- export const formatNumberFromStringError =
1967
- createTypeErrorFormatter<NumberFromStringError>((error) => {
1968
- return `The value ${error.value} could not be converted to a finite number.`;
1969
- });
1970
-
1971
2098
  /**
1972
2099
  * Array of a specific {@link Type}.
1973
2100
  *
@@ -2020,9 +2147,6 @@ export const array = <ElementType extends AnyType>(
2020
2147
  return ok(result);
2021
2148
  };
2022
2149
 
2023
- const to = (value: ReadonlyArray<InferType<ElementType>>) =>
2024
- value.map(element.to) as ReadonlyArray<InferInput<ElementType>>;
2025
-
2026
2150
  const fromParent = (
2027
2151
  value: ReadonlyArray<InferParent<ElementType>>,
2028
2152
  ): Result<
@@ -2048,18 +2172,10 @@ export const array = <ElementType extends AnyType>(
2048
2172
  return ok(result);
2049
2173
  };
2050
2174
 
2051
- const toParent = (values: ReadonlyArray<InferType<ElementType>>) =>
2052
- values.map(element.toParent) as ReadonlyArray<InferParent<ElementType>>;
2053
-
2054
2175
  return {
2055
- ...createType("Array", {
2056
- fromUnknown,
2057
- to,
2058
- fromParent,
2059
- toParent,
2060
- }),
2176
+ ...createType("Array", { fromUnknown, fromParent }),
2061
2177
  element,
2062
- } as ArrayType<ElementType>;
2178
+ };
2063
2179
  };
2064
2180
 
2065
2181
  /** ArrayType extends Type with an additional `element` property for reflection. */
@@ -2192,16 +2308,6 @@ export const record = <
2192
2308
  return ok(result);
2193
2309
  };
2194
2310
 
2195
- const to = (
2196
- value: Readonly<Record<KeyT, InferType<Value>>>,
2197
- ): Readonly<Record<KeyInput, InferInput<Value>>> =>
2198
- Object.fromEntries(
2199
- Object.entries(value).map(([key, val]) => [
2200
- keyType.to(key as KeyT),
2201
- valueType.to(val),
2202
- ]),
2203
- ) as Readonly<Record<KeyInput, InferInput<Value>>>;
2204
-
2205
2311
  const fromParent = (
2206
2312
  value: Readonly<Record<KeyParent, InferParent<Value>>>,
2207
2313
  ): Result<
@@ -2246,22 +2352,10 @@ export const record = <
2246
2352
  return ok(result);
2247
2353
  };
2248
2354
 
2249
- const toParent = (
2250
- value: Readonly<Record<KeyT, InferType<Value>>>,
2251
- ): Readonly<Record<KeyParent, InferParent<Value>>> =>
2252
- Object.fromEntries(
2253
- Object.entries(value).map(([key, val]) => [
2254
- keyType.toParent(key as KeyT),
2255
- valueType.toParent(val),
2256
- ]),
2257
- ) as Readonly<Record<KeyParent, InferParent<Value>>>;
2258
-
2259
2355
  return {
2260
2356
  ...createType("Record", {
2261
2357
  fromUnknown,
2262
- to,
2263
2358
  fromParent,
2264
- toParent,
2265
2359
  }),
2266
2360
  key: keyType,
2267
2361
  value: valueType,
@@ -2561,25 +2655,6 @@ export function object(
2561
2655
  return ok(result);
2562
2656
  };
2563
2657
 
2564
- const to = (value: any) => {
2565
- const entries: [string, any][] = [];
2566
- for (const key of propKeys) {
2567
- if (!(key in value) && isOptionalType(props[key])) {
2568
- continue;
2569
- }
2570
- entries.push([key, props[key].to(value[key])]);
2571
- }
2572
- if (record) {
2573
- const recordEntries = Object.entries(value).filter(
2574
- ([key]) => !propKeys.includes(key),
2575
- );
2576
- for (const [key, val] of recordEntries) {
2577
- entries.push([record.key.to(key), record.value.to(val)]);
2578
- }
2579
- }
2580
- return Object.fromEntries(entries);
2581
- };
2582
-
2583
2658
  const fromParent = (value: any): Result<any, any> => {
2584
2659
  const errors: Record<string, any> = {};
2585
2660
  const result: Record<string, any> = {};
@@ -2631,31 +2706,10 @@ export function object(
2631
2706
  return ok(result);
2632
2707
  };
2633
2708
 
2634
- const toParent = (value: any) => {
2635
- const entries: [string, any][] = [];
2636
- for (const key of propKeys) {
2637
- if (!(key in value) && isOptionalType(props[key])) {
2638
- continue;
2639
- }
2640
- entries.push([key, props[key].toParent(value[key])]);
2641
- }
2642
- if (record) {
2643
- const recordEntries = Object.entries(value).filter(
2644
- ([key]) => !propKeys.includes(key),
2645
- );
2646
- for (const [key, val] of recordEntries) {
2647
- entries.push([record.key.toParent(key), record.value.toParent(val)]);
2648
- }
2649
- }
2650
- return Object.fromEntries(entries);
2651
- };
2652
-
2653
2709
  return {
2654
2710
  ...createType(record ? "ObjectWithRecord" : "Object", {
2655
2711
  fromUnknown,
2656
- to,
2657
2712
  fromParent,
2658
- toParent,
2659
2713
  }),
2660
2714
  props,
2661
2715
  ...(record ? { record } : {}),
@@ -2897,7 +2951,6 @@ export function union(...args: ReadonlyArray<any>): any {
2897
2951
  * cases.
2898
2952
  */
2899
2953
 
2900
- /* eslint-disable */
2901
2954
  const members = args.map((arg) => (isType(arg) ? arg : literal(arg)));
2902
2955
 
2903
2956
  const fromUnknown = (value: unknown) => {
@@ -2915,23 +2968,13 @@ export function union(...args: ReadonlyArray<any>): any {
2915
2968
  });
2916
2969
  };
2917
2970
 
2918
- const to = (value: any) => {
2919
- for (const member of members) {
2920
- if (member.is(value)) return member.to(value);
2921
- }
2922
- assert(false, "No matching member found in Union Type `to` function");
2923
- };
2924
-
2925
2971
  return {
2926
2972
  ...createType("Union", {
2927
2973
  fromUnknown,
2928
- to,
2929
2974
  fromParent: fromUnknown,
2930
- toParent: to,
2931
2975
  }),
2932
2976
  members,
2933
2977
  };
2934
- /* eslint-enable */
2935
2978
  }
2936
2979
 
2937
2980
  /**
@@ -3025,20 +3068,10 @@ export const recursive = <ParentType extends AnyType>(
3025
3068
  type ??= create();
3026
3069
  return type.fromUnknown(value);
3027
3070
  },
3028
- to: (value: T) => {
3029
- type ??= create();
3030
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
3031
- return type.to(value);
3032
- },
3033
3071
  fromParent: (value: Parent) => {
3034
3072
  type ??= create();
3035
3073
  return type.fromParent(value);
3036
3074
  },
3037
- toParent: (value: T) => {
3038
- type ??= create();
3039
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
3040
- return type.toParent(value);
3041
- },
3042
3075
  is: (value: unknown): value is T => {
3043
3076
  type ??= create();
3044
3077
  return type.is(value);
@@ -3176,9 +3209,6 @@ export const tuple = <Elements extends [AnyType, ...ReadonlyArray<AnyType>]>(
3176
3209
  return ok(result);
3177
3210
  };
3178
3211
 
3179
- const to = (value: { [K in keyof Elements]: InferType<Elements[K]> }) =>
3180
- value.map((val, index) => elements[index].to(val) as IntentionalNever);
3181
-
3182
3212
  const fromParent = (value: {
3183
3213
  [K in keyof Elements]: InferParent<Elements[K]>;
3184
3214
  }) => {
@@ -3210,21 +3240,10 @@ export const tuple = <Elements extends [AnyType, ...ReadonlyArray<AnyType>]>(
3210
3240
  return ok(result as { [K in keyof Elements]: InferType<Elements[K]> });
3211
3241
  };
3212
3242
 
3213
- const toParent = (value: { [K in keyof Elements]: InferType<Elements[K]> }): {
3214
- [K in keyof Elements]: InferParent<Elements[K]>;
3215
- } =>
3216
- value.map(
3217
- (val, index) => elements[index].toParent(val) as IntentionalNever,
3218
- ) as {
3219
- [K in keyof Elements]: InferParent<Elements[K]>;
3220
- };
3221
-
3222
3243
  return {
3223
3244
  ...createType("Tuple", {
3224
3245
  fromUnknown,
3225
- to,
3226
3246
  fromParent,
3227
- toParent,
3228
3247
  }),
3229
3248
  elements,
3230
3249
  } as IntentionalNever;
@@ -3300,37 +3319,41 @@ export const formatInt64Error = createTypeErrorFormatter<Int64Error>(
3300
3319
  `The value ${error.value} is not a valid 64-bit signed integer (Int64).`,
3301
3320
  );
3302
3321
 
3303
- export const BigIntFromString = transform(
3304
- String,
3305
- BigInt,
3306
- (value) =>
3307
- trySync(
3308
- () => globalThis.BigInt(value),
3309
- (): BigIntFromStringError => ({ type: "BigIntFromString", value }),
3310
- ),
3311
- (value) => value.toString(),
3312
- );
3322
+ // // co s timhle? je to string, ze ktereho lze udelat bigint
3313
3323
 
3314
- export interface BigIntFromStringError extends TypeError<"BigIntFromString"> {}
3324
+ // export const BigIntFromString = transform(
3325
+ // String,
3326
+ // BigInt,
3327
+ // (value) =>
3328
+ // trySync(
3329
+ // () => globalThis.BigInt(value),
3330
+ // (): BigIntFromStringError => ({ type: "BigIntFromString", value }),
3331
+ // ),
3332
+ // (value) => value.toString(),
3333
+ // );
3315
3334
 
3316
- export const formatBigIntFromStringError =
3317
- createTypeErrorFormatter<BigIntFromStringError>(
3318
- (error) => `The value ${error.value} could not be converted to a BigInt.`,
3319
- );
3335
+ // export interface BigIntFromStringError extends TypeError<"BigIntFromString"> {}
3336
+
3337
+ // export const formatBigIntFromStringError =
3338
+ // createTypeErrorFormatter<BigIntFromStringError>(
3339
+ // (error) => `The value ${error.value} could not be converted to a BigInt.`,
3340
+ // );
3320
3341
 
3321
3342
  /**
3322
3343
  * Stringified {@link Int64}.
3323
3344
  *
3324
- * @category Number
3345
+ * @category String
3325
3346
  */
3326
- export const Int64String = brand("Int64", String, (value) => {
3327
- const bigint = BigIntFromString.fromParent(value);
3328
- if (bigint.ok) {
3329
- const int64 = Int64.fromParent(globalThis.BigInt(value));
3330
- if (int64.ok) return ok(value);
3331
- }
3332
- return err<Int64StringError>({ type: "Int64String", value });
3333
- });
3347
+ export const Int64String = brand("Int64", NonEmptyTrimmedString, (value) =>
3348
+ trySync(
3349
+ () => {
3350
+ const maybeInt = globalThis.BigInt(value);
3351
+ Int64.orThrow(maybeInt);
3352
+ return value;
3353
+ },
3354
+ (): Int64StringError => ({ type: "Int64String", value }),
3355
+ ),
3356
+ );
3334
3357
 
3335
3358
  export type Int64String = typeof Int64String.Type;
3336
3359
 
@@ -3435,42 +3458,14 @@ export const JsonArray = array(JsonValue);
3435
3458
  */
3436
3459
  export const JsonObject = record(String, JsonValue);
3437
3460
 
3438
- /**
3439
- * Transform Type that parses a JSON into a {@link JsonValue} and serializes a
3440
- * JsonValue back into a JSON string.
3441
- *
3442
- * ### Example
3443
- *
3444
- * ```ts
3445
- * JsonValueFromString.from(`{"key":"value"}`); // -> ok({ key: "value" })
3446
- * JsonValueFromString.to({ key: "value" }); // -> '{"key":"value"}'
3447
- * ```
3448
- *
3449
- * @category String
3450
- */
3451
- export const JsonValueFromString = transform(
3452
- String,
3453
- JsonValue,
3454
- (value) =>
3455
- trySync(
3456
- () => JSON.parse(value) as JsonValue,
3457
- (error): JsonValueFromStringError => ({
3458
- type: `JsonValueFromString`,
3459
- value,
3460
- message: globalThis.String(error),
3461
- }),
3462
- ),
3463
- (value) => JSON.stringify(value),
3464
- );
3465
-
3466
- export interface JsonValueFromStringError
3467
- extends TypeError<"JsonValueFromString"> {
3468
- readonly message: string;
3469
- }
3470
-
3471
- export const formatJsonValueFromStringError =
3472
- createTypeErrorFormatter<JsonValueFromStringError>(
3473
- (error) => `Invalid JSONValue: ${error.value}. Error: ${error.message}`,
3461
+ export const parseJson = (value: string): Result<JsonValue, JsonError> =>
3462
+ trySync(
3463
+ () => JSON.parse(value) as JsonValue,
3464
+ (error): JsonError => ({
3465
+ type: `Json`,
3466
+ value,
3467
+ message: globalThis.String(error),
3468
+ }),
3474
3469
  );
3475
3470
 
3476
3471
  /**
@@ -3479,20 +3474,15 @@ export const formatJsonValueFromStringError =
3479
3474
  * ### Example
3480
3475
  *
3481
3476
  * ```ts
3482
- * const result = Json.from('{"key":"value"}'); // -> ok('{"key":"value"}')
3483
- * const error = Json.from("invalid json"); // -> err({ type: "Json", value: "invalid json", message: "Unexpected token i in JSON at position 0" })
3477
+ * const result = Json.from('{"key":"value"}'); // ok
3478
+ * const error = Json.from("invalid json"); // err
3484
3479
  * ```
3485
3480
  *
3486
3481
  * @category String
3487
3482
  */
3488
3483
  export const Json = brand("Json", String, (value) => {
3489
- const result = JsonValueFromString.fromParent(value);
3490
- if (!result.ok)
3491
- return err<JsonError>({
3492
- type: "Json",
3493
- value,
3494
- message: result.error.message,
3495
- });
3484
+ const result = parseJson(value);
3485
+ if (!result.ok) return result;
3496
3486
  return ok(value);
3497
3487
  });
3498
3488
 
@@ -3506,6 +3496,80 @@ export const formatJsonError = createTypeErrorFormatter<JsonError>(
3506
3496
  (error) => `Invalid JSON: ${error.value}. Error: ${error.message}`,
3507
3497
  );
3508
3498
 
3499
+ export const jsonValueToJson = (value: JsonValue): Json =>
3500
+ JSON.stringify(value) as Json;
3501
+
3502
+ export const jsonToJsonValue = (value: Json): JsonValue =>
3503
+ JSON.parse(value) as JsonValue;
3504
+
3505
+ /**
3506
+ * Creates a branded JSON string {@link Type} and type-safe conversion functions
3507
+ * for a given Type.
3508
+ *
3509
+ * This factory creates:
3510
+ *
3511
+ * 1. A branded string Type that validates JSON parsing and structural conformity
3512
+ * 2. A serialization function (Type → branded JSON string)
3513
+ * 3. A parsing function (branded JSON string → Type, skipping validation)
3514
+ *
3515
+ * Optimized for Evolu's SQLite workflow where we store typed JSON strings and
3516
+ * need type-safe conversions without double parsing.
3517
+ *
3518
+ * ### Example
3519
+ *
3520
+ * ```ts
3521
+ * const Person = object({
3522
+ * name: NonEmptyString100,
3523
+ * age: FiniteNumber,
3524
+ * });
3525
+ * type Person = typeof Person.Type;
3526
+ *
3527
+ * const [PersonJson, personToPersonJson, personJsonToPerson] = json(
3528
+ * Person,
3529
+ * "PersonJson",
3530
+ * );
3531
+ * // string & Brand<"PersonJson">
3532
+ * type PersonJson = typeof PersonJson.Type;
3533
+ *
3534
+ * // Usage:
3535
+ * const person: Person = { name: "Alice", age: 30 };
3536
+ * const jsonString = personToPersonJson(person); // PersonJson
3537
+ * const backToPerson = personJsonToPerson(jsonString); // Person
3538
+ * ```
3539
+ */
3540
+ export const json = <T extends AnyType, Name extends TypeName>(
3541
+ type: T,
3542
+ name: Name,
3543
+ ): [
3544
+ BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>,
3545
+ (
3546
+ value: InferType<T>,
3547
+ ) => InferType<
3548
+ BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>
3549
+ >,
3550
+ (
3551
+ value: InferType<
3552
+ BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>
3553
+ >,
3554
+ ) => InferType<T>,
3555
+ ] => {
3556
+ const BrandedJsonType = brand(name, String, (value) => {
3557
+ const parseResult = parseJson(value);
3558
+ if (!parseResult.ok) return parseResult;
3559
+
3560
+ const validationResult = type.fromUnknown(parseResult.value);
3561
+ if (!validationResult.ok) return validationResult;
3562
+
3563
+ return ok(value);
3564
+ }) as BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>;
3565
+
3566
+ return [
3567
+ BrandedJsonType,
3568
+ jsonValueToJson as IntentionalNever,
3569
+ jsonToJsonValue as IntentionalNever,
3570
+ ];
3571
+ };
3572
+
3509
3573
  /**
3510
3574
  * Optional {@link Type}.
3511
3575
  *
@@ -3530,9 +3594,7 @@ export const formatJsonError = createTypeErrorFormatter<JsonError>(
3530
3594
  export const optional = <T extends AnyType>(type: T): OptionalType<T> => ({
3531
3595
  ...createType("Optional", {
3532
3596
  fromUnknown: type.fromUnknown,
3533
- to: type.to,
3534
3597
  fromParent: type.fromParent,
3535
- toParent: type.toParent,
3536
3598
  }),
3537
3599
  parent: type,
3538
3600
  });
@@ -3556,7 +3618,7 @@ export const isOptionalType = (x: unknown): x is OptionalType<any> =>
3556
3618
  /**
3557
3619
  * Creates a partial object type where all properties are optional.
3558
3620
  *
3559
- * This is useful when you want to validate an object in which none of the keys
3621
+ * This is useful when we want to validate an object in which none of the keys
3560
3622
  * are required, but if they are present they must conform to their
3561
3623
  * corresponding Types.
3562
3624
  *
@@ -3661,68 +3723,39 @@ export function omit<T extends ObjectType<any>, Keys extends keyof T["props"]>(
3661
3723
  return object(newProps);
3662
3724
  }
3663
3725
 
3726
+ export const maxMutationSize = 655360;
3727
+
3664
3728
  /**
3665
- * Creates a transform Type that serializes a given `Type` into a branded JSON
3666
- * string. The transformation is reversible, ensuring that we can safely parse
3667
- * it back.
3668
- *
3669
- * ### Example
3670
- *
3671
- * ```ts
3672
- * const Person = object({
3673
- * name: NonEmptyString50,
3674
- * age: FiniteNumber,
3675
- * });
3676
- * type Person = typeof Person.Type;
3677
- *
3678
- * const PersonJson = json(Person, "PersonJson");
3679
- * // string & Brand<"PersonJson">
3680
- * type PersonJson = typeof PersonJson.Type;
3681
- *
3682
- * // Person -> string & Brand<"PersonJson">
3683
- * const personJson = PersonJson.from({ name: "Alice", age: 30 });
3684
- * expect(personJson).toEqual(ok('{"name":"Alice","age":30}'));
3685
- *
3686
- * // string & Brand<"PersonJson"> -> Person
3687
- * const person = PersonJson.to(personJson);
3688
- *
3689
- * // serialize/parse any JSON value
3690
- * const AnyJson = json(JsonValue, "AnyJson");
3691
- * ```
3729
+ * Evolu has to limit the maximum mutation size. Otherwise, sync couldn't use
3730
+ * the `maxProtocolMessageRangesSize`. The max size is 640KB in bytes, measured
3731
+ * via MessagePack. Evolu Protocol DbChange will be smaller thanks to various
3732
+ * optimizations.
3692
3733
  */
3693
- export const json = <T extends AnyType, Name extends TypeName>(
3734
+ export const validMutationSize = <T extends AnyType>(
3694
3735
  type: T,
3695
- name: Name,
3696
- ): TransformType<
3697
- T,
3698
- BrandType<
3699
- typeof String,
3700
- Name,
3701
- JsonValueFromStringError | T["Errors"],
3702
- StringError
3703
- >
3704
- > => {
3705
- type E = JsonValueFromStringError | T["Errors"];
3706
-
3707
- const BrandedJsonString: BrandType<typeof String, Name, E, StringError> =
3708
- brand(name, String, (value) => {
3709
- const jsonValue = JsonValueFromString.fromParent(value);
3710
- if (!jsonValue.ok) return jsonValue as Err<E>;
3711
-
3712
- const parsed = type.fromUnknown(jsonValue.value);
3713
- if (!parsed.ok) return parsed as Err<E>;
3736
+ ): BrandType<T, "ValidMutationSize", ValidMutationSizeError, InferErrors<T>> =>
3737
+ brand("ValidMutationSize", type, (value) =>
3738
+ pack(value).byteLength <= maxMutationSize
3739
+ ? ok(value)
3740
+ : err<ValidMutationSizeError>({ type: "ValidMutationSize", value }),
3741
+ );
3714
3742
 
3715
- return ok(value as InferType<typeof BrandedJsonString>);
3716
- });
3743
+ export interface ValidMutationSizeError
3744
+ extends TypeError<"ValidMutationSize"> {}
3717
3745
 
3718
- return transform(
3719
- type,
3720
- BrandedJsonString,
3721
- (value) => ok(JSON.stringify(value) as InferType<typeof BrandedJsonString>),
3722
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
3723
- (value) => JSON.parse(value) as InferType<T>,
3746
+ export const formatValidMutationSizeError =
3747
+ createTypeErrorFormatter<ValidMutationSizeError>(
3748
+ (error) =>
3749
+ `The mutation size exceeds the maximum limit of ${maxMutationSize} bytes. The provided mutation has a size of ${pack(error.value).byteLength} bytes.`,
3724
3750
  );
3725
- };
3751
+
3752
+ export type ValidMutationSize<Props extends Record<string, AnyType>> =
3753
+ BrandType<
3754
+ ObjectType<Props>,
3755
+ "ValidMutationSize",
3756
+ ValidMutationSizeError,
3757
+ InferErrors<ObjectType<Props>>
3758
+ >;
3726
3759
 
3727
3760
  /**
3728
3761
  * Union of all `TypeError`s defined in the `Type.ts` file, including base type
@@ -3749,16 +3782,16 @@ export type TypeErrors<ExtraErrors extends TypeError = never> =
3749
3782
  | InstanceOfError
3750
3783
  | EvoluTypeError
3751
3784
  | CurrencyCodeError
3752
- | DateIsoStringError
3785
+ | DateIsoError
3753
3786
  | TrimmedError
3754
3787
  | MinLengthError
3755
3788
  | MaxLengthError
3756
3789
  | LengthError
3757
3790
  | MnemonicError
3758
3791
  | RegexError
3759
- | NanoIdError
3760
3792
  | SimplePasswordError
3761
3793
  | IdError
3794
+ | TableIdError
3762
3795
  | PositiveError
3763
3796
  | NegativeError
3764
3797
  | NonPositiveError
@@ -3774,10 +3807,9 @@ export type TypeErrors<ExtraErrors extends TypeError = never> =
3774
3807
  | BetweenError
3775
3808
  | LiteralError
3776
3809
  | Int64Error
3777
- | BigIntFromStringError
3778
3810
  | Int64StringError
3779
- | JsonValueFromStringError
3780
3811
  | JsonError
3812
+ | ValidMutationSizeError
3781
3813
  | ExtraErrors
3782
3814
  // Composite errors
3783
3815
  | ArrayError<TypeErrors<ExtraErrors>>
@@ -3792,70 +3824,74 @@ export type TypeErrors<ExtraErrors extends TypeError = never> =
3792
3824
  | TupleError<TypeErrors<ExtraErrors>>;
3793
3825
 
3794
3826
  /**
3795
- * Creates a unified error formatter that handles both Evolu Type's built-in
3796
- * {@link TypeErrors} and custom errors. It also lets us override the default
3797
- * formatting for specific errors.
3827
+ * Formats Evolu Type errors into user-friendly messages.
3798
3828
  *
3799
- * If you prefer not to reuse any built-in error formatters, you can write your
3800
- * own `formatTypeError` function from scratch.
3829
+ * Evolu Type typed errors ensure every error type must have a formatter.
3830
+ * TypeScript enforces this at compile-time, preventing unhandled validation
3831
+ * errors from reaching users.
3801
3832
  *
3802
- * ### Examples
3833
+ * The `createFormatTypeError` function handles both built-in {@link TypeErrors}
3834
+ * and custom errors, and lets us override default formatting for specific
3835
+ * errors.
3836
+ *
3837
+ * ### Example
3803
3838
  *
3804
3839
  * ```ts
3805
- * const formatError = createFormatTypeError();
3806
- * console.log(formatError({ type: "String", value: 42 }));
3807
- * // "A value 42 is not a string."
3840
+ * const formatTypeError = createFormatTypeError<
3841
+ * MinLengthError | MaxLengthError
3842
+ * >((error): string => {
3843
+ * switch (error.type) {
3844
+ * case "MinLength":
3845
+ * return `Text must be at least ${error.min} character${error.min === 1 ? "" : "s"} long`;
3846
+ * case "MaxLength":
3847
+ * return `Text is too long (maximum ${error.max} characters)`;
3848
+ * }
3849
+ * });
3808
3850
  * ```
3809
3851
  *
3810
- * A custom `formatTypeError` function:
3852
+ * Alternatively, write a custom formatter from scratch without using
3853
+ * `createFormatTypeError`. This gives us full control over error formatting:
3811
3854
  *
3812
3855
  * ```ts
3813
- * type AppErrors =
3814
- * | ValidMutationSizeError
3856
+ * const Person = object({
3857
+ * name: NonEmptyTrimmedString100,
3858
+ * age: optional(PositiveInt),
3859
+ * });
3860
+ *
3861
+ * // Define only the errors actually used by Person Type
3862
+ * type PersonErrors =
3815
3863
  * | StringError
3816
- * | MinLengthError
3817
3864
  * | MaxLengthError
3818
- * | NullError
3819
- * | IdError
3865
+ * | MinLengthError
3820
3866
  * | TrimmedError
3821
- * | MnemonicError
3822
- * | LiteralError
3823
- * // Composite errors
3824
- * | ObjectError<Record<string, AppErrors>>
3825
- * | UnionError<AppErrors>;
3826
- *
3827
- * const formatTypeError: TypeErrorFormatter<AppErrors> = (error) => {
3828
- * // In the real code, we would use the createTypeErrorFormatter helper
3829
- * // that safely stringifies error value.
3867
+ * | PositiveError
3868
+ * | NonNegativeError
3869
+ * | IntError
3870
+ * | NumberError
3871
+ * | ObjectError<Record<string, PersonErrors>>;
3872
+ *
3873
+ * const formatTypeError: TypeErrorFormatter<PersonErrors> = (error) => {
3830
3874
  * switch (error.type) {
3831
- * case "Id":
3832
- * return `Invalid Id on table: ${error.table}.`;
3833
- * case "MaxLength":
3834
- * return `Max length is ${error.max}.`;
3835
- * case "MinLength":
3836
- * return `Min length is ${error.min}.`;
3837
- * case "Mnemonic":
3838
- * return `Invalid mnemonic: ${String(error.value)}`;
3839
- * case "Null":
3840
- * return `Not null`;
3841
3875
  * case "String":
3842
- * // We can reuse existing formatter.
3843
3876
  * return formatStringError(error);
3877
+ * case "Number":
3878
+ * return "Must be a number";
3879
+ * case "MinLength":
3880
+ * return `Must be at least ${error.min} characters`;
3881
+ * case "MaxLength":
3882
+ * return `Cannot exceed ${error.max} characters`;
3844
3883
  * case "Trimmed":
3845
- * return "Value is not trimmed.";
3846
- * case "ValidMutationSize":
3847
- * return "A developer made an error, this should not happen.";
3848
- * case "Literal":
3849
- * return formatLiteralError(error);
3850
- * // Composite Types
3851
- * case "Union":
3852
- * return `Union errors: ${error.errors.map(formatTypeError).join(", ")}`;
3884
+ * return "Cannot have leading or trailing spaces";
3885
+ * case "Positive":
3886
+ * return "Must be a positive number";
3887
+ * case "NonNegative":
3888
+ * return "Must be zero or positive";
3889
+ * case "Int":
3890
+ * return "Must be an integer";
3853
3891
  * case "Object": {
3854
- * if (
3855
- * error.reason.kind === "ExtraKeys" ||
3856
- * error.reason.kind === "NotObject"
3857
- * )
3858
- * return "A developer made an error, this should not happen.";
3892
+ * if (error.reason.kind === "NotObject") return "Must be an object";
3893
+ * if (error.reason.kind === "ExtraKeys")
3894
+ * return "Contains unexpected fields";
3859
3895
  * const firstError = Object.values(error.reason.errors).find(
3860
3896
  * (e) => e !== undefined,
3861
3897
  * )!;
@@ -3902,8 +3938,8 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
3902
3938
  return formatIsTypeError(error);
3903
3939
  case "CurrencyCode":
3904
3940
  return formatCurrencyCodeError(error);
3905
- case "DateIsoString":
3906
- return formatDateIsoStringError(error);
3941
+ case "DateIso":
3942
+ return formatDateIsoError(error);
3907
3943
  case "Trimmed":
3908
3944
  return formatTrimmedError(error);
3909
3945
  case "MinLength":
@@ -3918,6 +3954,8 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
3918
3954
  return formatRegexError(error);
3919
3955
  case "Id":
3920
3956
  return formatIdError(error);
3957
+ case "TableId":
3958
+ return formatTableIdError(error);
3921
3959
  case "Positive":
3922
3960
  return formatPositiveError(error);
3923
3961
  case "Negative":
@@ -3948,14 +3986,12 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
3948
3986
  return formatLiteralError(error);
3949
3987
  case "Int64":
3950
3988
  return formatInt64Error(error);
3951
- case "BigIntFromString":
3952
- return formatBigIntFromStringError(error);
3953
3989
  case "Int64String":
3954
3990
  return formatInt64StringError(error);
3955
- case "JsonValueFromString":
3956
- return formatJsonValueFromStringError(error);
3957
3991
  case "Json":
3958
3992
  return formatJsonError(error);
3993
+ case "ValidMutationSize":
3994
+ return formatValidMutationSizeError(error);
3959
3995
  // Composite Types
3960
3996
  case "SimplePassword":
3961
3997
  return formatSimplePasswordError(formatTypeError)(error);