@evolu/common 6.0.1-preview.3 → 6.0.1-preview.30

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 (173) hide show
  1. package/dist/src/Array.d.ts +58 -5
  2. package/dist/src/Array.d.ts.map +1 -1
  3. package/dist/src/Array.js +53 -5
  4. package/dist/src/Assert.d.ts +6 -16
  5. package/dist/src/Assert.d.ts.map +1 -1
  6. package/dist/src/Assert.js +6 -18
  7. package/dist/src/Brand.d.ts +75 -0
  8. package/dist/src/Brand.d.ts.map +1 -0
  9. package/dist/src/Brand.js +1 -0
  10. package/dist/src/Buffer.d.ts +1 -1
  11. package/dist/src/Buffer.d.ts.map +1 -1
  12. package/dist/src/Buffer.js +8 -7
  13. package/dist/src/Cache.d.ts +44 -0
  14. package/dist/src/Cache.d.ts.map +1 -0
  15. package/dist/src/Cache.js +52 -0
  16. package/dist/src/Callbacks.d.ts +45 -12
  17. package/dist/src/Callbacks.d.ts.map +1 -1
  18. package/dist/src/Callbacks.js +14 -7
  19. package/dist/src/Console.d.ts +31 -6
  20. package/dist/src/Console.d.ts.map +1 -1
  21. package/dist/src/Console.js +72 -9
  22. package/dist/src/Crypto.d.ts +61 -34
  23. package/dist/src/Crypto.d.ts.map +1 -1
  24. package/dist/src/Crypto.js +32 -45
  25. package/dist/src/Evolu/Db.d.ts +158 -65
  26. package/dist/src/Evolu/Db.d.ts.map +1 -1
  27. package/dist/src/Evolu/Db.js +286 -694
  28. package/dist/src/Evolu/Diff.d.ts +3 -3
  29. package/dist/src/Evolu/Diff.d.ts.map +1 -1
  30. package/dist/src/Evolu/Diff.js +7 -5
  31. package/dist/src/Evolu/Evolu.d.ts +208 -133
  32. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  33. package/dist/src/Evolu/Evolu.js +188 -183
  34. package/dist/src/Evolu/Internal.d.ts +0 -2
  35. package/dist/src/Evolu/Internal.d.ts.map +1 -1
  36. package/dist/src/Evolu/Internal.js +0 -2
  37. package/dist/src/Evolu/LocalAuth.d.ts +150 -0
  38. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
  39. package/dist/src/Evolu/LocalAuth.js +174 -0
  40. package/dist/src/Evolu/Owner.d.ts +264 -120
  41. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  42. package/dist/src/Evolu/Owner.js +130 -104
  43. package/dist/src/Evolu/Platform.d.ts +9 -7
  44. package/dist/src/Evolu/Platform.d.ts.map +1 -1
  45. package/dist/src/Evolu/Protocol.d.ts +277 -232
  46. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  47. package/dist/src/Evolu/Protocol.js +603 -378
  48. package/dist/src/Evolu/Public.d.ts +6 -8
  49. package/dist/src/Evolu/Public.d.ts.map +1 -1
  50. package/dist/src/Evolu/Public.js +2 -3
  51. package/dist/src/Evolu/PublicKysely.js +3 -3
  52. package/dist/src/Evolu/Query.d.ts +2 -1
  53. package/dist/src/Evolu/Query.d.ts.map +1 -1
  54. package/dist/src/Evolu/Relay.d.ts +92 -7
  55. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  56. package/dist/src/Evolu/Relay.js +243 -76
  57. package/dist/src/Evolu/Schema.d.ts +129 -73
  58. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  59. package/dist/src/Evolu/Schema.js +169 -89
  60. package/dist/src/Evolu/Storage.d.ts +212 -26
  61. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  62. package/dist/src/Evolu/Storage.js +137 -79
  63. package/dist/src/Evolu/Sync.d.ts +68 -13
  64. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  65. package/dist/src/Evolu/Sync.js +422 -20
  66. package/dist/src/Evolu/Timestamp.d.ts +85 -27
  67. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  68. package/dist/src/Evolu/Timestamp.js +77 -18
  69. package/dist/src/Identicon.d.ts +35 -0
  70. package/dist/src/Identicon.d.ts.map +1 -0
  71. package/dist/src/Identicon.js +143 -0
  72. package/dist/src/Instances.d.ts +34 -0
  73. package/dist/src/Instances.d.ts.map +1 -0
  74. package/dist/src/Instances.js +44 -0
  75. package/dist/src/ManyToManyMap.d.ts +71 -10
  76. package/dist/src/ManyToManyMap.d.ts.map +1 -1
  77. package/dist/src/ManyToManyMap.js +41 -6
  78. package/dist/src/Number.d.ts +4 -3
  79. package/dist/src/Number.d.ts.map +1 -1
  80. package/dist/src/Number.js +5 -4
  81. package/dist/src/Platform.d.ts +20 -0
  82. package/dist/src/Platform.d.ts.map +1 -0
  83. package/dist/src/Platform.js +22 -0
  84. package/dist/src/Random.d.ts +3 -2
  85. package/dist/src/Random.d.ts.map +1 -1
  86. package/dist/src/Resources.d.ts +118 -0
  87. package/dist/src/Resources.d.ts.map +1 -0
  88. package/dist/src/Resources.js +197 -0
  89. package/dist/src/Result.d.ts +184 -52
  90. package/dist/src/Result.d.ts.map +1 -1
  91. package/dist/src/Result.js +30 -241
  92. package/dist/src/Skiplist.js +2 -1
  93. package/dist/src/Sqlite.d.ts +63 -5
  94. package/dist/src/Sqlite.d.ts.map +1 -1
  95. package/dist/src/Sqlite.js +110 -9
  96. package/dist/src/Task.d.ts +586 -0
  97. package/dist/src/Task.d.ts.map +1 -0
  98. package/dist/src/Task.js +469 -0
  99. package/dist/src/Time.d.ts +66 -1
  100. package/dist/src/Time.d.ts.map +1 -1
  101. package/dist/src/Time.js +99 -5
  102. package/dist/src/Type.d.ts +621 -340
  103. package/dist/src/Type.d.ts.map +1 -1
  104. package/dist/src/Type.js +665 -464
  105. package/dist/src/Types.d.ts +1 -75
  106. package/dist/src/Types.d.ts.map +1 -1
  107. package/dist/src/WebSocket.d.ts +5 -2
  108. package/dist/src/WebSocket.d.ts.map +1 -1
  109. package/dist/src/WebSocket.js +12 -18
  110. package/dist/src/Worker.d.ts +39 -11
  111. package/dist/src/Worker.d.ts.map +1 -1
  112. package/dist/src/Worker.js +22 -4
  113. package/dist/src/index.d.ts +7 -2
  114. package/dist/src/index.d.ts.map +1 -1
  115. package/dist/src/index.js +7 -2
  116. package/package.json +14 -13
  117. package/src/Array.ts +76 -11
  118. package/src/Assert.ts +6 -24
  119. package/src/Brand.ts +75 -0
  120. package/src/Buffer.ts +7 -7
  121. package/src/Cache.ts +85 -0
  122. package/src/Callbacks.ts +62 -22
  123. package/src/Console.ts +91 -11
  124. package/src/Crypto.ts +97 -82
  125. package/src/Evolu/Db.ts +514 -1020
  126. package/src/Evolu/Diff.ts +7 -5
  127. package/src/Evolu/Evolu.ts +464 -355
  128. package/src/Evolu/Internal.ts +0 -2
  129. package/src/Evolu/LocalAuth.ts +463 -0
  130. package/src/Evolu/Owner.ts +369 -228
  131. package/src/Evolu/Platform.ts +9 -9
  132. package/src/Evolu/Protocol.ts +859 -676
  133. package/src/Evolu/Public.ts +7 -14
  134. package/src/Evolu/PublicKysely.ts +3 -3
  135. package/src/Evolu/Query.ts +2 -1
  136. package/src/Evolu/Relay.ts +420 -92
  137. package/src/Evolu/Schema.ts +391 -191
  138. package/src/Evolu/Storage.ts +451 -118
  139. package/src/Evolu/Sync.ts +720 -37
  140. package/src/Evolu/Timestamp.ts +88 -35
  141. package/src/Identicon.ts +197 -0
  142. package/src/Instances.ts +90 -0
  143. package/src/ManyToManyMap.ts +124 -24
  144. package/src/Number.ts +6 -10
  145. package/src/Platform.ts +26 -0
  146. package/src/Random.ts +3 -2
  147. package/src/Resources.ts +367 -0
  148. package/src/Result.ts +191 -54
  149. package/src/Skiplist.ts +1 -1
  150. package/src/Sqlite.ts +122 -17
  151. package/src/Task.ts +901 -0
  152. package/src/Time.ts +180 -5
  153. package/src/Type.ts +1083 -727
  154. package/src/Types.ts +1 -77
  155. package/src/WebSocket.ts +27 -25
  156. package/src/Worker.ts +72 -23
  157. package/src/index.ts +7 -2
  158. package/dist/src/Evolu/Config.d.ts +0 -69
  159. package/dist/src/Evolu/Config.d.ts.map +0 -1
  160. package/dist/src/Evolu/Config.js +0 -9
  161. package/dist/src/Evolu/Kysely.d.ts +0 -6
  162. package/dist/src/Evolu/Kysely.d.ts.map +0 -1
  163. package/dist/src/Evolu/Kysely.js +0 -21
  164. package/dist/src/NanoId.d.ts +0 -27
  165. package/dist/src/NanoId.d.ts.map +0 -1
  166. package/dist/src/NanoId.js +0 -6
  167. package/dist/src/Promise.d.ts +0 -180
  168. package/dist/src/Promise.d.ts.map +0 -1
  169. package/dist/src/Promise.js +0 -176
  170. package/src/Evolu/Config.ts +0 -83
  171. package/src/Evolu/Kysely.ts +0 -38
  172. package/src/NanoId.ts +0 -39
  173. package/src/Promise.ts +0 -295
package/dist/src/Type.js CHANGED
@@ -1,80 +1,186 @@
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
+ * Evolu Type supports [Standard Schema](https://standardschema.dev/) for
9
+ * interoperability with 40+ validation-compatible tools and frameworks.
7
10
  *
8
- * Evolu Type exists because no existing validation/parsing/transformation
9
- * library fully met our needs:
11
+ * Why another validation library?
10
12
  *
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.
13
+ * - **Result-based error handling** no exceptions for normal control flow.
14
+ * - **Typed errors with decoupled formatters** – validation logic ≠ user
15
+ * messages.
16
+ * - **Consistent constraints via {@link Brand}** every constraint becomes part
17
+ * of the type.
18
+ * - **No user-land chaining DSL** – designed with the upcoming ES pipe operator
19
+ * in mind.
20
+ * - **Selective validation** parent validations are skipped when already proved
21
+ * by typing.
22
+ * - **Simple, top-down implementation** – readable source code from top to bottom
23
+ * with no hidden magic; just plain functions and composition.
22
24
  *
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.
25
+ * ### Base Types Quick Start
27
26
  *
28
- * - Evolu `Type` is:
27
+ * ```ts
28
+ * // Validate unknown values
29
+ * const value: unknown = "hello";
30
+ * const stringResult = String.fromUnknown(value);
31
+ * if (!stringResult.ok) {
32
+ * // console.error(formatStringError(stringResult.error));
33
+ * return stringResult; // inside a function returning Result<string, _>
34
+ * }
35
+ * // Safe branch: value is now string
36
+ * const upper = stringResult.value.toUpperCase();
37
+ *
38
+ * // Type guard style
39
+ * if (String.is(value)) {
40
+ * // narrowed to string
41
+ * }
42
+ *
43
+ * // Composing: arrays & objects
44
+ * const Numbers = array(Number); // ReadonlyArray<number>
45
+ * const Point = object({ x: Number, y: Number });
46
+ *
47
+ * Numbers.from([1, 2, 3]); // ok
48
+ * Point.from({ x: 1, y: 2 }); // ok
49
+ * Point.from({ x: 1, y: "2" }); // err -> nested Number error
50
+ * ```
51
+ *
52
+ * ### Branding Basics
53
+ *
54
+ * Branding adds semantic meaning & constraints while preserving the runtime
55
+ * shape:
56
+ *
57
+ * ```ts
58
+ * const CurrencyCode = brand("CurrencyCode", String, (value) =>
59
+ * /^[A-Z]{3}$/.test(value)
60
+ * ? ok(value)
61
+ * : err<CurrencyCodeError>({ type: "CurrencyCode", value }),
62
+ * );
63
+ * type CurrencyCode = typeof CurrencyCode.Type; // string & Brand<"CurrencyCode">
64
+ *
65
+ * interface CurrencyCodeError extends TypeError<"CurrencyCode"> {}
66
+ *
67
+ * const formatCurrencyCodeError =
68
+ * createTypeErrorFormatter<CurrencyCodeError>(
69
+ * (error) => `Invalid currency code: ${error.value}`,
70
+ * );
71
+ *
72
+ * const r = CurrencyCode.from("USD"); // ok("USD")
73
+ * const e = CurrencyCode.from("usd"); // err(...)
74
+ * ```
75
+ *
76
+ * See also reusable brand factories like `minLength`, `maxLength`, `trimmed`,
77
+ * `positive`, `between`, etc.
78
+ *
79
+ * ### Objects & Optional Fields
80
+ *
81
+ * ```ts
82
+ * const User = object({
83
+ * name: NonEmptyTrimmedString100,
84
+ * age: optional(PositiveInt),
85
+ * });
86
+ * type User = typeof User.Type;
29
87
  *
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.
88
+ * User.from({ name: "Alice" }); // ok
89
+ * User.from({ name: "Alice", age: -1 }); // err(PositiveInt)
90
+ * ```
91
+ *
92
+ * ### Deriving JSON String Types
93
+ *
94
+ * ```ts
95
+ * const Person = object({
96
+ * name: NonEmptyString50,
97
+ * // Did you know that JSON.stringify converts NaN (a number) into null?
98
+ * // To prevent this, use FiniteNumber.
99
+ * age: FiniteNumber,
100
+ * });
101
+ * type Person = typeof Person.Type;
102
+ *
103
+ * const [PersonJson, personToPersonJson, personJsonToPerson] = json(
104
+ * Person,
105
+ * "PersonJson",
106
+ * );
107
+ * // string & Brand<"PersonJson">
108
+ * type PersonJson = typeof PersonJson.Type;
109
+ *
110
+ * const person = Person.orThrow({
111
+ * name: "Alice",
112
+ * age: 30,
113
+ * });
34
114
  *
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:
115
+ * const personJson = personToPersonJson(person);
116
+ * expect(personJsonToPerson(personJson)).toEqual(person);
117
+ * ```
39
118
  *
40
- * `Unknown` -> `String` -> `TrimmedString` -> `NonEmptyTrimmedString100`
119
+ * ### Error Formatting
41
120
  *
42
- * For `NonEmptyTrimmedString100`, the parent Type is `TrimmedString`. For
43
- * `TrimmedString`, the parent Type is `String`.
121
+ * Evolu separates validation logic from human-readable messages. There are two
122
+ * layers:
44
123
  *
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.
124
+ * 1. Per-type formatters (e.g. `formatStringError`) simple, focused, already
125
+ * used earlier in the quick start example.
126
+ * 2. A unified formatter via `createFormatTypeError` composes all built-in and
127
+ * custom errors (including nested composite types) and lets us override
128
+ * selected messages.
48
129
  *
49
- * Speaking of `fromParent` and `toParent`, those functions exist to bypass
50
- * parent Types when we can rely on TypeScript types.
130
+ * #### 1. Per-Type Formatter (recap)
51
131
  *
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")`.
132
+ * ```ts
133
+ * const r = String.fromUnknown(42);
134
+ * if (!r.ok) console.error(formatStringError(r.error));
135
+ * ```
136
+ *
137
+ * #### 2. Unified Formatter with Overrides
138
+ *
139
+ * ```ts
140
+ * // Override only what we care about; fall back to built-ins for the rest.
141
+ * const formatTypeError = createFormatTypeError((error) => {
142
+ * if (error.type === "MinLength") return `Min length is ${error.min}`;
143
+ * });
144
+ *
145
+ * const User = object({ name: NonEmptyTrimmedString100 });
146
+ * const resultUser = User.from({ name: "" });
147
+ * if (!resultUser.ok) console.error(formatTypeError(resultUser.error));
148
+ *
149
+ * const badPoint = object({ x: Number, y: Number }).from({
150
+ * x: 1,
151
+ * y: "foo",
152
+ * });
153
+ * if (!badPoint.ok) console.error(formatTypeError(badPoint.error));
154
+ * ```
155
+ *
156
+ * The unified formatter walks nested structures (object / array / record /
157
+ * tuple / union) and applies overrides only where specified, greatly reducing
158
+ * boilerplate when formatting complex validation errors.
57
159
  *
58
160
  * ### Tip
59
161
  *
60
162
  * If necessary, write `globalThis.String` instead of `String` to avoid naming
61
- * clashes with Base Types.
163
+ * clashes with native types.
62
164
  *
63
- * ### Design Decision:
165
+ * ### Design Decision: No Bidirectional Transformations
64
166
  *
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.
167
+ * Evolu Type intentionally does not support bidirectional transformations. It
168
+ * previously did, but supporting that while keeping typed error fidelity added
169
+ * complexity that hurt readability & reliability. Most persistence pipelines
170
+ * (e.g. SQLite) already require explicit mapping of query results, so implicit
171
+ * reverse transforms would not buy much. We may revisit this if we can design a
172
+ * minimal, 100% safe API that preserves simplicity.
69
173
  *
70
174
  * @module
71
175
  */
176
+ import { utf8ToBytes } from "@noble/ciphers/utils.js";
177
+ import { sha256 } from "@noble/hashes/sha2.js";
72
178
  import * as bip39 from "@scure/bip39";
73
- import { wordlist } from "@scure/bip39/wordlists/english";
74
- import { assert } from "./Assert.js";
75
- import { identity } from "./Function.js";
179
+ import { wordlist } from "@scure/bip39/wordlists/english.js";
180
+ import { pack } from "msgpackr";
76
181
  import { isPlainObject } from "./Object.js";
77
- import { err, ok, trySync } from "./Result.js";
182
+ import { hasNodeBuffer } from "./Platform.js";
183
+ import { err, getOrNull, getOrThrow, ok, trySync } from "./Result.js";
78
184
  import { safelyStringifyUnknownValue } from "./String.js";
79
185
  const EvoluTypeSymbol = Symbol("evolu.Type");
80
186
  /**
@@ -88,6 +194,8 @@ const createType = (name, definition) => ({
88
194
  name,
89
195
  is: (value) => definition.fromUnknown(value).ok,
90
196
  from: definition.fromUnknown,
197
+ orThrow: (value) => getOrThrow(definition.fromUnknown(value)),
198
+ orNull: (value) => getOrNull(definition.fromUnknown(value)),
91
199
  [EvoluTypeSymbol]: true,
92
200
  Type: undefined,
93
201
  Input: undefined,
@@ -95,6 +203,24 @@ const createType = (name, definition) => ({
95
203
  Parent: undefined,
96
204
  ParentError: undefined,
97
205
  Errors: undefined,
206
+ "~standard": {
207
+ version: 1,
208
+ vendor: "evolu",
209
+ validate: (value) => {
210
+ const result = definition.fromUnknown(value);
211
+ if (result.ok) {
212
+ return { value: result.value };
213
+ }
214
+ cachedStandardSchemaFormatTypeError ??= createFormatTypeError();
215
+ return {
216
+ issues: typeErrorToStandardSchemaIssues(result.error, cachedStandardSchemaFormatTypeError),
217
+ };
218
+ },
219
+ types: {
220
+ input: undefined,
221
+ output: undefined,
222
+ },
223
+ },
98
224
  });
99
225
  /**
100
226
  * Creates a formatter function for {@link TypeError}.
@@ -117,12 +243,6 @@ export const createTypeErrorFormatter = (format) => (error) => format({ ...error
117
243
  * Base {@link Type}.
118
244
  *
119
245
  * A Base Type validates that a value conforms to a specific TypeScript type.
120
- * Unlike refinements or transformations, Base Types establish the fundamental
121
- * shape of a value before any branding or transformation occurs.
122
- *
123
- * - To **refine** a Base Type further, use the {@link brand} Type Factory.
124
- * - To **transform** a Base Type into a different representation, use the
125
- * {@link transform} Type Factory.
126
246
  *
127
247
  * ### Example
128
248
  *
@@ -144,9 +264,7 @@ export const createTypeErrorFormatter = (format) => (error) => format({ ...error
144
264
  */
145
265
  export const base = (name, fromUnknown) => createType(name, {
146
266
  fromUnknown,
147
- to: identity,
148
267
  fromParent: (ok), // `fromParent` relies on types, so it can't fail for the Base Type
149
- toParent: identity,
150
268
  });
151
269
  /**
152
270
  * Creates a formatter function for a base {@link TypeError}.
@@ -233,7 +351,7 @@ export const instanceOf = (ctor) => ({
233
351
  : err({ type: "InstanceOf", value, ctor: ctor.name })),
234
352
  ctor,
235
353
  });
236
- export const formatInstanceOfError = createTypeErrorFormatter((error) => `Value ${error.value} is not an instance of ${error.ctor}`);
354
+ export const formatInstanceOfError = createTypeErrorFormatter((error) => `The value ${error.value} is not an instance of ${error.ctor}.`);
237
355
  /**
238
356
  * JavaScript Date.
239
357
  *
@@ -280,9 +398,7 @@ export function brand(name, parent, refine) {
280
398
  return {
281
399
  ...createType("Brand", {
282
400
  fromUnknown,
283
- to: identity,
284
401
  fromParent: (refine ?? ok),
285
- toParent: identity,
286
402
  }),
287
403
  brand: name,
288
404
  parentType: parent,
@@ -296,7 +412,7 @@ export function brand(name, parent, refine) {
296
412
  export const CurrencyCode = brand("CurrencyCode", String, (value) => /^[A-Z]{3}$/.test(value)
297
413
  ? ok(value)
298
414
  : err({ type: "CurrencyCode", value }));
299
- export const formatCurrencyCodeError = createTypeErrorFormatter((error) => `Invalid currency code: ${error.value}`);
415
+ export const formatCurrencyCodeError = createTypeErrorFormatter((error) => `Invalid currency code: ${error.value}.`);
300
416
  /**
301
417
  * ISO 8601 date-time string.
302
418
  *
@@ -312,37 +428,39 @@ export const formatCurrencyCodeError = createTypeErrorFormatter((error) => `Inva
312
428
  * ### Example
313
429
  *
314
430
  * ```ts
315
- * const result = DateIsoString.from("2023-01-01T12:00:00.000Z"); // ok
316
- * const error = DateIsoString.from("10000-01-01T00:00:00.000Z"); // err
431
+ * const result = DateIso.from("2023-01-01T12:00:00.000Z"); // ok
432
+ * const error = DateIso.from("10000-01-01T00:00:00.000Z"); // err
317
433
  * ```
318
434
  *
319
435
  * @category String
320
436
  */
321
- export const DateIsoString = brand("DateIso", String, (value) => {
437
+ export const DateIso = brand("DateIso", String, (value) => {
322
438
  if (value.length !== 24) {
323
- return err({ type: "DateIsoString", value });
439
+ return err({ type: "DateIso", value });
324
440
  }
325
- if (isNaN(globalThis.Date.parse(value))) {
326
- return err({ type: "DateIsoString", value });
441
+ const parsed = globalThis.Date.parse(value);
442
+ if (isNaN(parsed)) {
443
+ return err({ type: "DateIso", value });
444
+ }
445
+ // Round-trip test: ensure the string is actually a proper ISO format
446
+ const roundTrip = new globalThis.Date(parsed).toISOString();
447
+ if (roundTrip !== value) {
448
+ return err({ type: "DateIso", value });
327
449
  }
328
450
  return ok(value);
329
451
  });
330
- export const formatDateIsoStringError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid ISO 8601 date string.`);
452
+ export const formatDateIsoError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid ISO 8601 date string.`);
453
+ export const dateToDateIso = (value) => DateIso.fromParent(value.toISOString());
454
+ export const dateIsoToDate = (value) => new globalThis.Date(value);
331
455
  /**
332
456
  * Trimmed string.
333
457
  *
334
- * This Type Factory does not transform; it only validates whether a string has
335
- * no leading or trailing whitespaces. To trim a string, use {@link trim} Type
336
- * Factory.
458
+ * This Type Factory validates whether a string has no leading or trailing
459
+ * whitespaces.
337
460
  *
338
- * ### Examples
461
+ * ### Example
339
462
  *
340
463
  * ```ts
341
- * // this Type already exists
342
- * const TrimmedString = trimmed(String);
343
- * type TrimmedString = typeof TrimmedString.Type;
344
- *
345
- * // we can make any branded Type trimmed:
346
464
  * const TrimmedNonEmptyString = trimmed(minLength(1)(String));
347
465
  * // string & Brand<"MinLength1"> & Brand<"Trimmed">
348
466
  * type TrimmedNonEmptyString = typeof TrimmedNonEmptyString.Type;
@@ -353,33 +471,7 @@ export const formatDateIsoStringError = createTypeErrorFormatter((error) => `The
353
471
  export const trimmed = (parent) => brand("Trimmed", parent, (value) => value.trim().length === value.length
354
472
  ? ok(value)
355
473
  : err({ type: "Trimmed", value }));
356
- export const formatTrimmedError = createTypeErrorFormatter((error) => `A value ${error.value} is not trimmed`);
357
- /**
358
- * Trims leading and trailing whitespace from a string.
359
- *
360
- * This Type Factory **transforms** the input string by removing whitespace from
361
- * both ends. For validation only, use {@link trimmed} Type Factory.
362
- *
363
- * ### Example
364
- *
365
- * ```ts
366
- * const TrimString = trim(String);
367
- * expect(TrimString.from("a ")).toEqual(ok("a"));
368
- * expect(TrimString.fromParent("a ").value).toEqual("a");
369
- *
370
- * const TrimNonEmptyString = trim(NonEmptyString);
371
- * expect(TrimNonEmptyString.from("a " as NonEmptyString)).toEqual(ok("a"));
372
- * expect(
373
- * TrimNonEmptyString.fromParent("a " as NonEmptyString).value,
374
- * ).toEqual("a");
375
- * ```
376
- *
377
- * **Note:** This transformation is irreversible. Calling `toParent` will not
378
- * restore the original representation.
379
- *
380
- * @category String
381
- */
382
- export const trim = (parent) => transform(parent, trimmed(parent), (value) => ok(value.trim()), (value) => value);
474
+ export const formatTrimmedError = createTypeErrorFormatter((error) => `The value ${error.value} must be trimmed.`);
383
475
  /**
384
476
  * Trimmed string
385
477
  *
@@ -389,6 +481,7 @@ export const trim = (parent) => transform(parent, trimmed(parent), (value) => ok
389
481
  * @category String
390
482
  */
391
483
  export const TrimmedString = trimmed(String);
484
+ export const trim = (value) => value.trim();
392
485
  /**
393
486
  * Minimum length.
394
487
  *
@@ -403,7 +496,7 @@ export const TrimmedString = trimmed(String);
403
496
  * @category Array
404
497
  */
405
498
  export const minLength = (min) => (parent) => brand(`MinLength${min}`, parent, (value) => value.length >= min ? ok(value) : err({ type: "MinLength", value, min }));
406
- export const formatMinLengthError = createTypeErrorFormatter((error) => `Value ${error.value} does not meet the minimum length of ${error.min}.`);
499
+ export const formatMinLengthError = createTypeErrorFormatter((error) => `The value ${error.value} does not meet the minimum length of ${error.min}.`);
407
500
  /**
408
501
  * Maximum length.
409
502
  *
@@ -418,7 +511,7 @@ export const formatMinLengthError = createTypeErrorFormatter((error) => `Value $
418
511
  * @category Array
419
512
  */
420
513
  export const maxLength = (max) => (parent) => brand(`MaxLength${max}`, parent, (value) => value.length <= max ? ok(value) : err({ type: "MaxLength", value, max }));
421
- export const formatMaxLengthError = createTypeErrorFormatter((error) => `Value ${error.value} exceeds the maximum length of ${error.max}.`);
514
+ export const formatMaxLengthError = createTypeErrorFormatter((error) => `The value ${error.value} exceeds the maximum length of ${error.max}.`);
422
515
  /**
423
516
  * Exact length.
424
517
  *
@@ -435,7 +528,7 @@ export const formatMaxLengthError = createTypeErrorFormatter((error) => `Value $
435
528
  export const length = (exact) => (parent) => brand(`Length${exact}`, parent, (value) => value.length === exact
436
529
  ? ok(value)
437
530
  : err({ type: "Length", value, exact }));
438
- export const formatLengthError = createTypeErrorFormatter((error) => `Value ${error.value} does not have the required length of ${error.exact}.`);
531
+ export const formatLengthError = createTypeErrorFormatter((error) => `The value ${error.value} does not have the required length of ${error.exact}.`);
439
532
  /** @category String */
440
533
  export const NonEmptyString = minLength(1)(String);
441
534
  /** @category String */
@@ -468,7 +561,7 @@ export const NonEmptyTrimmedString1000 = minLength(1)(TrimmedString1000);
468
561
  export const Mnemonic = brand("Mnemonic", NonEmptyTrimmedString, (value) => bip39.validateMnemonic(value, wordlist)
469
562
  ? ok(value)
470
563
  : err({ type: "Mnemonic", value }));
471
- export const formatMnemonicError = createTypeErrorFormatter((error) => `Invalid BIP39 mnemonic: ${error.value}`);
564
+ export const formatMnemonicError = createTypeErrorFormatter((error) => `Invalid BIP39 mnemonic: ${error.value}.`);
472
565
  /**
473
566
  * String matching a regular expression.
474
567
  *
@@ -492,11 +585,11 @@ export const regex = (name, pattern) => {
492
585
  : err({ type: "Regex", name, value, pattern });
493
586
  });
494
587
  };
495
- export const formatRegexError = createTypeErrorFormatter((error) => `Value ${error.value} does not match the pattern for ${error.name}: ${error.pattern}`);
588
+ export const formatRegexError = createTypeErrorFormatter((error) => `The value ${error.value} does not match the pattern for ${error.name}: ${error.pattern}.`);
496
589
  /**
497
- * URL-safe Base64 string.
590
+ * URL-safe string.
498
591
  *
499
- * A `Base64Url` string uses a limited alphabet that is URL-safe:
592
+ * A `UrlSafeString` uses a limited alphabet that is safe for URLs:
500
593
  *
501
594
  * - Uppercase letters (`A-Z`)
502
595
  * - Lowercase letters (`a-z`)
@@ -504,29 +597,88 @@ export const formatRegexError = createTypeErrorFormatter((error) => `Value ${err
504
597
  * - Dash (`-`)
505
598
  * - Underscore (`_`)
506
599
  *
600
+ * This is the same character set used by Base64Url encoding, but this type does
601
+ * not validate that the string is actually Base64Url-encoded data.
602
+ *
507
603
  * ### Example
508
604
  *
509
605
  * ```ts
510
- * const result = Base64Url.from("abc123_-");
606
+ * const result = UrlSafeString.from("abc123_-");
511
607
  * if (result.ok) {
512
- * console.log("Valid Base64Url string:", result.value);
608
+ * console.log("Valid URL-safe string:", result.value);
513
609
  * } else {
514
- * console.error("Invalid Base64Url string:", result.error);
610
+ * console.error("Invalid URL-safe string:", result.error);
515
611
  * }
516
612
  * ```
517
613
  *
518
614
  * @category String
519
615
  */
520
- export const Base64Url = regex("Base64Url", /^[A-Za-z0-9_-]+$/)(String);
616
+ export const UrlSafeString = regex("UrlSafeString", /^[A-Za-z0-9_-]+$/)(String);
521
617
  /**
522
- * Simple alphanumeric string for naming.
618
+ * Base64Url without padding.
523
619
  *
524
- * A `SimpleName` string uses a limited, safe alphabet for naming purposes:
620
+ * Encode with {@link uint8ArrayToBase64Url}, decode with
621
+ * {@link base64UrlToUint8Array}.
525
622
  *
526
- * - Uppercase letters (`A-Z`)
527
- * - Lowercase letters (`a-z`)
528
- * - Digits (`0-9`)
529
- * - Dash (`-`)
623
+ * @category String
624
+ */
625
+ export const Base64Url = brand("Base64Url", String, (value) => {
626
+ // Round-trip validation ensures consistency across different base64url
627
+ // implementations (Node.js Buffer, native browser API, manual fallback).
628
+ // Only strings that decode and encode identically are accepted.
629
+ let roundTrip;
630
+ try {
631
+ roundTrip = uint8ArrayToBase64Url(base64UrlToUint8Array(value));
632
+ }
633
+ catch {
634
+ //
635
+ }
636
+ return roundTrip === value
637
+ ? ok(value)
638
+ : err({ type: "Base64Url", value });
639
+ });
640
+ export const formatBase64UrlError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid Base64Url string.`);
641
+ const base64UrlOptions = { alphabet: "base64url", omitPadding: true };
642
+ /** Encodes a Uint8Array to a {@link Base64Url} string. */
643
+ export const uint8ArrayToBase64Url = hasNodeBuffer
644
+ ? (bytes) => globalThis.Buffer.from(bytes).toString("base64url")
645
+ : // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
646
+ typeof globalThis.Uint8Array.prototype?.toBase64 !== "undefined"
647
+ ? (bytes) =>
648
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
649
+ bytes.toBase64(base64UrlOptions)
650
+ : (bytes) => {
651
+ const binaryString = Array.from(bytes, (byte) => globalThis.String.fromCodePoint(byte)).join("");
652
+ const base64 = globalThis.btoa(binaryString);
653
+ return base64
654
+ .replace(/\+/g, "-")
655
+ .replace(/\//g, "_")
656
+ .replace(/=/g, "");
657
+ };
658
+ /** Decodes a {@link Base64Url} string to a Uint8Array. */
659
+ export const base64UrlToUint8Array = hasNodeBuffer
660
+ ? (str) => {
661
+ const nodeBuffer = globalThis.Buffer.from(str, "base64url");
662
+ return new globalThis.Uint8Array(nodeBuffer);
663
+ }
664
+ : // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
665
+ typeof globalThis.Uint8Array?.fromBase64 !== "undefined"
666
+ ? (str) =>
667
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
668
+ globalThis.Uint8Array.fromBase64(str, base64UrlOptions)
669
+ : (str) => {
670
+ let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
671
+ while (base64.length % 4 !== 0) {
672
+ base64 += "=";
673
+ }
674
+ const binaryString = globalThis.atob(base64);
675
+ return globalThis.Uint8Array.from(binaryString, (c) => c.charCodeAt(0));
676
+ };
677
+ /**
678
+ * Simple alphanumeric string for naming in file systems, URLs, and identifiers.
679
+ *
680
+ * Uses the same safe alphabet as {@link UrlSafeString} (letters, digits, `-`,
681
+ * `_`). See `UrlSafeString` for details.
530
682
  *
531
683
  * The string must be between 1 and 42 characters.
532
684
  *
@@ -543,13 +695,9 @@ export const Base64Url = regex("Base64Url", /^[A-Za-z0-9_-]+$/)(String);
543
695
  *
544
696
  * @category String
545
697
  */
546
- export const SimpleName = regex("SimpleName", /^[a-z0-9-]{1,42}$/i)(String);
547
- /**
548
- * Default NanoId.
549
- *
550
- * @category String
551
- */
552
- export const NanoId = regex("NanoId", /^[A-Za-z0-9_-]{21}$/)(String);
698
+ export const SimpleName = brand("SimpleName", UrlSafeString, (value) => value.length >= 1 && value.length <= 42
699
+ ? ok(value)
700
+ : err({ type: "SimpleName", value }));
553
701
  /**
554
702
  * Trimmed string between 8 and 64 characters, branded as `SimplePassword`.
555
703
  *
@@ -558,15 +706,48 @@ export const NanoId = regex("NanoId", /^[A-Za-z0-9_-]{21}$/)(String);
558
706
  export const SimplePassword = brand("SimplePassword", minLength(8)(maxLength(64)(TrimmedString)));
559
707
  export const formatSimplePasswordError = (formatTypeError) => createTypeErrorFormatter((error) => `Invalid password: ${formatTypeError(error.parentError)}`);
560
708
  /**
561
- * `Id` {@link Type}.
709
+ * Globally unique identifier.
562
710
  *
563
- * Represents a unique identifier with exactly 21 characters, using NanoID's
564
- * standard format (`A-Za-z0-9_-`).
711
+ * **Evolu Id** is 16 random bytes from a cryptographically secure random
712
+ * generator, encoded as 22-character Base64Url string. This provides strong
713
+ * collision resistance for distributed ID generation.
714
+ *
715
+ * ### Design Rationale
716
+ *
717
+ * Why Evolu Id over alternatives:
718
+ *
719
+ * - **NanoID**: No standard binary serialization format, and uses only ~126 bits
720
+ * of entropy (21 characters from 64-symbol alphabet) compared to Evolu Id's
721
+ * 128 bits.
722
+ * - **UUID (v4)**: String format is 36 characters (with hyphens) compared to
723
+ * Evolu Id's 22 characters. While UUIDs can be stored as 16 bytes, their
724
+ * standard string representation is verbose.
725
+ * - **UUID v7**: Includes timestamp in the ID, which leaks information about when
726
+ * data was created. This is a privacy concern for local-first applications
727
+ * where creation time must remain private.
728
+ *
729
+ * Evolu Id provides 128 bits of entropy, compact string representation (22
730
+ * characters), standard and native string serialization (Base64Url), and no
731
+ * privacy leaks.
732
+ *
733
+ * ### Future Consideration
734
+ *
735
+ * For database-heavy workloads where insert performance is critical, a hybrid
736
+ * approach could be considered: `timestamp ^ H(cluster_id, timestamp >> N)`
737
+ * where H is a keyed hash function and N is a configurable parameter. This
738
+ * would maintain spatial locality for database caches (improving insert
739
+ * performance by an order of magnitude) while adding entropy to prevent
740
+ * timestamp leakage and correlation across systems. The parameter N would allow
741
+ * trading off cache locality (larger N = better locality) versus entropy
742
+ * distribution. See https://brooker.co.za/blog/2025/10/22/uuidv7.html for
743
+ * details on this approach.
565
744
  *
566
745
  * @category String
567
746
  */
568
- export const Id = regex("Id", /^[A-Za-z0-9_-]{21}$/)(String);
569
- export const idTypeValueLength = 21;
747
+ export const Id = brand("Id", String, (value) => value.length === 22 && Base64Url.fromParent(value).ok
748
+ ? ok(value)
749
+ : err({ type: "Id", value }));
750
+ export const formatIdError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid Id.`);
570
751
  /**
571
752
  * Creates an {@link Id}.
572
753
  *
@@ -575,11 +756,53 @@ export const idTypeValueLength = 21;
575
756
  * ```ts
576
757
  * // string & Brand<"Id">
577
758
  * const id = createId(deps);
759
+ *
760
+ * // string & Brand<"Id"> & Brand<"Todo">
761
+ * const todoId = createId<"Todo">(deps);
578
762
  * ```
579
763
  */
580
- export const createId = (deps) => deps.nanoIdLib.nanoid();
764
+ export const createId = (deps) => uint8ArrayToBase64Url(deps.randomBytes.create(16));
581
765
  /**
582
- * Type Factory to create branded {@link Id} Type for a specific table.
766
+ * Creates an {@link Id} from a string using SHA-256.
767
+ *
768
+ * When integrating with external systems that use different ID formats, use
769
+ * this function to convert external IDs into valid Evolu IDs.
770
+ *
771
+ * In Evolu's CRDT, the ID serves as the unique identifier for conflict
772
+ * resolution across distributed clients. When multiple clients create records
773
+ * with the same external identifier, they must resolve to the same Evolu ID to
774
+ * ensure data consistency.
775
+ *
776
+ * ### Example
777
+ *
778
+ * ```ts
779
+ * // Both clients will generate the same ID
780
+ * const id1 = createIdFromString("user-api-123");
781
+ * const id2 = createIdFromString("user-api-123");
782
+ * console.log(id1 === id2); // true
783
+ *
784
+ * upsert("todo", {
785
+ * id: createIdFromString("external-todo-456"),
786
+ * title: "Synced from external system",
787
+ * });
788
+ * ```
789
+ *
790
+ * **Important**: This transformation is one-way. We cannot recover the original
791
+ * external string from the generated {@link Id}. If we need to preserve the
792
+ * original external ID, store it in a separate column.
793
+ *
794
+ * @category String
795
+ */
796
+ export const createIdFromString = (value) => {
797
+ const hash = sha256(utf8ToBytes(value));
798
+ // Take first 16 bytes of hash and convert to Id
799
+ const id = idBytesToId(hash.slice(0, 16));
800
+ return id;
801
+ };
802
+ /**
803
+ * Creates a branded {@link Id} Type for a table's primary key.
804
+ *
805
+ * The table name becomes an additional brand for type safety.
583
806
  *
584
807
  * ### Example
585
808
  *
@@ -592,32 +815,36 @@ export const createId = (deps) => deps.nanoIdLib.nanoid();
592
815
  * @category String
593
816
  */
594
817
  export const id = (table) => {
595
- const fromParent = (value) => {
596
- const idResult = Id.fromParent(value);
597
- if (!idResult.ok) {
598
- return err({ type: "Id", value, table });
599
- }
600
- return ok(idResult.value);
601
- };
602
818
  const fromUnknown = (value) => {
603
819
  const parentResult = String.fromUnknown(value);
604
820
  if (!parentResult.ok)
605
821
  return parentResult;
606
822
  return fromParent(parentResult.value);
607
823
  };
824
+ const fromParent = (value) => {
825
+ const idResult = Id.fromParent(value);
826
+ if (!idResult.ok) {
827
+ return err({ type: "TableId", value, table });
828
+ }
829
+ return ok(idResult.value);
830
+ };
608
831
  return {
609
- ...createType("Id", {
610
- fromUnknown,
611
- to: (value) => value,
612
- fromParent,
613
- toParent: (value) => value,
614
- }),
832
+ ...createType("Id", { fromUnknown, fromParent }),
615
833
  table,
616
834
  };
617
835
  };
618
- export const formatIdError = createTypeErrorFormatter((error) => `Invalid ${error.type} table Id: ${error.value}`);
619
- /**
620
- * Positive number.
836
+ export const formatTableIdError = createTypeErrorFormatter((error) => `Invalid Id for table ${error.table}: ${error.value}.`);
837
+ /** Binary representation of an {@link Id}. */
838
+ export const IdBytes = brand("IdBytes", length(16)(Uint8Array));
839
+ export const idBytesTypeValueLength = 16;
840
+ export const idToIdBytes = (id) =>
841
+ // Id is Base64Url (validated by Id.from), cast is safe
842
+ base64UrlToUint8Array(id);
843
+ export const idBytesToId = (idBytes) =>
844
+ // Base64Url encoding of 16 bytes always produces valid Id (22 chars)
845
+ uint8ArrayToBase64Url(idBytes);
846
+ /**
847
+ * Positive number (> 0).
621
848
  *
622
849
  * ### Example
623
850
  *
@@ -631,9 +858,9 @@ export const formatIdError = createTypeErrorFormatter((error) => `Invalid ${erro
631
858
  * @category Number
632
859
  */
633
860
  export const positive = (parent) => brand("Positive", parent, (value) => value > 0 ? ok(value) : err({ type: "Positive", value }));
634
- export const formatPositiveError = createTypeErrorFormatter((error) => `The value ${error.value} is not positive.`);
861
+ export const formatPositiveError = createTypeErrorFormatter((error) => `The value ${error.value} must be positive (> 0).`);
635
862
  /**
636
- * Negative number.
863
+ * Negative number (< 0).
637
864
  *
638
865
  * ### Example
639
866
  *
@@ -644,9 +871,9 @@ export const formatPositiveError = createTypeErrorFormatter((error) => `The valu
644
871
  * @category Number
645
872
  */
646
873
  export const negative = (parent) => brand("Negative", parent, (value) => value < 0 ? ok(value) : err({ type: "Negative", value }));
647
- export const formatNegativeError = createTypeErrorFormatter((error) => `The value ${error.value} is not negative.`);
874
+ export const formatNegativeError = createTypeErrorFormatter((error) => `The value ${error.value} must be negative (< 0).`);
648
875
  /**
649
- * Non-positive number.
876
+ * Non-positive number (≤ 0).
650
877
  *
651
878
  * ### Example
652
879
  *
@@ -659,9 +886,9 @@ export const formatNegativeError = createTypeErrorFormatter((error) => `The valu
659
886
  export const nonPositive = (parent) => brand("NonPositive", parent, (value) => value <= 0
660
887
  ? ok(value)
661
888
  : err({ type: "NonPositive", value }));
662
- export const formatNonPositiveError = createTypeErrorFormatter((error) => `The value ${error.value} is not non-positive.`);
889
+ export const formatNonPositiveError = createTypeErrorFormatter((error) => `The value ${error.value} must be non-positive (≤ 0).`);
663
890
  /**
664
- * Non-negative number.
891
+ * Non-negative number (≥ 0).
665
892
  *
666
893
  * ### Example
667
894
  *
@@ -674,14 +901,30 @@ export const formatNonPositiveError = createTypeErrorFormatter((error) => `The v
674
901
  export const nonNegative = (parent) => brand("NonNegative", parent, (value) => value >= 0
675
902
  ? ok(value)
676
903
  : err({ type: "NonNegative", value }));
677
- export const formatNonNegativeError = createTypeErrorFormatter((error) => `The value ${error.value} is not non-negative.`);
678
- /** @category Number */
904
+ export const formatNonNegativeError = createTypeErrorFormatter((error) => `The value ${error.value} must be non-negative (≥ 0).`);
905
+ /**
906
+ * Non-negative number (≥ 0).
907
+ *
908
+ * @category Number
909
+ */
679
910
  export const NonNegativeNumber = nonNegative(Number);
680
- /** @category Number */
911
+ /**
912
+ * Positive number (> 0).
913
+ *
914
+ * @category Number
915
+ */
681
916
  export const PositiveNumber = positive(NonNegativeNumber);
682
- /** @category Number */
917
+ /**
918
+ * Non-positive number (≤ 0).
919
+ *
920
+ * @category Number
921
+ */
683
922
  export const NonPositiveNumber = nonPositive(Number);
684
- /** @category Number */
923
+ /**
924
+ * Negative number (< 0).
925
+ *
926
+ * @category Number
927
+ */
685
928
  export const NegativeNumber = negative(NonPositiveNumber);
686
929
  /**
687
930
  * Integer within the safe range of JavaScript numbers.
@@ -697,20 +940,38 @@ export const NegativeNumber = negative(NonPositiveNumber);
697
940
  export const int = (parent) => brand("Int", parent, (value) => globalThis.Number.isSafeInteger(value)
698
941
  ? ok(value)
699
942
  : err({ type: "Int", value }));
700
- export const formatIntError = createTypeErrorFormatter((error) => `The value ${error.value} is not an integer.`);
943
+ export const formatIntError = createTypeErrorFormatter((error) => `The value ${error.value} must be an integer.`);
701
944
  /**
702
945
  * Integer within the safe range of JavaScript numbers.
703
946
  *
704
947
  * @category Number
705
948
  */
706
949
  export const Int = int(Number);
707
- /** @category Number */
950
+ /**
951
+ * Non-negative integer (≥ 0).
952
+ *
953
+ * @category Number
954
+ */
708
955
  export const NonNegativeInt = nonNegative(Int);
709
- /** @category Number */
956
+ /**
957
+ * Positive integer (> 0).
958
+ *
959
+ * @category Number
960
+ */
710
961
  export const PositiveInt = positive(NonNegativeInt);
711
- /** @category Number */
962
+ /** Maximum safe positive integer value for practically infinite operations. */
963
+ export const maxPositiveInt = PositiveInt.orThrow(globalThis.Number.MAX_SAFE_INTEGER);
964
+ /**
965
+ * Non-positive integer (≤ 0).
966
+ *
967
+ * @category Number
968
+ */
712
969
  export const NonPositiveInt = nonPositive(Int);
713
- /** @category Number */
970
+ /**
971
+ * Negative integer (< 0).
972
+ *
973
+ * @category Number
974
+ */
714
975
  export const NegativeInt = negative(NonPositiveInt);
715
976
  /**
716
977
  * Number greater than a specified value.
@@ -750,7 +1011,7 @@ export const formatLessThanOrEqualToError = createTypeErrorFormatter((error) =>
750
1011
  export const nonNaN = (parent) => brand("NonNaN", parent, (value) => !globalThis.Number.isNaN(value)
751
1012
  ? ok(value)
752
1013
  : err({ type: "NonNaN", value }));
753
- export const formatNonNaNError = createTypeErrorFormatter((error) => `The value ${error.value} is NaN (not a number).`);
1014
+ export const formatNonNaNError = createTypeErrorFormatter(() => `The value must not be NaN.`);
754
1015
  /** @category Number */
755
1016
  export const NonNaNNumber = nonNaN(Number);
756
1017
  /**
@@ -761,7 +1022,7 @@ export const NonNaNNumber = nonNaN(Number);
761
1022
  export const finite = (parent) => brand("Finite", parent, (value) => globalThis.Number.isFinite(value)
762
1023
  ? ok(value)
763
1024
  : err({ type: "Finite", value }));
764
- export const formatFiniteError = createTypeErrorFormatter((error) => `The value ${error.value} is not finite.`);
1025
+ export const formatFiniteError = createTypeErrorFormatter((error) => `The value ${error.value} must be finite.`);
765
1026
  /**
766
1027
  * Finite number.
767
1028
  *
@@ -802,8 +1063,6 @@ export const between = (min, max) => (parent) => brand(`Between${min}-${max}`, p
802
1063
  ? ok(value)
803
1064
  : err({ type: "Between", value, min, max }));
804
1065
  export const formatBetweenError = createTypeErrorFormatter((error) => `The value ${error.value} is not between ${error.min} and ${error.max}, inclusive.`);
805
- /** @category Number */
806
- export const Between1And10 = between(1, 10)(Number);
807
1066
  /**
808
1067
  * Literal {@link Type}.
809
1068
  *
@@ -828,90 +1087,12 @@ export const literal = (expected) => {
828
1087
  return {
829
1088
  ...createType("Literal", {
830
1089
  fromUnknown,
831
- to: identity,
832
1090
  fromParent: ok,
833
- toParent: identity,
834
1091
  }),
835
1092
  expected,
836
1093
  };
837
1094
  };
838
1095
  export const formatLiteralError = createTypeErrorFormatter((error) => `The value ${error.value} is not strictly equal to the expected literal: ${globalThis.String(error.expected)}.`);
839
- /**
840
- * {@link Type} that transforms values between `FromType` and `ToType`.
841
- *
842
- * - `fromParent`: Converts `FromType` to `ToType`, may fail.
843
- * - `toParent`: Converts `ToType` back to `FromType`, must not fail.
844
- *
845
- * ### Example
846
- *
847
- * // TODO: Examples
848
- *
849
- * @category Base Factories
850
- */
851
- export const transform = (fromType, toType, fromParent, toParent) => {
852
- const fromUnknown = (value) => {
853
- const parentResult = fromType.fromUnknown(value);
854
- if (!parentResult.ok)
855
- return parentResult;
856
- return fromParent(parentResult.value);
857
- };
858
- const to = (value) => fromType.to(toParent(value));
859
- return {
860
- ...createType("Transform", {
861
- fromUnknown,
862
- to,
863
- fromParent,
864
- toParent,
865
- }),
866
- is: toType.is,
867
- fromType,
868
- toType,
869
- };
870
- };
871
- /**
872
- * Trims leading and trailing whitespace from a string.
873
- *
874
- * ### Example
875
- *
876
- * ```ts
877
- * expect(TrimString.from("a ")).toEqual(ok("a"));
878
- * expect(TrimString.fromParent("a ").value).toEqual("a");
879
- * ```
880
- *
881
- * @category String
882
- */
883
- export const TrimString = trim(String);
884
- /**
885
- * Transforms a {@link Date} into a {@link DateIsoString} string and vice versa.
886
- *
887
- * ### Example
888
- *
889
- * TODO:
890
- *
891
- * @category String
892
- */
893
- export const DateIso = transform(Date, DateIsoString, (value) => DateIsoString.fromParent(value.toISOString()), (value) => new globalThis.Date(value));
894
- /**
895
- * Transforms a {@link NonEmptyTrimmedString} into a {@link FiniteNumber}.
896
- *
897
- * ### Example
898
- *
899
- * ```ts
900
- * NumberFromString.from("42"); // ok(42)
901
- * NumberFromString.from("abc"); // err({ type: "NumberFromString", value: "abc" })
902
- * ```
903
- *
904
- * @category Number
905
- */
906
- export const NumberFromString = transform(NonEmptyTrimmedString, FiniteNumber, (value) => {
907
- const result = FiniteNumber.fromParent(globalThis.Number(value));
908
- if (!result.ok)
909
- return err({ type: "NumberFromString", value });
910
- return result;
911
- }, (num) => num.toString());
912
- export const formatNumberFromStringError = createTypeErrorFormatter((error) => {
913
- return `The value ${error.value} could not be converted to a finite number.`;
914
- });
915
1096
  /**
916
1097
  * Array of a specific {@link Type}.
917
1098
  *
@@ -954,7 +1135,6 @@ export const array = (element) => {
954
1135
  }
955
1136
  return ok(result);
956
1137
  };
957
- const to = (value) => value.map(element.to);
958
1138
  const fromParent = (value) => {
959
1139
  const result = [];
960
1140
  for (let i = 0; i < value.length; i++) {
@@ -974,14 +1154,8 @@ export const array = (element) => {
974
1154
  }
975
1155
  return ok(result);
976
1156
  };
977
- const toParent = (values) => values.map(element.toParent);
978
1157
  return {
979
- ...createType("Array", {
980
- fromUnknown,
981
- to,
982
- fromParent,
983
- toParent,
984
- }),
1158
+ ...createType("Array", { fromUnknown, fromParent }),
985
1159
  element,
986
1160
  };
987
1161
  };
@@ -1055,10 +1229,6 @@ export const record = (keyType, valueType) => {
1055
1229
  }
1056
1230
  return ok(result);
1057
1231
  };
1058
- const to = (value) => Object.fromEntries(Object.entries(value).map(([key, val]) => [
1059
- keyType.to(key),
1060
- valueType.to(val),
1061
- ]));
1062
1232
  const fromParent = (value) => {
1063
1233
  const result = {};
1064
1234
  for (const [rawKey, rawValue] of Object.entries(value)) {
@@ -1090,16 +1260,10 @@ export const record = (keyType, valueType) => {
1090
1260
  }
1091
1261
  return ok(result);
1092
1262
  };
1093
- const toParent = (value) => Object.fromEntries(Object.entries(value).map(([key, val]) => [
1094
- keyType.toParent(key),
1095
- valueType.toParent(val),
1096
- ]));
1097
1263
  return {
1098
1264
  ...createType("Record", {
1099
1265
  fromUnknown,
1100
- to,
1101
1266
  fromParent,
1102
- toParent,
1103
1267
  }),
1104
1268
  key: keyType,
1105
1269
  value: valueType,
@@ -1186,22 +1350,6 @@ export function object(props, record) {
1186
1350
  }
1187
1351
  return ok(result);
1188
1352
  };
1189
- const to = (value) => {
1190
- const entries = [];
1191
- for (const key of propKeys) {
1192
- if (!(key in value) && isOptionalType(props[key])) {
1193
- continue;
1194
- }
1195
- entries.push([key, props[key].to(value[key])]);
1196
- }
1197
- if (record) {
1198
- const recordEntries = Object.entries(value).filter(([key]) => !propKeys.includes(key));
1199
- for (const [key, val] of recordEntries) {
1200
- entries.push([record.key.to(key), record.value.to(val)]);
1201
- }
1202
- }
1203
- return Object.fromEntries(entries);
1204
- };
1205
1353
  const fromParent = (value) => {
1206
1354
  const errors = {};
1207
1355
  const result = {};
@@ -1249,28 +1397,10 @@ export function object(props, record) {
1249
1397
  }
1250
1398
  return ok(result);
1251
1399
  };
1252
- const toParent = (value) => {
1253
- const entries = [];
1254
- for (const key of propKeys) {
1255
- if (!(key in value) && isOptionalType(props[key])) {
1256
- continue;
1257
- }
1258
- entries.push([key, props[key].toParent(value[key])]);
1259
- }
1260
- if (record) {
1261
- const recordEntries = Object.entries(value).filter(([key]) => !propKeys.includes(key));
1262
- for (const [key, val] of recordEntries) {
1263
- entries.push([record.key.toParent(key), record.value.toParent(val)]);
1264
- }
1265
- }
1266
- return Object.fromEntries(entries);
1267
- };
1268
1400
  return {
1269
1401
  ...createType(record ? "ObjectWithRecord" : "Object", {
1270
1402
  fromUnknown,
1271
- to,
1272
1403
  fromParent,
1273
- toParent,
1274
1404
  }),
1275
1405
  props,
1276
1406
  ...(record ? { record } : {}),
@@ -1323,7 +1453,6 @@ export function union(...args) {
1323
1453
  * - Generate a specialized function to improve validation performance for such
1324
1454
  * cases.
1325
1455
  */
1326
- /* eslint-disable */
1327
1456
  const members = args.map((arg) => (isType(arg) ? arg : literal(arg)));
1328
1457
  const fromUnknown = (value) => {
1329
1458
  const errors = [];
@@ -1339,23 +1468,13 @@ export function union(...args) {
1339
1468
  errors,
1340
1469
  });
1341
1470
  };
1342
- const to = (value) => {
1343
- for (const member of members) {
1344
- if (member.is(value))
1345
- return member.to(value);
1346
- }
1347
- assert(false, "No matching member found in Union Type `to` function");
1348
- };
1349
1471
  return {
1350
1472
  ...createType("Union", {
1351
1473
  fromUnknown,
1352
- to,
1353
1474
  fromParent: fromUnknown,
1354
- toParent: to,
1355
1475
  }),
1356
1476
  members,
1357
1477
  };
1358
- /* eslint-enable */
1359
1478
  }
1360
1479
  export const formatUnionError = (formatTypeError) => createTypeErrorFormatter((error) => {
1361
1480
  const formattedErrors = error.errors
@@ -1411,20 +1530,10 @@ export const recursive = (create) => {
1411
1530
  type ??= create();
1412
1531
  return type.fromUnknown(value);
1413
1532
  },
1414
- to: (value) => {
1415
- type ??= create();
1416
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
1417
- return type.to(value);
1418
- },
1419
1533
  fromParent: (value) => {
1420
1534
  type ??= create();
1421
1535
  return type.fromParent(value);
1422
1536
  },
1423
- toParent: (value) => {
1424
- type ??= create();
1425
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
1426
- return type.toParent(value);
1427
- },
1428
1537
  is: (value) => {
1429
1538
  type ??= create();
1430
1539
  return type.is(value);
@@ -1528,7 +1637,6 @@ export const tuple = (...elements) => {
1528
1637
  }
1529
1638
  return ok(result);
1530
1639
  };
1531
- const to = (value) => value.map((val, index) => elements[index].to(val));
1532
1640
  const fromParent = (value) => {
1533
1641
  if (!Array.isArray(value) || value.length !== elements.length) {
1534
1642
  return err({
@@ -1555,13 +1663,10 @@ export const tuple = (...elements) => {
1555
1663
  }
1556
1664
  return ok(result);
1557
1665
  };
1558
- const toParent = (value) => value.map((val, index) => elements[index].toParent(val));
1559
1666
  return {
1560
1667
  ...createType("Tuple", {
1561
1668
  fromUnknown,
1562
- to,
1563
1669
  fromParent,
1564
- toParent,
1565
1670
  }),
1566
1671
  elements,
1567
1672
  };
@@ -1595,22 +1700,32 @@ export const Int64 = brand("Int64", BigInt, (value) => value >= -922337203685477
1595
1700
  ? ok(value)
1596
1701
  : err({ type: "Int64", value }));
1597
1702
  export const formatInt64Error = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid 64-bit signed integer (Int64).`);
1598
- export const BigIntFromString = transform(String, BigInt, (value) => trySync(() => globalThis.BigInt(value), () => ({ type: "BigIntFromString", value })), (value) => value.toString());
1599
- export const formatBigIntFromStringError = createTypeErrorFormatter((error) => `The value ${error.value} could not be converted to a BigInt.`);
1703
+ // // co s timhle? je to string, ze ktereho lze udelat bigint
1704
+ // export const BigIntFromString = transform(
1705
+ // String,
1706
+ // BigInt,
1707
+ // (value) =>
1708
+ // trySync(
1709
+ // () => globalThis.BigInt(value),
1710
+ // (): BigIntFromStringError => ({ type: "BigIntFromString", value }),
1711
+ // ),
1712
+ // (value) => value.toString(),
1713
+ // );
1714
+ // export interface BigIntFromStringError extends TypeError<"BigIntFromString"> {}
1715
+ // export const formatBigIntFromStringError =
1716
+ // createTypeErrorFormatter<BigIntFromStringError>(
1717
+ // (error) => `The value ${error.value} could not be converted to a BigInt.`,
1718
+ // );
1600
1719
  /**
1601
1720
  * Stringified {@link Int64}.
1602
1721
  *
1603
- * @category Number
1722
+ * @category String
1604
1723
  */
1605
- export const Int64String = brand("Int64", String, (value) => {
1606
- const bigint = BigIntFromString.fromParent(value);
1607
- if (bigint.ok) {
1608
- const int64 = Int64.fromParent(globalThis.BigInt(value));
1609
- if (int64.ok)
1610
- return ok(value);
1611
- }
1612
- return err({ type: "Int64String", value });
1613
- });
1724
+ export const Int64String = brand("Int64", NonEmptyTrimmedString, (value) => trySync(() => {
1725
+ const maybeInt = globalThis.BigInt(value);
1726
+ Int64.orThrow(maybeInt);
1727
+ return value;
1728
+ }, () => ({ type: "Int64String", value })));
1614
1729
  export const formatInt64StringError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid Int64 string.`);
1615
1730
  /**
1616
1731
  * JSON-compatible value: string, {@link FiniteNumber}, boolean, null,
@@ -1631,48 +1746,83 @@ export const JsonArray = array(JsonValue);
1631
1746
  * @category Object
1632
1747
  */
1633
1748
  export const JsonObject = record(String, JsonValue);
1634
- /**
1635
- * Transform Type that parses a JSON into a {@link JsonValue} and serializes a
1636
- * JsonValue back into a JSON string.
1637
- *
1638
- * ### Example
1639
- *
1640
- * ```ts
1641
- * JsonValueFromString.from(`{"key":"value"}`); // -> ok({ key: "value" })
1642
- * JsonValueFromString.to({ key: "value" }); // -> '{"key":"value"}'
1643
- * ```
1644
- *
1645
- * @category String
1646
- */
1647
- export const JsonValueFromString = transform(String, JsonValue, (value) => trySync(() => JSON.parse(value), (error) => ({
1648
- type: `JsonValueFromString`,
1749
+ export const parseJson = (value) => trySync(() => JSON.parse(value), (error) => ({
1750
+ type: `Json`,
1649
1751
  value,
1650
1752
  message: globalThis.String(error),
1651
- })), (value) => JSON.stringify(value));
1652
- export const formatJsonValueFromStringError = createTypeErrorFormatter((error) => `Invalid JSONValue: ${error.value}. Error: ${error.message}`);
1753
+ }));
1653
1754
  /**
1654
1755
  * JSON-string {@link Type}.
1655
1756
  *
1656
1757
  * ### Example
1657
1758
  *
1658
1759
  * ```ts
1659
- * const result = Json.from('{"key":"value"}'); // -> ok('{"key":"value"}')
1660
- * const error = Json.from("invalid json"); // -> err({ type: "Json", value: "invalid json", message: "Unexpected token i in JSON at position 0" })
1760
+ * const result = Json.from('{"key":"value"}'); // ok
1761
+ * const error = Json.from("invalid json"); // err
1661
1762
  * ```
1662
1763
  *
1663
1764
  * @category String
1664
1765
  */
1665
1766
  export const Json = brand("Json", String, (value) => {
1666
- const result = JsonValueFromString.fromParent(value);
1767
+ const result = parseJson(value);
1667
1768
  if (!result.ok)
1668
- return err({
1669
- type: "Json",
1670
- value,
1671
- message: result.error.message,
1672
- });
1769
+ return result;
1673
1770
  return ok(value);
1674
1771
  });
1675
1772
  export const formatJsonError = createTypeErrorFormatter((error) => `Invalid JSON: ${error.value}. Error: ${error.message}`);
1773
+ export const jsonValueToJson = (value) => JSON.stringify(value);
1774
+ export const jsonToJsonValue = (value) => JSON.parse(value);
1775
+ /**
1776
+ * Creates a branded JSON string {@link Type} and type-safe conversion functions
1777
+ * for a given Type.
1778
+ *
1779
+ * This factory creates:
1780
+ *
1781
+ * 1. A branded string Type that validates JSON parsing and structural conformity
1782
+ * 2. A serialization function (Type → branded JSON string)
1783
+ * 3. A parsing function (branded JSON string → Type, skipping validation)
1784
+ *
1785
+ * Optimized for Evolu's SQLite workflow where we store typed JSON strings and
1786
+ * need type-safe conversions without double parsing.
1787
+ *
1788
+ * ### Example
1789
+ *
1790
+ * ```ts
1791
+ * const Person = object({
1792
+ * name: NonEmptyString100,
1793
+ * age: FiniteNumber,
1794
+ * });
1795
+ * type Person = typeof Person.Type;
1796
+ *
1797
+ * const [PersonJson, personToPersonJson, personJsonToPerson] = json(
1798
+ * Person,
1799
+ * "PersonJson",
1800
+ * );
1801
+ * // string & Brand<"PersonJson">
1802
+ * type PersonJson = typeof PersonJson.Type;
1803
+ *
1804
+ * // Usage:
1805
+ * const person: Person = { name: "Alice", age: 30 };
1806
+ * const jsonString = personToPersonJson(person); // PersonJson
1807
+ * const backToPerson = personJsonToPerson(jsonString); // Person
1808
+ * ```
1809
+ */
1810
+ export const json = (type, name) => {
1811
+ const BrandedJsonType = brand(name, String, (value) => {
1812
+ const parseResult = parseJson(value);
1813
+ if (!parseResult.ok)
1814
+ return parseResult;
1815
+ const validationResult = type.fromUnknown(parseResult.value);
1816
+ if (!validationResult.ok)
1817
+ return validationResult;
1818
+ return ok(value);
1819
+ });
1820
+ return [
1821
+ BrandedJsonType,
1822
+ jsonValueToJson,
1823
+ jsonToJsonValue,
1824
+ ];
1825
+ };
1676
1826
  /**
1677
1827
  * Optional {@link Type}.
1678
1828
  *
@@ -1697,9 +1847,7 @@ export const formatJsonError = createTypeErrorFormatter((error) => `Invalid JSON
1697
1847
  export const optional = (type) => ({
1698
1848
  ...createType("Optional", {
1699
1849
  fromUnknown: type.fromUnknown,
1700
- to: type.to,
1701
1850
  fromParent: type.fromParent,
1702
- toParent: type.toParent,
1703
1851
  }),
1704
1852
  parent: type,
1705
1853
  });
@@ -1708,7 +1856,7 @@ export const isOptionalType = (x) => typeof x === "object" && x != null && "name
1708
1856
  /**
1709
1857
  * Creates a partial object type where all properties are optional.
1710
1858
  *
1711
- * This is useful when you want to validate an object in which none of the keys
1859
+ * This is useful when we want to validate an object in which none of the keys
1712
1860
  * are required, but if they are present they must conform to their
1713
1861
  * corresponding Types.
1714
1862
  *
@@ -1779,114 +1927,86 @@ export function omit(objectType, ...keys) {
1779
1927
  }
1780
1928
  return object(newProps);
1781
1929
  }
1930
+ export const maxMutationSize = 655360;
1782
1931
  /**
1783
- * Creates a transform Type that serializes a given `Type` into a branded JSON
1784
- * string. The transformation is reversible, ensuring that we can safely parse
1785
- * it back.
1786
- *
1787
- * ### Example
1788
- *
1789
- * ```ts
1790
- * const Person = object({
1791
- * name: NonEmptyString50,
1792
- * age: FiniteNumber,
1793
- * });
1794
- * type Person = typeof Person.Type;
1795
- *
1796
- * const PersonJson = json(Person, "PersonJson");
1797
- * // string & Brand<"PersonJson">
1798
- * type PersonJson = typeof PersonJson.Type;
1799
- *
1800
- * // Person -> string & Brand<"PersonJson">
1801
- * const personJson = PersonJson.from({ name: "Alice", age: 30 });
1802
- * expect(personJson).toEqual(ok('{"name":"Alice","age":30}'));
1803
- *
1804
- * // string & Brand<"PersonJson"> -> Person
1805
- * const person = PersonJson.to(personJson);
1806
- *
1807
- * // serialize/parse any JSON value
1808
- * const AnyJson = json(JsonValue, "AnyJson");
1809
- * ```
1932
+ * Evolu has to limit the maximum mutation size. Otherwise, sync couldn't use
1933
+ * the `maxProtocolMessageRangesSize`. The max size is 640KB in bytes, measured
1934
+ * via MessagePack. Evolu Protocol DbChange will be smaller thanks to various
1935
+ * optimizations.
1810
1936
  */
1811
- export const json = (type, name) => {
1812
- const BrandedJsonString = brand(name, String, (value) => {
1813
- const jsonValue = JsonValueFromString.fromParent(value);
1814
- if (!jsonValue.ok)
1815
- return jsonValue;
1816
- const parsed = type.fromUnknown(jsonValue.value);
1817
- if (!parsed.ok)
1818
- return parsed;
1819
- return ok(value);
1820
- });
1821
- return transform(type, BrandedJsonString, (value) => ok(JSON.stringify(value)),
1822
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
1823
- (value) => JSON.parse(value));
1824
- };
1937
+ export const validMutationSize = (type) => brand("ValidMutationSize", type, (value) => pack(value).byteLength <= maxMutationSize
1938
+ ? ok(value)
1939
+ : err({ type: "ValidMutationSize", value }));
1940
+ export const formatValidMutationSizeError = createTypeErrorFormatter((error) => `The mutation size exceeds the maximum limit of ${maxMutationSize} bytes. The provided mutation has a size of ${pack(error.value).byteLength} bytes.`);
1825
1941
  /**
1826
- * Creates a unified error formatter that handles both Evolu Type's built-in
1827
- * {@link TypeErrors} and custom errors. It also lets us override the default
1828
- * formatting for specific errors.
1942
+ * Formats Evolu Type errors into user-friendly messages.
1943
+ *
1944
+ * Evolu Type typed errors ensure every error type must have a formatter.
1945
+ * TypeScript enforces this at compile-time, preventing unhandled validation
1946
+ * errors from reaching users.
1829
1947
  *
1830
- * If you prefer not to reuse any built-in error formatters, you can write your
1831
- * own `formatTypeError` function from scratch.
1948
+ * The `createFormatTypeError` function handles both built-in {@link TypeErrors}
1949
+ * and custom errors, and lets us override default formatting for specific
1950
+ * errors.
1832
1951
  *
1833
- * ### Examples
1952
+ * ### Example
1834
1953
  *
1835
1954
  * ```ts
1836
- * const formatError = createFormatTypeError();
1837
- * console.log(formatError({ type: "String", value: 42 }));
1838
- * // "A value 42 is not a string."
1955
+ * const formatTypeError = createFormatTypeError<
1956
+ * MinLengthError | MaxLengthError
1957
+ * >((error): string => {
1958
+ * switch (error.type) {
1959
+ * case "MinLength":
1960
+ * return `Text must be at least ${error.min} character${error.min === 1 ? "" : "s"} long`;
1961
+ * case "MaxLength":
1962
+ * return `Text is too long (maximum ${error.max} characters)`;
1963
+ * }
1964
+ * });
1839
1965
  * ```
1840
1966
  *
1841
- * A custom `formatTypeError` function:
1967
+ * Alternatively, write a custom formatter from scratch without using
1968
+ * `createFormatTypeError`. This gives us full control over error formatting:
1842
1969
  *
1843
1970
  * ```ts
1844
- * type AppErrors =
1845
- * | ValidMutationSizeError
1971
+ * const Person = object({
1972
+ * name: NonEmptyTrimmedString100,
1973
+ * age: optional(PositiveInt),
1974
+ * });
1975
+ *
1976
+ * // Define only the errors actually used by Person Type
1977
+ * type PersonErrors =
1846
1978
  * | StringError
1847
- * | MinLengthError
1848
1979
  * | MaxLengthError
1849
- * | NullError
1850
- * | IdError
1980
+ * | MinLengthError
1851
1981
  * | TrimmedError
1852
- * | MnemonicError
1853
- * | LiteralError
1854
- * // Composite errors
1855
- * | ObjectError<Record<string, AppErrors>>
1856
- * | UnionError<AppErrors>;
1857
- *
1858
- * const formatTypeError: TypeErrorFormatter<AppErrors> = (error) => {
1859
- * // In the real code, we would use the createTypeErrorFormatter helper
1860
- * // that safely stringifies error value.
1982
+ * | PositiveError
1983
+ * | NonNegativeError
1984
+ * | IntError
1985
+ * | NumberError
1986
+ * | ObjectError<Record<string, PersonErrors>>;
1987
+ *
1988
+ * const formatTypeError: TypeErrorFormatter<PersonErrors> = (error) => {
1861
1989
  * switch (error.type) {
1862
- * case "Id":
1863
- * return `Invalid Id on table: ${error.table}.`;
1864
- * case "MaxLength":
1865
- * return `Max length is ${error.max}.`;
1866
- * case "MinLength":
1867
- * return `Min length is ${error.min}.`;
1868
- * case "Mnemonic":
1869
- * return `Invalid mnemonic: ${String(error.value)}`;
1870
- * case "Null":
1871
- * return `Not null`;
1872
1990
  * case "String":
1873
- * // We can reuse existing formatter.
1874
1991
  * return formatStringError(error);
1992
+ * case "Number":
1993
+ * return "Must be a number";
1994
+ * case "MinLength":
1995
+ * return `Must be at least ${error.min} characters`;
1996
+ * case "MaxLength":
1997
+ * return `Cannot exceed ${error.max} characters`;
1875
1998
  * case "Trimmed":
1876
- * return "Value is not trimmed.";
1877
- * case "ValidMutationSize":
1878
- * return "A developer made an error, this should not happen.";
1879
- * case "Literal":
1880
- * return formatLiteralError(error);
1881
- * // Composite Types
1882
- * case "Union":
1883
- * return `Union errors: ${error.errors.map(formatTypeError).join(", ")}`;
1999
+ * return "Cannot have leading or trailing spaces";
2000
+ * case "Positive":
2001
+ * return "Must be a positive number";
2002
+ * case "NonNegative":
2003
+ * return "Must be zero or positive";
2004
+ * case "Int":
2005
+ * return "Must be an integer";
1884
2006
  * case "Object": {
1885
- * if (
1886
- * error.reason.kind === "ExtraKeys" ||
1887
- * error.reason.kind === "NotObject"
1888
- * )
1889
- * return "A developer made an error, this should not happen.";
2007
+ * if (error.reason.kind === "NotObject") return "Must be an object";
2008
+ * if (error.reason.kind === "ExtraKeys")
2009
+ * return "Contains unexpected fields";
1890
2010
  * const firstError = Object.values(error.reason.errors).find(
1891
2011
  * (e) => e !== undefined,
1892
2012
  * )!;
@@ -1927,8 +2047,8 @@ export const createFormatTypeError = (extraFormatter) => {
1927
2047
  return formatIsTypeError(error);
1928
2048
  case "CurrencyCode":
1929
2049
  return formatCurrencyCodeError(error);
1930
- case "DateIsoString":
1931
- return formatDateIsoStringError(error);
2050
+ case "DateIso":
2051
+ return formatDateIsoError(error);
1932
2052
  case "Trimmed":
1933
2053
  return formatTrimmedError(error);
1934
2054
  case "MinLength":
@@ -1943,6 +2063,8 @@ export const createFormatTypeError = (extraFormatter) => {
1943
2063
  return formatRegexError(error);
1944
2064
  case "Id":
1945
2065
  return formatIdError(error);
2066
+ case "TableId":
2067
+ return formatTableIdError(error);
1946
2068
  case "Positive":
1947
2069
  return formatPositiveError(error);
1948
2070
  case "Negative":
@@ -1973,14 +2095,12 @@ export const createFormatTypeError = (extraFormatter) => {
1973
2095
  return formatLiteralError(error);
1974
2096
  case "Int64":
1975
2097
  return formatInt64Error(error);
1976
- case "BigIntFromString":
1977
- return formatBigIntFromStringError(error);
1978
2098
  case "Int64String":
1979
2099
  return formatInt64StringError(error);
1980
- case "JsonValueFromString":
1981
- return formatJsonValueFromStringError(error);
1982
2100
  case "Json":
1983
2101
  return formatJsonError(error);
2102
+ case "ValidMutationSize":
2103
+ return formatValidMutationSizeError(error);
1984
2104
  // Composite Types
1985
2105
  case "SimplePassword":
1986
2106
  return formatSimplePasswordError(formatTypeError)(error);
@@ -1996,7 +2116,88 @@ export const createFormatTypeError = (extraFormatter) => {
1996
2116
  return formatUnionError(formatTypeError)(error);
1997
2117
  case "Tuple":
1998
2118
  return formatTupleError(formatTypeError)(error);
2119
+ default: {
2120
+ // Fallback for unknown error types
2121
+ const unknownError = error;
2122
+ return `A value ${safelyStringifyUnknownValue(unknownError.value)} is not valid for type ${unknownError.type}.`;
2123
+ }
1999
2124
  }
2000
2125
  };
2001
2126
  return formatTypeError;
2002
2127
  };
2128
+ /**
2129
+ * Converts an Evolu {@link TypeError} to Standard Schema V1 issues format.
2130
+ *
2131
+ * This function recursively converts Evolu's typed errors into the Standard
2132
+ * Schema issue format with proper path tracking for nested structures.
2133
+ *
2134
+ * @category Utilities
2135
+ */
2136
+ export const typeErrorToStandardSchemaIssues = (error, formatTypeError, path = []) => {
2137
+ if (error.type === "Array") {
2138
+ const arrayError = error;
2139
+ if (arrayError.reason.kind === "NotArray") {
2140
+ return [{ message: formatTypeError(error), path }];
2141
+ }
2142
+ return typeErrorToStandardSchemaIssues(arrayError.reason.error, formatTypeError, [...path, arrayError.reason.index]);
2143
+ }
2144
+ if (error.type === "Object") {
2145
+ const objectError = error;
2146
+ if (objectError.reason.kind === "NotObject" ||
2147
+ objectError.reason.kind === "ExtraKeys") {
2148
+ return [{ message: formatTypeError(error), path }];
2149
+ }
2150
+ const issues = [];
2151
+ for (const [key, propError] of Object.entries(objectError.reason.errors)) {
2152
+ issues.push(...typeErrorToStandardSchemaIssues(propError, formatTypeError, [...path, key]));
2153
+ }
2154
+ return issues;
2155
+ }
2156
+ if (error.type === "ObjectWithRecord") {
2157
+ const objectWithRecordError = error;
2158
+ if (objectWithRecordError.reason.kind === "NotObject") {
2159
+ return [{ message: formatTypeError(error), path }];
2160
+ }
2161
+ if (objectWithRecordError.reason.kind === "IndexKey" ||
2162
+ objectWithRecordError.reason.kind === "IndexValue") {
2163
+ return typeErrorToStandardSchemaIssues(objectWithRecordError.reason.error, formatTypeError, [...path, objectWithRecordError.reason.key]);
2164
+ }
2165
+ const issues = [];
2166
+ for (const [key, propError] of Object.entries(objectWithRecordError.reason.errors)) {
2167
+ issues.push(...typeErrorToStandardSchemaIssues(propError, formatTypeError, [...path, key]));
2168
+ }
2169
+ return issues;
2170
+ }
2171
+ if (error.type === "Record") {
2172
+ const recordError = error;
2173
+ if (recordError.reason.kind === "NotRecord") {
2174
+ return [{ message: formatTypeError(error), path }];
2175
+ }
2176
+ return typeErrorToStandardSchemaIssues(recordError.reason.error, formatTypeError, [...path, recordError.reason.key]);
2177
+ }
2178
+ if (error.type === "Tuple") {
2179
+ const tupleError = error;
2180
+ if (tupleError.reason.kind === "InvalidLength") {
2181
+ return [{ message: formatTypeError(error), path }];
2182
+ }
2183
+ return typeErrorToStandardSchemaIssues(tupleError.reason.error, formatTypeError, [...path, tupleError.reason.index]);
2184
+ }
2185
+ if (error.type === "Union") {
2186
+ const unionError = error;
2187
+ return unionError.errors.flatMap((err) => typeErrorToStandardSchemaIssues(err, formatTypeError, path));
2188
+ }
2189
+ if (error.type === "Brand") {
2190
+ const brandError = error;
2191
+ if ("parentError" in brandError) {
2192
+ return typeErrorToStandardSchemaIssues(brandError.parentError, formatTypeError, path);
2193
+ }
2194
+ return [{ message: formatTypeError(error), path }];
2195
+ }
2196
+ return [{ message: formatTypeError(error), path }];
2197
+ };
2198
+ /**
2199
+ * Shared formatter cache for Standard Schema integration - avoids circular
2200
+ * dependency by lazily creating the formatter on first use rather than during
2201
+ * module initialization.
2202
+ */
2203
+ let cachedStandardSchemaFormatTypeError;