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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/dist/src/Assert.d.ts.map +1 -1
  2. package/dist/src/Assert.js +1 -1
  3. package/dist/src/Buffer.d.ts +1 -1
  4. package/dist/src/Buffer.d.ts.map +1 -1
  5. package/dist/src/Buffer.js +1 -1
  6. package/dist/src/CallbackRegistry.d.ts +53 -0
  7. package/dist/src/CallbackRegistry.d.ts.map +1 -0
  8. package/dist/src/CallbackRegistry.js +25 -0
  9. package/dist/src/Console.d.ts +31 -6
  10. package/dist/src/Console.d.ts.map +1 -1
  11. package/dist/src/Console.js +72 -9
  12. package/dist/src/Crypto.d.ts +48 -37
  13. package/dist/src/Crypto.d.ts.map +1 -1
  14. package/dist/src/Crypto.js +27 -50
  15. package/dist/src/Evolu/Db.d.ts +138 -66
  16. package/dist/src/Evolu/Db.d.ts.map +1 -1
  17. package/dist/src/Evolu/Db.js +248 -645
  18. package/dist/src/Evolu/Diff.d.ts +3 -3
  19. package/dist/src/Evolu/Diff.d.ts.map +1 -1
  20. package/dist/src/Evolu/Diff.js +7 -5
  21. package/dist/src/Evolu/Evolu.d.ts +79 -116
  22. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  23. package/dist/src/Evolu/Evolu.js +275 -132
  24. package/dist/src/Evolu/Internal.d.ts +0 -2
  25. package/dist/src/Evolu/Internal.d.ts.map +1 -1
  26. package/dist/src/Evolu/Internal.js +0 -2
  27. package/dist/src/Evolu/LocalAuth.d.ts +144 -0
  28. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
  29. package/dist/src/Evolu/LocalAuth.js +171 -0
  30. package/dist/src/Evolu/Owner.d.ts +129 -83
  31. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  32. package/dist/src/Evolu/Owner.js +80 -89
  33. package/dist/src/Evolu/Platform.d.ts +9 -7
  34. package/dist/src/Evolu/Platform.d.ts.map +1 -1
  35. package/dist/src/Evolu/Protocol.d.ts +114 -191
  36. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  37. package/dist/src/Evolu/Protocol.js +409 -416
  38. package/dist/src/Evolu/Public.d.ts +6 -8
  39. package/dist/src/Evolu/Public.d.ts.map +1 -1
  40. package/dist/src/Evolu/Public.js +2 -3
  41. package/dist/src/Evolu/PublicKysely.js +3 -3
  42. package/dist/src/Evolu/Relay.d.ts +1 -2
  43. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  44. package/dist/src/Evolu/Relay.js +11 -9
  45. package/dist/src/Evolu/Schema.d.ts +88 -27
  46. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  47. package/dist/src/Evolu/Schema.js +141 -24
  48. package/dist/src/Evolu/Storage.d.ts +158 -14
  49. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  50. package/dist/src/Evolu/Storage.js +32 -32
  51. package/dist/src/Evolu/Sync.d.ts +77 -13
  52. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  53. package/dist/src/Evolu/Sync.js +453 -20
  54. package/dist/src/Evolu/Timestamp.d.ts +29 -27
  55. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  56. package/dist/src/Evolu/Timestamp.js +20 -18
  57. package/dist/src/ManyToManyMap.d.ts +74 -10
  58. package/dist/src/ManyToManyMap.d.ts.map +1 -1
  59. package/dist/src/ManyToManyMap.js +41 -6
  60. package/dist/src/Random.d.ts +3 -2
  61. package/dist/src/Random.d.ts.map +1 -1
  62. package/dist/src/RefCountedResourceManager.d.ts +119 -0
  63. package/dist/src/RefCountedResourceManager.d.ts.map +1 -0
  64. package/dist/src/RefCountedResourceManager.js +197 -0
  65. package/dist/src/Result.d.ts +144 -22
  66. package/dist/src/Result.d.ts.map +1 -1
  67. package/dist/src/Result.js +5 -2
  68. package/dist/src/Sqlite.d.ts +20 -4
  69. package/dist/src/Sqlite.d.ts.map +1 -1
  70. package/dist/src/Sqlite.js +50 -8
  71. package/dist/src/Task.d.ts +511 -0
  72. package/dist/src/Task.d.ts.map +1 -0
  73. package/dist/src/Task.js +410 -0
  74. package/dist/src/Time.d.ts +59 -0
  75. package/dist/src/Time.d.ts.map +1 -1
  76. package/dist/src/Time.js +87 -4
  77. package/dist/src/Type.d.ts +431 -341
  78. package/dist/src/Type.d.ts.map +1 -1
  79. package/dist/src/Type.js +458 -466
  80. package/dist/src/WebSocket.d.ts +5 -2
  81. package/dist/src/WebSocket.d.ts.map +1 -1
  82. package/dist/src/WebSocket.js +12 -13
  83. package/dist/src/Worker.d.ts +39 -11
  84. package/dist/src/Worker.d.ts.map +1 -1
  85. package/dist/src/Worker.js +22 -4
  86. package/dist/src/index.d.ts +2 -3
  87. package/dist/src/index.d.ts.map +1 -1
  88. package/dist/src/index.js +2 -3
  89. package/package.json +7 -7
  90. package/src/Assert.ts +2 -4
  91. package/src/Buffer.ts +1 -1
  92. package/src/CallbackRegistry.ts +84 -0
  93. package/src/Console.ts +91 -11
  94. package/src/Crypto.ts +78 -91
  95. package/src/Evolu/Db.ts +455 -947
  96. package/src/Evolu/Diff.ts +7 -5
  97. package/src/Evolu/Evolu.ts +545 -307
  98. package/src/Evolu/Internal.ts +0 -2
  99. package/src/Evolu/LocalAuth.ts +422 -0
  100. package/src/Evolu/Owner.ts +191 -131
  101. package/src/Evolu/Platform.ts +9 -9
  102. package/src/Evolu/Protocol.ts +536 -653
  103. package/src/Evolu/Public.ts +7 -9
  104. package/src/Evolu/PublicKysely.ts +3 -3
  105. package/src/Evolu/Relay.ts +17 -12
  106. package/src/Evolu/Schema.ts +271 -66
  107. package/src/Evolu/Storage.ts +263 -55
  108. package/src/Evolu/Sync.ts +758 -37
  109. package/src/Evolu/Timestamp.ts +30 -35
  110. package/src/ManyToManyMap.ts +127 -24
  111. package/src/Random.ts +3 -2
  112. package/src/RefCountedResourceManager.ts +368 -0
  113. package/src/Result.ts +149 -23
  114. package/src/Sqlite.ts +59 -24
  115. package/src/Task.ts +779 -0
  116. package/src/Time.ts +168 -4
  117. package/src/Type.ts +657 -695
  118. package/src/WebSocket.ts +23 -17
  119. package/src/Worker.ts +72 -23
  120. package/src/index.ts +2 -3
  121. package/dist/src/Callbacks.d.ts +0 -20
  122. package/dist/src/Callbacks.d.ts.map +0 -1
  123. package/dist/src/Callbacks.js +0 -18
  124. package/dist/src/Evolu/Config.d.ts +0 -82
  125. package/dist/src/Evolu/Config.d.ts.map +0 -1
  126. package/dist/src/Evolu/Config.js +0 -9
  127. package/dist/src/Evolu/Kysely.d.ts +0 -6
  128. package/dist/src/Evolu/Kysely.d.ts.map +0 -1
  129. package/dist/src/Evolu/Kysely.js +0 -21
  130. package/dist/src/NanoId.d.ts +0 -27
  131. package/dist/src/NanoId.d.ts.map +0 -1
  132. package/dist/src/NanoId.js +0 -6
  133. package/dist/src/Promise.d.ts +0 -180
  134. package/dist/src/Promise.d.ts.map +0 -1
  135. package/dist/src/Promise.js +0 -176
  136. package/src/Callbacks.ts +0 -43
  137. package/src/Evolu/Config.ts +0 -97
  138. package/src/Evolu/Kysely.ts +0 -38
  139. package/src/NanoId.ts +0 -39
  140. package/src/Promise.ts +0 -295
package/dist/src/Type.js CHANGED
@@ -1,82 +1,182 @@
1
- /* eslint-disable jsdoc/no-undefined-types */
2
1
  /**
3
- * 🧩 Validation, Parsing, and Transformation
2
+ * 🧩 Type-safe runtime types
4
3
  *
5
- * ## 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.
6
7
  *
7
- * You probably know [Zod](https://zod.dev). Evolu has {@link Type}.
8
+ * Why another validation library?
8
9
  *
9
- * Evolu Type exists because no existing validation/parsing/transformation
10
- * library fully met our needs:
10
+ * - **Result-based error handling** no exceptions for normal control flow.
11
+ * - **Typed errors with decoupled formatters** – validation logic ≠ user
12
+ * messages.
13
+ * - **Consistent constraints via {@link Brand}** – every constraint becomes part
14
+ * of the type.
15
+ * - **No user-land chaining DSL** – designed with the upcoming ES pipe operator
16
+ * in mind.
17
+ * - **Selective validation** – parent validations are skipped when already proved
18
+ * by typing.
19
+ * - **Simple, top-down implementation** – readable source code from top to bottom
20
+ * with no hidden magic; just plain functions and composition.
11
21
  *
12
- * - **Result-based error handling**: Leveraging {@link Result} instead of throwing
13
- * exceptions.
14
- * - **Consistent constraints**: Enforcing {@link Brand} for all constraints.
15
- * - **Typed errors with decoupled formatters**: Avoiding coupling error messages
16
- * with validators.
17
- * - **No user-land chaining**: Designed with ES pipe operator in mind.
18
- * - **Selective validation/transformation**: Skipping parent Type validations and
19
- * transformations when TypeScript's type system can be relied upon.
20
- * - **Bidirectional transformations**: Supporting transformations in both
21
- * directions.
22
- * - **Minimal and transparent code**: No runtime dependencies or hidden magic.
22
+ * ### Base Types Quick Start
23
23
  *
24
- * **Note**: A proper quickstart guide is on the way. In the meantime, each type
25
- * includes its own usage example, and you can (and should) check the tests for
26
- * practical demonstrations of the API. Or dang, just read the code. It's
27
- * simple.
24
+ * ```ts
25
+ * // Validate unknown values
26
+ * const value: unknown = "hello";
27
+ * const stringResult = String.fromUnknown(value);
28
+ * if (!stringResult.ok) {
29
+ * // console.error(formatStringError(stringResult.error));
30
+ * return stringResult; // inside a function returning Result<string, _>
31
+ * }
32
+ * // Safe branch: value is now string
33
+ * const upper = stringResult.value.toUpperCase();
34
+ *
35
+ * // Type guard style
36
+ * if (String.is(value)) {
37
+ * // narrowed to string
38
+ * }
39
+ *
40
+ * // Composing: arrays & objects
41
+ * const Numbers = array(Number); // ReadonlyArray<number>
42
+ * const Point = object({ x: Number, y: Number });
43
+ *
44
+ * Numbers.from([1, 2, 3]); // ok
45
+ * Point.from({ x: 1, y: 2 }); // ok
46
+ * Point.from({ x: 1, y: "2" }); // err -> nested Number error
47
+ * ```
48
+ *
49
+ * ### Branding Basics
50
+ *
51
+ * Branding adds semantic meaning & constraints while preserving the runtime
52
+ * shape:
53
+ *
54
+ * ```ts
55
+ * const CurrencyCode = brand("CurrencyCode", String, (value) =>
56
+ * /^[A-Z]{3}$/.test(value)
57
+ * ? ok(value)
58
+ * : err<CurrencyCodeError>({ type: "CurrencyCode", value }),
59
+ * );
60
+ * type CurrencyCode = typeof CurrencyCode.Type; // string & Brand<"CurrencyCode">
61
+ *
62
+ * interface CurrencyCodeError extends TypeError<"CurrencyCode"> {}
63
+ *
64
+ * const formatCurrencyCodeError =
65
+ * createTypeErrorFormatter<CurrencyCodeError>(
66
+ * (error) => `Invalid currency code: ${error.value}`,
67
+ * );
68
+ *
69
+ * const r = CurrencyCode.from("USD"); // ok("USD")
70
+ * const e = CurrencyCode.from("usd"); // err(...)
71
+ * ```
72
+ *
73
+ * See also reusable brand factories like `minLength`, `maxLength`, `trimmed`,
74
+ * `positive`, `between`, etc.
75
+ *
76
+ * ### Objects & Optional Fields
77
+ *
78
+ * ```ts
79
+ * const User = object({
80
+ * name: NonEmptyTrimmedString100,
81
+ * age: optional(PositiveInt),
82
+ * });
83
+ * type User = typeof User.Type;
84
+ *
85
+ * User.from({ name: "Alice" }); // ok
86
+ * User.from({ name: "Alice", age: -1 }); // err(PositiveInt)
87
+ * ```
88
+ *
89
+ * ### Deriving JSON String Types
90
+ *
91
+ * ```ts
92
+ * const Person = object({
93
+ * name: NonEmptyString50,
94
+ * // Did you know that JSON.stringify converts NaN (a number) into null?
95
+ * // To prevent this, use FiniteNumber.
96
+ * age: FiniteNumber,
97
+ * });
98
+ * type Person = typeof Person.Type;
99
+ *
100
+ * const [PersonJson, personToPersonJson, personJsonToPerson] = json(
101
+ * Person,
102
+ * "PersonJson",
103
+ * );
104
+ * // string & Brand<"PersonJson">
105
+ * type PersonJson = typeof PersonJson.Type;
28
106
  *
29
- * - Evolu `Type` is:
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.
107
+ * const person = Person.orThrow({
108
+ * name: "Alice",
109
+ * age: 30,
110
+ * });
34
111
  *
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:
112
+ * const personJson = personToPersonJson(person);
113
+ * expect(personJsonToPerson(personJson)).toEqual(person);
114
+ * ```
39
115
  *
40
- * `Unknown` -> `String` -> `TrimmedString` -> `NonEmptyTrimmedString100`
116
+ * ### Error Formatting
41
117
  *
42
- * For `NonEmptyTrimmedString100`, the parent Type is `TrimmedString`. For
43
- * `TrimmedString`, the parent Type is `String`.
118
+ * Evolu separates validation logic from human-readable messages. There are two
119
+ * layers:
44
120
  *
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.
121
+ * 1. Per-type formatters (e.g. `formatStringError`) simple, focused, already
122
+ * used earlier in the quick start example.
123
+ * 2. A unified formatter via `createFormatTypeError` composes all built-in and
124
+ * custom errors (including nested composite types) and lets us override
125
+ * selected messages.
48
126
  *
49
- * Speaking of `fromParent` and `toParent`, those functions exist to bypass
50
- * parent Types when we can rely on TypeScript types.
127
+ * #### 1. Per-Type Formatter (recap)
51
128
  *
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")`.
129
+ * ```ts
130
+ * const r = String.fromUnknown(42);
131
+ * if (!r.ok) console.error(formatStringError(r.error));
132
+ * ```
133
+ *
134
+ * #### 2. Unified Formatter with Overrides
135
+ *
136
+ * ```ts
137
+ * // Override only what we care about; fall back to built-ins for the rest.
138
+ * const formatTypeError = createFormatTypeError((error) => {
139
+ * if (error.type === "MinLength") return `Min length is ${error.min}`;
140
+ * });
141
+ *
142
+ * const User = object({ name: NonEmptyTrimmedString100 });
143
+ * const resultUser = User.from({ name: "" });
144
+ * if (!resultUser.ok) console.error(formatTypeError(resultUser.error));
145
+ *
146
+ * const badPoint = object({ x: Number, y: Number }).from({
147
+ * x: 1,
148
+ * y: "foo",
149
+ * });
150
+ * if (!badPoint.ok) console.error(formatTypeError(badPoint.error));
151
+ * ```
152
+ *
153
+ * The unified formatter walks nested structures (object / array / record /
154
+ * tuple / union) and applies overrides only where specified, greatly reducing
155
+ * boilerplate when formatting complex validation errors.
57
156
  *
58
157
  * ### Tip
59
158
  *
60
159
  * If necessary, write `globalThis.String` instead of `String` to avoid naming
61
- * clashes with Base Types.
160
+ * clashes with native types.
62
161
  *
63
- * ### Design Decision:
162
+ * ### Design Decision: No Bidirectional Transformations
64
163
  *
65
- * While the `from` function can fail, the `to` function cannot. This simplifies
66
- * the model by ensuring that every valid input has a corresponding valid
67
- * output, eliminating the risk of edge cases caused by irreversible
68
- * operations.
164
+ * Evolu Type intentionally does not support bidirectional transformations. It
165
+ * previously did, but supporting that while keeping typed error fidelity added
166
+ * complexity that hurt readability & reliability. Most persistence pipelines
167
+ * (e.g. SQLite) already require explicit mapping of query results, so implicit
168
+ * reverse transforms would not buy much. We may revisit this if we can design a
169
+ * minimal, 100% safe API that preserves simplicity.
69
170
  *
70
171
  * @module
71
172
  */
72
- import { utf8ToBytes } from "@noble/ciphers/utils";
73
- import { sha256 } from "@noble/hashes/sha2";
173
+ import { utf8ToBytes } from "@noble/ciphers/utils.js";
174
+ import { sha256 } from "@noble/hashes/sha2.js";
74
175
  import * as bip39 from "@scure/bip39";
75
- import { wordlist } from "@scure/bip39/wordlists/english";
76
- import { assert } from "./Assert.js";
77
- import { identity } from "./Function.js";
176
+ import { wordlist } from "@scure/bip39/wordlists/english.js";
177
+ import { pack } from "msgpackr";
78
178
  import { isPlainObject } from "./Object.js";
79
- import { err, ok, trySync } from "./Result.js";
179
+ import { err, getOrThrow, ok, trySync } from "./Result.js";
80
180
  import { safelyStringifyUnknownValue } from "./String.js";
81
181
  const EvoluTypeSymbol = Symbol("evolu.Type");
82
182
  /**
@@ -90,6 +190,7 @@ const createType = (name, definition) => ({
90
190
  name,
91
191
  is: (value) => definition.fromUnknown(value).ok,
92
192
  from: definition.fromUnknown,
193
+ orThrow: (value) => getOrThrow(definition.fromUnknown(value)),
93
194
  [EvoluTypeSymbol]: true,
94
195
  Type: undefined,
95
196
  Input: undefined,
@@ -119,12 +220,6 @@ export const createTypeErrorFormatter = (format) => (error) => format({ ...error
119
220
  * Base {@link Type}.
120
221
  *
121
222
  * A Base Type validates that a value conforms to a specific TypeScript type.
122
- * Unlike refinements or transformations, Base Types establish the fundamental
123
- * shape of a value before any branding or transformation occurs.
124
- *
125
- * - To **refine** a Base Type further, use the {@link brand} Type Factory.
126
- * - To **transform** a Base Type into a different representation, use the
127
- * {@link transform} Type Factory.
128
223
  *
129
224
  * ### Example
130
225
  *
@@ -146,9 +241,7 @@ export const createTypeErrorFormatter = (format) => (error) => format({ ...error
146
241
  */
147
242
  export const base = (name, fromUnknown) => createType(name, {
148
243
  fromUnknown,
149
- to: identity,
150
244
  fromParent: (ok), // `fromParent` relies on types, so it can't fail for the Base Type
151
- toParent: identity,
152
245
  });
153
246
  /**
154
247
  * Creates a formatter function for a base {@link TypeError}.
@@ -282,9 +375,7 @@ export function brand(name, parent, refine) {
282
375
  return {
283
376
  ...createType("Brand", {
284
377
  fromUnknown,
285
- to: identity,
286
378
  fromParent: (refine ?? ok),
287
- toParent: identity,
288
379
  }),
289
380
  brand: name,
290
381
  parentType: parent,
@@ -314,37 +405,39 @@ export const formatCurrencyCodeError = createTypeErrorFormatter((error) => `Inva
314
405
  * ### Example
315
406
  *
316
407
  * ```ts
317
- * const result = DateIsoString.from("2023-01-01T12:00:00.000Z"); // ok
318
- * const error = DateIsoString.from("10000-01-01T00:00:00.000Z"); // err
408
+ * const result = DateIso.from("2023-01-01T12:00:00.000Z"); // ok
409
+ * const error = DateIso.from("10000-01-01T00:00:00.000Z"); // err
319
410
  * ```
320
411
  *
321
412
  * @category String
322
413
  */
323
- export const DateIsoString = brand("DateIso", String, (value) => {
414
+ export const DateIso = brand("DateIso", String, (value) => {
324
415
  if (value.length !== 24) {
325
- return err({ type: "DateIsoString", value });
416
+ return err({ type: "DateIso", value });
326
417
  }
327
- if (isNaN(globalThis.Date.parse(value))) {
328
- return err({ type: "DateIsoString", value });
418
+ const parsed = globalThis.Date.parse(value);
419
+ if (isNaN(parsed)) {
420
+ return err({ type: "DateIso", value });
421
+ }
422
+ // Round-trip test: ensure the string is actually a proper ISO format
423
+ const roundTrip = new globalThis.Date(parsed).toISOString();
424
+ if (roundTrip !== value) {
425
+ return err({ type: "DateIso", value });
329
426
  }
330
427
  return ok(value);
331
428
  });
332
- export const formatDateIsoStringError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid ISO 8601 date string.`);
429
+ export const formatDateIsoError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid ISO 8601 date string.`);
430
+ export const dateToDateIso = (value) => DateIso.fromParent(value.toISOString());
431
+ export const dateIsoToDate = (value) => new globalThis.Date(value);
333
432
  /**
334
433
  * Trimmed string.
335
434
  *
336
- * This Type Factory does not transform; it only validates whether a string has
337
- * no leading or trailing whitespaces. To trim a string, use {@link trim} Type
338
- * Factory.
435
+ * This Type Factory validates whether a string has no leading or trailing
436
+ * whitespaces.
339
437
  *
340
- * ### Examples
438
+ * ### Example
341
439
  *
342
440
  * ```ts
343
- * // this Type already exists
344
- * const TrimmedString = trimmed(String);
345
- * type TrimmedString = typeof TrimmedString.Type;
346
- *
347
- * // we can make any branded Type trimmed:
348
441
  * const TrimmedNonEmptyString = trimmed(minLength(1)(String));
349
442
  * // string & Brand<"MinLength1"> & Brand<"Trimmed">
350
443
  * type TrimmedNonEmptyString = typeof TrimmedNonEmptyString.Type;
@@ -356,32 +449,6 @@ export const trimmed = (parent) => brand("Trimmed", parent, (value) => value.tri
356
449
  ? ok(value)
357
450
  : err({ type: "Trimmed", value }));
358
451
  export const formatTrimmedError = createTypeErrorFormatter((error) => `A value ${error.value} is not trimmed`);
359
- /**
360
- * Trims leading and trailing whitespace from a string.
361
- *
362
- * This Type Factory **transforms** the input string by removing whitespace from
363
- * both ends. For validation only, use {@link trimmed} Type Factory.
364
- *
365
- * ### Example
366
- *
367
- * ```ts
368
- * const TrimString = trim(String);
369
- * expect(TrimString.from("a ")).toEqual(ok("a"));
370
- * expect(TrimString.fromParent("a ").value).toEqual("a");
371
- *
372
- * const TrimNonEmptyString = trim(NonEmptyString);
373
- * expect(TrimNonEmptyString.from("a " as NonEmptyString)).toEqual(ok("a"));
374
- * expect(
375
- * TrimNonEmptyString.fromParent("a " as NonEmptyString).value,
376
- * ).toEqual("a");
377
- * ```
378
- *
379
- * **Note:** This transformation is irreversible. Calling `toParent` will not
380
- * restore the original representation.
381
- *
382
- * @category String
383
- */
384
- export const trim = (parent) => transform(parent, trimmed(parent), (value) => ok(value.trim()), (value) => value);
385
452
  /**
386
453
  * Trimmed string
387
454
  *
@@ -391,6 +458,7 @@ export const trim = (parent) => transform(parent, trimmed(parent), (value) => ok
391
458
  * @category String
392
459
  */
393
460
  export const TrimmedString = trimmed(String);
461
+ export const trim = (value) => value.trim();
394
462
  /**
395
463
  * Minimum length.
396
464
  *
@@ -496,9 +564,9 @@ export const regex = (name, pattern) => {
496
564
  };
497
565
  export const formatRegexError = createTypeErrorFormatter((error) => `Value ${error.value} does not match the pattern for ${error.name}: ${error.pattern}`);
498
566
  /**
499
- * URL-safe Base64 string.
567
+ * URL-safe string.
500
568
  *
501
- * A `Base64Url` string uses a limited alphabet that is URL-safe:
569
+ * A `UrlSafeString` uses a limited alphabet that is safe for URLs:
502
570
  *
503
571
  * - Uppercase letters (`A-Z`)
504
572
  * - Lowercase letters (`a-z`)
@@ -506,34 +574,89 @@ export const formatRegexError = createTypeErrorFormatter((error) => `Value ${err
506
574
  * - Dash (`-`)
507
575
  * - Underscore (`_`)
508
576
  *
577
+ * This is the same character set used by Base64Url encoding, but this type does
578
+ * not validate that the string is actually Base64Url-encoded data.
579
+ *
509
580
  * ### Example
510
581
  *
511
582
  * ```ts
512
- * const result = Base64Url.from("abc123_-");
583
+ * const result = UrlSafeString.from("abc123_-");
513
584
  * if (result.ok) {
514
- * console.log("Valid Base64Url string:", result.value);
585
+ * console.log("Valid URL-safe string:", result.value);
515
586
  * } else {
516
- * console.error("Invalid Base64Url string:", result.error);
587
+ * console.error("Invalid URL-safe string:", result.error);
517
588
  * }
518
589
  * ```
519
590
  *
520
591
  * @category String
521
592
  */
522
- export const Base64Url = regex("Base64Url", /^[A-Za-z0-9_-]+$/)(String);
593
+ export const UrlSafeString = regex("UrlSafeString", /^[A-Za-z0-9_-]+$/)(String);
523
594
  /**
524
- * Alphabet used for Base64Url encoding. This is copied from the `nanoid`
525
- * library to avoid dependency on a specific version of `nanoid`.
595
+ * Base64Url without padding.
596
+ *
597
+ * Encode with {@link uint8ArrayToBase64Url}, decode with
598
+ * {@link base64UrlToUint8Array}.
599
+ *
600
+ * @category String
526
601
  */
527
- export const base64UrlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
602
+ export const Base64Url = brand("Base64Url", String, (value) => {
603
+ // Round-trip validation ensures consistency across different base64url
604
+ // implementations (Node.js Buffer, native browser API, manual fallback).
605
+ // Only strings that decode and encode identically are accepted.
606
+ let roundTrip;
607
+ try {
608
+ roundTrip = uint8ArrayToBase64Url(base64UrlToUint8Array(value));
609
+ }
610
+ catch {
611
+ //
612
+ }
613
+ return roundTrip === value
614
+ ? ok(value)
615
+ : err({ type: "Base64Url", value });
616
+ });
617
+ export const formatBase64UrlError = createTypeErrorFormatter((error) => `Value ${error.value} is not a valid Base64Url string.`);
618
+ const hasNodeBuffer = typeof globalThis.Buffer !== "undefined";
619
+ const base64UrlOptions = { alphabet: "base64url", omitPadding: true };
620
+ /** Encodes a Uint8Array to a {@link Base64Url} string. */
621
+ export const uint8ArrayToBase64Url = hasNodeBuffer
622
+ ? (bytes) => globalThis.Buffer.from(bytes).toString("base64url")
623
+ : // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
624
+ typeof globalThis.Uint8Array.prototype?.toBase64 !== "undefined"
625
+ ? (bytes) =>
626
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
627
+ bytes.toBase64(base64UrlOptions)
628
+ : (bytes) => {
629
+ const binaryString = Array.from(bytes, (byte) => globalThis.String.fromCodePoint(byte)).join("");
630
+ const base64 = globalThis.btoa(binaryString);
631
+ return base64
632
+ .replace(/\+/g, "-")
633
+ .replace(/\//g, "_")
634
+ .replace(/=/g, "");
635
+ };
636
+ /** Decodes a {@link Base64Url} string to a Uint8Array. */
637
+ export const base64UrlToUint8Array = hasNodeBuffer
638
+ ? (str) => {
639
+ const nodeBuffer = globalThis.Buffer.from(str, "base64url");
640
+ return new globalThis.Uint8Array(nodeBuffer);
641
+ }
642
+ : // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
643
+ typeof globalThis.Uint8Array?.fromBase64 !== "undefined"
644
+ ? (str) =>
645
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
646
+ globalThis.Uint8Array.fromBase64(str, base64UrlOptions)
647
+ : (str) => {
648
+ let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
649
+ while (base64.length % 4 !== 0) {
650
+ base64 += "=";
651
+ }
652
+ const binaryString = globalThis.atob(base64);
653
+ return globalThis.Uint8Array.from(binaryString, (c) => c.charCodeAt(0));
654
+ };
528
655
  /**
529
- * Simple alphanumeric string for naming.
656
+ * Simple alphanumeric string for naming in file systems, URLs, and identifiers.
530
657
  *
531
- * A `SimpleName` string uses a limited, safe alphabet for naming purposes:
532
- *
533
- * - Uppercase letters (`A-Z`)
534
- * - Lowercase letters (`a-z`)
535
- * - Digits (`0-9`)
536
- * - Dash (`-`)
658
+ * Uses the same safe alphabet as {@link UrlSafeString} (letters, digits, `-`,
659
+ * `_`). See `UrlSafeString` for details.
537
660
  *
538
661
  * The string must be between 1 and 42 characters.
539
662
  *
@@ -550,13 +673,9 @@ export const base64UrlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_G
550
673
  *
551
674
  * @category String
552
675
  */
553
- export const SimpleName = regex("SimpleName", /^[a-z0-9-]{1,42}$/i)(String);
554
- /**
555
- * Default NanoId.
556
- *
557
- * @category String
558
- */
559
- export const NanoId = regex("NanoId", /^[A-Za-z0-9_-]{21}$/)(String);
676
+ export const SimpleName = brand("SimpleName", UrlSafeString, (value) => value.length >= 1 && value.length <= 42
677
+ ? ok(value)
678
+ : err({ type: "SimpleName", value }));
560
679
  /**
561
680
  * Trimmed string between 8 and 64 characters, branded as `SimplePassword`.
562
681
  *
@@ -565,15 +684,36 @@ export const NanoId = regex("NanoId", /^[A-Za-z0-9_-]{21}$/)(String);
565
684
  export const SimplePassword = brand("SimplePassword", minLength(8)(maxLength(64)(TrimmedString)));
566
685
  export const formatSimplePasswordError = (formatTypeError) => createTypeErrorFormatter((error) => `Invalid password: ${formatTypeError(error.parentError)}`);
567
686
  /**
568
- * `Id` {@link Type}.
687
+ * Globally unique identifier.
688
+ *
689
+ * **Evolu Id** is 16 random bytes from a cryptographically secure random
690
+ * generator, encoded as 22-character Base64Url string. This provides strong
691
+ * collision resistance for distributed ID generation.
692
+ *
693
+ * ### Design Rationale
694
+ *
695
+ * Why Evolu Id over alternatives:
569
696
  *
570
- * Represents a unique identifier with exactly 21 characters, using NanoID's
571
- * standard format (`A-Za-z0-9_-`).
697
+ * - **NanoID**: No standard binary serialization format, and uses only ~126 bits
698
+ * of entropy (21 characters from 64-symbol alphabet) compared to Evolu Id's
699
+ * 128 bits.
700
+ * - **UUID (v4)**: String format is 36 characters (with hyphens) compared to
701
+ * Evolu Id's 22 characters. While UUIDs can be stored as 16 bytes, their
702
+ * standard string representation is verbose.
703
+ * - **UUID v7**: Includes timestamp in the ID, which leaks information about when
704
+ * data was created. This is a privacy concern for local-first applications
705
+ * where creation time must remain private.
706
+ *
707
+ * Evolu Id provides 128 bits of entropy, compact string representation (22
708
+ * characters), standard and native string serialization (Base64Url), and no
709
+ * privacy leaks.
572
710
  *
573
711
  * @category String
574
712
  */
575
- export const Id = regex("Id", /^[A-Za-z0-9_-]{21}$/)(String);
576
- export const idTypeValueLength = 21;
713
+ export const Id = brand("Id", String, (value) => value.length === 22 && Base64Url.fromParent(value).ok
714
+ ? ok(value)
715
+ : err({ type: "Id", value }));
716
+ export const formatIdError = createTypeErrorFormatter((error) => `Value ${error.value} is not a valid Id.`);
577
717
  /**
578
718
  * Creates an {@link Id}.
579
719
  *
@@ -587,13 +727,12 @@ export const idTypeValueLength = 21;
587
727
  * const todoId = createId<"Todo">(deps);
588
728
  * ```
589
729
  */
590
- export const createId = (deps) => deps.nanoIdLib.nanoid();
730
+ export const createId = (deps) => uint8ArrayToBase64Url(deps.randomBytes.create(16));
591
731
  /**
592
732
  * Creates an {@link Id} from a string using SHA-256.
593
733
  *
594
- * Evolu table IDs must follow a fixed 21-character NanoID format. When
595
- * integrating with external systems that use different ID formats, use this
596
- * function to convert external IDs into valid Evolu IDs.
734
+ * When integrating with external systems that use different ID formats, use
735
+ * this function to convert external IDs into valid Evolu IDs.
597
736
  *
598
737
  * In Evolu's CRDT, the ID serves as the unique identifier for conflict
599
738
  * resolution across distributed clients. When multiple clients create records
@@ -614,30 +753,22 @@ export const createId = (deps) => deps.nanoIdLib.nanoid();
614
753
  * });
615
754
  * ```
616
755
  *
617
- * **Important**: This transformation is one-way. You cannot recover the
618
- * original external string from the generated {@link Id}. If you need to
619
- * preserve the original external ID, store it in a separate column.
756
+ * **Important**: This transformation is one-way. We cannot recover the original
757
+ * external string from the generated {@link Id}. If we need to preserve the
758
+ * original external ID, store it in a separate column.
620
759
  *
621
760
  * @category String
622
761
  */
623
762
  export const createIdFromString = (value) => {
624
763
  const hash = sha256(utf8ToBytes(value));
625
- let output = "";
626
- let buffer = 0;
627
- let bits = 0;
628
- for (const byte of hash) {
629
- buffer = (buffer << 8) | byte;
630
- bits += 8;
631
- while (bits >= 6 && output.length < idTypeValueLength) {
632
- bits -= 6;
633
- const index = (buffer >> bits) & 0b111111;
634
- output += base64UrlAlphabet[index];
635
- }
636
- }
637
- return output;
764
+ // Take first 16 bytes of hash and convert to Id
765
+ const id = idBytesToId(hash.slice(0, 16));
766
+ return id;
638
767
  };
639
768
  /**
640
- * Type Factory to create branded {@link Id} Type for a specific table.
769
+ * Creates a branded {@link Id} Type for a table's primary key.
770
+ *
771
+ * The table name becomes an additional brand for type safety.
641
772
  *
642
773
  * ### Example
643
774
  *
@@ -650,30 +781,34 @@ export const createIdFromString = (value) => {
650
781
  * @category String
651
782
  */
652
783
  export const id = (table) => {
653
- const fromParent = (value) => {
654
- const idResult = Id.fromParent(value);
655
- if (!idResult.ok) {
656
- return err({ type: "Id", value, table });
657
- }
658
- return ok(idResult.value);
659
- };
660
784
  const fromUnknown = (value) => {
661
785
  const parentResult = String.fromUnknown(value);
662
786
  if (!parentResult.ok)
663
787
  return parentResult;
664
788
  return fromParent(parentResult.value);
665
789
  };
790
+ const fromParent = (value) => {
791
+ const idResult = Id.fromParent(value);
792
+ if (!idResult.ok) {
793
+ return err({ type: "TableId", value, table });
794
+ }
795
+ return ok(idResult.value);
796
+ };
666
797
  return {
667
- ...createType("Id", {
668
- fromUnknown,
669
- to: (value) => value,
670
- fromParent,
671
- toParent: (value) => value,
672
- }),
798
+ ...createType("Id", { fromUnknown, fromParent }),
673
799
  table,
674
800
  };
675
801
  };
676
- export const formatIdError = createTypeErrorFormatter((error) => `Invalid ${error.type} table Id: ${error.value}`);
802
+ export const formatTableIdError = createTypeErrorFormatter((error) => `Invalid ${error.type} table Id: ${error.value}`);
803
+ /** Binary representation of an {@link Id}. */
804
+ export const IdBytes = brand("IdBytes", length(16)(Uint8Array));
805
+ export const idBytesTypeValueLength = 16;
806
+ export const idToIdBytes = (id) =>
807
+ // Id is Base64Url (validated by Id.from), cast is safe
808
+ base64UrlToUint8Array(id);
809
+ export const idBytesToId = (idBytes) =>
810
+ // Base64Url encoding of 16 bytes always produces valid Id (22 chars)
811
+ uint8ArrayToBase64Url(idBytes);
677
812
  /**
678
813
  * Positive number.
679
814
  *
@@ -766,6 +901,8 @@ export const Int = int(Number);
766
901
  export const NonNegativeInt = nonNegative(Int);
767
902
  /** @category Number */
768
903
  export const PositiveInt = positive(NonNegativeInt);
904
+ /** Maximum safe positive integer value for practically infinite operations. */
905
+ export const maxPositiveInt = PositiveInt.orThrow(globalThis.Number.MAX_SAFE_INTEGER);
769
906
  /** @category Number */
770
907
  export const NonPositiveInt = nonPositive(Int);
771
908
  /** @category Number */
@@ -860,8 +997,6 @@ export const between = (min, max) => (parent) => brand(`Between${min}-${max}`, p
860
997
  ? ok(value)
861
998
  : err({ type: "Between", value, min, max }));
862
999
  export const formatBetweenError = createTypeErrorFormatter((error) => `The value ${error.value} is not between ${error.min} and ${error.max}, inclusive.`);
863
- /** @category Number */
864
- export const Between1And10 = between(1, 10)(Number);
865
1000
  /**
866
1001
  * Literal {@link Type}.
867
1002
  *
@@ -886,94 +1021,12 @@ export const literal = (expected) => {
886
1021
  return {
887
1022
  ...createType("Literal", {
888
1023
  fromUnknown,
889
- to: identity,
890
1024
  fromParent: ok,
891
- toParent: identity,
892
1025
  }),
893
1026
  expected,
894
1027
  };
895
1028
  };
896
1029
  export const formatLiteralError = createTypeErrorFormatter((error) => `The value ${error.value} is not strictly equal to the expected literal: ${globalThis.String(error.expected)}.`);
897
- /**
898
- * {@link Type} that transforms values between `FromType` and `ToType`.
899
- *
900
- * - `fromParent`: Converts `FromType` to `ToType`, may fail.
901
- * - `toParent`: Converts `ToType` back to `FromType`, must not fail.
902
- *
903
- * ### Example
904
- *
905
- * // TODO: Examples
906
- *
907
- * @category Base Factories
908
- */
909
- export const transform = (fromType, toType, fromParent, toParent) => {
910
- const fromUnknown = (value) => {
911
- const parentResult = fromType.fromUnknown(value);
912
- if (!parentResult.ok)
913
- return parentResult;
914
- return fromParent(parentResult.value);
915
- };
916
- const to = (value) => fromType.to(toParent(value));
917
- return {
918
- ...createType("Transform", {
919
- fromUnknown,
920
- to,
921
- fromParent,
922
- toParent,
923
- }),
924
- is: toType.is,
925
- fromType,
926
- toType,
927
- };
928
- };
929
- /**
930
- * Trims leading and trailing whitespace from a string.
931
- *
932
- * ### Example
933
- *
934
- * ```ts
935
- * expect(TrimString.from("a ")).toEqual(ok("a"));
936
- * expect(TrimString.fromParent("a ").value).toEqual("a");
937
- * ```
938
- *
939
- * @category String
940
- */
941
- export const TrimString = trim(String);
942
- /**
943
- * Transforms a {@link Date} into a {@link DateIsoString} string and vice versa.
944
- *
945
- * ### Example
946
- *
947
- * ```ts
948
- * DateIso.from(new Date("2023-12-25T10:30:00.000Z")); // ok("2023-12-25T10:30:00.000Z")
949
- * DateIso.to("2023-12-25T10:30:00.000Z"); // Date object
950
- * DateIso.from(new Date("invalid")); // err({ type: "DateIsoString", value: "Invalid Date" })
951
- * ```
952
- *
953
- * @category String
954
- */
955
- export const DateIso = transform(Date, DateIsoString, (value) => DateIsoString.fromParent(value.toISOString()), (value) => new globalThis.Date(value));
956
- /**
957
- * Transforms a {@link NonEmptyTrimmedString} into a {@link FiniteNumber}.
958
- *
959
- * ### Example
960
- *
961
- * ```ts
962
- * NumberFromString.from("42"); // ok(42)
963
- * NumberFromString.from("abc"); // err({ type: "NumberFromString", value: "abc" })
964
- * ```
965
- *
966
- * @category Number
967
- */
968
- export const NumberFromString = transform(NonEmptyTrimmedString, FiniteNumber, (value) => {
969
- const result = FiniteNumber.fromParent(globalThis.Number(value));
970
- if (!result.ok)
971
- return err({ type: "NumberFromString", value });
972
- return result;
973
- }, (num) => num.toString());
974
- export const formatNumberFromStringError = createTypeErrorFormatter((error) => {
975
- return `The value ${error.value} could not be converted to a finite number.`;
976
- });
977
1030
  /**
978
1031
  * Array of a specific {@link Type}.
979
1032
  *
@@ -1016,7 +1069,6 @@ export const array = (element) => {
1016
1069
  }
1017
1070
  return ok(result);
1018
1071
  };
1019
- const to = (value) => value.map(element.to);
1020
1072
  const fromParent = (value) => {
1021
1073
  const result = [];
1022
1074
  for (let i = 0; i < value.length; i++) {
@@ -1036,14 +1088,8 @@ export const array = (element) => {
1036
1088
  }
1037
1089
  return ok(result);
1038
1090
  };
1039
- const toParent = (values) => values.map(element.toParent);
1040
1091
  return {
1041
- ...createType("Array", {
1042
- fromUnknown,
1043
- to,
1044
- fromParent,
1045
- toParent,
1046
- }),
1092
+ ...createType("Array", { fromUnknown, fromParent }),
1047
1093
  element,
1048
1094
  };
1049
1095
  };
@@ -1117,10 +1163,6 @@ export const record = (keyType, valueType) => {
1117
1163
  }
1118
1164
  return ok(result);
1119
1165
  };
1120
- const to = (value) => Object.fromEntries(Object.entries(value).map(([key, val]) => [
1121
- keyType.to(key),
1122
- valueType.to(val),
1123
- ]));
1124
1166
  const fromParent = (value) => {
1125
1167
  const result = {};
1126
1168
  for (const [rawKey, rawValue] of Object.entries(value)) {
@@ -1152,16 +1194,10 @@ export const record = (keyType, valueType) => {
1152
1194
  }
1153
1195
  return ok(result);
1154
1196
  };
1155
- const toParent = (value) => Object.fromEntries(Object.entries(value).map(([key, val]) => [
1156
- keyType.toParent(key),
1157
- valueType.toParent(val),
1158
- ]));
1159
1197
  return {
1160
1198
  ...createType("Record", {
1161
1199
  fromUnknown,
1162
- to,
1163
1200
  fromParent,
1164
- toParent,
1165
1201
  }),
1166
1202
  key: keyType,
1167
1203
  value: valueType,
@@ -1248,22 +1284,6 @@ export function object(props, record) {
1248
1284
  }
1249
1285
  return ok(result);
1250
1286
  };
1251
- const to = (value) => {
1252
- const entries = [];
1253
- for (const key of propKeys) {
1254
- if (!(key in value) && isOptionalType(props[key])) {
1255
- continue;
1256
- }
1257
- entries.push([key, props[key].to(value[key])]);
1258
- }
1259
- if (record) {
1260
- const recordEntries = Object.entries(value).filter(([key]) => !propKeys.includes(key));
1261
- for (const [key, val] of recordEntries) {
1262
- entries.push([record.key.to(key), record.value.to(val)]);
1263
- }
1264
- }
1265
- return Object.fromEntries(entries);
1266
- };
1267
1287
  const fromParent = (value) => {
1268
1288
  const errors = {};
1269
1289
  const result = {};
@@ -1311,28 +1331,10 @@ export function object(props, record) {
1311
1331
  }
1312
1332
  return ok(result);
1313
1333
  };
1314
- const toParent = (value) => {
1315
- const entries = [];
1316
- for (const key of propKeys) {
1317
- if (!(key in value) && isOptionalType(props[key])) {
1318
- continue;
1319
- }
1320
- entries.push([key, props[key].toParent(value[key])]);
1321
- }
1322
- if (record) {
1323
- const recordEntries = Object.entries(value).filter(([key]) => !propKeys.includes(key));
1324
- for (const [key, val] of recordEntries) {
1325
- entries.push([record.key.toParent(key), record.value.toParent(val)]);
1326
- }
1327
- }
1328
- return Object.fromEntries(entries);
1329
- };
1330
1334
  return {
1331
1335
  ...createType(record ? "ObjectWithRecord" : "Object", {
1332
1336
  fromUnknown,
1333
- to,
1334
1337
  fromParent,
1335
- toParent,
1336
1338
  }),
1337
1339
  props,
1338
1340
  ...(record ? { record } : {}),
@@ -1385,7 +1387,6 @@ export function union(...args) {
1385
1387
  * - Generate a specialized function to improve validation performance for such
1386
1388
  * cases.
1387
1389
  */
1388
- /* eslint-disable */
1389
1390
  const members = args.map((arg) => (isType(arg) ? arg : literal(arg)));
1390
1391
  const fromUnknown = (value) => {
1391
1392
  const errors = [];
@@ -1401,23 +1402,13 @@ export function union(...args) {
1401
1402
  errors,
1402
1403
  });
1403
1404
  };
1404
- const to = (value) => {
1405
- for (const member of members) {
1406
- if (member.is(value))
1407
- return member.to(value);
1408
- }
1409
- assert(false, "No matching member found in Union Type `to` function");
1410
- };
1411
1405
  return {
1412
1406
  ...createType("Union", {
1413
1407
  fromUnknown,
1414
- to,
1415
1408
  fromParent: fromUnknown,
1416
- toParent: to,
1417
1409
  }),
1418
1410
  members,
1419
1411
  };
1420
- /* eslint-enable */
1421
1412
  }
1422
1413
  export const formatUnionError = (formatTypeError) => createTypeErrorFormatter((error) => {
1423
1414
  const formattedErrors = error.errors
@@ -1473,20 +1464,10 @@ export const recursive = (create) => {
1473
1464
  type ??= create();
1474
1465
  return type.fromUnknown(value);
1475
1466
  },
1476
- to: (value) => {
1477
- type ??= create();
1478
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
1479
- return type.to(value);
1480
- },
1481
1467
  fromParent: (value) => {
1482
1468
  type ??= create();
1483
1469
  return type.fromParent(value);
1484
1470
  },
1485
- toParent: (value) => {
1486
- type ??= create();
1487
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
1488
- return type.toParent(value);
1489
- },
1490
1471
  is: (value) => {
1491
1472
  type ??= create();
1492
1473
  return type.is(value);
@@ -1590,7 +1571,6 @@ export const tuple = (...elements) => {
1590
1571
  }
1591
1572
  return ok(result);
1592
1573
  };
1593
- const to = (value) => value.map((val, index) => elements[index].to(val));
1594
1574
  const fromParent = (value) => {
1595
1575
  if (!Array.isArray(value) || value.length !== elements.length) {
1596
1576
  return err({
@@ -1617,13 +1597,10 @@ export const tuple = (...elements) => {
1617
1597
  }
1618
1598
  return ok(result);
1619
1599
  };
1620
- const toParent = (value) => value.map((val, index) => elements[index].toParent(val));
1621
1600
  return {
1622
1601
  ...createType("Tuple", {
1623
1602
  fromUnknown,
1624
- to,
1625
1603
  fromParent,
1626
- toParent,
1627
1604
  }),
1628
1605
  elements,
1629
1606
  };
@@ -1657,22 +1634,32 @@ export const Int64 = brand("Int64", BigInt, (value) => value >= -922337203685477
1657
1634
  ? ok(value)
1658
1635
  : err({ type: "Int64", value }));
1659
1636
  export const formatInt64Error = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid 64-bit signed integer (Int64).`);
1660
- export const BigIntFromString = transform(String, BigInt, (value) => trySync(() => globalThis.BigInt(value), () => ({ type: "BigIntFromString", value })), (value) => value.toString());
1661
- export const formatBigIntFromStringError = createTypeErrorFormatter((error) => `The value ${error.value} could not be converted to a BigInt.`);
1637
+ // // co s timhle? je to string, ze ktereho lze udelat bigint
1638
+ // export const BigIntFromString = transform(
1639
+ // String,
1640
+ // BigInt,
1641
+ // (value) =>
1642
+ // trySync(
1643
+ // () => globalThis.BigInt(value),
1644
+ // (): BigIntFromStringError => ({ type: "BigIntFromString", value }),
1645
+ // ),
1646
+ // (value) => value.toString(),
1647
+ // );
1648
+ // export interface BigIntFromStringError extends TypeError<"BigIntFromString"> {}
1649
+ // export const formatBigIntFromStringError =
1650
+ // createTypeErrorFormatter<BigIntFromStringError>(
1651
+ // (error) => `The value ${error.value} could not be converted to a BigInt.`,
1652
+ // );
1662
1653
  /**
1663
1654
  * Stringified {@link Int64}.
1664
1655
  *
1665
- * @category Number
1656
+ * @category String
1666
1657
  */
1667
- export const Int64String = brand("Int64", String, (value) => {
1668
- const bigint = BigIntFromString.fromParent(value);
1669
- if (bigint.ok) {
1670
- const int64 = Int64.fromParent(globalThis.BigInt(value));
1671
- if (int64.ok)
1672
- return ok(value);
1673
- }
1674
- return err({ type: "Int64String", value });
1675
- });
1658
+ export const Int64String = brand("Int64", NonEmptyTrimmedString, (value) => trySync(() => {
1659
+ const maybeInt = globalThis.BigInt(value);
1660
+ Int64.orThrow(maybeInt);
1661
+ return value;
1662
+ }, () => ({ type: "Int64String", value })));
1676
1663
  export const formatInt64StringError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid Int64 string.`);
1677
1664
  /**
1678
1665
  * JSON-compatible value: string, {@link FiniteNumber}, boolean, null,
@@ -1693,48 +1680,83 @@ export const JsonArray = array(JsonValue);
1693
1680
  * @category Object
1694
1681
  */
1695
1682
  export const JsonObject = record(String, JsonValue);
1696
- /**
1697
- * Transform Type that parses a JSON into a {@link JsonValue} and serializes a
1698
- * JsonValue back into a JSON string.
1699
- *
1700
- * ### Example
1701
- *
1702
- * ```ts
1703
- * JsonValueFromString.from(`{"key":"value"}`); // -> ok({ key: "value" })
1704
- * JsonValueFromString.to({ key: "value" }); // -> '{"key":"value"}'
1705
- * ```
1706
- *
1707
- * @category String
1708
- */
1709
- export const JsonValueFromString = transform(String, JsonValue, (value) => trySync(() => JSON.parse(value), (error) => ({
1710
- type: `JsonValueFromString`,
1683
+ export const parseJson = (value) => trySync(() => JSON.parse(value), (error) => ({
1684
+ type: `Json`,
1711
1685
  value,
1712
1686
  message: globalThis.String(error),
1713
- })), (value) => JSON.stringify(value));
1714
- export const formatJsonValueFromStringError = createTypeErrorFormatter((error) => `Invalid JSONValue: ${error.value}. Error: ${error.message}`);
1687
+ }));
1715
1688
  /**
1716
1689
  * JSON-string {@link Type}.
1717
1690
  *
1718
1691
  * ### Example
1719
1692
  *
1720
1693
  * ```ts
1721
- * const result = Json.from('{"key":"value"}'); // -> ok('{"key":"value"}')
1722
- * const error = Json.from("invalid json"); // -> err({ type: "Json", value: "invalid json", message: "Unexpected token i in JSON at position 0" })
1694
+ * const result = Json.from('{"key":"value"}'); // ok
1695
+ * const error = Json.from("invalid json"); // err
1723
1696
  * ```
1724
1697
  *
1725
1698
  * @category String
1726
1699
  */
1727
1700
  export const Json = brand("Json", String, (value) => {
1728
- const result = JsonValueFromString.fromParent(value);
1701
+ const result = parseJson(value);
1729
1702
  if (!result.ok)
1730
- return err({
1731
- type: "Json",
1732
- value,
1733
- message: result.error.message,
1734
- });
1703
+ return result;
1735
1704
  return ok(value);
1736
1705
  });
1737
1706
  export const formatJsonError = createTypeErrorFormatter((error) => `Invalid JSON: ${error.value}. Error: ${error.message}`);
1707
+ export const jsonValueToJson = (value) => JSON.stringify(value);
1708
+ export const jsonToJsonValue = (value) => JSON.parse(value);
1709
+ /**
1710
+ * Creates a branded JSON string {@link Type} and type-safe conversion functions
1711
+ * for a given Type.
1712
+ *
1713
+ * This factory creates:
1714
+ *
1715
+ * 1. A branded string Type that validates JSON parsing and structural conformity
1716
+ * 2. A serialization function (Type → branded JSON string)
1717
+ * 3. A parsing function (branded JSON string → Type, skipping validation)
1718
+ *
1719
+ * Optimized for Evolu's SQLite workflow where we store typed JSON strings and
1720
+ * need type-safe conversions without double parsing.
1721
+ *
1722
+ * ### Example
1723
+ *
1724
+ * ```ts
1725
+ * const Person = object({
1726
+ * name: NonEmptyString100,
1727
+ * age: FiniteNumber,
1728
+ * });
1729
+ * type Person = typeof Person.Type;
1730
+ *
1731
+ * const [PersonJson, personToPersonJson, personJsonToPerson] = json(
1732
+ * Person,
1733
+ * "PersonJson",
1734
+ * );
1735
+ * // string & Brand<"PersonJson">
1736
+ * type PersonJson = typeof PersonJson.Type;
1737
+ *
1738
+ * // Usage:
1739
+ * const person: Person = { name: "Alice", age: 30 };
1740
+ * const jsonString = personToPersonJson(person); // PersonJson
1741
+ * const backToPerson = personJsonToPerson(jsonString); // Person
1742
+ * ```
1743
+ */
1744
+ export const json = (type, name) => {
1745
+ const BrandedJsonType = brand(name, String, (value) => {
1746
+ const parseResult = parseJson(value);
1747
+ if (!parseResult.ok)
1748
+ return parseResult;
1749
+ const validationResult = type.fromUnknown(parseResult.value);
1750
+ if (!validationResult.ok)
1751
+ return validationResult;
1752
+ return ok(value);
1753
+ });
1754
+ return [
1755
+ BrandedJsonType,
1756
+ jsonValueToJson,
1757
+ jsonToJsonValue,
1758
+ ];
1759
+ };
1738
1760
  /**
1739
1761
  * Optional {@link Type}.
1740
1762
  *
@@ -1759,9 +1781,7 @@ export const formatJsonError = createTypeErrorFormatter((error) => `Invalid JSON
1759
1781
  export const optional = (type) => ({
1760
1782
  ...createType("Optional", {
1761
1783
  fromUnknown: type.fromUnknown,
1762
- to: type.to,
1763
1784
  fromParent: type.fromParent,
1764
- toParent: type.toParent,
1765
1785
  }),
1766
1786
  parent: type,
1767
1787
  });
@@ -1770,7 +1790,7 @@ export const isOptionalType = (x) => typeof x === "object" && x != null && "name
1770
1790
  /**
1771
1791
  * Creates a partial object type where all properties are optional.
1772
1792
  *
1773
- * This is useful when you want to validate an object in which none of the keys
1793
+ * This is useful when we want to validate an object in which none of the keys
1774
1794
  * are required, but if they are present they must conform to their
1775
1795
  * corresponding Types.
1776
1796
  *
@@ -1841,114 +1861,86 @@ export function omit(objectType, ...keys) {
1841
1861
  }
1842
1862
  return object(newProps);
1843
1863
  }
1864
+ export const maxMutationSize = 655360;
1844
1865
  /**
1845
- * Creates a transform Type that serializes a given `Type` into a branded JSON
1846
- * string. The transformation is reversible, ensuring that we can safely parse
1847
- * it back.
1848
- *
1849
- * ### Example
1850
- *
1851
- * ```ts
1852
- * const Person = object({
1853
- * name: NonEmptyString50,
1854
- * age: FiniteNumber,
1855
- * });
1856
- * type Person = typeof Person.Type;
1857
- *
1858
- * const PersonJson = json(Person, "PersonJson");
1859
- * // string & Brand<"PersonJson">
1860
- * type PersonJson = typeof PersonJson.Type;
1861
- *
1862
- * // Person -> string & Brand<"PersonJson">
1863
- * const personJson = PersonJson.from({ name: "Alice", age: 30 });
1864
- * expect(personJson).toEqual(ok('{"name":"Alice","age":30}'));
1865
- *
1866
- * // string & Brand<"PersonJson"> -> Person
1867
- * const person = PersonJson.to(personJson);
1868
- *
1869
- * // serialize/parse any JSON value
1870
- * const AnyJson = json(JsonValue, "AnyJson");
1871
- * ```
1866
+ * Evolu has to limit the maximum mutation size. Otherwise, sync couldn't use
1867
+ * the `maxProtocolMessageRangesSize`. The max size is 640KB in bytes, measured
1868
+ * via MessagePack. Evolu Protocol DbChange will be smaller thanks to various
1869
+ * optimizations.
1872
1870
  */
1873
- export const json = (type, name) => {
1874
- const BrandedJsonString = brand(name, String, (value) => {
1875
- const jsonValue = JsonValueFromString.fromParent(value);
1876
- if (!jsonValue.ok)
1877
- return jsonValue;
1878
- const parsed = type.fromUnknown(jsonValue.value);
1879
- if (!parsed.ok)
1880
- return parsed;
1881
- return ok(value);
1882
- });
1883
- return transform(type, BrandedJsonString, (value) => ok(JSON.stringify(value)),
1884
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
1885
- (value) => JSON.parse(value));
1886
- };
1871
+ export const validMutationSize = (type) => brand("ValidMutationSize", type, (value) => pack(value).byteLength <= maxMutationSize
1872
+ ? ok(value)
1873
+ : err({ type: "ValidMutationSize", value }));
1874
+ 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.`);
1887
1875
  /**
1888
- * Creates a unified error formatter that handles both Evolu Type's built-in
1889
- * {@link TypeErrors} and custom errors. It also lets us override the default
1890
- * formatting for specific errors.
1876
+ * Formats Evolu Type errors into user-friendly messages.
1877
+ *
1878
+ * Evolu Type typed errors ensure every error type must have a formatter.
1879
+ * TypeScript enforces this at compile-time, preventing unhandled validation
1880
+ * errors from reaching users.
1891
1881
  *
1892
- * If you prefer not to reuse any built-in error formatters, you can write your
1893
- * own `formatTypeError` function from scratch.
1882
+ * The `createFormatTypeError` function handles both built-in {@link TypeErrors}
1883
+ * and custom errors, and lets us override default formatting for specific
1884
+ * errors.
1894
1885
  *
1895
- * ### Examples
1886
+ * ### Example
1896
1887
  *
1897
1888
  * ```ts
1898
- * const formatError = createFormatTypeError();
1899
- * console.log(formatError({ type: "String", value: 42 }));
1900
- * // "A value 42 is not a string."
1889
+ * const formatTypeError = createFormatTypeError<
1890
+ * MinLengthError | MaxLengthError
1891
+ * >((error): string => {
1892
+ * switch (error.type) {
1893
+ * case "MinLength":
1894
+ * return `Text must be at least ${error.min} character${error.min === 1 ? "" : "s"} long`;
1895
+ * case "MaxLength":
1896
+ * return `Text is too long (maximum ${error.max} characters)`;
1897
+ * }
1898
+ * });
1901
1899
  * ```
1902
1900
  *
1903
- * A custom `formatTypeError` function:
1901
+ * Alternatively, write a custom formatter from scratch without using
1902
+ * `createFormatTypeError`. This gives us full control over error formatting:
1904
1903
  *
1905
1904
  * ```ts
1906
- * type AppErrors =
1907
- * | ValidMutationSizeError
1905
+ * const Person = object({
1906
+ * name: NonEmptyTrimmedString100,
1907
+ * age: optional(PositiveInt),
1908
+ * });
1909
+ *
1910
+ * // Define only the errors actually used by Person Type
1911
+ * type PersonErrors =
1908
1912
  * | StringError
1909
- * | MinLengthError
1910
1913
  * | MaxLengthError
1911
- * | NullError
1912
- * | IdError
1914
+ * | MinLengthError
1913
1915
  * | TrimmedError
1914
- * | MnemonicError
1915
- * | LiteralError
1916
- * // Composite errors
1917
- * | ObjectError<Record<string, AppErrors>>
1918
- * | UnionError<AppErrors>;
1919
- *
1920
- * const formatTypeError: TypeErrorFormatter<AppErrors> = (error) => {
1921
- * // In the real code, we would use the createTypeErrorFormatter helper
1922
- * // that safely stringifies error value.
1916
+ * | PositiveError
1917
+ * | NonNegativeError
1918
+ * | IntError
1919
+ * | NumberError
1920
+ * | ObjectError<Record<string, PersonErrors>>;
1921
+ *
1922
+ * const formatTypeError: TypeErrorFormatter<PersonErrors> = (error) => {
1923
1923
  * switch (error.type) {
1924
- * case "Id":
1925
- * return `Invalid Id on table: ${error.table}.`;
1926
- * case "MaxLength":
1927
- * return `Max length is ${error.max}.`;
1928
- * case "MinLength":
1929
- * return `Min length is ${error.min}.`;
1930
- * case "Mnemonic":
1931
- * return `Invalid mnemonic: ${String(error.value)}`;
1932
- * case "Null":
1933
- * return `Not null`;
1934
1924
  * case "String":
1935
- * // We can reuse existing formatter.
1936
1925
  * return formatStringError(error);
1926
+ * case "Number":
1927
+ * return "Must be a number";
1928
+ * case "MinLength":
1929
+ * return `Must be at least ${error.min} characters`;
1930
+ * case "MaxLength":
1931
+ * return `Cannot exceed ${error.max} characters`;
1937
1932
  * case "Trimmed":
1938
- * return "Value is not trimmed.";
1939
- * case "ValidMutationSize":
1940
- * return "A developer made an error, this should not happen.";
1941
- * case "Literal":
1942
- * return formatLiteralError(error);
1943
- * // Composite Types
1944
- * case "Union":
1945
- * return `Union errors: ${error.errors.map(formatTypeError).join(", ")}`;
1933
+ * return "Cannot have leading or trailing spaces";
1934
+ * case "Positive":
1935
+ * return "Must be a positive number";
1936
+ * case "NonNegative":
1937
+ * return "Must be zero or positive";
1938
+ * case "Int":
1939
+ * return "Must be an integer";
1946
1940
  * case "Object": {
1947
- * if (
1948
- * error.reason.kind === "ExtraKeys" ||
1949
- * error.reason.kind === "NotObject"
1950
- * )
1951
- * return "A developer made an error, this should not happen.";
1941
+ * if (error.reason.kind === "NotObject") return "Must be an object";
1942
+ * if (error.reason.kind === "ExtraKeys")
1943
+ * return "Contains unexpected fields";
1952
1944
  * const firstError = Object.values(error.reason.errors).find(
1953
1945
  * (e) => e !== undefined,
1954
1946
  * )!;
@@ -1989,8 +1981,8 @@ export const createFormatTypeError = (extraFormatter) => {
1989
1981
  return formatIsTypeError(error);
1990
1982
  case "CurrencyCode":
1991
1983
  return formatCurrencyCodeError(error);
1992
- case "DateIsoString":
1993
- return formatDateIsoStringError(error);
1984
+ case "DateIso":
1985
+ return formatDateIsoError(error);
1994
1986
  case "Trimmed":
1995
1987
  return formatTrimmedError(error);
1996
1988
  case "MinLength":
@@ -2005,6 +1997,8 @@ export const createFormatTypeError = (extraFormatter) => {
2005
1997
  return formatRegexError(error);
2006
1998
  case "Id":
2007
1999
  return formatIdError(error);
2000
+ case "TableId":
2001
+ return formatTableIdError(error);
2008
2002
  case "Positive":
2009
2003
  return formatPositiveError(error);
2010
2004
  case "Negative":
@@ -2035,14 +2029,12 @@ export const createFormatTypeError = (extraFormatter) => {
2035
2029
  return formatLiteralError(error);
2036
2030
  case "Int64":
2037
2031
  return formatInt64Error(error);
2038
- case "BigIntFromString":
2039
- return formatBigIntFromStringError(error);
2040
2032
  case "Int64String":
2041
2033
  return formatInt64StringError(error);
2042
- case "JsonValueFromString":
2043
- return formatJsonValueFromStringError(error);
2044
2034
  case "Json":
2045
2035
  return formatJsonError(error);
2036
+ case "ValidMutationSize":
2037
+ return formatValidMutationSizeError(error);
2046
2038
  // Composite Types
2047
2039
  case "SimplePassword":
2048
2040
  return formatSimplePasswordError(formatTypeError)(error);