@evolu/common 8.9.0 → 8.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/dist/src/Bytes.d.ts +647 -0
  2. package/dist/src/Bytes.d.ts.map +1 -0
  3. package/dist/src/{Binary.js → Bytes.js} +266 -16
  4. package/dist/src/Config.d.ts +142 -0
  5. package/dist/src/Config.d.ts.map +1 -0
  6. package/dist/src/Config.js +181 -0
  7. package/dist/src/Fs.d.ts +302 -0
  8. package/dist/src/Fs.d.ts.map +1 -0
  9. package/dist/src/Fs.js +111 -0
  10. package/dist/src/Number.d.ts +50 -7
  11. package/dist/src/Number.d.ts.map +1 -1
  12. package/dist/src/Number.js +47 -8
  13. package/dist/src/Object.d.ts +32 -0
  14. package/dist/src/Object.d.ts.map +1 -1
  15. package/dist/src/Object.js +41 -0
  16. package/dist/src/Sqlite.js +1 -1
  17. package/dist/src/Time.d.ts +74 -12
  18. package/dist/src/Time.d.ts.map +1 -1
  19. package/dist/src/Time.js +40 -2
  20. package/dist/src/Type.d.ts +1451 -79
  21. package/dist/src/Type.d.ts.map +1 -1
  22. package/dist/src/Type.js +1303 -96
  23. package/dist/src/index.d.ts +3 -1
  24. package/dist/src/index.d.ts.map +1 -1
  25. package/dist/src/index.js +3 -1
  26. package/dist/src/intl/_en.d.ts +24 -1
  27. package/dist/src/intl/_en.d.ts.map +1 -1
  28. package/dist/src/intl/_en.js +20 -0
  29. package/dist/src/intl/ar.d.ts +24 -1
  30. package/dist/src/intl/ar.d.ts.map +1 -1
  31. package/dist/src/intl/ar.js +20 -0
  32. package/dist/src/intl/bn.d.ts +24 -1
  33. package/dist/src/intl/bn.d.ts.map +1 -1
  34. package/dist/src/intl/bn.js +20 -0
  35. package/dist/src/intl/ca.d.ts +24 -1
  36. package/dist/src/intl/ca.d.ts.map +1 -1
  37. package/dist/src/intl/ca.js +20 -0
  38. package/dist/src/intl/cs.d.ts +24 -1
  39. package/dist/src/intl/cs.d.ts.map +1 -1
  40. package/dist/src/intl/cs.js +20 -0
  41. package/dist/src/intl/da.d.ts +24 -1
  42. package/dist/src/intl/da.d.ts.map +1 -1
  43. package/dist/src/intl/da.js +20 -0
  44. package/dist/src/intl/de.d.ts +24 -1
  45. package/dist/src/intl/de.d.ts.map +1 -1
  46. package/dist/src/intl/de.js +20 -0
  47. package/dist/src/intl/el.d.ts +24 -1
  48. package/dist/src/intl/el.d.ts.map +1 -1
  49. package/dist/src/intl/el.js +20 -0
  50. package/dist/src/intl/es.d.ts +24 -1
  51. package/dist/src/intl/es.d.ts.map +1 -1
  52. package/dist/src/intl/es.js +20 -0
  53. package/dist/src/intl/fa.d.ts +24 -1
  54. package/dist/src/intl/fa.d.ts.map +1 -1
  55. package/dist/src/intl/fa.js +20 -0
  56. package/dist/src/intl/fi.d.ts +24 -1
  57. package/dist/src/intl/fi.d.ts.map +1 -1
  58. package/dist/src/intl/fi.js +20 -0
  59. package/dist/src/intl/fil.d.ts +24 -1
  60. package/dist/src/intl/fil.d.ts.map +1 -1
  61. package/dist/src/intl/fil.js +20 -0
  62. package/dist/src/intl/fr.d.ts +24 -1
  63. package/dist/src/intl/fr.d.ts.map +1 -1
  64. package/dist/src/intl/fr.js +20 -0
  65. package/dist/src/intl/he.d.ts +24 -1
  66. package/dist/src/intl/he.d.ts.map +1 -1
  67. package/dist/src/intl/he.js +20 -0
  68. package/dist/src/intl/hi.d.ts +24 -1
  69. package/dist/src/intl/hi.d.ts.map +1 -1
  70. package/dist/src/intl/hi.js +20 -0
  71. package/dist/src/intl/hr.d.ts +24 -1
  72. package/dist/src/intl/hr.d.ts.map +1 -1
  73. package/dist/src/intl/hr.js +20 -0
  74. package/dist/src/intl/hu.d.ts +22 -1
  75. package/dist/src/intl/hu.d.ts.map +1 -1
  76. package/dist/src/intl/hu.js +18 -0
  77. package/dist/src/intl/id.d.ts +24 -1
  78. package/dist/src/intl/id.d.ts.map +1 -1
  79. package/dist/src/intl/id.js +20 -0
  80. package/dist/src/intl/it.d.ts +24 -1
  81. package/dist/src/intl/it.d.ts.map +1 -1
  82. package/dist/src/intl/it.js +20 -0
  83. package/dist/src/intl/ja.d.ts +24 -1
  84. package/dist/src/intl/ja.d.ts.map +1 -1
  85. package/dist/src/intl/ja.js +20 -0
  86. package/dist/src/intl/ko.d.ts +24 -1
  87. package/dist/src/intl/ko.d.ts.map +1 -1
  88. package/dist/src/intl/ko.js +20 -0
  89. package/dist/src/intl/ml.d.ts +24 -1
  90. package/dist/src/intl/ml.d.ts.map +1 -1
  91. package/dist/src/intl/ml.js +20 -0
  92. package/dist/src/intl/mr.d.ts +24 -1
  93. package/dist/src/intl/mr.d.ts.map +1 -1
  94. package/dist/src/intl/mr.js +20 -0
  95. package/dist/src/intl/ms.d.ts +24 -1
  96. package/dist/src/intl/ms.d.ts.map +1 -1
  97. package/dist/src/intl/ms.js +20 -0
  98. package/dist/src/intl/nb.d.ts +22 -1
  99. package/dist/src/intl/nb.d.ts.map +1 -1
  100. package/dist/src/intl/nb.js +18 -0
  101. package/dist/src/intl/nl.d.ts +24 -1
  102. package/dist/src/intl/nl.d.ts.map +1 -1
  103. package/dist/src/intl/nl.js +20 -0
  104. package/dist/src/intl/pa.d.ts +24 -1
  105. package/dist/src/intl/pa.d.ts.map +1 -1
  106. package/dist/src/intl/pa.js +20 -0
  107. package/dist/src/intl/pl.d.ts +23 -0
  108. package/dist/src/intl/pl.d.ts.map +1 -1
  109. package/dist/src/intl/pl.js +20 -0
  110. package/dist/src/intl/pt-BR.d.ts +24 -1
  111. package/dist/src/intl/pt-BR.d.ts.map +1 -1
  112. package/dist/src/intl/pt-BR.js +20 -0
  113. package/dist/src/intl/pt.d.ts +24 -1
  114. package/dist/src/intl/pt.d.ts.map +1 -1
  115. package/dist/src/intl/pt.js +20 -0
  116. package/dist/src/intl/ro.d.ts +24 -1
  117. package/dist/src/intl/ro.d.ts.map +1 -1
  118. package/dist/src/intl/ro.js +20 -0
  119. package/dist/src/intl/sk.d.ts +24 -1
  120. package/dist/src/intl/sk.d.ts.map +1 -1
  121. package/dist/src/intl/sk.js +20 -0
  122. package/dist/src/intl/sl.d.ts +24 -1
  123. package/dist/src/intl/sl.d.ts.map +1 -1
  124. package/dist/src/intl/sl.js +20 -0
  125. package/dist/src/intl/sv.d.ts +24 -1
  126. package/dist/src/intl/sv.d.ts.map +1 -1
  127. package/dist/src/intl/sv.js +20 -0
  128. package/dist/src/intl/sw.d.ts +21 -0
  129. package/dist/src/intl/sw.d.ts.map +1 -1
  130. package/dist/src/intl/sw.js +18 -0
  131. package/dist/src/intl/ta.d.ts +24 -1
  132. package/dist/src/intl/ta.d.ts.map +1 -1
  133. package/dist/src/intl/ta.js +20 -0
  134. package/dist/src/intl/te.d.ts +24 -1
  135. package/dist/src/intl/te.d.ts.map +1 -1
  136. package/dist/src/intl/te.js +20 -0
  137. package/dist/src/intl/th.d.ts +24 -1
  138. package/dist/src/intl/th.d.ts.map +1 -1
  139. package/dist/src/intl/th.js +20 -0
  140. package/dist/src/intl/tr.d.ts +24 -1
  141. package/dist/src/intl/tr.d.ts.map +1 -1
  142. package/dist/src/intl/tr.js +20 -0
  143. package/dist/src/intl/uk.d.ts +80 -57
  144. package/dist/src/intl/uk.d.ts.map +1 -1
  145. package/dist/src/intl/uk.js +174 -149
  146. package/dist/src/intl/ur.d.ts +24 -1
  147. package/dist/src/intl/ur.d.ts.map +1 -1
  148. package/dist/src/intl/ur.js +20 -0
  149. package/dist/src/intl/vi.d.ts +24 -1
  150. package/dist/src/intl/vi.d.ts.map +1 -1
  151. package/dist/src/intl/vi.js +20 -0
  152. package/dist/src/intl/zh-CN.d.ts +24 -1
  153. package/dist/src/intl/zh-CN.d.ts.map +1 -1
  154. package/dist/src/intl/zh-CN.js +20 -0
  155. package/dist/src/intl/zh-TW.d.ts +24 -1
  156. package/dist/src/intl/zh-TW.d.ts.map +1 -1
  157. package/dist/src/intl/zh-TW.js +20 -0
  158. package/dist/src/local-first/Protocol.d.ts +1 -1
  159. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  160. package/dist/src/local-first/Protocol.js +1 -1
  161. package/dist/src/local-first/Schema.d.ts +10 -0
  162. package/dist/src/local-first/Schema.d.ts.map +1 -1
  163. package/dist/src/local-first/Storage.js +1 -1
  164. package/dist/src/local-first/Timestamp.js +1 -1
  165. package/package.json +1 -1
  166. package/src/{Binary.test.ts → Bytes.test.ts} +286 -1
  167. package/src/{Binary.ts → Bytes.ts} +652 -21
  168. package/src/Config.test.ts +672 -0
  169. package/src/Config.ts +410 -0
  170. package/src/Fs.test.ts +105 -0
  171. package/src/Fs.ts +414 -0
  172. package/src/Number.test.ts +82 -18
  173. package/src/Number.ts +76 -8
  174. package/src/Object.test.ts +115 -1
  175. package/src/Object.ts +44 -0
  176. package/src/Sqlite.ts +1 -1
  177. package/src/Time.test.ts +82 -11
  178. package/src/Time.ts +141 -16
  179. package/src/Type.test.ts +3114 -157
  180. package/src/Type.ts +2553 -196
  181. package/src/index.ts +3 -1
  182. package/src/intl/_en.ts +70 -0
  183. package/src/intl/ar.ts +71 -0
  184. package/src/intl/bn.ts +70 -0
  185. package/src/intl/ca.ts +70 -0
  186. package/src/intl/cs.ts +70 -0
  187. package/src/intl/da.ts +70 -0
  188. package/src/intl/de.ts +70 -0
  189. package/src/intl/el.ts +70 -0
  190. package/src/intl/es.ts +70 -0
  191. package/src/intl/fa.ts +70 -0
  192. package/src/intl/fi.ts +70 -0
  193. package/src/intl/fil.ts +70 -0
  194. package/src/intl/fr.ts +70 -0
  195. package/src/intl/he.ts +70 -0
  196. package/src/intl/hi.ts +70 -0
  197. package/src/intl/hr.ts +70 -0
  198. package/src/intl/hu.ts +69 -0
  199. package/src/intl/id.ts +70 -0
  200. package/src/intl/intl.test.ts +819 -1
  201. package/src/intl/it.ts +70 -0
  202. package/src/intl/ja.ts +70 -0
  203. package/src/intl/ko.ts +68 -0
  204. package/src/intl/ml.ts +70 -0
  205. package/src/intl/mr.ts +70 -0
  206. package/src/intl/ms.ts +71 -0
  207. package/src/intl/nb.ts +69 -0
  208. package/src/intl/nl.ts +70 -0
  209. package/src/intl/pa.ts +70 -0
  210. package/src/intl/pl.ts +63 -0
  211. package/src/intl/pt-BR.ts +70 -0
  212. package/src/intl/pt.ts +71 -0
  213. package/src/intl/ro.ts +70 -0
  214. package/src/intl/sk.ts +71 -0
  215. package/src/intl/sl.ts +70 -0
  216. package/src/intl/sv.ts +70 -0
  217. package/src/intl/sw.ts +62 -0
  218. package/src/intl/ta.ts +70 -0
  219. package/src/intl/te.ts +70 -0
  220. package/src/intl/th.ts +68 -0
  221. package/src/intl/tr.ts +70 -0
  222. package/src/intl/uk.ts +228 -155
  223. package/src/intl/ur.ts +70 -0
  224. package/src/intl/vi.ts +70 -0
  225. package/src/intl/zh-CN.ts +68 -0
  226. package/src/intl/zh-TW.ts +68 -0
  227. package/src/local-first/Protocol.test.ts +1 -1
  228. package/src/local-first/Protocol.ts +1 -1
  229. package/src/local-first/Schema.ts +11 -0
  230. package/src/local-first/Storage.ts +1 -1
  231. package/src/local-first/Timestamp.ts +1 -1
  232. package/dist/src/Binary.d.ts +0 -254
  233. package/dist/src/Binary.d.ts.map +0 -1
@@ -1,4 +1,5 @@
1
- import { test } from "node:test";
1
+ import nodeAssert from "node:assert/strict";
2
+ import { describe, it, test } from "node:test";
2
3
  import { assertEqual, assertFalse, assertSame, assertTrue } from "./Assert.ts";
3
4
 
4
5
  import type { Brand } from "./Brand.ts";
@@ -8,6 +9,7 @@ import {
8
9
  createMutableRecord,
9
10
  emptyRecord,
10
11
  excludeProp,
12
+ filterObjectKeys,
11
13
  getObjectKind,
12
14
  getOwnProp,
13
15
  isFunction,
@@ -205,6 +207,118 @@ test("mapObject", () => {
205
207
  assertEqual(mapped, { u1: "u1:10", u2: "u2:20" });
206
208
  });
207
209
 
210
+ describe("filterObjectKeys", () => {
211
+ it("preserves field types and makes selected fields optional", () => {
212
+ const symbol = Symbol("ignored");
213
+ const source = {
214
+ APP_PORT: "4000",
215
+ other: 1,
216
+ 42: true,
217
+ [symbol]: "ignored",
218
+ };
219
+ const selected = filterObjectKeys(source, (key) => {
220
+ assertType<typeof key, string>();
221
+ return key.startsWith("APP_") || key === "42";
222
+ });
223
+ assertType<
224
+ typeof selected,
225
+ {
226
+ readonly APP_PORT?: string;
227
+ readonly other?: number;
228
+ readonly 42?: boolean;
229
+ }
230
+ >();
231
+ assertEqual(selected, { APP_PORT: "4000", 42: true });
232
+ assertFalse(selected === source);
233
+ assertEqual(
234
+ filterObjectKeys(source, () => false),
235
+ {},
236
+ );
237
+
238
+ type UserId = string & Brand<"UserId">;
239
+ const users: ReadonlyRecord<UserId, number> = { ["u1" as UserId]: 1 };
240
+ const selectedUsers = filterObjectKeys(users, () => true);
241
+ assertType<
242
+ typeof selectedUsers,
243
+ Readonly<Partial<Record<UserId, number>>>
244
+ >();
245
+ assertEqual(selectedUsers, { u1: 1 });
246
+
247
+ const reject = () => {
248
+ // @ts-expect-error filterObjectKeys requires an object source.
249
+ filterObjectKeys("text", () => true);
250
+ // @ts-expect-error Selected properties are readonly.
251
+ selected.APP_PORT = "5000";
252
+ };
253
+ assertType<typeof reject, () => void>();
254
+ });
255
+
256
+ it("preserves descriptors without reading getters", () => {
257
+ let reads = 0;
258
+ const getter = () => {
259
+ reads++;
260
+ return "value";
261
+ };
262
+ const source = Object.defineProperties(Object.create({ inherited: 1 }), {
263
+ visible: { value: { nested: true }, enumerable: true, writable: true },
264
+ hidden: { value: undefined },
265
+ accessor: { get: getter, enumerable: true, configurable: true },
266
+ excluded: { get: getter },
267
+ [Symbol("ignored")]: { get: getter },
268
+ });
269
+ const visited: Array<string> = [];
270
+ const selected = filterObjectKeys(source, (key) => {
271
+ visited.push(key);
272
+ return key !== "excluded";
273
+ });
274
+ assertEqual(visited, ["visible", "hidden", "accessor", "excluded"]);
275
+ assertEqual(Object.getOwnPropertyNames(selected), [
276
+ "visible",
277
+ "hidden",
278
+ "accessor",
279
+ ]);
280
+ for (const key of Object.getOwnPropertyNames(selected)) {
281
+ nodeAssert.deepEqual(
282
+ Object.getOwnPropertyDescriptor(selected, key),
283
+ Object.getOwnPropertyDescriptor(source, key),
284
+ );
285
+ }
286
+ assertEqual(reads, 0);
287
+ assertFalse(Object.hasOwn(selected, "inherited"));
288
+ assertEqual(Object.getOwnPropertySymbols(selected), []);
289
+ assertSame(Object.getPrototypeOf(selected), Object.prototype);
290
+ assertSame(
291
+ Reflect.get(selected, "visible"),
292
+ Reflect.get(source, "visible"),
293
+ );
294
+ assertSame(Reflect.get(selected, "accessor"), "value");
295
+ assertEqual(reads, 1);
296
+ });
297
+
298
+ it("safely copies special keys and handles arrays as objects", () => {
299
+ const source = createMutableRecord();
300
+ source.__proto__ = "value";
301
+ Object.defineProperty(source, "constructor", {
302
+ value: "constructor",
303
+ enumerable: true,
304
+ });
305
+ const selected = filterObjectKeys(source, () => true);
306
+ assertTrue(Object.hasOwn(selected, "__proto__"));
307
+ assertSame(Object.getPrototypeOf(selected), Object.prototype);
308
+ assertEqual(selected, {
309
+ ["__proto__"]: "value",
310
+ constructor: "constructor",
311
+ });
312
+
313
+ const values: ReadonlyArray<number> = [10, 20];
314
+ const array = filterObjectKeys(values, (key) => key === "0");
315
+ assertFalse(Array.isArray(array));
316
+ assertEqual(array, { 0: 10 });
317
+ assertType<(typeof array)[0], number | undefined>();
318
+ assertType<typeof array.map, ReadonlyArray<number>["map"] | undefined>();
319
+ });
320
+ });
321
+
208
322
  test("excludeProp", () => {
209
323
  const obj = { a: 1, b: 2, c: 3 };
210
324
 
package/src/Object.ts CHANGED
@@ -243,6 +243,50 @@ export const mapObject = <K extends string, V, U>(
243
243
  return out;
244
244
  };
245
245
 
246
+ /**
247
+ * Selects own string-keyed properties while preserving their descriptors.
248
+ *
249
+ * Visits enumerable and non-enumerable properties, ignoring inherited and
250
+ * symbol properties. The predicate receives only the key; property values and
251
+ * getters are never read. The result is a new ordinary object with matching
252
+ * descriptors, including accessor functions and property flags. Declared
253
+ * properties are optional because the predicate may exclude any of them.
254
+ * Numeric property names are visited as strings.
255
+ *
256
+ * ### Example
257
+ *
258
+ * ```ts
259
+ * import {
260
+ * assertEqual,
261
+ * assertType,
262
+ * filterObjectKeys,
263
+ * } from "@evolu/common";
264
+ *
265
+ * const source = { APP_PORT: "4000", HOME: "/home/evolu" };
266
+ * const selected = filterObjectKeys(source, (key) =>
267
+ * key.startsWith("APP_"),
268
+ * );
269
+ *
270
+ * assertEqual(selected, { APP_PORT: "4000" });
271
+ * assertType<
272
+ * typeof selected,
273
+ * { readonly APP_PORT?: string; readonly HOME?: string }
274
+ * >();
275
+ * ```
276
+ */
277
+ export const filterObjectKeys = <T extends object>(
278
+ source: T,
279
+ predicate: (key: string) => boolean,
280
+ ): Readonly<Partial<Pick<T, Exclude<keyof T, symbol>>>> => {
281
+ const selected: Partial<Pick<T, Exclude<keyof T, symbol>>> = {};
282
+ for (const key of Object.getOwnPropertyNames(source)) {
283
+ if (!predicate(key)) continue;
284
+ const descriptor = Object.getOwnPropertyDescriptor(source, key)!;
285
+ Object.defineProperty(selected, key, descriptor);
286
+ }
287
+ return selected;
288
+ };
289
+
246
290
  /** Conditionally excludes a property from an object. */
247
291
  export const excludeProp = <T extends object, K extends keyof T>(
248
292
  obj: T,
package/src/Sqlite.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { Brand } from "./Brand.ts";
8
- import { bytesToHex, hexToBytes } from "./Binary.ts";
8
+ import { bytesToHex, hexToBytes } from "./Bytes.ts";
9
9
  import type { EncryptionKey } from "./Crypto.ts";
10
10
  import type { Eq } from "./Eq.ts";
11
11
  import { createEqObject, eqString, eqUint8Array } from "./Eq.ts";
package/src/Time.test.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { afterEach, describe, it, mock } from "node:test";
2
2
  import {
3
3
  assertEqual,
4
+ assertErr,
5
+ assertOk,
4
6
  assertFalse,
5
7
  assertLength,
6
8
  assertSame,
@@ -37,19 +39,21 @@ import {
37
39
  saturateMillis,
38
40
  testCreateTime,
39
41
  } from "./Time.ts";
40
- import { assertType, type DateIso, NonNaNNumber } from "./Type.ts";
42
+ import {
43
+ localizeTypes,
44
+ object,
45
+ typeErrorToIssues,
46
+ type InferErrors,
47
+ assertType,
48
+ type DateIso,
49
+ NonNaNNumber,
50
+ } from "./Type.ts";
41
51
 
42
52
  const negativeMillisCause = {
43
53
  type: "NonNegative",
44
54
  value: -1,
45
55
  };
46
56
 
47
- const assertThrowsWithCause = (run: () => unknown, cause: unknown): void => {
48
- const error = assertThrowsInstanceOf(run, Error);
49
- assertEqual(error.message, "getOrThrow");
50
- assertEqual(error.cause, cause);
51
- };
52
-
53
57
  describe("Time", () => {
54
58
  afterEach(() => {
55
59
  mock.restoreAll();
@@ -218,14 +222,17 @@ describe("Time", () => {
218
222
  );
219
223
  mock.method(Date, "now", () => -1);
220
224
 
221
- assertThrowsWithCause(
225
+ const error = assertThrowsInstanceOf(
222
226
  () =>
223
227
  createTime().setTimeout(
224
228
  () => undefined,
225
229
  PositiveMillis.orThrow(2 ** 31),
226
230
  ),
227
- negativeMillisCause,
231
+ Error,
228
232
  );
233
+
234
+ assertEqual(error.message, "The value -1 must be non-negative (>= 0).");
235
+ assertEqual(error.cause, negativeMillisCause);
229
236
  assertEqual(setTimeout.mock.callCount(), 0);
230
237
  });
231
238
 
@@ -247,7 +254,10 @@ describe("Time", () => {
247
254
  );
248
255
  now = -1;
249
256
 
250
- assertThrowsWithCause(() => callbacks[0](), negativeMillisCause);
257
+ const error = assertThrowsInstanceOf(() => callbacks[0](), Error);
258
+
259
+ assertEqual(error.message, "The value -1 must be non-negative (>= 0).");
260
+ assertEqual(error.cause, negativeMillisCause);
251
261
  assertLength(callbacks, 1);
252
262
  });
253
263
 
@@ -445,7 +455,16 @@ describe("Time", () => {
445
455
  it("setTimeout rejects a deadline after maxMillis", () => {
446
456
  const time = testCreateTime({ startAt: maxMillis });
447
457
 
448
- assertThrowsWithCause(() => time.setTimeout(() => undefined, "1ms"), {
458
+ const error = assertThrowsInstanceOf(
459
+ () => time.setTimeout(() => undefined, "1ms"),
460
+ Error,
461
+ );
462
+
463
+ assertEqual(
464
+ error.message,
465
+ "The value 281474976710655 must be less than 281474976710655.",
466
+ );
467
+ assertEqual(error.cause, {
449
468
  type: "LessThan281474976710655",
450
469
  value: maxMillis + 1,
451
470
  max: maxMillis + 1,
@@ -727,6 +746,58 @@ describe("Time", () => {
727
746
  });
728
747
 
729
748
  describe("DurationLiteral", () => {
749
+ it("reports a dedicated error with the original validation failure", () => {
750
+ for (const value of ["60s", "", 1, null, undefined]) {
751
+ const result = DurationLiteral.fromUnknown(value);
752
+ assertErr(result);
753
+ assertType<typeof result.error.type, "DurationLiteral">();
754
+ assertSame(result.error.value, value);
755
+ assertEqual(result.error.cause.type, "Union");
756
+ assertEqual(result.error.cause.errors.length, 1);
757
+ }
758
+ const result = DurationLiteral.fromUnknown("60s", { errors: "all" });
759
+ assertErr(result);
760
+ assertEqual(result.error.cause.errors.length, 7);
761
+ assertEqual(
762
+ DurationLiteral.formatError(result.error),
763
+ 'The value "60s" is not a duration literal. Use a value such as "500ms" or "1.5s".',
764
+ );
765
+ assertType<
766
+ InferErrors<typeof DurationLiteral>["type"],
767
+ "DurationLiteral"
768
+ >();
769
+ assertOk(DurationLiteral.from("1.5s"), "1.5s");
770
+ assertEqual(DurationLiteral.to("1.5s"), "1.5s");
771
+ // @ts-expect-error DurationLiteral Input rejects "60s".
772
+ const _invalidInput: typeof DurationLiteral.Input = "60s";
773
+ });
774
+
775
+ it("localizes the named error after composition and preserves its path", async () => {
776
+ const { first } = localizeTypes(
777
+ { Value: DurationLiteral },
778
+ { first: { DurationLiteral: () => "First message." } },
779
+ );
780
+ const { second } = localizeTypes(
781
+ { Settings: object({ value: first.Value }) },
782
+ {
783
+ second: {
784
+ Object: () => "Object.",
785
+ DurationLiteral: () => "Second message.",
786
+ },
787
+ },
788
+ );
789
+ const result = second.Settings.fromUnknown({ value: "60s" });
790
+ assertErr(result);
791
+ assertEqual(second.Settings.formatError(result.error), "Second message.");
792
+ assertEqual(typeErrorToIssues(second.Settings, result.error), [
793
+ { path: ["value"], message: "Second message." },
794
+ ]);
795
+ assertEqual(
796
+ await second.Settings["~standard"].validate({ value: "60s" }),
797
+ { issues: [{ path: ["value"], message: "Second message." }] },
798
+ );
799
+ });
800
+
730
801
  it("valid durations", () => {
731
802
  // Milliseconds
732
803
  assertType<"1ms" extends DurationLiteral ? true : false, true>();
package/src/Time.ts CHANGED
@@ -1,15 +1,31 @@
1
1
  /**
2
2
  * Time representations, durations, and scheduling utilities.
3
3
  *
4
+ * Durations follow a pattern that other quantities in Evolu repeat, such as
5
+ * sizes in Bytes.ts:
6
+ *
7
+ * - {@link Millis} is the canonical unit, a validated number of milliseconds.
8
+ * - {@link DurationLiteral} is the human-readable form, such as `"1.5s"`,
9
+ * validated at compile time and runtime.
10
+ * - {@link Duration} is what APIs accept: `DurationLiteral | Millis`.
11
+ * - {@link durationToMillis} normalizes a `Duration` to `Millis`.
12
+ *
4
13
  * @module
5
14
  */
6
15
 
16
+ import { safelyStringifyUnknownValue } from "./String.ts";
7
17
  import { assert } from "./Assert.ts";
8
18
  import type { Brand } from "./Brand.ts";
9
19
  import { exhaustiveCheck } from "./Function.ts";
10
20
  import type { yieldNow } from "./Task.ts";
11
21
  import {
12
22
  brand,
23
+ createTypeWithError,
24
+ type Type,
25
+ type TypeError,
26
+ type UnionError,
27
+ type UnionType,
28
+ type TemplateLiteralType,
13
29
  type DateIso,
14
30
  Digit,
15
31
  Digit1To23,
@@ -418,8 +434,18 @@ export type Duration = DurationLiteral | Millis;
418
434
  */
419
435
  export type PositiveDuration = DurationLiteral | PositiveMillis;
420
436
 
437
+ // Keep these annotations concrete. Generic unit wrappers add thousands of
438
+ // compiler instantiations to pnpm bench:type.
421
439
  /** Milliseconds duration: `"1ms"` to `"999ms"`. See {@link DurationLiteral}. */
422
- export const DurationLiteralMilliseconds = /*#__PURE__*/ union(
440
+ export const DurationLiteralMilliseconds: UnionType<
441
+ readonly [
442
+ TemplateLiteralType<readonly [typeof Digit1To9, "ms"]>,
443
+ TemplateLiteralType<readonly [typeof Digit1To9, typeof Digit, "ms"]>,
444
+ TemplateLiteralType<
445
+ readonly [typeof Digit1To9, typeof Digit, typeof Digit, "ms"]
446
+ >,
447
+ ]
448
+ > = /*#__PURE__*/ union(
423
449
  /*#__PURE__*/ templateLiteral(Digit1To9, "ms"),
424
450
  /*#__PURE__*/ templateLiteral(Digit1To9, Digit, "ms"),
425
451
  /*#__PURE__*/ templateLiteral(Digit1To9, Digit, Digit, "ms"),
@@ -431,7 +457,14 @@ export type DurationLiteralMilliseconds =
431
457
  * Seconds duration: `"1s"` to `"59s"` or `"1.1s"` to `"59.9s"`. See
432
458
  * {@link DurationLiteral}.
433
459
  */
434
- export const DurationLiteralSeconds = /*#__PURE__*/ union(
460
+ export const DurationLiteralSeconds: UnionType<
461
+ readonly [
462
+ TemplateLiteralType<readonly [typeof Digit1To59, "s"]>,
463
+ TemplateLiteralType<
464
+ readonly [typeof Digit1To59, ".", typeof Digit1To9, "s"]
465
+ >,
466
+ ]
467
+ > = /*#__PURE__*/ union(
435
468
  /*#__PURE__*/ templateLiteral(Digit1To59, "s"),
436
469
  /*#__PURE__*/ templateLiteral(Digit1To59, ".", Digit1To9, "s"),
437
470
  );
@@ -441,7 +474,14 @@ export type DurationLiteralSeconds = typeof DurationLiteralSeconds.Output;
441
474
  * Minutes duration: `"1m"` to `"59m"` or `"1.1m"` to `"59.9m"`. See
442
475
  * {@link DurationLiteral}.
443
476
  */
444
- export const DurationLiteralMinutes = /*#__PURE__*/ union(
477
+ export const DurationLiteralMinutes: UnionType<
478
+ readonly [
479
+ TemplateLiteralType<readonly [typeof Digit1To59, "m"]>,
480
+ TemplateLiteralType<
481
+ readonly [typeof Digit1To59, ".", typeof Digit1To9, "m"]
482
+ >,
483
+ ]
484
+ > = /*#__PURE__*/ union(
445
485
  /*#__PURE__*/ templateLiteral(Digit1To59, "m"),
446
486
  /*#__PURE__*/ templateLiteral(Digit1To59, ".", Digit1To9, "m"),
447
487
  );
@@ -451,7 +491,14 @@ export type DurationLiteralMinutes = typeof DurationLiteralMinutes.Output;
451
491
  * Hours duration: `"1h"` to `"23h"` or `"1.1h"` to `"23.9h"`. See
452
492
  * {@link DurationLiteral}.
453
493
  */
454
- export const DurationLiteralHours = /*#__PURE__*/ union(
494
+ export const DurationLiteralHours: UnionType<
495
+ readonly [
496
+ TemplateLiteralType<readonly [typeof Digit1To23, "h"]>,
497
+ TemplateLiteralType<
498
+ readonly [typeof Digit1To23, ".", typeof Digit1To9, "h"]
499
+ >,
500
+ ]
501
+ > = /*#__PURE__*/ union(
455
502
  /*#__PURE__*/ templateLiteral(Digit1To23, "h"),
456
503
  /*#__PURE__*/ templateLiteral(Digit1To23, ".", Digit1To9, "h"),
457
504
  );
@@ -461,7 +508,14 @@ export type DurationLiteralHours = typeof DurationLiteralHours.Output;
461
508
  * Days duration: `"1d"` to `"6d"` or `"1.1d"` to `"6.9d"`. See
462
509
  * {@link DurationLiteral}.
463
510
  */
464
- export const DurationLiteralDays = /*#__PURE__*/ union(
511
+ export const DurationLiteralDays: UnionType<
512
+ readonly [
513
+ TemplateLiteralType<readonly [typeof Digit1To6, "d"]>,
514
+ TemplateLiteralType<
515
+ readonly [typeof Digit1To6, ".", typeof Digit1To9, "d"]
516
+ >,
517
+ ]
518
+ > = /*#__PURE__*/ union(
465
519
  /*#__PURE__*/ templateLiteral(Digit1To6, "d"),
466
520
  /*#__PURE__*/ templateLiteral(Digit1To6, ".", Digit1To9, "d"),
467
521
  );
@@ -471,7 +525,14 @@ export type DurationLiteralDays = typeof DurationLiteralDays.Output;
471
525
  * Weeks duration: `"1w"` to `"51w"` or `"1.1w"` to `"51.9w"`. See
472
526
  * {@link DurationLiteral}.
473
527
  */
474
- export const DurationLiteralWeeks = /*#__PURE__*/ union(
528
+ export const DurationLiteralWeeks: UnionType<
529
+ readonly [
530
+ TemplateLiteralType<readonly [typeof Digit1To51, "w"]>,
531
+ TemplateLiteralType<
532
+ readonly [typeof Digit1To51, ".", typeof Digit1To9, "w"]
533
+ >,
534
+ ]
535
+ > = /*#__PURE__*/ union(
475
536
  /*#__PURE__*/ templateLiteral(Digit1To51, "w"),
476
537
  /*#__PURE__*/ templateLiteral(Digit1To51, ".", Digit1To9, "w"),
477
538
  );
@@ -481,12 +542,38 @@ export type DurationLiteralWeeks = typeof DurationLiteralWeeks.Output;
481
542
  * Years duration: `"1y"` to `"99y"` or `"1.1y"` to `"99.9y"`. See
482
543
  * {@link DurationLiteral}.
483
544
  */
484
- export const DurationLiteralYears = /*#__PURE__*/ union(
545
+ export const DurationLiteralYears: UnionType<
546
+ readonly [
547
+ TemplateLiteralType<readonly [typeof Digit1To99, "y"]>,
548
+ TemplateLiteralType<
549
+ readonly [typeof Digit1To99, ".", typeof Digit1To9, "y"]
550
+ >,
551
+ ]
552
+ > = /*#__PURE__*/ union(
485
553
  /*#__PURE__*/ templateLiteral(Digit1To99, "y"),
486
554
  /*#__PURE__*/ templateLiteral(Digit1To99, ".", Digit1To9, "y"),
487
555
  );
488
556
  export type DurationLiteralYears = typeof DurationLiteralYears.Output;
489
557
 
558
+ export type DurationLiteral =
559
+ | DurationLiteralMilliseconds
560
+ | DurationLiteralSeconds
561
+ | DurationLiteralMinutes
562
+ | DurationLiteralHours
563
+ | DurationLiteralDays
564
+ | DurationLiteralWeeks
565
+ | DurationLiteralYears;
566
+
567
+ const durationLiteralSyntax = /*#__PURE__*/ union(
568
+ DurationLiteralMilliseconds,
569
+ DurationLiteralSeconds,
570
+ DurationLiteralMinutes,
571
+ DurationLiteralHours,
572
+ DurationLiteralDays,
573
+ DurationLiteralWeeks,
574
+ DurationLiteralYears,
575
+ );
576
+
490
577
  /**
491
578
  * Duration literal Type with compile-time and runtime validation.
492
579
  *
@@ -514,17 +601,55 @@ export type DurationLiteralYears = typeof DurationLiteralYears.Output;
514
601
  *
515
602
  * See {@link Duration} for a type that also accepts {@link Millis}. Use
516
603
  * {@link durationToMillis} to convert to milliseconds.
604
+ *
605
+ * Invalid values produce a {@link DurationLiteralError}.
606
+ *
607
+ * ### Example
608
+ *
609
+ * ```ts
610
+ * import {
611
+ * assertFalse,
612
+ * assertOk,
613
+ * assertType,
614
+ * DurationLiteral,
615
+ * } from "@evolu/common";
616
+ *
617
+ * // The TypeScript type accepts valid spellings and rejects the rest.
618
+ * const literal: DurationLiteral = "1.5s";
619
+ * assertType<Extract<DurationLiteral, "1000ms" | "60s" | "0s">, never>();
620
+ *
621
+ * // The runtime Type validates the same grammar.
622
+ * assertOk(DurationLiteral.fromUnknown(literal), "1.5s");
623
+ * assertFalse(DurationLiteral.is("1000ms"));
624
+ * ```
517
625
  */
518
- export const DurationLiteral = /*#__PURE__*/ union(
519
- DurationLiteralMilliseconds,
520
- DurationLiteralSeconds,
521
- DurationLiteralMinutes,
522
- DurationLiteralHours,
523
- DurationLiteralDays,
524
- DurationLiteralWeeks,
525
- DurationLiteralYears,
626
+ export const DurationLiteral: Type<
627
+ "DurationLiteral",
628
+ DurationLiteral,
629
+ DurationLiteral,
630
+ DurationLiteralError
631
+ > = /*#__PURE__*/ createTypeWithError(
632
+ "DurationLiteral",
633
+ durationLiteralSyntax,
634
+ (cause, value): DurationLiteralError => ({
635
+ type: "DurationLiteral",
636
+ value,
637
+ cause,
638
+ }),
639
+ (error) =>
640
+ `The value ${safelyStringifyUnknownValue(error.value)} is not a duration literal. Use a value such as "500ms" or "1.5s".`,
526
641
  );
527
- export type DurationLiteral = typeof DurationLiteral.Output;
642
+
643
+ /** Error returned when {@link DurationLiteral} rejects a value. */
644
+ export interface DurationLiteralError extends TypeError<"DurationLiteral"> {
645
+ readonly value: unknown;
646
+ /**
647
+ * The underlying union failure, retained for diagnostics.
648
+ *
649
+ * With `{ errors: "all" }`, includes every failed alternative.
650
+ */
651
+ readonly cause: UnionError;
652
+ }
528
653
 
529
654
  /**
530
655
  * Converts a duration to milliseconds.