@evolu/common 6.0.1-preview.8 → 7.0.0

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 (184) hide show
  1. package/README.md +2 -2
  2. package/dist/src/Array.d.ts +256 -12
  3. package/dist/src/Array.d.ts.map +1 -1
  4. package/dist/src/Array.js +215 -9
  5. package/dist/src/Assert.d.ts +0 -13
  6. package/dist/src/Assert.d.ts.map +1 -1
  7. package/dist/src/Assert.js +0 -15
  8. package/dist/src/Brand.d.ts +75 -0
  9. package/dist/src/Brand.d.ts.map +1 -0
  10. package/dist/src/Brand.js +1 -0
  11. package/dist/src/Buffer.d.ts +1 -1
  12. package/dist/src/Buffer.d.ts.map +1 -1
  13. package/dist/src/Buffer.js +8 -7
  14. package/dist/src/Cache.d.ts +44 -0
  15. package/dist/src/Cache.d.ts.map +1 -0
  16. package/dist/src/Cache.js +52 -0
  17. package/dist/src/Callbacks.d.ts +45 -12
  18. package/dist/src/Callbacks.d.ts.map +1 -1
  19. package/dist/src/Callbacks.js +14 -7
  20. package/dist/src/Console.d.ts +31 -6
  21. package/dist/src/Console.d.ts.map +1 -1
  22. package/dist/src/Console.js +72 -9
  23. package/dist/src/Crypto.d.ts +56 -42
  24. package/dist/src/Crypto.d.ts.map +1 -1
  25. package/dist/src/Crypto.js +40 -53
  26. package/dist/src/Evolu/Db.d.ts +162 -74
  27. package/dist/src/Evolu/Db.d.ts.map +1 -1
  28. package/dist/src/Evolu/Db.js +284 -702
  29. package/dist/src/Evolu/Diff.d.ts +3 -3
  30. package/dist/src/Evolu/Diff.d.ts.map +1 -1
  31. package/dist/src/Evolu/Diff.js +7 -5
  32. package/dist/src/Evolu/Evolu.d.ts +214 -134
  33. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  34. package/dist/src/Evolu/Evolu.js +189 -180
  35. package/dist/src/Evolu/Internal.d.ts +0 -2
  36. package/dist/src/Evolu/Internal.d.ts.map +1 -1
  37. package/dist/src/Evolu/Internal.js +0 -2
  38. package/dist/src/Evolu/LocalAuth.d.ts +150 -0
  39. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
  40. package/dist/src/Evolu/LocalAuth.js +174 -0
  41. package/dist/src/Evolu/Owner.d.ts +273 -120
  42. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  43. package/dist/src/Evolu/Owner.js +130 -104
  44. package/dist/src/Evolu/Platform.d.ts +9 -7
  45. package/dist/src/Evolu/Platform.d.ts.map +1 -1
  46. package/dist/src/Evolu/Protocol.d.ts +268 -240
  47. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  48. package/dist/src/Evolu/Protocol.js +600 -454
  49. package/dist/src/Evolu/Public.d.ts +6 -8
  50. package/dist/src/Evolu/Public.d.ts.map +1 -1
  51. package/dist/src/Evolu/Public.js +2 -3
  52. package/dist/src/Evolu/PublicKysely.d.ts.map +1 -1
  53. package/dist/src/Evolu/PublicKysely.js +3 -4
  54. package/dist/src/Evolu/Query.d.ts +2 -1
  55. package/dist/src/Evolu/Query.d.ts.map +1 -1
  56. package/dist/src/Evolu/Query.js +1 -1
  57. package/dist/src/Evolu/Relay.d.ts +91 -8
  58. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  59. package/dist/src/Evolu/Relay.js +214 -88
  60. package/dist/src/Evolu/Schema.d.ts +125 -47
  61. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  62. package/dist/src/Evolu/Schema.js +175 -31
  63. package/dist/src/Evolu/Storage.d.ts +249 -27
  64. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  65. package/dist/src/Evolu/Storage.js +198 -92
  66. package/dist/src/Evolu/Sync.d.ts +68 -13
  67. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  68. package/dist/src/Evolu/Sync.js +469 -20
  69. package/dist/src/Evolu/Timestamp.d.ts +83 -30
  70. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  71. package/dist/src/Evolu/Timestamp.js +79 -34
  72. package/dist/src/Identicon.d.ts +35 -0
  73. package/dist/src/Identicon.d.ts.map +1 -0
  74. package/dist/src/Identicon.js +143 -0
  75. package/dist/src/Instances.d.ts +34 -0
  76. package/dist/src/Instances.d.ts.map +1 -0
  77. package/dist/src/Instances.js +44 -0
  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/Object.d.ts +10 -4
  82. package/dist/src/Object.d.ts.map +1 -1
  83. package/dist/src/Object.js +9 -3
  84. package/dist/src/Platform.d.ts +20 -0
  85. package/dist/src/Platform.d.ts.map +1 -0
  86. package/dist/src/Platform.js +22 -0
  87. package/dist/src/Random.d.ts +3 -2
  88. package/dist/src/Random.d.ts.map +1 -1
  89. package/dist/src/Relation.d.ts +79 -0
  90. package/dist/src/Relation.d.ts.map +1 -0
  91. package/dist/src/Relation.js +127 -0
  92. package/dist/src/Resources.d.ts +118 -0
  93. package/dist/src/Resources.d.ts.map +1 -0
  94. package/dist/src/Resources.js +197 -0
  95. package/dist/src/Result.d.ts +184 -52
  96. package/dist/src/Result.d.ts.map +1 -1
  97. package/dist/src/Result.js +30 -241
  98. package/dist/src/Skiplist.js +2 -1
  99. package/dist/src/Sqlite.d.ts +89 -5
  100. package/dist/src/Sqlite.d.ts.map +1 -1
  101. package/dist/src/Sqlite.js +136 -9
  102. package/dist/src/Task.d.ts +586 -0
  103. package/dist/src/Task.d.ts.map +1 -0
  104. package/dist/src/Task.js +469 -0
  105. package/dist/src/Time.d.ts +66 -1
  106. package/dist/src/Time.d.ts.map +1 -1
  107. package/dist/src/Time.js +99 -5
  108. package/dist/src/Type.d.ts +676 -343
  109. package/dist/src/Type.d.ts.map +1 -1
  110. package/dist/src/Type.js +718 -467
  111. package/dist/src/Types.d.ts +1 -75
  112. package/dist/src/Types.d.ts.map +1 -1
  113. package/dist/src/WebSocket.d.ts +5 -2
  114. package/dist/src/WebSocket.d.ts.map +1 -1
  115. package/dist/src/WebSocket.js +12 -18
  116. package/dist/src/Worker.d.ts +39 -11
  117. package/dist/src/Worker.d.ts.map +1 -1
  118. package/dist/src/Worker.js +22 -4
  119. package/dist/src/index.d.ts +8 -3
  120. package/dist/src/index.d.ts.map +1 -1
  121. package/dist/src/index.js +8 -3
  122. package/package.json +15 -14
  123. package/src/Array.ts +301 -19
  124. package/src/Assert.ts +0 -21
  125. package/src/Brand.ts +75 -0
  126. package/src/Buffer.ts +7 -7
  127. package/src/Cache.ts +85 -0
  128. package/src/Callbacks.ts +62 -22
  129. package/src/Console.ts +91 -11
  130. package/src/Crypto.ts +94 -90
  131. package/src/Evolu/Db.ts +519 -1026
  132. package/src/Evolu/Diff.ts +7 -5
  133. package/src/Evolu/Evolu.ts +477 -361
  134. package/src/Evolu/Internal.ts +0 -2
  135. package/src/Evolu/LocalAuth.ts +463 -0
  136. package/src/Evolu/Owner.ts +355 -228
  137. package/src/Evolu/Platform.ts +9 -9
  138. package/src/Evolu/Protocol.ts +842 -742
  139. package/src/Evolu/Public.ts +7 -14
  140. package/src/Evolu/PublicKysely.ts +4 -5
  141. package/src/Evolu/Query.ts +3 -2
  142. package/src/Evolu/Relay.ts +406 -103
  143. package/src/Evolu/Schema.ts +323 -91
  144. package/src/Evolu/Storage.ts +559 -137
  145. package/src/Evolu/Sync.ts +819 -36
  146. package/src/Evolu/Timestamp.ts +90 -58
  147. package/src/Identicon.ts +197 -0
  148. package/src/Instances.ts +90 -0
  149. package/src/Number.ts +6 -10
  150. package/src/Object.ts +13 -5
  151. package/src/Platform.ts +26 -0
  152. package/src/Random.ts +3 -2
  153. package/src/Relation.ts +234 -0
  154. package/src/Resources.ts +367 -0
  155. package/src/Result.ts +191 -54
  156. package/src/Skiplist.ts +1 -1
  157. package/src/Sqlite.ts +152 -17
  158. package/src/Task.ts +901 -0
  159. package/src/Time.ts +180 -5
  160. package/src/Type.ts +1135 -730
  161. package/src/Types.ts +1 -77
  162. package/src/WebSocket.ts +27 -25
  163. package/src/Worker.ts +72 -23
  164. package/src/index.ts +8 -3
  165. package/dist/src/Evolu/Config.d.ts +0 -69
  166. package/dist/src/Evolu/Config.d.ts.map +0 -1
  167. package/dist/src/Evolu/Config.js +0 -9
  168. package/dist/src/Evolu/Kysely.d.ts +0 -6
  169. package/dist/src/Evolu/Kysely.d.ts.map +0 -1
  170. package/dist/src/Evolu/Kysely.js +0 -21
  171. package/dist/src/ManyToManyMap.d.ts +0 -26
  172. package/dist/src/ManyToManyMap.d.ts.map +0 -1
  173. package/dist/src/ManyToManyMap.js +0 -92
  174. package/dist/src/NanoId.d.ts +0 -27
  175. package/dist/src/NanoId.d.ts.map +0 -1
  176. package/dist/src/NanoId.js +0 -6
  177. package/dist/src/Promise.d.ts +0 -180
  178. package/dist/src/Promise.d.ts.map +0 -1
  179. package/dist/src/Promise.js +0 -176
  180. package/src/Evolu/Config.ts +0 -83
  181. package/src/Evolu/Kysely.ts +0 -38
  182. package/src/ManyToManyMap.ts +0 -140
  183. package/src/NanoId.ts +0 -39
  184. package/src/Promise.ts +0 -295
package/dist/src/Type.js CHANGED
@@ -1,80 +1,213 @@
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
+ * composable typed error telling us exactly why validation failed.
5
7
  *
6
- * You probably know [Zod](https://zod.dev). Evolu has {@link Type}.
8
+ * Why another validation library?
7
9
  *
8
- * Evolu Type exists because no existing validation/parsing/transformation
9
- * library fully met our needs:
10
+ * - **Result-based error handling** no exceptions for normal control flow.
11
+ * - **Typed errors with decoupled formatters** – validation logic ≠ user
12
+ * messages.
13
+ * - **Consistent constraints via {@link Brand}** – every constraint becomes part
14
+ * of the type.
15
+ * - **Skippable validation** – parent validations can be skipped when already
16
+ * proved by types.
17
+ * - **Simple, top-down implementation** – readable source code from top to
18
+ * bottom.
19
+ * - **No user-land chaining DSL** – prepared for TC39 Hack pipes.
10
20
  *
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.
21
+ * A distinctive feature of Evolu Type compared to other validation libraries is
22
+ * that it returns typed errors rather than string messages. This allows
23
+ * TypeScript to enforce that all validation errors are handled by type
24
+ * checking, significantly improving the developer experience.
22
25
  *
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.
26
+ * Evolu Type supports [Standard Schema](https://standardschema.dev/) for
27
+ * interoperability with 40+ validation-compatible tools and frameworks.
27
28
  *
28
- * - Evolu `Type` is:
29
+ * ### Base Types Quick Start
29
30
  *
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.
31
+ * ```ts
32
+ * // Validate unknown values
33
+ * const value: unknown = "hello";
34
+ * const stringResult = String.fromUnknown(value);
35
+ * if (!stringResult.ok) {
36
+ * // console.error(formatStringError(stringResult.error));
37
+ * return stringResult; // inside a function returning Result<string, _>
38
+ * }
39
+ * // Safe branch: value is now string
40
+ * const upper = stringResult.value.toUpperCase();
41
+ *
42
+ * // Type guard style
43
+ * if (String.is(value)) {
44
+ * // narrowed to string
45
+ * }
46
+ *
47
+ * // Composing: arrays & objects
48
+ * const Numbers = array(Number); // ReadonlyArray<number>
49
+ * const Point = object({ x: Number, y: Number });
50
+ *
51
+ * Numbers.from([1, 2, 3]); // ok
52
+ * Point.from({ x: 1, y: 2 }); // ok
53
+ * Point.from({ x: 1, y: "2" }); // err -> nested Number error
54
+ * ```
55
+ *
56
+ * ### Branding Basics
57
+ *
58
+ * Branding adds semantic meaning & constraints while preserving the runtime
59
+ * shape:
60
+ *
61
+ * ```ts
62
+ * const CurrencyCode = brand("CurrencyCode", String, (value) =>
63
+ * /^[A-Z]{3}$/.test(value)
64
+ * ? ok(value)
65
+ * : err<CurrencyCodeError>({ type: "CurrencyCode", value }),
66
+ * );
67
+ * type CurrencyCode = typeof CurrencyCode.Type; // string & Brand<"CurrencyCode">
68
+ *
69
+ * interface CurrencyCodeError extends TypeError<"CurrencyCode"> {}
70
+ *
71
+ * const formatCurrencyCodeError =
72
+ * createTypeErrorFormatter<CurrencyCodeError>(
73
+ * (error) => `Invalid currency code: ${error.value}`,
74
+ * );
75
+ *
76
+ * const r = CurrencyCode.from("USD"); // ok("USD")
77
+ * const e = CurrencyCode.from("usd"); // err(...)
78
+ * ```
79
+ *
80
+ * See also reusable brand factories like `minLength`, `maxLength`, `trimmed`,
81
+ * `positive`, `between`, etc.
34
82
  *
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:
83
+ * ### Objects & Optional Fields
39
84
  *
40
- * `Unknown` -> `String` -> `TrimmedString` -> `NonEmptyTrimmedString100`
85
+ * ```ts
86
+ * const User = object({
87
+ * name: NonEmptyTrimmedString100,
88
+ * age: optional(PositiveInt),
89
+ * });
90
+ * type User = typeof User.Type;
91
+ *
92
+ * User.from({ name: "Alice" }); // ok
93
+ * User.from({ name: "Alice", age: -1 }); // err(PositiveInt)
94
+ * ```
95
+ *
96
+ * ### Deriving JSON String Types
41
97
  *
42
- * For `NonEmptyTrimmedString100`, the parent Type is `TrimmedString`. For
43
- * `TrimmedString`, the parent Type is `String`.
98
+ * ```ts
99
+ * const Person = object({
100
+ * name: NonEmptyString50,
101
+ * // Did you know that JSON.stringify converts NaN (a number) into null?
102
+ * // To prevent this, use FiniteNumber.
103
+ * age: FiniteNumber,
104
+ * });
105
+ * type Person = typeof Person.Type;
106
+ *
107
+ * const [PersonJson, personToPersonJson, personJsonToPerson] = json(
108
+ * Person,
109
+ * "PersonJson",
110
+ * );
111
+ * // string & Brand<"PersonJson">
112
+ * type PersonJson = typeof PersonJson.Type;
113
+ *
114
+ * const person = Person.orThrow({
115
+ * name: "Alice",
116
+ * age: 30,
117
+ * });
118
+ *
119
+ * const personJson = personToPersonJson(person);
120
+ * expect(personJsonToPerson(personJson)).toEqual(person);
121
+ * ```
122
+ *
123
+ * ### Error Formatting
124
+ *
125
+ * Evolu separates validation logic from human-readable messages. There are two
126
+ * layers:
127
+ *
128
+ * 1. Per-type formatters (e.g. `formatStringError`) – simple, focused, already
129
+ * used earlier in the quick start example.
130
+ * 2. A unified formatter via `createFormatTypeError` – composes all built-in and
131
+ * custom errors (including nested composite types) and lets us override
132
+ * selected messages.
133
+ *
134
+ * #### 1. Per-Type Formatter (recap)
135
+ *
136
+ * ```ts
137
+ * const r = String.fromUnknown(42);
138
+ * if (!r.ok) console.error(formatStringError(r.error));
139
+ * ```
140
+ *
141
+ * #### 2. Unified Formatter with Overrides
142
+ *
143
+ * ```ts
144
+ * // Override only what we care about; fall back to built-ins for the rest.
145
+ * const formatTypeError = createFormatTypeError((error) => {
146
+ * if (error.type === "MinLength") return `Min length is ${error.min}`;
147
+ * });
44
148
  *
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.
149
+ * const User = object({ name: NonEmptyTrimmedString100 });
150
+ * const resultUser = User.from({ name: "" });
151
+ * if (!resultUser.ok) console.error(formatTypeError(resultUser.error));
48
152
  *
49
- * Speaking of `fromParent` and `toParent`, those functions exist to bypass
50
- * parent Types when we can rely on TypeScript types.
153
+ * const badPoint = object({ x: Number, y: Number }).from({
154
+ * x: 1,
155
+ * y: "foo",
156
+ * });
157
+ * if (!badPoint.ok) console.error(formatTypeError(badPoint.error));
158
+ * ```
51
159
  *
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")`.
160
+ * The unified formatter walks nested structures (object / array / record /
161
+ * tuple / union) and applies overrides only where specified, greatly reducing
162
+ * boilerplate when formatting complex validation errors.
57
163
  *
58
164
  * ### Tip
59
165
  *
60
166
  * If necessary, write `globalThis.String` instead of `String` to avoid naming
61
- * clashes with Base Types.
167
+ * clashes with native types.
168
+ *
169
+ * ### Design Decision: No Bidirectional Transformations
170
+ *
171
+ * Evolu Type intentionally does not support bidirectional transformations. It
172
+ * previously did, but supporting that while keeping typed error fidelity added
173
+ * complexity that hurt readability & reliability. Most persistence pipelines
174
+ * (e.g. SQLite) already require explicit mapping of query results, so implicit
175
+ * reverse transforms would not buy much. We may revisit this if we can design a
176
+ * minimal, 100% safe API that preserves simplicity.
177
+ *
178
+ * ### Prepared for TC39 Hack Pipes
179
+ *
180
+ * Take a look how `SimplePassword` is defined:
181
+ *
182
+ * ```ts
183
+ * export const SimplePassword = brand(
184
+ * "SimplePassword",
185
+ * minLength(8)(maxLength(64)(TrimmedString)),
186
+ * );
187
+ * ```
188
+ *
189
+ * Nested functions are often OK (if not, make a helper) and read well, but with
190
+ * TC39 Hack pipes it would be clearer:
62
191
  *
63
- * ### Design Decision:
192
+ * ```ts
193
+ * // TrimmedString
194
+ * // |> minLength(8)(%)
195
+ * // |> maxLength(64)(%)
196
+ * // |> brand("SimplePassword", %)
197
+ * ```
64
198
  *
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.
199
+ * Note `minLength` and `maxLength` are curried because they are factories.
69
200
  *
70
201
  * @module
71
202
  */
203
+ import { utf8ToBytes } from "@noble/ciphers/utils.js";
204
+ import { sha256 } from "@noble/hashes/sha2.js";
72
205
  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";
206
+ import { wordlist } from "@scure/bip39/wordlists/english.js";
207
+ import { pack } from "msgpackr";
76
208
  import { isPlainObject } from "./Object.js";
77
- import { err, ok, trySync } from "./Result.js";
209
+ import { hasNodeBuffer } from "./Platform.js";
210
+ import { err, getOrNull, getOrThrow, ok, trySync } from "./Result.js";
78
211
  import { safelyStringifyUnknownValue } from "./String.js";
79
212
  const EvoluTypeSymbol = Symbol("evolu.Type");
80
213
  /**
@@ -88,6 +221,8 @@ const createType = (name, definition) => ({
88
221
  name,
89
222
  is: (value) => definition.fromUnknown(value).ok,
90
223
  from: definition.fromUnknown,
224
+ orThrow: (value) => getOrThrow(definition.fromUnknown(value)),
225
+ orNull: (value) => getOrNull(definition.fromUnknown(value)),
91
226
  [EvoluTypeSymbol]: true,
92
227
  Type: undefined,
93
228
  Input: undefined,
@@ -95,6 +230,24 @@ const createType = (name, definition) => ({
95
230
  Parent: undefined,
96
231
  ParentError: undefined,
97
232
  Errors: undefined,
233
+ "~standard": {
234
+ version: 1,
235
+ vendor: "evolu",
236
+ validate: (value) => {
237
+ const result = definition.fromUnknown(value);
238
+ if (result.ok) {
239
+ return { value: result.value };
240
+ }
241
+ cachedStandardSchemaFormatTypeError ??= createFormatTypeError();
242
+ return {
243
+ issues: typeErrorToStandardSchemaIssues(result.error, cachedStandardSchemaFormatTypeError),
244
+ };
245
+ },
246
+ types: {
247
+ input: undefined,
248
+ output: undefined,
249
+ },
250
+ },
98
251
  });
99
252
  /**
100
253
  * Creates a formatter function for {@link TypeError}.
@@ -117,12 +270,6 @@ export const createTypeErrorFormatter = (format) => (error) => format({ ...error
117
270
  * Base {@link Type}.
118
271
  *
119
272
  * 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
273
  *
127
274
  * ### Example
128
275
  *
@@ -144,9 +291,7 @@ export const createTypeErrorFormatter = (format) => (error) => format({ ...error
144
291
  */
145
292
  export const base = (name, fromUnknown) => createType(name, {
146
293
  fromUnknown,
147
- to: identity,
148
294
  fromParent: (ok), // `fromParent` relies on types, so it can't fail for the Base Type
149
- toParent: identity,
150
295
  });
151
296
  /**
152
297
  * Creates a formatter function for a base {@link TypeError}.
@@ -233,7 +378,7 @@ export const instanceOf = (ctor) => ({
233
378
  : err({ type: "InstanceOf", value, ctor: ctor.name })),
234
379
  ctor,
235
380
  });
236
- export const formatInstanceOfError = createTypeErrorFormatter((error) => `Value ${error.value} is not an instance of ${error.ctor}`);
381
+ export const formatInstanceOfError = createTypeErrorFormatter((error) => `The value ${error.value} is not an instance of ${error.ctor}.`);
237
382
  /**
238
383
  * JavaScript Date.
239
384
  *
@@ -280,9 +425,7 @@ export function brand(name, parent, refine) {
280
425
  return {
281
426
  ...createType("Brand", {
282
427
  fromUnknown,
283
- to: identity,
284
428
  fromParent: (refine ?? ok),
285
- toParent: identity,
286
429
  }),
287
430
  brand: name,
288
431
  parentType: parent,
@@ -296,7 +439,7 @@ export function brand(name, parent, refine) {
296
439
  export const CurrencyCode = brand("CurrencyCode", String, (value) => /^[A-Z]{3}$/.test(value)
297
440
  ? ok(value)
298
441
  : err({ type: "CurrencyCode", value }));
299
- export const formatCurrencyCodeError = createTypeErrorFormatter((error) => `Invalid currency code: ${error.value}`);
442
+ export const formatCurrencyCodeError = createTypeErrorFormatter((error) => `Invalid currency code: ${error.value}.`);
300
443
  /**
301
444
  * ISO 8601 date-time string.
302
445
  *
@@ -312,37 +455,39 @@ export const formatCurrencyCodeError = createTypeErrorFormatter((error) => `Inva
312
455
  * ### Example
313
456
  *
314
457
  * ```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
458
+ * const result = DateIso.from("2023-01-01T12:00:00.000Z"); // ok
459
+ * const error = DateIso.from("10000-01-01T00:00:00.000Z"); // err
317
460
  * ```
318
461
  *
319
462
  * @category String
320
463
  */
321
- export const DateIsoString = brand("DateIso", String, (value) => {
464
+ export const DateIso = brand("DateIso", String, (value) => {
322
465
  if (value.length !== 24) {
323
- return err({ type: "DateIsoString", value });
466
+ return err({ type: "DateIso", value });
324
467
  }
325
- if (isNaN(globalThis.Date.parse(value))) {
326
- return err({ type: "DateIsoString", value });
468
+ const parsed = globalThis.Date.parse(value);
469
+ if (isNaN(parsed)) {
470
+ return err({ type: "DateIso", value });
471
+ }
472
+ // Round-trip test: ensure the string is actually a proper ISO format
473
+ const roundTrip = new globalThis.Date(parsed).toISOString();
474
+ if (roundTrip !== value) {
475
+ return err({ type: "DateIso", value });
327
476
  }
328
477
  return ok(value);
329
478
  });
330
- export const formatDateIsoStringError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid ISO 8601 date string.`);
479
+ export const formatDateIsoError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid ISO 8601 date string.`);
480
+ export const dateToDateIso = (value) => DateIso.fromParent(value.toISOString());
481
+ export const dateIsoToDate = (value) => new globalThis.Date(value);
331
482
  /**
332
483
  * Trimmed string.
333
484
  *
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.
485
+ * This Type Factory validates whether a string has no leading or trailing
486
+ * whitespaces.
337
487
  *
338
- * ### Examples
488
+ * ### Example
339
489
  *
340
490
  * ```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
491
  * const TrimmedNonEmptyString = trimmed(minLength(1)(String));
347
492
  * // string & Brand<"MinLength1"> & Brand<"Trimmed">
348
493
  * type TrimmedNonEmptyString = typeof TrimmedNonEmptyString.Type;
@@ -353,33 +498,7 @@ export const formatDateIsoStringError = createTypeErrorFormatter((error) => `The
353
498
  export const trimmed = (parent) => brand("Trimmed", parent, (value) => value.trim().length === value.length
354
499
  ? ok(value)
355
500
  : 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);
501
+ export const formatTrimmedError = createTypeErrorFormatter((error) => `The value ${error.value} must be trimmed.`);
383
502
  /**
384
503
  * Trimmed string
385
504
  *
@@ -389,6 +508,7 @@ export const trim = (parent) => transform(parent, trimmed(parent), (value) => ok
389
508
  * @category String
390
509
  */
391
510
  export const TrimmedString = trimmed(String);
511
+ export const trim = (value) => value.trim();
392
512
  /**
393
513
  * Minimum length.
394
514
  *
@@ -403,7 +523,7 @@ export const TrimmedString = trimmed(String);
403
523
  * @category Array
404
524
  */
405
525
  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}.`);
526
+ export const formatMinLengthError = createTypeErrorFormatter((error) => `The value ${error.value} does not meet the minimum length of ${error.min}.`);
407
527
  /**
408
528
  * Maximum length.
409
529
  *
@@ -418,7 +538,7 @@ export const formatMinLengthError = createTypeErrorFormatter((error) => `Value $
418
538
  * @category Array
419
539
  */
420
540
  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}.`);
541
+ export const formatMaxLengthError = createTypeErrorFormatter((error) => `The value ${error.value} exceeds the maximum length of ${error.max}.`);
422
542
  /**
423
543
  * Exact length.
424
544
  *
@@ -435,7 +555,7 @@ export const formatMaxLengthError = createTypeErrorFormatter((error) => `Value $
435
555
  export const length = (exact) => (parent) => brand(`Length${exact}`, parent, (value) => value.length === exact
436
556
  ? ok(value)
437
557
  : err({ type: "Length", value, exact }));
438
- export const formatLengthError = createTypeErrorFormatter((error) => `Value ${error.value} does not have the required length of ${error.exact}.`);
558
+ export const formatLengthError = createTypeErrorFormatter((error) => `The value ${error.value} does not have the required length of ${error.exact}.`);
439
559
  /** @category String */
440
560
  export const NonEmptyString = minLength(1)(String);
441
561
  /** @category String */
@@ -468,7 +588,7 @@ export const NonEmptyTrimmedString1000 = minLength(1)(TrimmedString1000);
468
588
  export const Mnemonic = brand("Mnemonic", NonEmptyTrimmedString, (value) => bip39.validateMnemonic(value, wordlist)
469
589
  ? ok(value)
470
590
  : err({ type: "Mnemonic", value }));
471
- export const formatMnemonicError = createTypeErrorFormatter((error) => `Invalid BIP39 mnemonic: ${error.value}`);
591
+ export const formatMnemonicError = createTypeErrorFormatter((error) => `Invalid BIP39 mnemonic: ${error.value}.`);
472
592
  /**
473
593
  * String matching a regular expression.
474
594
  *
@@ -492,11 +612,11 @@ export const regex = (name, pattern) => {
492
612
  : err({ type: "Regex", name, value, pattern });
493
613
  });
494
614
  };
495
- export const formatRegexError = createTypeErrorFormatter((error) => `Value ${error.value} does not match the pattern for ${error.name}: ${error.pattern}`);
615
+ export const formatRegexError = createTypeErrorFormatter((error) => `The value ${error.value} does not match the pattern for ${error.name}: ${error.pattern}.`);
496
616
  /**
497
- * URL-safe Base64 string.
617
+ * URL-safe string.
498
618
  *
499
- * A `Base64Url` string uses a limited alphabet that is URL-safe:
619
+ * A `UrlSafeString` uses a limited alphabet that is safe for URLs:
500
620
  *
501
621
  * - Uppercase letters (`A-Z`)
502
622
  * - Lowercase letters (`a-z`)
@@ -504,31 +624,90 @@ export const formatRegexError = createTypeErrorFormatter((error) => `Value ${err
504
624
  * - Dash (`-`)
505
625
  * - Underscore (`_`)
506
626
  *
627
+ * This is the same character set used by Base64Url encoding, but this type does
628
+ * not validate that the string is actually Base64Url-encoded data.
629
+ *
507
630
  * ### Example
508
631
  *
509
632
  * ```ts
510
- * const result = Base64Url.from("abc123_-");
633
+ * const result = UrlSafeString.from("abc123_-");
511
634
  * if (result.ok) {
512
- * console.log("Valid Base64Url string:", result.value);
635
+ * console.log("Valid URL-safe string:", result.value);
513
636
  * } else {
514
- * console.error("Invalid Base64Url string:", result.error);
637
+ * console.error("Invalid URL-safe string:", result.error);
515
638
  * }
516
639
  * ```
517
640
  *
518
641
  * @category String
519
642
  */
520
- export const Base64Url = regex("Base64Url", /^[A-Za-z0-9_-]+$/)(String);
643
+ export const UrlSafeString = regex("UrlSafeString", /^[A-Za-z0-9_-]+$/)(String);
521
644
  /**
522
- * Simple alphanumeric string for naming.
645
+ * Base64Url without padding.
523
646
  *
524
- * A `SimpleName` string uses a limited, safe alphabet for naming purposes:
647
+ * Encode with {@link uint8ArrayToBase64Url}, decode with
648
+ * {@link base64UrlToUint8Array}.
525
649
  *
526
- * - Uppercase letters (`A-Z`)
527
- * - Lowercase letters (`a-z`)
528
- * - Digits (`0-9`)
529
- * - Dash (`-`)
650
+ * @category String
651
+ */
652
+ export const Base64Url = brand("Base64Url", String, (value) => {
653
+ // Round-trip validation ensures consistency across different base64url
654
+ // implementations (Node.js Buffer, native browser API, manual fallback).
655
+ // Only strings that decode and encode identically are accepted.
656
+ let roundTrip;
657
+ try {
658
+ roundTrip = uint8ArrayToBase64Url(base64UrlToUint8Array(value));
659
+ }
660
+ catch {
661
+ //
662
+ }
663
+ return roundTrip === value
664
+ ? ok(value)
665
+ : err({ type: "Base64Url", value });
666
+ });
667
+ export const formatBase64UrlError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid Base64Url string.`);
668
+ const base64UrlOptions = { alphabet: "base64url", omitPadding: true };
669
+ /** Encodes a Uint8Array to a {@link Base64Url} string. */
670
+ export const uint8ArrayToBase64Url = hasNodeBuffer
671
+ ? (bytes) => globalThis.Buffer.from(bytes).toString("base64url")
672
+ : // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
673
+ typeof globalThis.Uint8Array.prototype?.toBase64 !== "undefined"
674
+ ? (bytes) =>
675
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
676
+ bytes.toBase64(base64UrlOptions)
677
+ : (bytes) => {
678
+ const binaryString = Array.from(bytes, (byte) => globalThis.String.fromCodePoint(byte)).join("");
679
+ const base64 = globalThis.btoa(binaryString);
680
+ return base64
681
+ .replace(/\+/g, "-")
682
+ .replace(/\//g, "_")
683
+ .replace(/=/g, "");
684
+ };
685
+ /** Decodes a {@link Base64Url} string to a Uint8Array. */
686
+ export const base64UrlToUint8Array = hasNodeBuffer
687
+ ? (str) => {
688
+ const nodeBuffer = globalThis.Buffer.from(str, "base64url");
689
+ return new globalThis.Uint8Array(nodeBuffer);
690
+ }
691
+ : // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
692
+ typeof globalThis.Uint8Array?.fromBase64 !== "undefined"
693
+ ? (str) =>
694
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
695
+ globalThis.Uint8Array.fromBase64(str, base64UrlOptions)
696
+ : (str) => {
697
+ let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
698
+ while (base64.length % 4 !== 0) {
699
+ base64 += "=";
700
+ }
701
+ const binaryString = globalThis.atob(base64);
702
+ return globalThis.Uint8Array.from(binaryString, (c) => c.charCodeAt(0));
703
+ };
704
+ /**
705
+ * Simple alphanumeric string for naming in file systems, URLs, and identifiers.
706
+ *
707
+ * Uses the same safe alphabet as {@link UrlSafeString} (letters, digits, `-`,
708
+ * `_`). See `UrlSafeString` for details.
530
709
  *
531
- * The string must be between 1 and 42 characters.
710
+ * The string must be between 1 and 64 characters.
532
711
  *
533
712
  * ### Example
534
713
  *
@@ -543,43 +722,153 @@ export const Base64Url = regex("Base64Url", /^[A-Za-z0-9_-]+$/)(String);
543
722
  *
544
723
  * @category String
545
724
  */
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);
725
+ export const SimpleName = brand("SimpleName", UrlSafeString, (value) => value.length >= 1 && value.length <= 64
726
+ ? ok(value)
727
+ : err({ type: "SimpleName", value }));
553
728
  /**
554
729
  * Trimmed string between 8 and 64 characters, branded as `SimplePassword`.
555
730
  *
731
+ * Take a look how `SimplePassword` is defined:
732
+ *
733
+ * ```ts
734
+ * export const SimplePassword = brand(
735
+ * "SimplePassword",
736
+ * minLength(8)(maxLength(64)(TrimmedString)),
737
+ * );
738
+ * ```
739
+ *
740
+ * Nested functions are often OK (if not, make a helper), but with TC39 Hack
741
+ * pipes it would be clearer:
742
+ *
743
+ * ```ts
744
+ * // TrimmedString
745
+ * // |> minLength(8)(%)
746
+ * // |> maxLength(64)(%)
747
+ * // |> brand("SimplePassword", %)
748
+ * ```
749
+ *
556
750
  * @category String
557
751
  */
558
752
  export const SimplePassword = brand("SimplePassword", minLength(8)(maxLength(64)(TrimmedString)));
559
753
  export const formatSimplePasswordError = (formatTypeError) => createTypeErrorFormatter((error) => `Invalid password: ${formatTypeError(error.parentError)}`);
560
754
  /**
561
- * `Id` {@link Type}.
755
+ * Evolu Id: 16 bytes encoded as a 22‑character Base64Url string.
756
+ *
757
+ * There are three ways to create an Evolu Id:
758
+ *
759
+ * - {@link createId} – default cryptographically secure random bytes
760
+ * (privacy‑preserving)
761
+ * - {@link createIdFromString} – deterministic: first 16 bytes of SHA‑256 of a
762
+ * string
763
+ * - {@link createIdAsUuidv7} – optional: embeds timestamp bits (UUID v7 layout)
764
+ *
765
+ * Privacy: the default random Id does not leak creation time and is safe to
766
+ * share or log. The UUID v7 variant leaks creation time anywhere the Id is
767
+ * copied (logs, URLs, exports); only use it when you explicitly want insertion
768
+ * locality for very large write‑heavy tables and accept timestamp exposure.
769
+ *
770
+ * ### Future
562
771
  *
563
- * Represents a unique identifier with exactly 21 characters, using NanoID's
564
- * standard format (`A-Za-z0-9_-`).
772
+ * A possible hybrid masked‑time approach (`timestamp ^ H(cluster_id, timestamp
773
+ *
774
+ * > > N)`) could provide locality without exposing raw creation time. See
775
+ * > > https://brooker.co.za/blog/2025/10/22/uuidv7.html
565
776
  *
566
777
  * @category String
567
778
  */
568
- export const Id = regex("Id", /^[A-Za-z0-9_-]{21}$/)(String);
569
- export const idTypeValueLength = 21;
779
+ export const Id = brand("Id", String, (value) => value.length === 22 && Base64Url.fromParent(value).ok
780
+ ? ok(value)
781
+ : err({ type: "Id", value }));
782
+ export const formatIdError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid Id.`);
570
783
  /**
571
- * Creates an {@link Id}.
784
+ * Creates a random {@link Id}. This is the recommended default.
785
+ *
786
+ * Use {@link createIdFromString} for deterministic mapping of external IDs or
787
+ * {@link createIdAsUuidv7} when you accept timestamp leakage for index
788
+ * locality.
572
789
  *
573
790
  * ### Example
574
791
  *
575
792
  * ```ts
576
- * // string & Brand<"Id">
577
793
  * const id = createId(deps);
794
+ * const todoId = createId<"Todo">(deps);
578
795
  * ```
579
796
  */
580
- export const createId = (deps) => deps.nanoIdLib.nanoid();
797
+ export const createId = (deps) => {
798
+ const id = uint8ArrayToBase64Url(deps.randomBytes.create(16));
799
+ return id;
800
+ };
581
801
  /**
582
- * Type Factory to create branded {@link Id} Type for a specific table.
802
+ * Creates an {@link Id} from a string using SHA-256.
803
+ *
804
+ * When integrating with external systems that use different ID formats, use
805
+ * this function to convert external IDs into valid Evolu IDs.
806
+ *
807
+ * In Evolu's CRDT, the ID serves as the unique identifier for conflict
808
+ * resolution across distributed clients. When multiple clients create records
809
+ * with the same external identifier, they must resolve to the same Evolu ID to
810
+ * ensure data consistency.
811
+ *
812
+ * ### Example
813
+ *
814
+ * ```ts
815
+ * // Both clients will generate the same ID
816
+ * const id1 = createIdFromString("user-api-123");
817
+ * const id2 = createIdFromString("user-api-123");
818
+ * console.log(id1 === id2); // true
819
+ *
820
+ * upsert("todo", {
821
+ * id: createIdFromString("external-todo-456"),
822
+ * title: "Synced from external system",
823
+ * });
824
+ * ```
825
+ *
826
+ * **Important**: This transformation uses the first 16 bytes of SHA-256 hash of
827
+ * the string bytes, therefore it's not possible to recover the original
828
+ * external string from the generated {@link Id}. If you need to preserve the
829
+ * original external ID, store it in a separate column.
830
+ *
831
+ * @category String
832
+ */
833
+ export const createIdFromString = (value) => {
834
+ const hash = sha256(utf8ToBytes(value));
835
+ // Take first 16 bytes of hash and convert to Id
836
+ const id = idBytesToId(hash.slice(0, 16));
837
+ return id;
838
+ };
839
+ /**
840
+ * Creates an {@link Id} embedding timestamp bits (UUID v7 layout) before
841
+ * Base64Url encoding.
842
+ *
843
+ * Tradeoff: better insertion locality / index performance for huge datasets vs
844
+ * leaking creation time everywhere the Id appears. Evolu uses {@link createId}
845
+ * by default to avoid activity leakage; choose this only if you explicitly
846
+ * accept timestamp exposure.
847
+ *
848
+ * ### Example
849
+ *
850
+ * ```ts
851
+ * const id = createIdAsUuidv7({ randomBytes, time });
852
+ * const todoId = createIdAsUuidv7<"Todo">({ randomBytes, time });
853
+ * ```
854
+ */
855
+ export const createIdAsUuidv7 = (deps) => {
856
+ const id = deps.randomBytes.create(16);
857
+ const timestamp = globalThis.BigInt(deps.time.now());
858
+ id[0] = globalThis.Number((timestamp >> 40n) & 0xffn);
859
+ id[1] = globalThis.Number((timestamp >> 32n) & 0xffn);
860
+ id[2] = globalThis.Number((timestamp >> 24n) & 0xffn);
861
+ id[3] = globalThis.Number((timestamp >> 16n) & 0xffn);
862
+ id[4] = globalThis.Number((timestamp >> 8n) & 0xffn);
863
+ id[5] = globalThis.Number(timestamp & 0xffn);
864
+ id[6] = (id[6] & 0x0f) | 0x70;
865
+ id[8] = (id[8] & 0x3f) | 0x80;
866
+ return id;
867
+ };
868
+ /**
869
+ * Creates a branded {@link Id} Type for a table's primary key.
870
+ *
871
+ * The table name becomes an additional brand for type safety.
583
872
  *
584
873
  * ### Example
585
874
  *
@@ -592,32 +881,36 @@ export const createId = (deps) => deps.nanoIdLib.nanoid();
592
881
  * @category String
593
882
  */
594
883
  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
884
  const fromUnknown = (value) => {
603
885
  const parentResult = String.fromUnknown(value);
604
886
  if (!parentResult.ok)
605
887
  return parentResult;
606
888
  return fromParent(parentResult.value);
607
889
  };
890
+ const fromParent = (value) => {
891
+ const idResult = Id.fromParent(value);
892
+ if (!idResult.ok) {
893
+ return err({ type: "TableId", value, table });
894
+ }
895
+ return ok(idResult.value);
896
+ };
608
897
  return {
609
- ...createType("Id", {
610
- fromUnknown,
611
- to: (value) => value,
612
- fromParent,
613
- toParent: (value) => value,
614
- }),
898
+ ...createType("Id", { fromUnknown, fromParent }),
615
899
  table,
616
900
  };
617
901
  };
618
- export const formatIdError = createTypeErrorFormatter((error) => `Invalid ${error.type} table Id: ${error.value}`);
619
- /**
620
- * Positive number.
902
+ export const formatTableIdError = createTypeErrorFormatter((error) => `Invalid Id for table ${error.table}: ${error.value}.`);
903
+ /** Binary representation of an {@link Id}. */
904
+ export const IdBytes = brand("IdBytes", length(16)(Uint8Array));
905
+ export const idBytesTypeValueLength = 16;
906
+ export const idToIdBytes = (id) =>
907
+ // Id is Base64Url (validated by Id.from), cast is safe
908
+ base64UrlToUint8Array(id);
909
+ export const idBytesToId = (idBytes) =>
910
+ // Base64Url encoding of 16 bytes always produces valid Id (22 chars)
911
+ uint8ArrayToBase64Url(idBytes);
912
+ /**
913
+ * Positive number (> 0).
621
914
  *
622
915
  * ### Example
623
916
  *
@@ -631,9 +924,9 @@ export const formatIdError = createTypeErrorFormatter((error) => `Invalid ${erro
631
924
  * @category Number
632
925
  */
633
926
  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.`);
927
+ export const formatPositiveError = createTypeErrorFormatter((error) => `The value ${error.value} must be positive (> 0).`);
635
928
  /**
636
- * Negative number.
929
+ * Negative number (< 0).
637
930
  *
638
931
  * ### Example
639
932
  *
@@ -644,9 +937,9 @@ export const formatPositiveError = createTypeErrorFormatter((error) => `The valu
644
937
  * @category Number
645
938
  */
646
939
  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.`);
940
+ export const formatNegativeError = createTypeErrorFormatter((error) => `The value ${error.value} must be negative (< 0).`);
648
941
  /**
649
- * Non-positive number.
942
+ * Non-positive number (≤ 0).
650
943
  *
651
944
  * ### Example
652
945
  *
@@ -659,9 +952,9 @@ export const formatNegativeError = createTypeErrorFormatter((error) => `The valu
659
952
  export const nonPositive = (parent) => brand("NonPositive", parent, (value) => value <= 0
660
953
  ? ok(value)
661
954
  : err({ type: "NonPositive", value }));
662
- export const formatNonPositiveError = createTypeErrorFormatter((error) => `The value ${error.value} is not non-positive.`);
955
+ export const formatNonPositiveError = createTypeErrorFormatter((error) => `The value ${error.value} must be non-positive (≤ 0).`);
663
956
  /**
664
- * Non-negative number.
957
+ * Non-negative number (≥ 0).
665
958
  *
666
959
  * ### Example
667
960
  *
@@ -674,14 +967,30 @@ export const formatNonPositiveError = createTypeErrorFormatter((error) => `The v
674
967
  export const nonNegative = (parent) => brand("NonNegative", parent, (value) => value >= 0
675
968
  ? ok(value)
676
969
  : err({ type: "NonNegative", value }));
677
- export const formatNonNegativeError = createTypeErrorFormatter((error) => `The value ${error.value} is not non-negative.`);
678
- /** @category Number */
970
+ export const formatNonNegativeError = createTypeErrorFormatter((error) => `The value ${error.value} must be non-negative (≥ 0).`);
971
+ /**
972
+ * Non-negative number (≥ 0).
973
+ *
974
+ * @category Number
975
+ */
679
976
  export const NonNegativeNumber = nonNegative(Number);
680
- /** @category Number */
977
+ /**
978
+ * Positive number (> 0).
979
+ *
980
+ * @category Number
981
+ */
681
982
  export const PositiveNumber = positive(NonNegativeNumber);
682
- /** @category Number */
983
+ /**
984
+ * Non-positive number (≤ 0).
985
+ *
986
+ * @category Number
987
+ */
683
988
  export const NonPositiveNumber = nonPositive(Number);
684
- /** @category Number */
989
+ /**
990
+ * Negative number (< 0).
991
+ *
992
+ * @category Number
993
+ */
685
994
  export const NegativeNumber = negative(NonPositiveNumber);
686
995
  /**
687
996
  * Integer within the safe range of JavaScript numbers.
@@ -697,20 +1006,38 @@ export const NegativeNumber = negative(NonPositiveNumber);
697
1006
  export const int = (parent) => brand("Int", parent, (value) => globalThis.Number.isSafeInteger(value)
698
1007
  ? ok(value)
699
1008
  : err({ type: "Int", value }));
700
- export const formatIntError = createTypeErrorFormatter((error) => `The value ${error.value} is not an integer.`);
1009
+ export const formatIntError = createTypeErrorFormatter((error) => `The value ${error.value} must be an integer.`);
701
1010
  /**
702
1011
  * Integer within the safe range of JavaScript numbers.
703
1012
  *
704
1013
  * @category Number
705
1014
  */
706
1015
  export const Int = int(Number);
707
- /** @category Number */
1016
+ /**
1017
+ * Non-negative integer (≥ 0).
1018
+ *
1019
+ * @category Number
1020
+ */
708
1021
  export const NonNegativeInt = nonNegative(Int);
709
- /** @category Number */
1022
+ /**
1023
+ * Positive integer (> 0).
1024
+ *
1025
+ * @category Number
1026
+ */
710
1027
  export const PositiveInt = positive(NonNegativeInt);
711
- /** @category Number */
1028
+ /** Maximum safe positive integer value for practically infinite operations. */
1029
+ export const maxPositiveInt = PositiveInt.orThrow(globalThis.Number.MAX_SAFE_INTEGER);
1030
+ /**
1031
+ * Non-positive integer (≤ 0).
1032
+ *
1033
+ * @category Number
1034
+ */
712
1035
  export const NonPositiveInt = nonPositive(Int);
713
- /** @category Number */
1036
+ /**
1037
+ * Negative integer (< 0).
1038
+ *
1039
+ * @category Number
1040
+ */
714
1041
  export const NegativeInt = negative(NonPositiveInt);
715
1042
  /**
716
1043
  * Number greater than a specified value.
@@ -750,7 +1077,7 @@ export const formatLessThanOrEqualToError = createTypeErrorFormatter((error) =>
750
1077
  export const nonNaN = (parent) => brand("NonNaN", parent, (value) => !globalThis.Number.isNaN(value)
751
1078
  ? ok(value)
752
1079
  : err({ type: "NonNaN", value }));
753
- export const formatNonNaNError = createTypeErrorFormatter((error) => `The value ${error.value} is NaN (not a number).`);
1080
+ export const formatNonNaNError = createTypeErrorFormatter(() => `The value must not be NaN.`);
754
1081
  /** @category Number */
755
1082
  export const NonNaNNumber = nonNaN(Number);
756
1083
  /**
@@ -761,7 +1088,7 @@ export const NonNaNNumber = nonNaN(Number);
761
1088
  export const finite = (parent) => brand("Finite", parent, (value) => globalThis.Number.isFinite(value)
762
1089
  ? ok(value)
763
1090
  : err({ type: "Finite", value }));
764
- export const formatFiniteError = createTypeErrorFormatter((error) => `The value ${error.value} is not finite.`);
1091
+ export const formatFiniteError = createTypeErrorFormatter((error) => `The value ${error.value} must be finite.`);
765
1092
  /**
766
1093
  * Finite number.
767
1094
  *
@@ -802,8 +1129,6 @@ export const between = (min, max) => (parent) => brand(`Between${min}-${max}`, p
802
1129
  ? ok(value)
803
1130
  : err({ type: "Between", value, min, max }));
804
1131
  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
1132
  /**
808
1133
  * Literal {@link Type}.
809
1134
  *
@@ -828,90 +1153,12 @@ export const literal = (expected) => {
828
1153
  return {
829
1154
  ...createType("Literal", {
830
1155
  fromUnknown,
831
- to: identity,
832
1156
  fromParent: ok,
833
- toParent: identity,
834
1157
  }),
835
1158
  expected,
836
1159
  };
837
1160
  };
838
1161
  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
1162
  /**
916
1163
  * Array of a specific {@link Type}.
917
1164
  *
@@ -954,7 +1201,6 @@ export const array = (element) => {
954
1201
  }
955
1202
  return ok(result);
956
1203
  };
957
- const to = (value) => value.map(element.to);
958
1204
  const fromParent = (value) => {
959
1205
  const result = [];
960
1206
  for (let i = 0; i < value.length; i++) {
@@ -974,14 +1220,8 @@ export const array = (element) => {
974
1220
  }
975
1221
  return ok(result);
976
1222
  };
977
- const toParent = (values) => values.map(element.toParent);
978
1223
  return {
979
- ...createType("Array", {
980
- fromUnknown,
981
- to,
982
- fromParent,
983
- toParent,
984
- }),
1224
+ ...createType("Array", { fromUnknown, fromParent }),
985
1225
  element,
986
1226
  };
987
1227
  };
@@ -1055,10 +1295,6 @@ export const record = (keyType, valueType) => {
1055
1295
  }
1056
1296
  return ok(result);
1057
1297
  };
1058
- const to = (value) => Object.fromEntries(Object.entries(value).map(([key, val]) => [
1059
- keyType.to(key),
1060
- valueType.to(val),
1061
- ]));
1062
1298
  const fromParent = (value) => {
1063
1299
  const result = {};
1064
1300
  for (const [rawKey, rawValue] of Object.entries(value)) {
@@ -1090,16 +1326,10 @@ export const record = (keyType, valueType) => {
1090
1326
  }
1091
1327
  return ok(result);
1092
1328
  };
1093
- const toParent = (value) => Object.fromEntries(Object.entries(value).map(([key, val]) => [
1094
- keyType.toParent(key),
1095
- valueType.toParent(val),
1096
- ]));
1097
1329
  return {
1098
1330
  ...createType("Record", {
1099
1331
  fromUnknown,
1100
- to,
1101
1332
  fromParent,
1102
- toParent,
1103
1333
  }),
1104
1334
  key: keyType,
1105
1335
  value: valueType,
@@ -1186,22 +1416,6 @@ export function object(props, record) {
1186
1416
  }
1187
1417
  return ok(result);
1188
1418
  };
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
1419
  const fromParent = (value) => {
1206
1420
  const errors = {};
1207
1421
  const result = {};
@@ -1249,28 +1463,10 @@ export function object(props, record) {
1249
1463
  }
1250
1464
  return ok(result);
1251
1465
  };
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
1466
  return {
1269
1467
  ...createType(record ? "ObjectWithRecord" : "Object", {
1270
1468
  fromUnknown,
1271
- to,
1272
1469
  fromParent,
1273
- toParent,
1274
1470
  }),
1275
1471
  props,
1276
1472
  ...(record ? { record } : {}),
@@ -1323,7 +1519,6 @@ export function union(...args) {
1323
1519
  * - Generate a specialized function to improve validation performance for such
1324
1520
  * cases.
1325
1521
  */
1326
- /* eslint-disable */
1327
1522
  const members = args.map((arg) => (isType(arg) ? arg : literal(arg)));
1328
1523
  const fromUnknown = (value) => {
1329
1524
  const errors = [];
@@ -1339,23 +1534,13 @@ export function union(...args) {
1339
1534
  errors,
1340
1535
  });
1341
1536
  };
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
1537
  return {
1350
1538
  ...createType("Union", {
1351
1539
  fromUnknown,
1352
- to,
1353
1540
  fromParent: fromUnknown,
1354
- toParent: to,
1355
1541
  }),
1356
1542
  members,
1357
1543
  };
1358
- /* eslint-enable */
1359
1544
  }
1360
1545
  export const formatUnionError = (formatTypeError) => createTypeErrorFormatter((error) => {
1361
1546
  const formattedErrors = error.errors
@@ -1411,20 +1596,10 @@ export const recursive = (create) => {
1411
1596
  type ??= create();
1412
1597
  return type.fromUnknown(value);
1413
1598
  },
1414
- to: (value) => {
1415
- type ??= create();
1416
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
1417
- return type.to(value);
1418
- },
1419
1599
  fromParent: (value) => {
1420
1600
  type ??= create();
1421
1601
  return type.fromParent(value);
1422
1602
  },
1423
- toParent: (value) => {
1424
- type ??= create();
1425
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
1426
- return type.toParent(value);
1427
- },
1428
1603
  is: (value) => {
1429
1604
  type ??= create();
1430
1605
  return type.is(value);
@@ -1528,7 +1703,6 @@ export const tuple = (...elements) => {
1528
1703
  }
1529
1704
  return ok(result);
1530
1705
  };
1531
- const to = (value) => value.map((val, index) => elements[index].to(val));
1532
1706
  const fromParent = (value) => {
1533
1707
  if (!Array.isArray(value) || value.length !== elements.length) {
1534
1708
  return err({
@@ -1555,13 +1729,10 @@ export const tuple = (...elements) => {
1555
1729
  }
1556
1730
  return ok(result);
1557
1731
  };
1558
- const toParent = (value) => value.map((val, index) => elements[index].toParent(val));
1559
1732
  return {
1560
1733
  ...createType("Tuple", {
1561
1734
  fromUnknown,
1562
- to,
1563
1735
  fromParent,
1564
- toParent,
1565
1736
  }),
1566
1737
  elements,
1567
1738
  };
@@ -1595,22 +1766,16 @@ export const Int64 = brand("Int64", BigInt, (value) => value >= -922337203685477
1595
1766
  ? ok(value)
1596
1767
  : err({ type: "Int64", value }));
1597
1768
  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.`);
1600
1769
  /**
1601
1770
  * Stringified {@link Int64}.
1602
1771
  *
1603
- * @category Number
1772
+ * @category String
1604
1773
  */
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
- });
1774
+ export const Int64String = brand("Int64", NonEmptyTrimmedString, (value) => trySync(() => {
1775
+ const maybeInt = globalThis.BigInt(value);
1776
+ Int64.orThrow(maybeInt);
1777
+ return value;
1778
+ }, () => ({ type: "Int64String", value })));
1614
1779
  export const formatInt64StringError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid Int64 string.`);
1615
1780
  /**
1616
1781
  * JSON-compatible value: string, {@link FiniteNumber}, boolean, null,
@@ -1631,48 +1796,83 @@ export const JsonArray = array(JsonValue);
1631
1796
  * @category Object
1632
1797
  */
1633
1798
  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`,
1799
+ export const parseJson = (value) => trySync(() => JSON.parse(value), (error) => ({
1800
+ type: `Json`,
1649
1801
  value,
1650
1802
  message: globalThis.String(error),
1651
- })), (value) => JSON.stringify(value));
1652
- export const formatJsonValueFromStringError = createTypeErrorFormatter((error) => `Invalid JSONValue: ${error.value}. Error: ${error.message}`);
1803
+ }));
1653
1804
  /**
1654
1805
  * JSON-string {@link Type}.
1655
1806
  *
1656
1807
  * ### Example
1657
1808
  *
1658
1809
  * ```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" })
1810
+ * const result = Json.from('{"key":"value"}'); // ok
1811
+ * const error = Json.from("invalid json"); // err
1661
1812
  * ```
1662
1813
  *
1663
1814
  * @category String
1664
1815
  */
1665
1816
  export const Json = brand("Json", String, (value) => {
1666
- const result = JsonValueFromString.fromParent(value);
1817
+ const result = parseJson(value);
1667
1818
  if (!result.ok)
1668
- return err({
1669
- type: "Json",
1670
- value,
1671
- message: result.error.message,
1672
- });
1819
+ return result;
1673
1820
  return ok(value);
1674
1821
  });
1675
1822
  export const formatJsonError = createTypeErrorFormatter((error) => `Invalid JSON: ${error.value}. Error: ${error.message}`);
1823
+ export const jsonValueToJson = (value) => JSON.stringify(value);
1824
+ export const jsonToJsonValue = (value) => JSON.parse(value);
1825
+ /**
1826
+ * Creates a branded JSON string {@link Type} and type-safe conversion functions
1827
+ * for a given Type.
1828
+ *
1829
+ * This factory creates:
1830
+ *
1831
+ * 1. A branded string Type that validates JSON parsing and structural conformity
1832
+ * 2. A serialization function (Type → branded JSON string)
1833
+ * 3. A parsing function (branded JSON string → Type, skipping validation)
1834
+ *
1835
+ * Optimized for Evolu's SQLite workflow where we store typed JSON strings and
1836
+ * need type-safe conversions without double parsing.
1837
+ *
1838
+ * ### Example
1839
+ *
1840
+ * ```ts
1841
+ * const Person = object({
1842
+ * name: NonEmptyString100,
1843
+ * age: FiniteNumber,
1844
+ * });
1845
+ * type Person = typeof Person.Type;
1846
+ *
1847
+ * const [PersonJson, personToPersonJson, personJsonToPerson] = json(
1848
+ * Person,
1849
+ * "PersonJson",
1850
+ * );
1851
+ * // string & Brand<"PersonJson">
1852
+ * type PersonJson = typeof PersonJson.Type;
1853
+ *
1854
+ * // Usage:
1855
+ * const person: Person = { name: "Alice", age: 30 };
1856
+ * const jsonString = personToPersonJson(person); // PersonJson
1857
+ * const backToPerson = personJsonToPerson(jsonString); // Person
1858
+ * ```
1859
+ */
1860
+ export const json = (type, name) => {
1861
+ const BrandedJsonType = brand(name, String, (value) => {
1862
+ const parseResult = parseJson(value);
1863
+ if (!parseResult.ok)
1864
+ return parseResult;
1865
+ const validationResult = type.fromUnknown(parseResult.value);
1866
+ if (!validationResult.ok)
1867
+ return validationResult;
1868
+ return ok(value);
1869
+ });
1870
+ return [
1871
+ BrandedJsonType,
1872
+ jsonValueToJson,
1873
+ jsonToJsonValue,
1874
+ ];
1875
+ };
1676
1876
  /**
1677
1877
  * Optional {@link Type}.
1678
1878
  *
@@ -1697,9 +1897,7 @@ export const formatJsonError = createTypeErrorFormatter((error) => `Invalid JSON
1697
1897
  export const optional = (type) => ({
1698
1898
  ...createType("Optional", {
1699
1899
  fromUnknown: type.fromUnknown,
1700
- to: type.to,
1701
1900
  fromParent: type.fromParent,
1702
- toParent: type.toParent,
1703
1901
  }),
1704
1902
  parent: type,
1705
1903
  });
@@ -1708,7 +1906,7 @@ export const isOptionalType = (x) => typeof x === "object" && x != null && "name
1708
1906
  /**
1709
1907
  * Creates a partial object type where all properties are optional.
1710
1908
  *
1711
- * This is useful when you want to validate an object in which none of the keys
1909
+ * This is useful when we want to validate an object in which none of the keys
1712
1910
  * are required, but if they are present they must conform to their
1713
1911
  * corresponding Types.
1714
1912
  *
@@ -1779,114 +1977,86 @@ export function omit(objectType, ...keys) {
1779
1977
  }
1780
1978
  return object(newProps);
1781
1979
  }
1980
+ export const maxMutationSize = 655360;
1782
1981
  /**
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
- * ```
1982
+ * Evolu has to limit the maximum mutation size. Otherwise, sync couldn't use
1983
+ * the `maxProtocolMessageRangesSize`. The max size is 640KB in bytes, measured
1984
+ * via MessagePack. Evolu Protocol DbChange will be smaller thanks to various
1985
+ * optimizations.
1810
1986
  */
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
- };
1987
+ export const validMutationSize = (type) => brand("ValidMutationSize", type, (value) => pack(value).byteLength <= maxMutationSize
1988
+ ? ok(value)
1989
+ : err({ type: "ValidMutationSize", value }));
1990
+ 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
1991
  /**
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.
1992
+ * Formats Evolu Type errors into user-friendly messages.
1993
+ *
1994
+ * Evolu Type typed errors ensure every error type must have a formatter.
1995
+ * TypeScript enforces this at compile-time, preventing unhandled validation
1996
+ * errors from reaching users.
1829
1997
  *
1830
- * If you prefer not to reuse any built-in error formatters, you can write your
1831
- * own `formatTypeError` function from scratch.
1998
+ * The `createFormatTypeError` function handles both built-in {@link TypeErrors}
1999
+ * and custom errors, and lets us override default formatting for specific
2000
+ * errors.
1832
2001
  *
1833
- * ### Examples
2002
+ * ### Example
1834
2003
  *
1835
2004
  * ```ts
1836
- * const formatError = createFormatTypeError();
1837
- * console.log(formatError({ type: "String", value: 42 }));
1838
- * // "A value 42 is not a string."
2005
+ * const formatTypeError = createFormatTypeError<
2006
+ * MinLengthError | MaxLengthError
2007
+ * >((error): string => {
2008
+ * switch (error.type) {
2009
+ * case "MinLength":
2010
+ * return `Text must be at least ${error.min} character${error.min === 1 ? "" : "s"} long`;
2011
+ * case "MaxLength":
2012
+ * return `Text is too long (maximum ${error.max} characters)`;
2013
+ * }
2014
+ * });
1839
2015
  * ```
1840
2016
  *
1841
- * A custom `formatTypeError` function:
2017
+ * Alternatively, write a custom formatter from scratch without using
2018
+ * `createFormatTypeError`. This gives us full control over error formatting:
1842
2019
  *
1843
2020
  * ```ts
1844
- * type AppErrors =
1845
- * | ValidMutationSizeError
2021
+ * const Person = object({
2022
+ * name: NonEmptyTrimmedString100,
2023
+ * age: optional(PositiveInt),
2024
+ * });
2025
+ *
2026
+ * // Define only the errors actually used by Person Type
2027
+ * type PersonErrors =
1846
2028
  * | StringError
1847
- * | MinLengthError
1848
2029
  * | MaxLengthError
1849
- * | NullError
1850
- * | IdError
2030
+ * | MinLengthError
1851
2031
  * | 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.
2032
+ * | PositiveError
2033
+ * | NonNegativeError
2034
+ * | IntError
2035
+ * | NumberError
2036
+ * | ObjectError<Record<string, PersonErrors>>;
2037
+ *
2038
+ * const formatTypeError: TypeErrorFormatter<PersonErrors> = (error) => {
1861
2039
  * 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
2040
  * case "String":
1873
- * // We can reuse existing formatter.
1874
2041
  * return formatStringError(error);
2042
+ * case "Number":
2043
+ * return "Must be a number";
2044
+ * case "MinLength":
2045
+ * return `Must be at least ${error.min} characters`;
2046
+ * case "MaxLength":
2047
+ * return `Cannot exceed ${error.max} characters`;
1875
2048
  * 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(", ")}`;
2049
+ * return "Cannot have leading or trailing spaces";
2050
+ * case "Positive":
2051
+ * return "Must be a positive number";
2052
+ * case "NonNegative":
2053
+ * return "Must be zero or positive";
2054
+ * case "Int":
2055
+ * return "Must be an integer";
1884
2056
  * 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.";
2057
+ * if (error.reason.kind === "NotObject") return "Must be an object";
2058
+ * if (error.reason.kind === "ExtraKeys")
2059
+ * return "Contains unexpected fields";
1890
2060
  * const firstError = Object.values(error.reason.errors).find(
1891
2061
  * (e) => e !== undefined,
1892
2062
  * )!;
@@ -1927,8 +2097,8 @@ export const createFormatTypeError = (extraFormatter) => {
1927
2097
  return formatIsTypeError(error);
1928
2098
  case "CurrencyCode":
1929
2099
  return formatCurrencyCodeError(error);
1930
- case "DateIsoString":
1931
- return formatDateIsoStringError(error);
2100
+ case "DateIso":
2101
+ return formatDateIsoError(error);
1932
2102
  case "Trimmed":
1933
2103
  return formatTrimmedError(error);
1934
2104
  case "MinLength":
@@ -1943,6 +2113,8 @@ export const createFormatTypeError = (extraFormatter) => {
1943
2113
  return formatRegexError(error);
1944
2114
  case "Id":
1945
2115
  return formatIdError(error);
2116
+ case "TableId":
2117
+ return formatTableIdError(error);
1946
2118
  case "Positive":
1947
2119
  return formatPositiveError(error);
1948
2120
  case "Negative":
@@ -1973,14 +2145,12 @@ export const createFormatTypeError = (extraFormatter) => {
1973
2145
  return formatLiteralError(error);
1974
2146
  case "Int64":
1975
2147
  return formatInt64Error(error);
1976
- case "BigIntFromString":
1977
- return formatBigIntFromStringError(error);
1978
2148
  case "Int64String":
1979
2149
  return formatInt64StringError(error);
1980
- case "JsonValueFromString":
1981
- return formatJsonValueFromStringError(error);
1982
2150
  case "Json":
1983
2151
  return formatJsonError(error);
2152
+ case "ValidMutationSize":
2153
+ return formatValidMutationSizeError(error);
1984
2154
  // Composite Types
1985
2155
  case "SimplePassword":
1986
2156
  return formatSimplePasswordError(formatTypeError)(error);
@@ -1996,7 +2166,88 @@ export const createFormatTypeError = (extraFormatter) => {
1996
2166
  return formatUnionError(formatTypeError)(error);
1997
2167
  case "Tuple":
1998
2168
  return formatTupleError(formatTypeError)(error);
2169
+ default: {
2170
+ // Fallback for unknown error types
2171
+ const unknownError = error;
2172
+ return `A value ${safelyStringifyUnknownValue(unknownError.value)} is not valid for type ${unknownError.type}.`;
2173
+ }
1999
2174
  }
2000
2175
  };
2001
2176
  return formatTypeError;
2002
2177
  };
2178
+ /**
2179
+ * Converts an Evolu {@link TypeError} to Standard Schema V1 issues format.
2180
+ *
2181
+ * This function recursively converts Evolu's typed errors into the Standard
2182
+ * Schema issue format with proper path tracking for nested structures.
2183
+ *
2184
+ * @category Utilities
2185
+ */
2186
+ export const typeErrorToStandardSchemaIssues = (error, formatTypeError, path = []) => {
2187
+ if (error.type === "Array") {
2188
+ const arrayError = error;
2189
+ if (arrayError.reason.kind === "NotArray") {
2190
+ return [{ message: formatTypeError(error), path }];
2191
+ }
2192
+ return typeErrorToStandardSchemaIssues(arrayError.reason.error, formatTypeError, [...path, arrayError.reason.index]);
2193
+ }
2194
+ if (error.type === "Object") {
2195
+ const objectError = error;
2196
+ if (objectError.reason.kind === "NotObject" ||
2197
+ objectError.reason.kind === "ExtraKeys") {
2198
+ return [{ message: formatTypeError(error), path }];
2199
+ }
2200
+ const issues = [];
2201
+ for (const [key, propError] of Object.entries(objectError.reason.errors)) {
2202
+ issues.push(...typeErrorToStandardSchemaIssues(propError, formatTypeError, [...path, key]));
2203
+ }
2204
+ return issues;
2205
+ }
2206
+ if (error.type === "ObjectWithRecord") {
2207
+ const objectWithRecordError = error;
2208
+ if (objectWithRecordError.reason.kind === "NotObject") {
2209
+ return [{ message: formatTypeError(error), path }];
2210
+ }
2211
+ if (objectWithRecordError.reason.kind === "IndexKey" ||
2212
+ objectWithRecordError.reason.kind === "IndexValue") {
2213
+ return typeErrorToStandardSchemaIssues(objectWithRecordError.reason.error, formatTypeError, [...path, objectWithRecordError.reason.key]);
2214
+ }
2215
+ const issues = [];
2216
+ for (const [key, propError] of Object.entries(objectWithRecordError.reason.errors)) {
2217
+ issues.push(...typeErrorToStandardSchemaIssues(propError, formatTypeError, [...path, key]));
2218
+ }
2219
+ return issues;
2220
+ }
2221
+ if (error.type === "Record") {
2222
+ const recordError = error;
2223
+ if (recordError.reason.kind === "NotRecord") {
2224
+ return [{ message: formatTypeError(error), path }];
2225
+ }
2226
+ return typeErrorToStandardSchemaIssues(recordError.reason.error, formatTypeError, [...path, recordError.reason.key]);
2227
+ }
2228
+ if (error.type === "Tuple") {
2229
+ const tupleError = error;
2230
+ if (tupleError.reason.kind === "InvalidLength") {
2231
+ return [{ message: formatTypeError(error), path }];
2232
+ }
2233
+ return typeErrorToStandardSchemaIssues(tupleError.reason.error, formatTypeError, [...path, tupleError.reason.index]);
2234
+ }
2235
+ if (error.type === "Union") {
2236
+ const unionError = error;
2237
+ return unionError.errors.flatMap((err) => typeErrorToStandardSchemaIssues(err, formatTypeError, path));
2238
+ }
2239
+ if (error.type === "Brand") {
2240
+ const brandError = error;
2241
+ if ("parentError" in brandError) {
2242
+ return typeErrorToStandardSchemaIssues(brandError.parentError, formatTypeError, path);
2243
+ }
2244
+ return [{ message: formatTypeError(error), path }];
2245
+ }
2246
+ return [{ message: formatTypeError(error), path }];
2247
+ };
2248
+ /**
2249
+ * Shared formatter cache for Standard Schema integration - avoids circular
2250
+ * dependency by lazily creating the formatter on first use rather than during
2251
+ * module initialization.
2252
+ */
2253
+ let cachedStandardSchemaFormatTypeError;