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

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 +159 -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 +209 -124
  35. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  36. package/dist/src/Evolu/Owner.js +118 -104
  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 +256 -211
  40. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  41. package/dist/src/Evolu/Protocol.js +558 -359
  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 +72 -4
  49. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  50. package/dist/src/Evolu/Relay.js +124 -13
  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 +68 -13
  58. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  59. package/dist/src/Evolu/Sync.js +414 -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 +176 -51
  74. package/dist/src/Result.d.ts.map +1 -1
  75. package/dist/src/Result.js +30 -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 +474 -327
  86. package/dist/src/Type.d.ts.map +1 -1
  87. package/dist/src/Type.js +502 -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 +528 -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 +282 -227
  112. package/src/Evolu/Platform.ts +9 -9
  113. package/src/Evolu/Protocol.ts +788 -640
  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 +238 -18
  118. package/src/Evolu/Schema.ts +391 -191
  119. package/src/Evolu/Storage.ts +275 -55
  120. package/src/Evolu/Sync.ts +708 -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 +183 -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 +708 -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,48 @@ 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:
696
+ *
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.
562
706
  *
563
- * Represents a unique identifier with exactly 21 characters, using NanoID's
564
- * standard format (`A-Za-z0-9_-`).
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.
710
+ *
711
+ * ### Future Consideration
712
+ *
713
+ * For database-heavy workloads where insert performance is critical, a hybrid
714
+ * approach could be considered: `timestamp ^ H(cluster_id, timestamp >> N)`
715
+ * where H is a keyed hash function and N is a configurable parameter. This
716
+ * would maintain spatial locality for database caches (improving insert
717
+ * performance by an order of magnitude) while adding entropy to prevent
718
+ * timestamp leakage and correlation across systems. The parameter N would allow
719
+ * trading off cache locality (larger N = better locality) versus entropy
720
+ * distribution. See https://brooker.co.za/blog/2025/10/22/uuidv7.html for
721
+ * details on this approach.
565
722
  *
566
723
  * @category String
567
724
  */
568
- export const Id = regex("Id", /^[A-Za-z0-9_-]{21}$/)(String);
569
- export const idTypeValueLength = 21;
725
+ export const Id = brand("Id", String, (value) => value.length === 22 && Base64Url.fromParent(value).ok
726
+ ? ok(value)
727
+ : err({ type: "Id", value }));
728
+ export const formatIdError = createTypeErrorFormatter((error) => `Value ${error.value} is not a valid Id.`);
570
729
  /**
571
730
  * Creates an {@link Id}.
572
731
  *
@@ -575,11 +734,53 @@ export const idTypeValueLength = 21;
575
734
  * ```ts
576
735
  * // string & Brand<"Id">
577
736
  * const id = createId(deps);
737
+ *
738
+ * // string & Brand<"Id"> & Brand<"Todo">
739
+ * const todoId = createId<"Todo">(deps);
578
740
  * ```
579
741
  */
580
- export const createId = (deps) => deps.nanoIdLib.nanoid();
742
+ export const createId = (deps) => uint8ArrayToBase64Url(deps.randomBytes.create(16));
581
743
  /**
582
- * Type Factory to create branded {@link Id} Type for a specific table.
744
+ * Creates an {@link Id} from a string using SHA-256.
745
+ *
746
+ * When integrating with external systems that use different ID formats, use
747
+ * this function to convert external IDs into valid Evolu IDs.
748
+ *
749
+ * In Evolu's CRDT, the ID serves as the unique identifier for conflict
750
+ * resolution across distributed clients. When multiple clients create records
751
+ * with the same external identifier, they must resolve to the same Evolu ID to
752
+ * ensure data consistency.
753
+ *
754
+ * ### Example
755
+ *
756
+ * ```ts
757
+ * // Both clients will generate the same ID
758
+ * const id1 = createIdFromString("user-api-123");
759
+ * const id2 = createIdFromString("user-api-123");
760
+ * console.log(id1 === id2); // true
761
+ *
762
+ * upsert("todo", {
763
+ * id: createIdFromString("external-todo-456"),
764
+ * title: "Synced from external system",
765
+ * });
766
+ * ```
767
+ *
768
+ * **Important**: This transformation is one-way. We cannot recover the original
769
+ * external string from the generated {@link Id}. If we need to preserve the
770
+ * original external ID, store it in a separate column.
771
+ *
772
+ * @category String
773
+ */
774
+ export const createIdFromString = (value) => {
775
+ const hash = sha256(utf8ToBytes(value));
776
+ // Take first 16 bytes of hash and convert to Id
777
+ const id = idBytesToId(hash.slice(0, 16));
778
+ return id;
779
+ };
780
+ /**
781
+ * Creates a branded {@link Id} Type for a table's primary key.
782
+ *
783
+ * The table name becomes an additional brand for type safety.
583
784
  *
584
785
  * ### Example
585
786
  *
@@ -592,30 +793,34 @@ export const createId = (deps) => deps.nanoIdLib.nanoid();
592
793
  * @category String
593
794
  */
594
795
  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
796
  const fromUnknown = (value) => {
603
797
  const parentResult = String.fromUnknown(value);
604
798
  if (!parentResult.ok)
605
799
  return parentResult;
606
800
  return fromParent(parentResult.value);
607
801
  };
802
+ const fromParent = (value) => {
803
+ const idResult = Id.fromParent(value);
804
+ if (!idResult.ok) {
805
+ return err({ type: "TableId", value, table });
806
+ }
807
+ return ok(idResult.value);
808
+ };
608
809
  return {
609
- ...createType("Id", {
610
- fromUnknown,
611
- to: (value) => value,
612
- fromParent,
613
- toParent: (value) => value,
614
- }),
810
+ ...createType("Id", { fromUnknown, fromParent }),
615
811
  table,
616
812
  };
617
813
  };
618
- export const formatIdError = createTypeErrorFormatter((error) => `Invalid ${error.type} table Id: ${error.value}`);
814
+ export const formatTableIdError = createTypeErrorFormatter((error) => `Invalid ${error.type} table Id: ${error.value}`);
815
+ /** Binary representation of an {@link Id}. */
816
+ export const IdBytes = brand("IdBytes", length(16)(Uint8Array));
817
+ export const idBytesTypeValueLength = 16;
818
+ export const idToIdBytes = (id) =>
819
+ // Id is Base64Url (validated by Id.from), cast is safe
820
+ base64UrlToUint8Array(id);
821
+ export const idBytesToId = (idBytes) =>
822
+ // Base64Url encoding of 16 bytes always produces valid Id (22 chars)
823
+ uint8ArrayToBase64Url(idBytes);
619
824
  /**
620
825
  * Positive number.
621
826
  *
@@ -708,6 +913,8 @@ export const Int = int(Number);
708
913
  export const NonNegativeInt = nonNegative(Int);
709
914
  /** @category Number */
710
915
  export const PositiveInt = positive(NonNegativeInt);
916
+ /** Maximum safe positive integer value for practically infinite operations. */
917
+ export const maxPositiveInt = PositiveInt.orThrow(globalThis.Number.MAX_SAFE_INTEGER);
711
918
  /** @category Number */
712
919
  export const NonPositiveInt = nonPositive(Int);
713
920
  /** @category Number */
@@ -802,8 +1009,6 @@ export const between = (min, max) => (parent) => brand(`Between${min}-${max}`, p
802
1009
  ? ok(value)
803
1010
  : err({ type: "Between", value, min, max }));
804
1011
  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
1012
  /**
808
1013
  * Literal {@link Type}.
809
1014
  *
@@ -828,90 +1033,12 @@ export const literal = (expected) => {
828
1033
  return {
829
1034
  ...createType("Literal", {
830
1035
  fromUnknown,
831
- to: identity,
832
1036
  fromParent: ok,
833
- toParent: identity,
834
1037
  }),
835
1038
  expected,
836
1039
  };
837
1040
  };
838
1041
  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
1042
  /**
916
1043
  * Array of a specific {@link Type}.
917
1044
  *
@@ -954,7 +1081,6 @@ export const array = (element) => {
954
1081
  }
955
1082
  return ok(result);
956
1083
  };
957
- const to = (value) => value.map(element.to);
958
1084
  const fromParent = (value) => {
959
1085
  const result = [];
960
1086
  for (let i = 0; i < value.length; i++) {
@@ -974,14 +1100,8 @@ export const array = (element) => {
974
1100
  }
975
1101
  return ok(result);
976
1102
  };
977
- const toParent = (values) => values.map(element.toParent);
978
1103
  return {
979
- ...createType("Array", {
980
- fromUnknown,
981
- to,
982
- fromParent,
983
- toParent,
984
- }),
1104
+ ...createType("Array", { fromUnknown, fromParent }),
985
1105
  element,
986
1106
  };
987
1107
  };
@@ -1055,10 +1175,6 @@ export const record = (keyType, valueType) => {
1055
1175
  }
1056
1176
  return ok(result);
1057
1177
  };
1058
- const to = (value) => Object.fromEntries(Object.entries(value).map(([key, val]) => [
1059
- keyType.to(key),
1060
- valueType.to(val),
1061
- ]));
1062
1178
  const fromParent = (value) => {
1063
1179
  const result = {};
1064
1180
  for (const [rawKey, rawValue] of Object.entries(value)) {
@@ -1090,16 +1206,10 @@ export const record = (keyType, valueType) => {
1090
1206
  }
1091
1207
  return ok(result);
1092
1208
  };
1093
- const toParent = (value) => Object.fromEntries(Object.entries(value).map(([key, val]) => [
1094
- keyType.toParent(key),
1095
- valueType.toParent(val),
1096
- ]));
1097
1209
  return {
1098
1210
  ...createType("Record", {
1099
1211
  fromUnknown,
1100
- to,
1101
1212
  fromParent,
1102
- toParent,
1103
1213
  }),
1104
1214
  key: keyType,
1105
1215
  value: valueType,
@@ -1186,22 +1296,6 @@ export function object(props, record) {
1186
1296
  }
1187
1297
  return ok(result);
1188
1298
  };
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
1299
  const fromParent = (value) => {
1206
1300
  const errors = {};
1207
1301
  const result = {};
@@ -1249,28 +1343,10 @@ export function object(props, record) {
1249
1343
  }
1250
1344
  return ok(result);
1251
1345
  };
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
1346
  return {
1269
1347
  ...createType(record ? "ObjectWithRecord" : "Object", {
1270
1348
  fromUnknown,
1271
- to,
1272
1349
  fromParent,
1273
- toParent,
1274
1350
  }),
1275
1351
  props,
1276
1352
  ...(record ? { record } : {}),
@@ -1323,7 +1399,6 @@ export function union(...args) {
1323
1399
  * - Generate a specialized function to improve validation performance for such
1324
1400
  * cases.
1325
1401
  */
1326
- /* eslint-disable */
1327
1402
  const members = args.map((arg) => (isType(arg) ? arg : literal(arg)));
1328
1403
  const fromUnknown = (value) => {
1329
1404
  const errors = [];
@@ -1339,23 +1414,13 @@ export function union(...args) {
1339
1414
  errors,
1340
1415
  });
1341
1416
  };
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
1417
  return {
1350
1418
  ...createType("Union", {
1351
1419
  fromUnknown,
1352
- to,
1353
1420
  fromParent: fromUnknown,
1354
- toParent: to,
1355
1421
  }),
1356
1422
  members,
1357
1423
  };
1358
- /* eslint-enable */
1359
1424
  }
1360
1425
  export const formatUnionError = (formatTypeError) => createTypeErrorFormatter((error) => {
1361
1426
  const formattedErrors = error.errors
@@ -1411,20 +1476,10 @@ export const recursive = (create) => {
1411
1476
  type ??= create();
1412
1477
  return type.fromUnknown(value);
1413
1478
  },
1414
- to: (value) => {
1415
- type ??= create();
1416
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
1417
- return type.to(value);
1418
- },
1419
1479
  fromParent: (value) => {
1420
1480
  type ??= create();
1421
1481
  return type.fromParent(value);
1422
1482
  },
1423
- toParent: (value) => {
1424
- type ??= create();
1425
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
1426
- return type.toParent(value);
1427
- },
1428
1483
  is: (value) => {
1429
1484
  type ??= create();
1430
1485
  return type.is(value);
@@ -1528,7 +1583,6 @@ export const tuple = (...elements) => {
1528
1583
  }
1529
1584
  return ok(result);
1530
1585
  };
1531
- const to = (value) => value.map((val, index) => elements[index].to(val));
1532
1586
  const fromParent = (value) => {
1533
1587
  if (!Array.isArray(value) || value.length !== elements.length) {
1534
1588
  return err({
@@ -1555,13 +1609,10 @@ export const tuple = (...elements) => {
1555
1609
  }
1556
1610
  return ok(result);
1557
1611
  };
1558
- const toParent = (value) => value.map((val, index) => elements[index].toParent(val));
1559
1612
  return {
1560
1613
  ...createType("Tuple", {
1561
1614
  fromUnknown,
1562
- to,
1563
1615
  fromParent,
1564
- toParent,
1565
1616
  }),
1566
1617
  elements,
1567
1618
  };
@@ -1595,22 +1646,32 @@ export const Int64 = brand("Int64", BigInt, (value) => value >= -922337203685477
1595
1646
  ? ok(value)
1596
1647
  : err({ type: "Int64", value }));
1597
1648
  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.`);
1649
+ // // co s timhle? je to string, ze ktereho lze udelat bigint
1650
+ // export const BigIntFromString = transform(
1651
+ // String,
1652
+ // BigInt,
1653
+ // (value) =>
1654
+ // trySync(
1655
+ // () => globalThis.BigInt(value),
1656
+ // (): BigIntFromStringError => ({ type: "BigIntFromString", value }),
1657
+ // ),
1658
+ // (value) => value.toString(),
1659
+ // );
1660
+ // export interface BigIntFromStringError extends TypeError<"BigIntFromString"> {}
1661
+ // export const formatBigIntFromStringError =
1662
+ // createTypeErrorFormatter<BigIntFromStringError>(
1663
+ // (error) => `The value ${error.value} could not be converted to a BigInt.`,
1664
+ // );
1600
1665
  /**
1601
1666
  * Stringified {@link Int64}.
1602
1667
  *
1603
- * @category Number
1668
+ * @category String
1604
1669
  */
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
- });
1670
+ export const Int64String = brand("Int64", NonEmptyTrimmedString, (value) => trySync(() => {
1671
+ const maybeInt = globalThis.BigInt(value);
1672
+ Int64.orThrow(maybeInt);
1673
+ return value;
1674
+ }, () => ({ type: "Int64String", value })));
1614
1675
  export const formatInt64StringError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid Int64 string.`);
1615
1676
  /**
1616
1677
  * JSON-compatible value: string, {@link FiniteNumber}, boolean, null,
@@ -1631,48 +1692,83 @@ export const JsonArray = array(JsonValue);
1631
1692
  * @category Object
1632
1693
  */
1633
1694
  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`,
1695
+ export const parseJson = (value) => trySync(() => JSON.parse(value), (error) => ({
1696
+ type: `Json`,
1649
1697
  value,
1650
1698
  message: globalThis.String(error),
1651
- })), (value) => JSON.stringify(value));
1652
- export const formatJsonValueFromStringError = createTypeErrorFormatter((error) => `Invalid JSONValue: ${error.value}. Error: ${error.message}`);
1699
+ }));
1653
1700
  /**
1654
1701
  * JSON-string {@link Type}.
1655
1702
  *
1656
1703
  * ### Example
1657
1704
  *
1658
1705
  * ```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" })
1706
+ * const result = Json.from('{"key":"value"}'); // ok
1707
+ * const error = Json.from("invalid json"); // err
1661
1708
  * ```
1662
1709
  *
1663
1710
  * @category String
1664
1711
  */
1665
1712
  export const Json = brand("Json", String, (value) => {
1666
- const result = JsonValueFromString.fromParent(value);
1713
+ const result = parseJson(value);
1667
1714
  if (!result.ok)
1668
- return err({
1669
- type: "Json",
1670
- value,
1671
- message: result.error.message,
1672
- });
1715
+ return result;
1673
1716
  return ok(value);
1674
1717
  });
1675
1718
  export const formatJsonError = createTypeErrorFormatter((error) => `Invalid JSON: ${error.value}. Error: ${error.message}`);
1719
+ export const jsonValueToJson = (value) => JSON.stringify(value);
1720
+ export const jsonToJsonValue = (value) => JSON.parse(value);
1721
+ /**
1722
+ * Creates a branded JSON string {@link Type} and type-safe conversion functions
1723
+ * for a given Type.
1724
+ *
1725
+ * This factory creates:
1726
+ *
1727
+ * 1. A branded string Type that validates JSON parsing and structural conformity
1728
+ * 2. A serialization function (Type → branded JSON string)
1729
+ * 3. A parsing function (branded JSON string → Type, skipping validation)
1730
+ *
1731
+ * Optimized for Evolu's SQLite workflow where we store typed JSON strings and
1732
+ * need type-safe conversions without double parsing.
1733
+ *
1734
+ * ### Example
1735
+ *
1736
+ * ```ts
1737
+ * const Person = object({
1738
+ * name: NonEmptyString100,
1739
+ * age: FiniteNumber,
1740
+ * });
1741
+ * type Person = typeof Person.Type;
1742
+ *
1743
+ * const [PersonJson, personToPersonJson, personJsonToPerson] = json(
1744
+ * Person,
1745
+ * "PersonJson",
1746
+ * );
1747
+ * // string & Brand<"PersonJson">
1748
+ * type PersonJson = typeof PersonJson.Type;
1749
+ *
1750
+ * // Usage:
1751
+ * const person: Person = { name: "Alice", age: 30 };
1752
+ * const jsonString = personToPersonJson(person); // PersonJson
1753
+ * const backToPerson = personJsonToPerson(jsonString); // Person
1754
+ * ```
1755
+ */
1756
+ export const json = (type, name) => {
1757
+ const BrandedJsonType = brand(name, String, (value) => {
1758
+ const parseResult = parseJson(value);
1759
+ if (!parseResult.ok)
1760
+ return parseResult;
1761
+ const validationResult = type.fromUnknown(parseResult.value);
1762
+ if (!validationResult.ok)
1763
+ return validationResult;
1764
+ return ok(value);
1765
+ });
1766
+ return [
1767
+ BrandedJsonType,
1768
+ jsonValueToJson,
1769
+ jsonToJsonValue,
1770
+ ];
1771
+ };
1676
1772
  /**
1677
1773
  * Optional {@link Type}.
1678
1774
  *
@@ -1697,9 +1793,7 @@ export const formatJsonError = createTypeErrorFormatter((error) => `Invalid JSON
1697
1793
  export const optional = (type) => ({
1698
1794
  ...createType("Optional", {
1699
1795
  fromUnknown: type.fromUnknown,
1700
- to: type.to,
1701
1796
  fromParent: type.fromParent,
1702
- toParent: type.toParent,
1703
1797
  }),
1704
1798
  parent: type,
1705
1799
  });
@@ -1708,7 +1802,7 @@ export const isOptionalType = (x) => typeof x === "object" && x != null && "name
1708
1802
  /**
1709
1803
  * Creates a partial object type where all properties are optional.
1710
1804
  *
1711
- * This is useful when you want to validate an object in which none of the keys
1805
+ * This is useful when we want to validate an object in which none of the keys
1712
1806
  * are required, but if they are present they must conform to their
1713
1807
  * corresponding Types.
1714
1808
  *
@@ -1779,114 +1873,86 @@ export function omit(objectType, ...keys) {
1779
1873
  }
1780
1874
  return object(newProps);
1781
1875
  }
1876
+ export const maxMutationSize = 655360;
1782
1877
  /**
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
- * ```
1878
+ * Evolu has to limit the maximum mutation size. Otherwise, sync couldn't use
1879
+ * the `maxProtocolMessageRangesSize`. The max size is 640KB in bytes, measured
1880
+ * via MessagePack. Evolu Protocol DbChange will be smaller thanks to various
1881
+ * optimizations.
1810
1882
  */
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
- };
1883
+ export const validMutationSize = (type) => brand("ValidMutationSize", type, (value) => pack(value).byteLength <= maxMutationSize
1884
+ ? ok(value)
1885
+ : err({ type: "ValidMutationSize", value }));
1886
+ 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
1887
  /**
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.
1888
+ * Formats Evolu Type errors into user-friendly messages.
1829
1889
  *
1830
- * If you prefer not to reuse any built-in error formatters, you can write your
1831
- * own `formatTypeError` function from scratch.
1890
+ * Evolu Type typed errors ensure every error type must have a formatter.
1891
+ * TypeScript enforces this at compile-time, preventing unhandled validation
1892
+ * errors from reaching users.
1832
1893
  *
1833
- * ### Examples
1894
+ * The `createFormatTypeError` function handles both built-in {@link TypeErrors}
1895
+ * and custom errors, and lets us override default formatting for specific
1896
+ * errors.
1897
+ *
1898
+ * ### Example
1834
1899
  *
1835
1900
  * ```ts
1836
- * const formatError = createFormatTypeError();
1837
- * console.log(formatError({ type: "String", value: 42 }));
1838
- * // "A value 42 is not a string."
1901
+ * const formatTypeError = createFormatTypeError<
1902
+ * MinLengthError | MaxLengthError
1903
+ * >((error): string => {
1904
+ * switch (error.type) {
1905
+ * case "MinLength":
1906
+ * return `Text must be at least ${error.min} character${error.min === 1 ? "" : "s"} long`;
1907
+ * case "MaxLength":
1908
+ * return `Text is too long (maximum ${error.max} characters)`;
1909
+ * }
1910
+ * });
1839
1911
  * ```
1840
1912
  *
1841
- * A custom `formatTypeError` function:
1913
+ * Alternatively, write a custom formatter from scratch without using
1914
+ * `createFormatTypeError`. This gives us full control over error formatting:
1842
1915
  *
1843
1916
  * ```ts
1844
- * type AppErrors =
1845
- * | ValidMutationSizeError
1917
+ * const Person = object({
1918
+ * name: NonEmptyTrimmedString100,
1919
+ * age: optional(PositiveInt),
1920
+ * });
1921
+ *
1922
+ * // Define only the errors actually used by Person Type
1923
+ * type PersonErrors =
1846
1924
  * | StringError
1847
- * | MinLengthError
1848
1925
  * | MaxLengthError
1849
- * | NullError
1850
- * | IdError
1926
+ * | MinLengthError
1851
1927
  * | 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.
1928
+ * | PositiveError
1929
+ * | NonNegativeError
1930
+ * | IntError
1931
+ * | NumberError
1932
+ * | ObjectError<Record<string, PersonErrors>>;
1933
+ *
1934
+ * const formatTypeError: TypeErrorFormatter<PersonErrors> = (error) => {
1861
1935
  * 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
1936
  * case "String":
1873
- * // We can reuse existing formatter.
1874
1937
  * return formatStringError(error);
1938
+ * case "Number":
1939
+ * return "Must be a number";
1940
+ * case "MinLength":
1941
+ * return `Must be at least ${error.min} characters`;
1942
+ * case "MaxLength":
1943
+ * return `Cannot exceed ${error.max} characters`;
1875
1944
  * 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(", ")}`;
1945
+ * return "Cannot have leading or trailing spaces";
1946
+ * case "Positive":
1947
+ * return "Must be a positive number";
1948
+ * case "NonNegative":
1949
+ * return "Must be zero or positive";
1950
+ * case "Int":
1951
+ * return "Must be an integer";
1884
1952
  * 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.";
1953
+ * if (error.reason.kind === "NotObject") return "Must be an object";
1954
+ * if (error.reason.kind === "ExtraKeys")
1955
+ * return "Contains unexpected fields";
1890
1956
  * const firstError = Object.values(error.reason.errors).find(
1891
1957
  * (e) => e !== undefined,
1892
1958
  * )!;
@@ -1927,8 +1993,8 @@ export const createFormatTypeError = (extraFormatter) => {
1927
1993
  return formatIsTypeError(error);
1928
1994
  case "CurrencyCode":
1929
1995
  return formatCurrencyCodeError(error);
1930
- case "DateIsoString":
1931
- return formatDateIsoStringError(error);
1996
+ case "DateIso":
1997
+ return formatDateIsoError(error);
1932
1998
  case "Trimmed":
1933
1999
  return formatTrimmedError(error);
1934
2000
  case "MinLength":
@@ -1943,6 +2009,8 @@ export const createFormatTypeError = (extraFormatter) => {
1943
2009
  return formatRegexError(error);
1944
2010
  case "Id":
1945
2011
  return formatIdError(error);
2012
+ case "TableId":
2013
+ return formatTableIdError(error);
1946
2014
  case "Positive":
1947
2015
  return formatPositiveError(error);
1948
2016
  case "Negative":
@@ -1973,14 +2041,12 @@ export const createFormatTypeError = (extraFormatter) => {
1973
2041
  return formatLiteralError(error);
1974
2042
  case "Int64":
1975
2043
  return formatInt64Error(error);
1976
- case "BigIntFromString":
1977
- return formatBigIntFromStringError(error);
1978
2044
  case "Int64String":
1979
2045
  return formatInt64StringError(error);
1980
- case "JsonValueFromString":
1981
- return formatJsonValueFromStringError(error);
1982
2046
  case "Json":
1983
2047
  return formatJsonError(error);
2048
+ case "ValidMutationSize":
2049
+ return formatValidMutationSizeError(error);
1984
2050
  // Composite Types
1985
2051
  case "SimplePassword":
1986
2052
  return formatSimplePasswordError(formatTypeError)(error);