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

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 +69 -5
  2. package/dist/src/Array.d.ts.map +1 -1
  3. package/dist/src/Array.js +64 -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 +161 -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 +273 -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 +238 -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 +240 -26
  61. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  62. package/dist/src/Evolu/Storage.js +189 -91
  63. package/dist/src/Evolu/Sync.d.ts +67 -13
  64. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  65. package/dist/src/Evolu/Sync.js +441 -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 +622 -340
  103. package/dist/src/Type.d.ts.map +1 -1
  104. package/dist/src/Type.js +666 -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 +90 -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 +517 -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 +355 -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 +437 -93
  137. package/src/Evolu/Schema.ts +391 -191
  138. package/src/Evolu/Storage.ts +532 -135
  139. package/src/Evolu/Sync.ts +766 -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 +1084 -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,54 @@ 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 uses the first 16 bytes of SHA-256 hash of
791
+ * the string bytes, therefore it's not possible to recover the original
792
+ * external string from the generated {@link Id}. If you need to preserve the
793
+ * original external ID, store it in a separate column.
794
+ *
795
+ * @category String
796
+ */
797
+ export const createIdFromString = (value) => {
798
+ const hash = sha256(utf8ToBytes(value));
799
+ // Take first 16 bytes of hash and convert to Id
800
+ const id = idBytesToId(hash.slice(0, 16));
801
+ return id;
802
+ };
803
+ /**
804
+ * Creates a branded {@link Id} Type for a table's primary key.
805
+ *
806
+ * The table name becomes an additional brand for type safety.
583
807
  *
584
808
  * ### Example
585
809
  *
@@ -592,32 +816,36 @@ export const createId = (deps) => deps.nanoIdLib.nanoid();
592
816
  * @category String
593
817
  */
594
818
  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
819
  const fromUnknown = (value) => {
603
820
  const parentResult = String.fromUnknown(value);
604
821
  if (!parentResult.ok)
605
822
  return parentResult;
606
823
  return fromParent(parentResult.value);
607
824
  };
825
+ const fromParent = (value) => {
826
+ const idResult = Id.fromParent(value);
827
+ if (!idResult.ok) {
828
+ return err({ type: "TableId", value, table });
829
+ }
830
+ return ok(idResult.value);
831
+ };
608
832
  return {
609
- ...createType("Id", {
610
- fromUnknown,
611
- to: (value) => value,
612
- fromParent,
613
- toParent: (value) => value,
614
- }),
833
+ ...createType("Id", { fromUnknown, fromParent }),
615
834
  table,
616
835
  };
617
836
  };
618
- export const formatIdError = createTypeErrorFormatter((error) => `Invalid ${error.type} table Id: ${error.value}`);
619
- /**
620
- * Positive number.
837
+ export const formatTableIdError = createTypeErrorFormatter((error) => `Invalid Id for table ${error.table}: ${error.value}.`);
838
+ /** Binary representation of an {@link Id}. */
839
+ export const IdBytes = brand("IdBytes", length(16)(Uint8Array));
840
+ export const idBytesTypeValueLength = 16;
841
+ export const idToIdBytes = (id) =>
842
+ // Id is Base64Url (validated by Id.from), cast is safe
843
+ base64UrlToUint8Array(id);
844
+ export const idBytesToId = (idBytes) =>
845
+ // Base64Url encoding of 16 bytes always produces valid Id (22 chars)
846
+ uint8ArrayToBase64Url(idBytes);
847
+ /**
848
+ * Positive number (> 0).
621
849
  *
622
850
  * ### Example
623
851
  *
@@ -631,9 +859,9 @@ export const formatIdError = createTypeErrorFormatter((error) => `Invalid ${erro
631
859
  * @category Number
632
860
  */
633
861
  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.`);
862
+ export const formatPositiveError = createTypeErrorFormatter((error) => `The value ${error.value} must be positive (> 0).`);
635
863
  /**
636
- * Negative number.
864
+ * Negative number (< 0).
637
865
  *
638
866
  * ### Example
639
867
  *
@@ -644,9 +872,9 @@ export const formatPositiveError = createTypeErrorFormatter((error) => `The valu
644
872
  * @category Number
645
873
  */
646
874
  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.`);
875
+ export const formatNegativeError = createTypeErrorFormatter((error) => `The value ${error.value} must be negative (< 0).`);
648
876
  /**
649
- * Non-positive number.
877
+ * Non-positive number (≤ 0).
650
878
  *
651
879
  * ### Example
652
880
  *
@@ -659,9 +887,9 @@ export const formatNegativeError = createTypeErrorFormatter((error) => `The valu
659
887
  export const nonPositive = (parent) => brand("NonPositive", parent, (value) => value <= 0
660
888
  ? ok(value)
661
889
  : err({ type: "NonPositive", value }));
662
- export const formatNonPositiveError = createTypeErrorFormatter((error) => `The value ${error.value} is not non-positive.`);
890
+ export const formatNonPositiveError = createTypeErrorFormatter((error) => `The value ${error.value} must be non-positive (≤ 0).`);
663
891
  /**
664
- * Non-negative number.
892
+ * Non-negative number (≥ 0).
665
893
  *
666
894
  * ### Example
667
895
  *
@@ -674,14 +902,30 @@ export const formatNonPositiveError = createTypeErrorFormatter((error) => `The v
674
902
  export const nonNegative = (parent) => brand("NonNegative", parent, (value) => value >= 0
675
903
  ? ok(value)
676
904
  : err({ type: "NonNegative", value }));
677
- export const formatNonNegativeError = createTypeErrorFormatter((error) => `The value ${error.value} is not non-negative.`);
678
- /** @category Number */
905
+ export const formatNonNegativeError = createTypeErrorFormatter((error) => `The value ${error.value} must be non-negative (≥ 0).`);
906
+ /**
907
+ * Non-negative number (≥ 0).
908
+ *
909
+ * @category Number
910
+ */
679
911
  export const NonNegativeNumber = nonNegative(Number);
680
- /** @category Number */
912
+ /**
913
+ * Positive number (> 0).
914
+ *
915
+ * @category Number
916
+ */
681
917
  export const PositiveNumber = positive(NonNegativeNumber);
682
- /** @category Number */
918
+ /**
919
+ * Non-positive number (≤ 0).
920
+ *
921
+ * @category Number
922
+ */
683
923
  export const NonPositiveNumber = nonPositive(Number);
684
- /** @category Number */
924
+ /**
925
+ * Negative number (< 0).
926
+ *
927
+ * @category Number
928
+ */
685
929
  export const NegativeNumber = negative(NonPositiveNumber);
686
930
  /**
687
931
  * Integer within the safe range of JavaScript numbers.
@@ -697,20 +941,38 @@ export const NegativeNumber = negative(NonPositiveNumber);
697
941
  export const int = (parent) => brand("Int", parent, (value) => globalThis.Number.isSafeInteger(value)
698
942
  ? ok(value)
699
943
  : err({ type: "Int", value }));
700
- export const formatIntError = createTypeErrorFormatter((error) => `The value ${error.value} is not an integer.`);
944
+ export const formatIntError = createTypeErrorFormatter((error) => `The value ${error.value} must be an integer.`);
701
945
  /**
702
946
  * Integer within the safe range of JavaScript numbers.
703
947
  *
704
948
  * @category Number
705
949
  */
706
950
  export const Int = int(Number);
707
- /** @category Number */
951
+ /**
952
+ * Non-negative integer (≥ 0).
953
+ *
954
+ * @category Number
955
+ */
708
956
  export const NonNegativeInt = nonNegative(Int);
709
- /** @category Number */
957
+ /**
958
+ * Positive integer (> 0).
959
+ *
960
+ * @category Number
961
+ */
710
962
  export const PositiveInt = positive(NonNegativeInt);
711
- /** @category Number */
963
+ /** Maximum safe positive integer value for practically infinite operations. */
964
+ export const maxPositiveInt = PositiveInt.orThrow(globalThis.Number.MAX_SAFE_INTEGER);
965
+ /**
966
+ * Non-positive integer (≤ 0).
967
+ *
968
+ * @category Number
969
+ */
712
970
  export const NonPositiveInt = nonPositive(Int);
713
- /** @category Number */
971
+ /**
972
+ * Negative integer (< 0).
973
+ *
974
+ * @category Number
975
+ */
714
976
  export const NegativeInt = negative(NonPositiveInt);
715
977
  /**
716
978
  * Number greater than a specified value.
@@ -750,7 +1012,7 @@ export const formatLessThanOrEqualToError = createTypeErrorFormatter((error) =>
750
1012
  export const nonNaN = (parent) => brand("NonNaN", parent, (value) => !globalThis.Number.isNaN(value)
751
1013
  ? ok(value)
752
1014
  : err({ type: "NonNaN", value }));
753
- export const formatNonNaNError = createTypeErrorFormatter((error) => `The value ${error.value} is NaN (not a number).`);
1015
+ export const formatNonNaNError = createTypeErrorFormatter(() => `The value must not be NaN.`);
754
1016
  /** @category Number */
755
1017
  export const NonNaNNumber = nonNaN(Number);
756
1018
  /**
@@ -761,7 +1023,7 @@ export const NonNaNNumber = nonNaN(Number);
761
1023
  export const finite = (parent) => brand("Finite", parent, (value) => globalThis.Number.isFinite(value)
762
1024
  ? ok(value)
763
1025
  : err({ type: "Finite", value }));
764
- export const formatFiniteError = createTypeErrorFormatter((error) => `The value ${error.value} is not finite.`);
1026
+ export const formatFiniteError = createTypeErrorFormatter((error) => `The value ${error.value} must be finite.`);
765
1027
  /**
766
1028
  * Finite number.
767
1029
  *
@@ -802,8 +1064,6 @@ export const between = (min, max) => (parent) => brand(`Between${min}-${max}`, p
802
1064
  ? ok(value)
803
1065
  : err({ type: "Between", value, min, max }));
804
1066
  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
1067
  /**
808
1068
  * Literal {@link Type}.
809
1069
  *
@@ -828,90 +1088,12 @@ export const literal = (expected) => {
828
1088
  return {
829
1089
  ...createType("Literal", {
830
1090
  fromUnknown,
831
- to: identity,
832
1091
  fromParent: ok,
833
- toParent: identity,
834
1092
  }),
835
1093
  expected,
836
1094
  };
837
1095
  };
838
1096
  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
1097
  /**
916
1098
  * Array of a specific {@link Type}.
917
1099
  *
@@ -954,7 +1136,6 @@ export const array = (element) => {
954
1136
  }
955
1137
  return ok(result);
956
1138
  };
957
- const to = (value) => value.map(element.to);
958
1139
  const fromParent = (value) => {
959
1140
  const result = [];
960
1141
  for (let i = 0; i < value.length; i++) {
@@ -974,14 +1155,8 @@ export const array = (element) => {
974
1155
  }
975
1156
  return ok(result);
976
1157
  };
977
- const toParent = (values) => values.map(element.toParent);
978
1158
  return {
979
- ...createType("Array", {
980
- fromUnknown,
981
- to,
982
- fromParent,
983
- toParent,
984
- }),
1159
+ ...createType("Array", { fromUnknown, fromParent }),
985
1160
  element,
986
1161
  };
987
1162
  };
@@ -1055,10 +1230,6 @@ export const record = (keyType, valueType) => {
1055
1230
  }
1056
1231
  return ok(result);
1057
1232
  };
1058
- const to = (value) => Object.fromEntries(Object.entries(value).map(([key, val]) => [
1059
- keyType.to(key),
1060
- valueType.to(val),
1061
- ]));
1062
1233
  const fromParent = (value) => {
1063
1234
  const result = {};
1064
1235
  for (const [rawKey, rawValue] of Object.entries(value)) {
@@ -1090,16 +1261,10 @@ export const record = (keyType, valueType) => {
1090
1261
  }
1091
1262
  return ok(result);
1092
1263
  };
1093
- const toParent = (value) => Object.fromEntries(Object.entries(value).map(([key, val]) => [
1094
- keyType.toParent(key),
1095
- valueType.toParent(val),
1096
- ]));
1097
1264
  return {
1098
1265
  ...createType("Record", {
1099
1266
  fromUnknown,
1100
- to,
1101
1267
  fromParent,
1102
- toParent,
1103
1268
  }),
1104
1269
  key: keyType,
1105
1270
  value: valueType,
@@ -1186,22 +1351,6 @@ export function object(props, record) {
1186
1351
  }
1187
1352
  return ok(result);
1188
1353
  };
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
1354
  const fromParent = (value) => {
1206
1355
  const errors = {};
1207
1356
  const result = {};
@@ -1249,28 +1398,10 @@ export function object(props, record) {
1249
1398
  }
1250
1399
  return ok(result);
1251
1400
  };
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
1401
  return {
1269
1402
  ...createType(record ? "ObjectWithRecord" : "Object", {
1270
1403
  fromUnknown,
1271
- to,
1272
1404
  fromParent,
1273
- toParent,
1274
1405
  }),
1275
1406
  props,
1276
1407
  ...(record ? { record } : {}),
@@ -1323,7 +1454,6 @@ export function union(...args) {
1323
1454
  * - Generate a specialized function to improve validation performance for such
1324
1455
  * cases.
1325
1456
  */
1326
- /* eslint-disable */
1327
1457
  const members = args.map((arg) => (isType(arg) ? arg : literal(arg)));
1328
1458
  const fromUnknown = (value) => {
1329
1459
  const errors = [];
@@ -1339,23 +1469,13 @@ export function union(...args) {
1339
1469
  errors,
1340
1470
  });
1341
1471
  };
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
1472
  return {
1350
1473
  ...createType("Union", {
1351
1474
  fromUnknown,
1352
- to,
1353
1475
  fromParent: fromUnknown,
1354
- toParent: to,
1355
1476
  }),
1356
1477
  members,
1357
1478
  };
1358
- /* eslint-enable */
1359
1479
  }
1360
1480
  export const formatUnionError = (formatTypeError) => createTypeErrorFormatter((error) => {
1361
1481
  const formattedErrors = error.errors
@@ -1411,20 +1531,10 @@ export const recursive = (create) => {
1411
1531
  type ??= create();
1412
1532
  return type.fromUnknown(value);
1413
1533
  },
1414
- to: (value) => {
1415
- type ??= create();
1416
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
1417
- return type.to(value);
1418
- },
1419
1534
  fromParent: (value) => {
1420
1535
  type ??= create();
1421
1536
  return type.fromParent(value);
1422
1537
  },
1423
- toParent: (value) => {
1424
- type ??= create();
1425
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
1426
- return type.toParent(value);
1427
- },
1428
1538
  is: (value) => {
1429
1539
  type ??= create();
1430
1540
  return type.is(value);
@@ -1528,7 +1638,6 @@ export const tuple = (...elements) => {
1528
1638
  }
1529
1639
  return ok(result);
1530
1640
  };
1531
- const to = (value) => value.map((val, index) => elements[index].to(val));
1532
1641
  const fromParent = (value) => {
1533
1642
  if (!Array.isArray(value) || value.length !== elements.length) {
1534
1643
  return err({
@@ -1555,13 +1664,10 @@ export const tuple = (...elements) => {
1555
1664
  }
1556
1665
  return ok(result);
1557
1666
  };
1558
- const toParent = (value) => value.map((val, index) => elements[index].toParent(val));
1559
1667
  return {
1560
1668
  ...createType("Tuple", {
1561
1669
  fromUnknown,
1562
- to,
1563
1670
  fromParent,
1564
- toParent,
1565
1671
  }),
1566
1672
  elements,
1567
1673
  };
@@ -1595,22 +1701,32 @@ export const Int64 = brand("Int64", BigInt, (value) => value >= -922337203685477
1595
1701
  ? ok(value)
1596
1702
  : err({ type: "Int64", value }));
1597
1703
  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.`);
1704
+ // // co s timhle? je to string, ze ktereho lze udelat bigint
1705
+ // export const BigIntFromString = transform(
1706
+ // String,
1707
+ // BigInt,
1708
+ // (value) =>
1709
+ // trySync(
1710
+ // () => globalThis.BigInt(value),
1711
+ // (): BigIntFromStringError => ({ type: "BigIntFromString", value }),
1712
+ // ),
1713
+ // (value) => value.toString(),
1714
+ // );
1715
+ // export interface BigIntFromStringError extends TypeError<"BigIntFromString"> {}
1716
+ // export const formatBigIntFromStringError =
1717
+ // createTypeErrorFormatter<BigIntFromStringError>(
1718
+ // (error) => `The value ${error.value} could not be converted to a BigInt.`,
1719
+ // );
1600
1720
  /**
1601
1721
  * Stringified {@link Int64}.
1602
1722
  *
1603
- * @category Number
1723
+ * @category String
1604
1724
  */
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
- });
1725
+ export const Int64String = brand("Int64", NonEmptyTrimmedString, (value) => trySync(() => {
1726
+ const maybeInt = globalThis.BigInt(value);
1727
+ Int64.orThrow(maybeInt);
1728
+ return value;
1729
+ }, () => ({ type: "Int64String", value })));
1614
1730
  export const formatInt64StringError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid Int64 string.`);
1615
1731
  /**
1616
1732
  * JSON-compatible value: string, {@link FiniteNumber}, boolean, null,
@@ -1631,48 +1747,83 @@ export const JsonArray = array(JsonValue);
1631
1747
  * @category Object
1632
1748
  */
1633
1749
  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`,
1750
+ export const parseJson = (value) => trySync(() => JSON.parse(value), (error) => ({
1751
+ type: `Json`,
1649
1752
  value,
1650
1753
  message: globalThis.String(error),
1651
- })), (value) => JSON.stringify(value));
1652
- export const formatJsonValueFromStringError = createTypeErrorFormatter((error) => `Invalid JSONValue: ${error.value}. Error: ${error.message}`);
1754
+ }));
1653
1755
  /**
1654
1756
  * JSON-string {@link Type}.
1655
1757
  *
1656
1758
  * ### Example
1657
1759
  *
1658
1760
  * ```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" })
1761
+ * const result = Json.from('{"key":"value"}'); // ok
1762
+ * const error = Json.from("invalid json"); // err
1661
1763
  * ```
1662
1764
  *
1663
1765
  * @category String
1664
1766
  */
1665
1767
  export const Json = brand("Json", String, (value) => {
1666
- const result = JsonValueFromString.fromParent(value);
1768
+ const result = parseJson(value);
1667
1769
  if (!result.ok)
1668
- return err({
1669
- type: "Json",
1670
- value,
1671
- message: result.error.message,
1672
- });
1770
+ return result;
1673
1771
  return ok(value);
1674
1772
  });
1675
1773
  export const formatJsonError = createTypeErrorFormatter((error) => `Invalid JSON: ${error.value}. Error: ${error.message}`);
1774
+ export const jsonValueToJson = (value) => JSON.stringify(value);
1775
+ export const jsonToJsonValue = (value) => JSON.parse(value);
1776
+ /**
1777
+ * Creates a branded JSON string {@link Type} and type-safe conversion functions
1778
+ * for a given Type.
1779
+ *
1780
+ * This factory creates:
1781
+ *
1782
+ * 1. A branded string Type that validates JSON parsing and structural conformity
1783
+ * 2. A serialization function (Type → branded JSON string)
1784
+ * 3. A parsing function (branded JSON string → Type, skipping validation)
1785
+ *
1786
+ * Optimized for Evolu's SQLite workflow where we store typed JSON strings and
1787
+ * need type-safe conversions without double parsing.
1788
+ *
1789
+ * ### Example
1790
+ *
1791
+ * ```ts
1792
+ * const Person = object({
1793
+ * name: NonEmptyString100,
1794
+ * age: FiniteNumber,
1795
+ * });
1796
+ * type Person = typeof Person.Type;
1797
+ *
1798
+ * const [PersonJson, personToPersonJson, personJsonToPerson] = json(
1799
+ * Person,
1800
+ * "PersonJson",
1801
+ * );
1802
+ * // string & Brand<"PersonJson">
1803
+ * type PersonJson = typeof PersonJson.Type;
1804
+ *
1805
+ * // Usage:
1806
+ * const person: Person = { name: "Alice", age: 30 };
1807
+ * const jsonString = personToPersonJson(person); // PersonJson
1808
+ * const backToPerson = personJsonToPerson(jsonString); // Person
1809
+ * ```
1810
+ */
1811
+ export const json = (type, name) => {
1812
+ const BrandedJsonType = brand(name, String, (value) => {
1813
+ const parseResult = parseJson(value);
1814
+ if (!parseResult.ok)
1815
+ return parseResult;
1816
+ const validationResult = type.fromUnknown(parseResult.value);
1817
+ if (!validationResult.ok)
1818
+ return validationResult;
1819
+ return ok(value);
1820
+ });
1821
+ return [
1822
+ BrandedJsonType,
1823
+ jsonValueToJson,
1824
+ jsonToJsonValue,
1825
+ ];
1826
+ };
1676
1827
  /**
1677
1828
  * Optional {@link Type}.
1678
1829
  *
@@ -1697,9 +1848,7 @@ export const formatJsonError = createTypeErrorFormatter((error) => `Invalid JSON
1697
1848
  export const optional = (type) => ({
1698
1849
  ...createType("Optional", {
1699
1850
  fromUnknown: type.fromUnknown,
1700
- to: type.to,
1701
1851
  fromParent: type.fromParent,
1702
- toParent: type.toParent,
1703
1852
  }),
1704
1853
  parent: type,
1705
1854
  });
@@ -1708,7 +1857,7 @@ export const isOptionalType = (x) => typeof x === "object" && x != null && "name
1708
1857
  /**
1709
1858
  * Creates a partial object type where all properties are optional.
1710
1859
  *
1711
- * This is useful when you want to validate an object in which none of the keys
1860
+ * This is useful when we want to validate an object in which none of the keys
1712
1861
  * are required, but if they are present they must conform to their
1713
1862
  * corresponding Types.
1714
1863
  *
@@ -1779,114 +1928,86 @@ export function omit(objectType, ...keys) {
1779
1928
  }
1780
1929
  return object(newProps);
1781
1930
  }
1931
+ export const maxMutationSize = 655360;
1782
1932
  /**
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
- * ```
1933
+ * Evolu has to limit the maximum mutation size. Otherwise, sync couldn't use
1934
+ * the `maxProtocolMessageRangesSize`. The max size is 640KB in bytes, measured
1935
+ * via MessagePack. Evolu Protocol DbChange will be smaller thanks to various
1936
+ * optimizations.
1810
1937
  */
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
- };
1938
+ export const validMutationSize = (type) => brand("ValidMutationSize", type, (value) => pack(value).byteLength <= maxMutationSize
1939
+ ? ok(value)
1940
+ : err({ type: "ValidMutationSize", value }));
1941
+ 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
1942
  /**
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.
1943
+ * Formats Evolu Type errors into user-friendly messages.
1944
+ *
1945
+ * Evolu Type typed errors ensure every error type must have a formatter.
1946
+ * TypeScript enforces this at compile-time, preventing unhandled validation
1947
+ * errors from reaching users.
1829
1948
  *
1830
- * If you prefer not to reuse any built-in error formatters, you can write your
1831
- * own `formatTypeError` function from scratch.
1949
+ * The `createFormatTypeError` function handles both built-in {@link TypeErrors}
1950
+ * and custom errors, and lets us override default formatting for specific
1951
+ * errors.
1832
1952
  *
1833
- * ### Examples
1953
+ * ### Example
1834
1954
  *
1835
1955
  * ```ts
1836
- * const formatError = createFormatTypeError();
1837
- * console.log(formatError({ type: "String", value: 42 }));
1838
- * // "A value 42 is not a string."
1956
+ * const formatTypeError = createFormatTypeError<
1957
+ * MinLengthError | MaxLengthError
1958
+ * >((error): string => {
1959
+ * switch (error.type) {
1960
+ * case "MinLength":
1961
+ * return `Text must be at least ${error.min} character${error.min === 1 ? "" : "s"} long`;
1962
+ * case "MaxLength":
1963
+ * return `Text is too long (maximum ${error.max} characters)`;
1964
+ * }
1965
+ * });
1839
1966
  * ```
1840
1967
  *
1841
- * A custom `formatTypeError` function:
1968
+ * Alternatively, write a custom formatter from scratch without using
1969
+ * `createFormatTypeError`. This gives us full control over error formatting:
1842
1970
  *
1843
1971
  * ```ts
1844
- * type AppErrors =
1845
- * | ValidMutationSizeError
1972
+ * const Person = object({
1973
+ * name: NonEmptyTrimmedString100,
1974
+ * age: optional(PositiveInt),
1975
+ * });
1976
+ *
1977
+ * // Define only the errors actually used by Person Type
1978
+ * type PersonErrors =
1846
1979
  * | StringError
1847
- * | MinLengthError
1848
1980
  * | MaxLengthError
1849
- * | NullError
1850
- * | IdError
1981
+ * | MinLengthError
1851
1982
  * | 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.
1983
+ * | PositiveError
1984
+ * | NonNegativeError
1985
+ * | IntError
1986
+ * | NumberError
1987
+ * | ObjectError<Record<string, PersonErrors>>;
1988
+ *
1989
+ * const formatTypeError: TypeErrorFormatter<PersonErrors> = (error) => {
1861
1990
  * 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
1991
  * case "String":
1873
- * // We can reuse existing formatter.
1874
1992
  * return formatStringError(error);
1993
+ * case "Number":
1994
+ * return "Must be a number";
1995
+ * case "MinLength":
1996
+ * return `Must be at least ${error.min} characters`;
1997
+ * case "MaxLength":
1998
+ * return `Cannot exceed ${error.max} characters`;
1875
1999
  * 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(", ")}`;
2000
+ * return "Cannot have leading or trailing spaces";
2001
+ * case "Positive":
2002
+ * return "Must be a positive number";
2003
+ * case "NonNegative":
2004
+ * return "Must be zero or positive";
2005
+ * case "Int":
2006
+ * return "Must be an integer";
1884
2007
  * 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.";
2008
+ * if (error.reason.kind === "NotObject") return "Must be an object";
2009
+ * if (error.reason.kind === "ExtraKeys")
2010
+ * return "Contains unexpected fields";
1890
2011
  * const firstError = Object.values(error.reason.errors).find(
1891
2012
  * (e) => e !== undefined,
1892
2013
  * )!;
@@ -1927,8 +2048,8 @@ export const createFormatTypeError = (extraFormatter) => {
1927
2048
  return formatIsTypeError(error);
1928
2049
  case "CurrencyCode":
1929
2050
  return formatCurrencyCodeError(error);
1930
- case "DateIsoString":
1931
- return formatDateIsoStringError(error);
2051
+ case "DateIso":
2052
+ return formatDateIsoError(error);
1932
2053
  case "Trimmed":
1933
2054
  return formatTrimmedError(error);
1934
2055
  case "MinLength":
@@ -1943,6 +2064,8 @@ export const createFormatTypeError = (extraFormatter) => {
1943
2064
  return formatRegexError(error);
1944
2065
  case "Id":
1945
2066
  return formatIdError(error);
2067
+ case "TableId":
2068
+ return formatTableIdError(error);
1946
2069
  case "Positive":
1947
2070
  return formatPositiveError(error);
1948
2071
  case "Negative":
@@ -1973,14 +2096,12 @@ export const createFormatTypeError = (extraFormatter) => {
1973
2096
  return formatLiteralError(error);
1974
2097
  case "Int64":
1975
2098
  return formatInt64Error(error);
1976
- case "BigIntFromString":
1977
- return formatBigIntFromStringError(error);
1978
2099
  case "Int64String":
1979
2100
  return formatInt64StringError(error);
1980
- case "JsonValueFromString":
1981
- return formatJsonValueFromStringError(error);
1982
2101
  case "Json":
1983
2102
  return formatJsonError(error);
2103
+ case "ValidMutationSize":
2104
+ return formatValidMutationSizeError(error);
1984
2105
  // Composite Types
1985
2106
  case "SimplePassword":
1986
2107
  return formatSimplePasswordError(formatTypeError)(error);
@@ -1996,7 +2117,88 @@ export const createFormatTypeError = (extraFormatter) => {
1996
2117
  return formatUnionError(formatTypeError)(error);
1997
2118
  case "Tuple":
1998
2119
  return formatTupleError(formatTypeError)(error);
2120
+ default: {
2121
+ // Fallback for unknown error types
2122
+ const unknownError = error;
2123
+ return `A value ${safelyStringifyUnknownValue(unknownError.value)} is not valid for type ${unknownError.type}.`;
2124
+ }
1999
2125
  }
2000
2126
  };
2001
2127
  return formatTypeError;
2002
2128
  };
2129
+ /**
2130
+ * Converts an Evolu {@link TypeError} to Standard Schema V1 issues format.
2131
+ *
2132
+ * This function recursively converts Evolu's typed errors into the Standard
2133
+ * Schema issue format with proper path tracking for nested structures.
2134
+ *
2135
+ * @category Utilities
2136
+ */
2137
+ export const typeErrorToStandardSchemaIssues = (error, formatTypeError, path = []) => {
2138
+ if (error.type === "Array") {
2139
+ const arrayError = error;
2140
+ if (arrayError.reason.kind === "NotArray") {
2141
+ return [{ message: formatTypeError(error), path }];
2142
+ }
2143
+ return typeErrorToStandardSchemaIssues(arrayError.reason.error, formatTypeError, [...path, arrayError.reason.index]);
2144
+ }
2145
+ if (error.type === "Object") {
2146
+ const objectError = error;
2147
+ if (objectError.reason.kind === "NotObject" ||
2148
+ objectError.reason.kind === "ExtraKeys") {
2149
+ return [{ message: formatTypeError(error), path }];
2150
+ }
2151
+ const issues = [];
2152
+ for (const [key, propError] of Object.entries(objectError.reason.errors)) {
2153
+ issues.push(...typeErrorToStandardSchemaIssues(propError, formatTypeError, [...path, key]));
2154
+ }
2155
+ return issues;
2156
+ }
2157
+ if (error.type === "ObjectWithRecord") {
2158
+ const objectWithRecordError = error;
2159
+ if (objectWithRecordError.reason.kind === "NotObject") {
2160
+ return [{ message: formatTypeError(error), path }];
2161
+ }
2162
+ if (objectWithRecordError.reason.kind === "IndexKey" ||
2163
+ objectWithRecordError.reason.kind === "IndexValue") {
2164
+ return typeErrorToStandardSchemaIssues(objectWithRecordError.reason.error, formatTypeError, [...path, objectWithRecordError.reason.key]);
2165
+ }
2166
+ const issues = [];
2167
+ for (const [key, propError] of Object.entries(objectWithRecordError.reason.errors)) {
2168
+ issues.push(...typeErrorToStandardSchemaIssues(propError, formatTypeError, [...path, key]));
2169
+ }
2170
+ return issues;
2171
+ }
2172
+ if (error.type === "Record") {
2173
+ const recordError = error;
2174
+ if (recordError.reason.kind === "NotRecord") {
2175
+ return [{ message: formatTypeError(error), path }];
2176
+ }
2177
+ return typeErrorToStandardSchemaIssues(recordError.reason.error, formatTypeError, [...path, recordError.reason.key]);
2178
+ }
2179
+ if (error.type === "Tuple") {
2180
+ const tupleError = error;
2181
+ if (tupleError.reason.kind === "InvalidLength") {
2182
+ return [{ message: formatTypeError(error), path }];
2183
+ }
2184
+ return typeErrorToStandardSchemaIssues(tupleError.reason.error, formatTypeError, [...path, tupleError.reason.index]);
2185
+ }
2186
+ if (error.type === "Union") {
2187
+ const unionError = error;
2188
+ return unionError.errors.flatMap((err) => typeErrorToStandardSchemaIssues(err, formatTypeError, path));
2189
+ }
2190
+ if (error.type === "Brand") {
2191
+ const brandError = error;
2192
+ if ("parentError" in brandError) {
2193
+ return typeErrorToStandardSchemaIssues(brandError.parentError, formatTypeError, path);
2194
+ }
2195
+ return [{ message: formatTypeError(error), path }];
2196
+ }
2197
+ return [{ message: formatTypeError(error), path }];
2198
+ };
2199
+ /**
2200
+ * Shared formatter cache for Standard Schema integration - avoids circular
2201
+ * dependency by lazily creating the formatter on first use rather than during
2202
+ * module initialization.
2203
+ */
2204
+ let cachedStandardSchemaFormatTypeError;