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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/dist/src/Assert.d.ts +6 -3
  2. package/dist/src/Assert.d.ts.map +1 -1
  3. package/dist/src/Assert.js +7 -4
  4. package/dist/src/Brand.d.ts +75 -0
  5. package/dist/src/Brand.d.ts.map +1 -0
  6. package/dist/src/Brand.js +1 -0
  7. package/dist/src/Buffer.d.ts +1 -1
  8. package/dist/src/Buffer.d.ts.map +1 -1
  9. package/dist/src/Buffer.js +1 -1
  10. package/dist/src/CallbackRegistry.d.ts +53 -0
  11. package/dist/src/CallbackRegistry.d.ts.map +1 -0
  12. package/dist/src/CallbackRegistry.js +25 -0
  13. package/dist/src/Console.d.ts +31 -6
  14. package/dist/src/Console.d.ts.map +1 -1
  15. package/dist/src/Console.js +72 -9
  16. package/dist/src/Crypto.d.ts +61 -34
  17. package/dist/src/Crypto.d.ts.map +1 -1
  18. package/dist/src/Crypto.js +29 -42
  19. package/dist/src/Evolu/Db.d.ts +137 -66
  20. package/dist/src/Evolu/Db.d.ts.map +1 -1
  21. package/dist/src/Evolu/Db.js +300 -705
  22. package/dist/src/Evolu/Diff.d.ts +3 -3
  23. package/dist/src/Evolu/Diff.d.ts.map +1 -1
  24. package/dist/src/Evolu/Diff.js +7 -5
  25. package/dist/src/Evolu/Evolu.d.ts +200 -115
  26. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  27. package/dist/src/Evolu/Evolu.js +287 -157
  28. package/dist/src/Evolu/Internal.d.ts +0 -2
  29. package/dist/src/Evolu/Internal.d.ts.map +1 -1
  30. package/dist/src/Evolu/Internal.js +0 -2
  31. package/dist/src/Evolu/LocalAuth.d.ts +144 -0
  32. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
  33. package/dist/src/Evolu/LocalAuth.js +171 -0
  34. package/dist/src/Evolu/Owner.d.ts +152 -127
  35. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  36. package/dist/src/Evolu/Owner.js +88 -107
  37. package/dist/src/Evolu/Platform.d.ts +9 -7
  38. package/dist/src/Evolu/Platform.d.ts.map +1 -1
  39. package/dist/src/Evolu/Protocol.d.ts +170 -186
  40. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  41. package/dist/src/Evolu/Protocol.js +487 -343
  42. package/dist/src/Evolu/Public.d.ts +6 -8
  43. package/dist/src/Evolu/Public.d.ts.map +1 -1
  44. package/dist/src/Evolu/Public.js +2 -3
  45. package/dist/src/Evolu/PublicKysely.js +3 -3
  46. package/dist/src/Evolu/Query.d.ts +2 -1
  47. package/dist/src/Evolu/Query.d.ts.map +1 -1
  48. package/dist/src/Evolu/Relay.d.ts +4 -3
  49. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  50. package/dist/src/Evolu/Relay.js +50 -12
  51. package/dist/src/Evolu/Schema.d.ts +129 -73
  52. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  53. package/dist/src/Evolu/Schema.js +169 -89
  54. package/dist/src/Evolu/Storage.d.ts +159 -14
  55. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  56. package/dist/src/Evolu/Storage.js +42 -32
  57. package/dist/src/Evolu/Sync.d.ts +77 -13
  58. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  59. package/dist/src/Evolu/Sync.js +453 -20
  60. package/dist/src/Evolu/Timestamp.d.ts +35 -27
  61. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  62. package/dist/src/Evolu/Timestamp.js +27 -18
  63. package/dist/src/ManyToManyMap.d.ts +74 -10
  64. package/dist/src/ManyToManyMap.d.ts.map +1 -1
  65. package/dist/src/ManyToManyMap.js +41 -6
  66. package/dist/src/Number.d.ts +2 -1
  67. package/dist/src/Number.d.ts.map +1 -1
  68. package/dist/src/Random.d.ts +3 -2
  69. package/dist/src/Random.d.ts.map +1 -1
  70. package/dist/src/RefCountedResourceManager.d.ts +119 -0
  71. package/dist/src/RefCountedResourceManager.d.ts.map +1 -0
  72. package/dist/src/RefCountedResourceManager.js +197 -0
  73. package/dist/src/Result.d.ts +147 -51
  74. package/dist/src/Result.d.ts.map +1 -1
  75. package/dist/src/Result.js +7 -241
  76. package/dist/src/Sqlite.d.ts +21 -4
  77. package/dist/src/Sqlite.d.ts.map +1 -1
  78. package/dist/src/Sqlite.js +50 -8
  79. package/dist/src/Task.d.ts +511 -0
  80. package/dist/src/Task.d.ts.map +1 -0
  81. package/dist/src/Task.js +410 -0
  82. package/dist/src/Time.d.ts +59 -0
  83. package/dist/src/Time.d.ts.map +1 -1
  84. package/dist/src/Time.js +87 -4
  85. package/dist/src/Type.d.ts +462 -327
  86. package/dist/src/Type.d.ts.map +1 -1
  87. package/dist/src/Type.js +490 -436
  88. package/dist/src/Types.d.ts +0 -74
  89. package/dist/src/Types.d.ts.map +1 -1
  90. package/dist/src/WebSocket.d.ts +5 -2
  91. package/dist/src/WebSocket.d.ts.map +1 -1
  92. package/dist/src/WebSocket.js +12 -13
  93. package/dist/src/Worker.d.ts +39 -11
  94. package/dist/src/Worker.d.ts.map +1 -1
  95. package/dist/src/Worker.js +22 -4
  96. package/dist/src/index.d.ts +3 -3
  97. package/dist/src/index.d.ts.map +1 -1
  98. package/dist/src/index.js +3 -3
  99. package/package.json +14 -13
  100. package/src/Assert.ts +8 -7
  101. package/src/Brand.ts +75 -0
  102. package/src/Buffer.ts +1 -1
  103. package/src/CallbackRegistry.ts +84 -0
  104. package/src/Console.ts +91 -11
  105. package/src/Crypto.ts +94 -79
  106. package/src/Evolu/Db.ts +505 -1027
  107. package/src/Evolu/Diff.ts +7 -5
  108. package/src/Evolu/Evolu.ts +682 -329
  109. package/src/Evolu/Internal.ts +0 -2
  110. package/src/Evolu/LocalAuth.ts +422 -0
  111. package/src/Evolu/Owner.ts +221 -235
  112. package/src/Evolu/Platform.ts +9 -9
  113. package/src/Evolu/Protocol.ts +681 -614
  114. package/src/Evolu/Public.ts +7 -14
  115. package/src/Evolu/PublicKysely.ts +3 -3
  116. package/src/Evolu/Query.ts +2 -1
  117. package/src/Evolu/Relay.ts +62 -16
  118. package/src/Evolu/Schema.ts +391 -191
  119. package/src/Evolu/Storage.ts +275 -55
  120. package/src/Evolu/Sync.ts +758 -37
  121. package/src/Evolu/Timestamp.ts +38 -35
  122. package/src/ManyToManyMap.ts +127 -24
  123. package/src/Number.ts +2 -6
  124. package/src/Random.ts +3 -2
  125. package/src/RefCountedResourceManager.ts +368 -0
  126. package/src/Result.ts +152 -53
  127. package/src/Sqlite.ts +59 -23
  128. package/src/Task.ts +779 -0
  129. package/src/Time.ts +168 -4
  130. package/src/Type.ts +696 -660
  131. package/src/Types.ts +0 -76
  132. package/src/WebSocket.ts +23 -17
  133. package/src/Worker.ts +72 -23
  134. package/src/index.ts +3 -3
  135. package/dist/src/Callbacks.d.ts +0 -20
  136. package/dist/src/Callbacks.d.ts.map +0 -1
  137. package/dist/src/Callbacks.js +0 -18
  138. package/dist/src/Evolu/Config.d.ts +0 -69
  139. package/dist/src/Evolu/Config.d.ts.map +0 -1
  140. package/dist/src/Evolu/Config.js +0 -9
  141. package/dist/src/Evolu/Kysely.d.ts +0 -6
  142. package/dist/src/Evolu/Kysely.d.ts.map +0 -1
  143. package/dist/src/Evolu/Kysely.js +0 -21
  144. package/dist/src/NanoId.d.ts +0 -27
  145. package/dist/src/NanoId.d.ts.map +0 -1
  146. package/dist/src/NanoId.js +0 -6
  147. package/dist/src/Promise.d.ts +0 -180
  148. package/dist/src/Promise.d.ts.map +0 -1
  149. package/dist/src/Promise.js +0 -176
  150. package/src/Callbacks.ts +0 -43
  151. package/src/Evolu/Config.ts +0 -83
  152. package/src/Evolu/Kysely.ts +0 -38
  153. package/src/NanoId.ts +0 -39
  154. package/src/Promise.ts +0 -295
package/dist/src/Type.js CHANGED
@@ -1,80 +1,182 @@
1
1
  /**
2
- * 🧩 Validation, Parsing, and Transformation
2
+ * 🧩 Type-safe runtime types
3
3
  *
4
- * ## Intro
4
+ * Evolu {@link Type} is like a type guard that returns typed errors (via
5
+ * {@link Result}) instead of throwing. We either get a safely typed value or a
6
+ * precise, composable error value telling us exactly why validation failed.
5
7
  *
6
- * You probably know [Zod](https://zod.dev). Evolu has {@link Type}.
8
+ * Why another validation library?
7
9
  *
8
- * Evolu Type exists because no existing validation/parsing/transformation
9
- * library fully met our needs:
10
+ * - **Result-based error handling** no exceptions for normal control flow.
11
+ * - **Typed errors with decoupled formatters** – validation logic ≠ user
12
+ * messages.
13
+ * - **Consistent constraints via {@link Brand}** – every constraint becomes part
14
+ * of the type.
15
+ * - **No user-land chaining DSL** – designed with the upcoming ES pipe operator
16
+ * in mind.
17
+ * - **Selective validation** – parent validations are skipped when already proved
18
+ * by typing.
19
+ * - **Simple, top-down implementation** – readable source code from top to bottom
20
+ * with no hidden magic; just plain functions and composition.
10
21
  *
11
- * - **Result-based error handling**: Leveraging {@link Result} instead of throwing
12
- * exceptions.
13
- * - **Consistent constraints**: Enforcing {@link Brand} for all constraints.
14
- * - **Typed errors with decoupled formatters**: Avoiding coupling error messages
15
- * with validators.
16
- * - **No user-land chaining**: Designed with ES pipe operator in mind.
17
- * - **Selective validation/transformation**: Skipping parent Type validations and
18
- * transformations when TypeScript's type system can be relied upon.
19
- * - **Bidirectional transformations**: Supporting transformations in both
20
- * directions.
21
- * - **Minimal and transparent code**: No runtime dependencies or hidden magic.
22
+ * ### Base Types Quick Start
22
23
  *
23
- * **Note**: A proper quickstart guide is on the way. In the meantime, each type
24
- * includes its own usage example, and you can (and should) check the tests for
25
- * practical demonstrations of the API. Or dang, just read the code. It's
26
- * simple.
24
+ * ```ts
25
+ * // Validate unknown values
26
+ * const value: unknown = "hello";
27
+ * const stringResult = String.fromUnknown(value);
28
+ * if (!stringResult.ok) {
29
+ * // console.error(formatStringError(stringResult.error));
30
+ * return stringResult; // inside a function returning Result<string, _>
31
+ * }
32
+ * // Safe branch: value is now string
33
+ * const upper = stringResult.value.toUpperCase();
34
+ *
35
+ * // Type guard style
36
+ * if (String.is(value)) {
37
+ * // narrowed to string
38
+ * }
39
+ *
40
+ * // Composing: arrays & objects
41
+ * const Numbers = array(Number); // ReadonlyArray<number>
42
+ * const Point = object({ x: Number, y: Number });
43
+ *
44
+ * Numbers.from([1, 2, 3]); // ok
45
+ * Point.from({ x: 1, y: 2 }); // ok
46
+ * Point.from({ x: 1, y: "2" }); // err -> nested Number error
47
+ * ```
48
+ *
49
+ * ### Branding Basics
50
+ *
51
+ * Branding adds semantic meaning & constraints while preserving the runtime
52
+ * shape:
27
53
  *
28
- * - Evolu `Type` is:
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">
29
61
  *
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.
62
+ * interface CurrencyCodeError extends TypeError<"CurrencyCode"> {}
34
63
  *
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:
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
+ * ```
39
72
  *
40
- * `Unknown` -> `String` -> `TrimmedString` -> `NonEmptyTrimmedString100`
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;
41
84
  *
42
- * For `NonEmptyTrimmedString100`, the parent Type is `TrimmedString`. For
43
- * `TrimmedString`, the parent Type is `String`.
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;
106
+ *
107
+ * const person = Person.orThrow({
108
+ * name: "Alice",
109
+ * age: 30,
110
+ * });
111
+ *
112
+ * const personJson = personToPersonJson(person);
113
+ * expect(personJsonToPerson(personJson)).toEqual(person);
114
+ * ```
44
115
  *
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.
116
+ * ### Error Formatting
48
117
  *
49
- * Speaking of `fromParent` and `toParent`, those functions exist to bypass
50
- * parent Types when we can rely on TypeScript types.
118
+ * Evolu separates validation logic from human-readable messages. There are two
119
+ * layers:
51
120
  *
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")`.
121
+ * 1. Per-type formatters (e.g. `formatStringError`) simple, focused, already
122
+ * used earlier in the quick start example.
123
+ * 2. A unified formatter via `createFormatTypeError` composes all built-in and
124
+ * custom errors (including nested composite types) and lets us override
125
+ * selected messages.
126
+ *
127
+ * #### 1. Per-Type Formatter (recap)
128
+ *
129
+ * ```ts
130
+ * const r = String.fromUnknown(42);
131
+ * if (!r.ok) console.error(formatStringError(r.error));
132
+ * ```
133
+ *
134
+ * #### 2. Unified Formatter with Overrides
135
+ *
136
+ * ```ts
137
+ * // Override only what we care about; fall back to built-ins for the rest.
138
+ * const formatTypeError = createFormatTypeError((error) => {
139
+ * if (error.type === "MinLength") return `Min length is ${error.min}`;
140
+ * });
141
+ *
142
+ * const User = object({ name: NonEmptyTrimmedString100 });
143
+ * const resultUser = User.from({ name: "" });
144
+ * if (!resultUser.ok) console.error(formatTypeError(resultUser.error));
145
+ *
146
+ * const badPoint = object({ x: Number, y: Number }).from({
147
+ * x: 1,
148
+ * y: "foo",
149
+ * });
150
+ * if (!badPoint.ok) console.error(formatTypeError(badPoint.error));
151
+ * ```
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
  */
173
+ import { utf8ToBytes } from "@noble/ciphers/utils.js";
174
+ import { sha256 } from "@noble/hashes/sha2.js";
72
175
  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";
176
+ import { wordlist } from "@scure/bip39/wordlists/english.js";
177
+ import { pack } from "msgpackr";
76
178
  import { isPlainObject } from "./Object.js";
77
- import { err, ok, trySync } from "./Result.js";
179
+ import { err, getOrThrow, ok, trySync } from "./Result.js";
78
180
  import { safelyStringifyUnknownValue } from "./String.js";
79
181
  const EvoluTypeSymbol = Symbol("evolu.Type");
80
182
  /**
@@ -88,6 +190,7 @@ const createType = (name, definition) => ({
88
190
  name,
89
191
  is: (value) => definition.fromUnknown(value).ok,
90
192
  from: definition.fromUnknown,
193
+ orThrow: (value) => getOrThrow(definition.fromUnknown(value)),
91
194
  [EvoluTypeSymbol]: true,
92
195
  Type: undefined,
93
196
  Input: undefined,
@@ -117,12 +220,6 @@ export const createTypeErrorFormatter = (format) => (error) => format({ ...error
117
220
  * Base {@link Type}.
118
221
  *
119
222
  * 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
223
  *
127
224
  * ### Example
128
225
  *
@@ -144,9 +241,7 @@ export const createTypeErrorFormatter = (format) => (error) => format({ ...error
144
241
  */
145
242
  export const base = (name, fromUnknown) => createType(name, {
146
243
  fromUnknown,
147
- to: identity,
148
244
  fromParent: (ok), // `fromParent` relies on types, so it can't fail for the Base Type
149
- toParent: identity,
150
245
  });
151
246
  /**
152
247
  * Creates a formatter function for a base {@link TypeError}.
@@ -280,9 +375,7 @@ export function brand(name, parent, refine) {
280
375
  return {
281
376
  ...createType("Brand", {
282
377
  fromUnknown,
283
- to: identity,
284
378
  fromParent: (refine ?? ok),
285
- toParent: identity,
286
379
  }),
287
380
  brand: name,
288
381
  parentType: parent,
@@ -312,37 +405,39 @@ export const formatCurrencyCodeError = createTypeErrorFormatter((error) => `Inva
312
405
  * ### Example
313
406
  *
314
407
  * ```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
408
+ * const result = DateIso.from("2023-01-01T12:00:00.000Z"); // ok
409
+ * const error = DateIso.from("10000-01-01T00:00:00.000Z"); // err
317
410
  * ```
318
411
  *
319
412
  * @category String
320
413
  */
321
- export const DateIsoString = brand("DateIso", String, (value) => {
414
+ export const DateIso = brand("DateIso", String, (value) => {
322
415
  if (value.length !== 24) {
323
- return err({ type: "DateIsoString", value });
416
+ return err({ type: "DateIso", value });
417
+ }
418
+ const parsed = globalThis.Date.parse(value);
419
+ if (isNaN(parsed)) {
420
+ return err({ type: "DateIso", value });
324
421
  }
325
- if (isNaN(globalThis.Date.parse(value))) {
326
- return err({ type: "DateIsoString", value });
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 });
327
426
  }
328
427
  return ok(value);
329
428
  });
330
- 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);
331
432
  /**
332
433
  * Trimmed string.
333
434
  *
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.
435
+ * This Type Factory validates whether a string has no leading or trailing
436
+ * whitespaces.
337
437
  *
338
- * ### Examples
438
+ * ### Example
339
439
  *
340
440
  * ```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
441
  * const TrimmedNonEmptyString = trimmed(minLength(1)(String));
347
442
  * // string & Brand<"MinLength1"> & Brand<"Trimmed">
348
443
  * type TrimmedNonEmptyString = typeof TrimmedNonEmptyString.Type;
@@ -354,32 +449,6 @@ export const trimmed = (parent) => brand("Trimmed", parent, (value) => value.tri
354
449
  ? ok(value)
355
450
  : err({ type: "Trimmed", value }));
356
451
  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);
383
452
  /**
384
453
  * Trimmed string
385
454
  *
@@ -389,6 +458,7 @@ export const trim = (parent) => transform(parent, trimmed(parent), (value) => ok
389
458
  * @category String
390
459
  */
391
460
  export const TrimmedString = trimmed(String);
461
+ export const trim = (value) => value.trim();
392
462
  /**
393
463
  * Minimum length.
394
464
  *
@@ -494,9 +564,9 @@ export const regex = (name, pattern) => {
494
564
  };
495
565
  export const formatRegexError = createTypeErrorFormatter((error) => `Value ${error.value} does not match the pattern for ${error.name}: ${error.pattern}`);
496
566
  /**
497
- * URL-safe Base64 string.
567
+ * URL-safe string.
498
568
  *
499
- * A `Base64Url` string uses a limited alphabet that is URL-safe:
569
+ * A `UrlSafeString` uses a limited alphabet that is safe for URLs:
500
570
  *
501
571
  * - Uppercase letters (`A-Z`)
502
572
  * - Lowercase letters (`a-z`)
@@ -504,29 +574,89 @@ export const formatRegexError = createTypeErrorFormatter((error) => `Value ${err
504
574
  * - Dash (`-`)
505
575
  * - Underscore (`_`)
506
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
+ *
507
580
  * ### Example
508
581
  *
509
582
  * ```ts
510
- * const result = Base64Url.from("abc123_-");
583
+ * const result = UrlSafeString.from("abc123_-");
511
584
  * if (result.ok) {
512
- * console.log("Valid Base64Url string:", result.value);
585
+ * console.log("Valid URL-safe string:", result.value);
513
586
  * } else {
514
- * console.error("Invalid Base64Url string:", result.error);
587
+ * console.error("Invalid URL-safe string:", result.error);
515
588
  * }
516
589
  * ```
517
590
  *
518
591
  * @category String
519
592
  */
520
- export const Base64Url = regex("Base64Url", /^[A-Za-z0-9_-]+$/)(String);
593
+ export const UrlSafeString = regex("UrlSafeString", /^[A-Za-z0-9_-]+$/)(String);
521
594
  /**
522
- * Simple alphanumeric string for naming.
595
+ * Base64Url without padding.
523
596
  *
524
- * A `SimpleName` string uses a limited, safe alphabet for naming purposes:
597
+ * Encode with {@link uint8ArrayToBase64Url}, decode with
598
+ * {@link base64UrlToUint8Array}.
525
599
  *
526
- * - Uppercase letters (`A-Z`)
527
- * - Lowercase letters (`a-z`)
528
- * - Digits (`0-9`)
529
- * - Dash (`-`)
600
+ * @category String
601
+ */
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
+ };
655
+ /**
656
+ * Simple alphanumeric string for naming in file systems, URLs, and identifiers.
657
+ *
658
+ * Uses the same safe alphabet as {@link UrlSafeString} (letters, digits, `-`,
659
+ * `_`). See `UrlSafeString` for details.
530
660
  *
531
661
  * The string must be between 1 and 42 characters.
532
662
  *
@@ -543,13 +673,9 @@ export const Base64Url = regex("Base64Url", /^[A-Za-z0-9_-]+$/)(String);
543
673
  *
544
674
  * @category String
545
675
  */
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);
676
+ export const SimpleName = brand("SimpleName", UrlSafeString, (value) => value.length >= 1 && value.length <= 42
677
+ ? ok(value)
678
+ : err({ type: "SimpleName", value }));
553
679
  /**
554
680
  * Trimmed string between 8 and 64 characters, branded as `SimplePassword`.
555
681
  *
@@ -558,15 +684,36 @@ export const NanoId = regex("NanoId", /^[A-Za-z0-9_-]{21}$/)(String);
558
684
  export const SimplePassword = brand("SimplePassword", minLength(8)(maxLength(64)(TrimmedString)));
559
685
  export const formatSimplePasswordError = (formatTypeError) => createTypeErrorFormatter((error) => `Invalid password: ${formatTypeError(error.parentError)}`);
560
686
  /**
561
- * `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:
562
696
  *
563
- * Represents a unique identifier with exactly 21 characters, using NanoID's
564
- * 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.
565
710
  *
566
711
  * @category String
567
712
  */
568
- export const Id = regex("Id", /^[A-Za-z0-9_-]{21}$/)(String);
569
- 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.`);
570
717
  /**
571
718
  * Creates an {@link Id}.
572
719
  *
@@ -575,11 +722,53 @@ export const idTypeValueLength = 21;
575
722
  * ```ts
576
723
  * // string & Brand<"Id">
577
724
  * const id = createId(deps);
725
+ *
726
+ * // string & Brand<"Id"> & Brand<"Todo">
727
+ * const todoId = createId<"Todo">(deps);
578
728
  * ```
579
729
  */
580
- export const createId = (deps) => deps.nanoIdLib.nanoid();
730
+ export const createId = (deps) => uint8ArrayToBase64Url(deps.randomBytes.create(16));
731
+ /**
732
+ * Creates an {@link Id} from a string using SHA-256.
733
+ *
734
+ * When integrating with external systems that use different ID formats, use
735
+ * this function to convert external IDs into valid Evolu IDs.
736
+ *
737
+ * In Evolu's CRDT, the ID serves as the unique identifier for conflict
738
+ * resolution across distributed clients. When multiple clients create records
739
+ * with the same external identifier, they must resolve to the same Evolu ID to
740
+ * ensure data consistency.
741
+ *
742
+ * ### Example
743
+ *
744
+ * ```ts
745
+ * // Both clients will generate the same ID
746
+ * const id1 = createIdFromString("user-api-123");
747
+ * const id2 = createIdFromString("user-api-123");
748
+ * console.log(id1 === id2); // true
749
+ *
750
+ * upsert("todo", {
751
+ * id: createIdFromString("external-todo-456"),
752
+ * title: "Synced from external system",
753
+ * });
754
+ * ```
755
+ *
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.
759
+ *
760
+ * @category String
761
+ */
762
+ export const createIdFromString = (value) => {
763
+ const hash = sha256(utf8ToBytes(value));
764
+ // Take first 16 bytes of hash and convert to Id
765
+ const id = idBytesToId(hash.slice(0, 16));
766
+ return id;
767
+ };
581
768
  /**
582
- * 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.
583
772
  *
584
773
  * ### Example
585
774
  *
@@ -592,30 +781,34 @@ export const createId = (deps) => deps.nanoIdLib.nanoid();
592
781
  * @category String
593
782
  */
594
783
  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
784
  const fromUnknown = (value) => {
603
785
  const parentResult = String.fromUnknown(value);
604
786
  if (!parentResult.ok)
605
787
  return parentResult;
606
788
  return fromParent(parentResult.value);
607
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
+ };
608
797
  return {
609
- ...createType("Id", {
610
- fromUnknown,
611
- to: (value) => value,
612
- fromParent,
613
- toParent: (value) => value,
614
- }),
798
+ ...createType("Id", { fromUnknown, fromParent }),
615
799
  table,
616
800
  };
617
801
  };
618
- 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);
619
812
  /**
620
813
  * Positive number.
621
814
  *
@@ -708,6 +901,8 @@ export const Int = int(Number);
708
901
  export const NonNegativeInt = nonNegative(Int);
709
902
  /** @category Number */
710
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);
711
906
  /** @category Number */
712
907
  export const NonPositiveInt = nonPositive(Int);
713
908
  /** @category Number */
@@ -802,8 +997,6 @@ export const between = (min, max) => (parent) => brand(`Between${min}-${max}`, p
802
997
  ? ok(value)
803
998
  : err({ type: "Between", value, min, max }));
804
999
  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
1000
  /**
808
1001
  * Literal {@link Type}.
809
1002
  *
@@ -828,90 +1021,12 @@ export const literal = (expected) => {
828
1021
  return {
829
1022
  ...createType("Literal", {
830
1023
  fromUnknown,
831
- to: identity,
832
1024
  fromParent: ok,
833
- toParent: identity,
834
1025
  }),
835
1026
  expected,
836
1027
  };
837
1028
  };
838
1029
  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
1030
  /**
916
1031
  * Array of a specific {@link Type}.
917
1032
  *
@@ -954,7 +1069,6 @@ export const array = (element) => {
954
1069
  }
955
1070
  return ok(result);
956
1071
  };
957
- const to = (value) => value.map(element.to);
958
1072
  const fromParent = (value) => {
959
1073
  const result = [];
960
1074
  for (let i = 0; i < value.length; i++) {
@@ -974,14 +1088,8 @@ export const array = (element) => {
974
1088
  }
975
1089
  return ok(result);
976
1090
  };
977
- const toParent = (values) => values.map(element.toParent);
978
1091
  return {
979
- ...createType("Array", {
980
- fromUnknown,
981
- to,
982
- fromParent,
983
- toParent,
984
- }),
1092
+ ...createType("Array", { fromUnknown, fromParent }),
985
1093
  element,
986
1094
  };
987
1095
  };
@@ -1055,10 +1163,6 @@ export const record = (keyType, valueType) => {
1055
1163
  }
1056
1164
  return ok(result);
1057
1165
  };
1058
- const to = (value) => Object.fromEntries(Object.entries(value).map(([key, val]) => [
1059
- keyType.to(key),
1060
- valueType.to(val),
1061
- ]));
1062
1166
  const fromParent = (value) => {
1063
1167
  const result = {};
1064
1168
  for (const [rawKey, rawValue] of Object.entries(value)) {
@@ -1090,16 +1194,10 @@ export const record = (keyType, valueType) => {
1090
1194
  }
1091
1195
  return ok(result);
1092
1196
  };
1093
- const toParent = (value) => Object.fromEntries(Object.entries(value).map(([key, val]) => [
1094
- keyType.toParent(key),
1095
- valueType.toParent(val),
1096
- ]));
1097
1197
  return {
1098
1198
  ...createType("Record", {
1099
1199
  fromUnknown,
1100
- to,
1101
1200
  fromParent,
1102
- toParent,
1103
1201
  }),
1104
1202
  key: keyType,
1105
1203
  value: valueType,
@@ -1186,22 +1284,6 @@ export function object(props, record) {
1186
1284
  }
1187
1285
  return ok(result);
1188
1286
  };
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
1287
  const fromParent = (value) => {
1206
1288
  const errors = {};
1207
1289
  const result = {};
@@ -1249,28 +1331,10 @@ export function object(props, record) {
1249
1331
  }
1250
1332
  return ok(result);
1251
1333
  };
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
1334
  return {
1269
1335
  ...createType(record ? "ObjectWithRecord" : "Object", {
1270
1336
  fromUnknown,
1271
- to,
1272
1337
  fromParent,
1273
- toParent,
1274
1338
  }),
1275
1339
  props,
1276
1340
  ...(record ? { record } : {}),
@@ -1323,7 +1387,6 @@ export function union(...args) {
1323
1387
  * - Generate a specialized function to improve validation performance for such
1324
1388
  * cases.
1325
1389
  */
1326
- /* eslint-disable */
1327
1390
  const members = args.map((arg) => (isType(arg) ? arg : literal(arg)));
1328
1391
  const fromUnknown = (value) => {
1329
1392
  const errors = [];
@@ -1339,23 +1402,13 @@ export function union(...args) {
1339
1402
  errors,
1340
1403
  });
1341
1404
  };
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
1405
  return {
1350
1406
  ...createType("Union", {
1351
1407
  fromUnknown,
1352
- to,
1353
1408
  fromParent: fromUnknown,
1354
- toParent: to,
1355
1409
  }),
1356
1410
  members,
1357
1411
  };
1358
- /* eslint-enable */
1359
1412
  }
1360
1413
  export const formatUnionError = (formatTypeError) => createTypeErrorFormatter((error) => {
1361
1414
  const formattedErrors = error.errors
@@ -1411,20 +1464,10 @@ export const recursive = (create) => {
1411
1464
  type ??= create();
1412
1465
  return type.fromUnknown(value);
1413
1466
  },
1414
- to: (value) => {
1415
- type ??= create();
1416
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
1417
- return type.to(value);
1418
- },
1419
1467
  fromParent: (value) => {
1420
1468
  type ??= create();
1421
1469
  return type.fromParent(value);
1422
1470
  },
1423
- toParent: (value) => {
1424
- type ??= create();
1425
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
1426
- return type.toParent(value);
1427
- },
1428
1471
  is: (value) => {
1429
1472
  type ??= create();
1430
1473
  return type.is(value);
@@ -1528,7 +1571,6 @@ export const tuple = (...elements) => {
1528
1571
  }
1529
1572
  return ok(result);
1530
1573
  };
1531
- const to = (value) => value.map((val, index) => elements[index].to(val));
1532
1574
  const fromParent = (value) => {
1533
1575
  if (!Array.isArray(value) || value.length !== elements.length) {
1534
1576
  return err({
@@ -1555,13 +1597,10 @@ export const tuple = (...elements) => {
1555
1597
  }
1556
1598
  return ok(result);
1557
1599
  };
1558
- const toParent = (value) => value.map((val, index) => elements[index].toParent(val));
1559
1600
  return {
1560
1601
  ...createType("Tuple", {
1561
1602
  fromUnknown,
1562
- to,
1563
1603
  fromParent,
1564
- toParent,
1565
1604
  }),
1566
1605
  elements,
1567
1606
  };
@@ -1595,22 +1634,32 @@ export const Int64 = brand("Int64", BigInt, (value) => value >= -922337203685477
1595
1634
  ? ok(value)
1596
1635
  : err({ type: "Int64", value }));
1597
1636
  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.`);
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
+ // );
1600
1653
  /**
1601
1654
  * Stringified {@link Int64}.
1602
1655
  *
1603
- * @category Number
1656
+ * @category String
1604
1657
  */
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
- });
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 })));
1614
1663
  export const formatInt64StringError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid Int64 string.`);
1615
1664
  /**
1616
1665
  * JSON-compatible value: string, {@link FiniteNumber}, boolean, null,
@@ -1631,48 +1680,83 @@ export const JsonArray = array(JsonValue);
1631
1680
  * @category Object
1632
1681
  */
1633
1682
  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`,
1683
+ export const parseJson = (value) => trySync(() => JSON.parse(value), (error) => ({
1684
+ type: `Json`,
1649
1685
  value,
1650
1686
  message: globalThis.String(error),
1651
- })), (value) => JSON.stringify(value));
1652
- export const formatJsonValueFromStringError = createTypeErrorFormatter((error) => `Invalid JSONValue: ${error.value}. Error: ${error.message}`);
1687
+ }));
1653
1688
  /**
1654
1689
  * JSON-string {@link Type}.
1655
1690
  *
1656
1691
  * ### Example
1657
1692
  *
1658
1693
  * ```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" })
1694
+ * const result = Json.from('{"key":"value"}'); // ok
1695
+ * const error = Json.from("invalid json"); // err
1661
1696
  * ```
1662
1697
  *
1663
1698
  * @category String
1664
1699
  */
1665
1700
  export const Json = brand("Json", String, (value) => {
1666
- const result = JsonValueFromString.fromParent(value);
1701
+ const result = parseJson(value);
1667
1702
  if (!result.ok)
1668
- return err({
1669
- type: "Json",
1670
- value,
1671
- message: result.error.message,
1672
- });
1703
+ return result;
1673
1704
  return ok(value);
1674
1705
  });
1675
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
+ };
1676
1760
  /**
1677
1761
  * Optional {@link Type}.
1678
1762
  *
@@ -1697,9 +1781,7 @@ export const formatJsonError = createTypeErrorFormatter((error) => `Invalid JSON
1697
1781
  export const optional = (type) => ({
1698
1782
  ...createType("Optional", {
1699
1783
  fromUnknown: type.fromUnknown,
1700
- to: type.to,
1701
1784
  fromParent: type.fromParent,
1702
- toParent: type.toParent,
1703
1785
  }),
1704
1786
  parent: type,
1705
1787
  });
@@ -1708,7 +1790,7 @@ export const isOptionalType = (x) => typeof x === "object" && x != null && "name
1708
1790
  /**
1709
1791
  * Creates a partial object type where all properties are optional.
1710
1792
  *
1711
- * 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
1712
1794
  * are required, but if they are present they must conform to their
1713
1795
  * corresponding Types.
1714
1796
  *
@@ -1779,114 +1861,86 @@ export function omit(objectType, ...keys) {
1779
1861
  }
1780
1862
  return object(newProps);
1781
1863
  }
1864
+ export const maxMutationSize = 655360;
1782
1865
  /**
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
- * ```
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.
1810
1870
  */
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
- };
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.`);
1825
1875
  /**
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.
1876
+ * Formats Evolu Type errors into user-friendly messages.
1829
1877
  *
1830
- * If you prefer not to reuse any built-in error formatters, you can write your
1831
- * own `formatTypeError` function from scratch.
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.
1832
1881
  *
1833
- * ### Examples
1882
+ * The `createFormatTypeError` function handles both built-in {@link TypeErrors}
1883
+ * and custom errors, and lets us override default formatting for specific
1884
+ * errors.
1885
+ *
1886
+ * ### Example
1834
1887
  *
1835
1888
  * ```ts
1836
- * const formatError = createFormatTypeError();
1837
- * console.log(formatError({ type: "String", value: 42 }));
1838
- * // "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
+ * });
1839
1899
  * ```
1840
1900
  *
1841
- * A custom `formatTypeError` function:
1901
+ * Alternatively, write a custom formatter from scratch without using
1902
+ * `createFormatTypeError`. This gives us full control over error formatting:
1842
1903
  *
1843
1904
  * ```ts
1844
- * type AppErrors =
1845
- * | 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 =
1846
1912
  * | StringError
1847
- * | MinLengthError
1848
1913
  * | MaxLengthError
1849
- * | NullError
1850
- * | IdError
1914
+ * | MinLengthError
1851
1915
  * | 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.
1916
+ * | PositiveError
1917
+ * | NonNegativeError
1918
+ * | IntError
1919
+ * | NumberError
1920
+ * | ObjectError<Record<string, PersonErrors>>;
1921
+ *
1922
+ * const formatTypeError: TypeErrorFormatter<PersonErrors> = (error) => {
1861
1923
  * 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
1924
  * case "String":
1873
- * // We can reuse existing formatter.
1874
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`;
1875
1932
  * 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(", ")}`;
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";
1884
1940
  * 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.";
1941
+ * if (error.reason.kind === "NotObject") return "Must be an object";
1942
+ * if (error.reason.kind === "ExtraKeys")
1943
+ * return "Contains unexpected fields";
1890
1944
  * const firstError = Object.values(error.reason.errors).find(
1891
1945
  * (e) => e !== undefined,
1892
1946
  * )!;
@@ -1927,8 +1981,8 @@ export const createFormatTypeError = (extraFormatter) => {
1927
1981
  return formatIsTypeError(error);
1928
1982
  case "CurrencyCode":
1929
1983
  return formatCurrencyCodeError(error);
1930
- case "DateIsoString":
1931
- return formatDateIsoStringError(error);
1984
+ case "DateIso":
1985
+ return formatDateIsoError(error);
1932
1986
  case "Trimmed":
1933
1987
  return formatTrimmedError(error);
1934
1988
  case "MinLength":
@@ -1943,6 +1997,8 @@ export const createFormatTypeError = (extraFormatter) => {
1943
1997
  return formatRegexError(error);
1944
1998
  case "Id":
1945
1999
  return formatIdError(error);
2000
+ case "TableId":
2001
+ return formatTableIdError(error);
1946
2002
  case "Positive":
1947
2003
  return formatPositiveError(error);
1948
2004
  case "Negative":
@@ -1973,14 +2029,12 @@ export const createFormatTypeError = (extraFormatter) => {
1973
2029
  return formatLiteralError(error);
1974
2030
  case "Int64":
1975
2031
  return formatInt64Error(error);
1976
- case "BigIntFromString":
1977
- return formatBigIntFromStringError(error);
1978
2032
  case "Int64String":
1979
2033
  return formatInt64StringError(error);
1980
- case "JsonValueFromString":
1981
- return formatJsonValueFromStringError(error);
1982
2034
  case "Json":
1983
2035
  return formatJsonError(error);
2036
+ case "ValidMutationSize":
2037
+ return formatValidMutationSizeError(error);
1984
2038
  // Composite Types
1985
2039
  case "SimplePassword":
1986
2040
  return formatSimplePasswordError(formatTypeError)(error);