@evolu/common 8.9.0 → 8.11.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 (337) 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/Console.d.ts +62 -7
  8. package/dist/src/Console.d.ts.map +1 -1
  9. package/dist/src/Console.js +20 -4
  10. package/dist/src/Crypto.d.ts +76 -4
  11. package/dist/src/Crypto.d.ts.map +1 -1
  12. package/dist/src/Crypto.js +55 -4
  13. package/dist/src/Error.d.ts +45 -0
  14. package/dist/src/Error.d.ts.map +1 -1
  15. package/dist/src/Error.js +69 -0
  16. package/dist/src/Fs.d.ts +376 -0
  17. package/dist/src/Fs.d.ts.map +1 -0
  18. package/dist/src/Fs.js +113 -0
  19. package/dist/src/Identicon.d.ts +2 -2
  20. package/dist/src/Identicon.js +2 -2
  21. package/dist/src/LeakDetector.d.ts +22 -3
  22. package/dist/src/LeakDetector.d.ts.map +1 -1
  23. package/dist/src/LeakDetector.js +12 -2
  24. package/dist/src/LockManager.d.ts +8 -0
  25. package/dist/src/LockManager.d.ts.map +1 -1
  26. package/dist/src/LockManager.js +6 -0
  27. package/dist/src/Number.d.ts +50 -7
  28. package/dist/src/Number.d.ts.map +1 -1
  29. package/dist/src/Number.js +47 -8
  30. package/dist/src/Object.d.ts +32 -0
  31. package/dist/src/Object.d.ts.map +1 -1
  32. package/dist/src/Object.js +46 -0
  33. package/dist/src/Platform.d.ts +47 -7
  34. package/dist/src/Platform.d.ts.map +1 -1
  35. package/dist/src/Platform.js +24 -5
  36. package/dist/src/Random.d.ts +25 -2
  37. package/dist/src/Random.d.ts.map +1 -1
  38. package/dist/src/Random.js +14 -2
  39. package/dist/src/Resource.d.ts +156 -1
  40. package/dist/src/Resource.d.ts.map +1 -1
  41. package/dist/src/Resource.js +201 -72
  42. package/dist/src/Schedule.d.ts +11 -10
  43. package/dist/src/Schedule.d.ts.map +1 -1
  44. package/dist/src/Schedule.js +1 -1
  45. package/dist/src/Sqlite.d.ts +132 -16
  46. package/dist/src/Sqlite.d.ts.map +1 -1
  47. package/dist/src/Sqlite.js +64 -10
  48. package/dist/src/Task.d.ts +15 -4
  49. package/dist/src/Task.d.ts.map +1 -1
  50. package/dist/src/Task.js +41 -15
  51. package/dist/src/Test.d.ts +9 -0
  52. package/dist/src/Test.d.ts.map +1 -1
  53. package/dist/src/Test.js +4 -0
  54. package/dist/src/Time.d.ts +179 -20
  55. package/dist/src/Time.d.ts.map +1 -1
  56. package/dist/src/Time.js +95 -6
  57. package/dist/src/Type.d.ts +3056 -1539
  58. package/dist/src/Type.d.ts.map +1 -1
  59. package/dist/src/Type.js +2548 -584
  60. package/dist/src/WebSocket.d.ts +164 -13
  61. package/dist/src/WebSocket.d.ts.map +1 -1
  62. package/dist/src/WebSocket.js +133 -24
  63. package/dist/src/Worker.d.ts +90 -8
  64. package/dist/src/Worker.d.ts.map +1 -1
  65. package/dist/src/Worker.js +28 -2
  66. package/dist/src/index.d.ts +9 -8
  67. package/dist/src/index.d.ts.map +1 -1
  68. package/dist/src/index.js +5 -4
  69. package/dist/src/intl/_en.d.ts +24 -1
  70. package/dist/src/intl/_en.d.ts.map +1 -1
  71. package/dist/src/intl/_en.js +20 -0
  72. package/dist/src/intl/ar.d.ts +24 -1
  73. package/dist/src/intl/ar.d.ts.map +1 -1
  74. package/dist/src/intl/ar.js +20 -0
  75. package/dist/src/intl/bn.d.ts +24 -1
  76. package/dist/src/intl/bn.d.ts.map +1 -1
  77. package/dist/src/intl/bn.js +20 -0
  78. package/dist/src/intl/ca.d.ts +24 -1
  79. package/dist/src/intl/ca.d.ts.map +1 -1
  80. package/dist/src/intl/ca.js +20 -0
  81. package/dist/src/intl/cs.d.ts +24 -1
  82. package/dist/src/intl/cs.d.ts.map +1 -1
  83. package/dist/src/intl/cs.js +20 -0
  84. package/dist/src/intl/da.d.ts +24 -1
  85. package/dist/src/intl/da.d.ts.map +1 -1
  86. package/dist/src/intl/da.js +20 -0
  87. package/dist/src/intl/de.d.ts +24 -1
  88. package/dist/src/intl/de.d.ts.map +1 -1
  89. package/dist/src/intl/de.js +20 -0
  90. package/dist/src/intl/el.d.ts +24 -1
  91. package/dist/src/intl/el.d.ts.map +1 -1
  92. package/dist/src/intl/el.js +20 -0
  93. package/dist/src/intl/es.d.ts +24 -1
  94. package/dist/src/intl/es.d.ts.map +1 -1
  95. package/dist/src/intl/es.js +20 -0
  96. package/dist/src/intl/fa.d.ts +24 -1
  97. package/dist/src/intl/fa.d.ts.map +1 -1
  98. package/dist/src/intl/fa.js +20 -0
  99. package/dist/src/intl/fi.d.ts +24 -1
  100. package/dist/src/intl/fi.d.ts.map +1 -1
  101. package/dist/src/intl/fi.js +20 -0
  102. package/dist/src/intl/fil.d.ts +24 -1
  103. package/dist/src/intl/fil.d.ts.map +1 -1
  104. package/dist/src/intl/fil.js +20 -0
  105. package/dist/src/intl/fr.d.ts +24 -1
  106. package/dist/src/intl/fr.d.ts.map +1 -1
  107. package/dist/src/intl/fr.js +20 -0
  108. package/dist/src/intl/he.d.ts +24 -1
  109. package/dist/src/intl/he.d.ts.map +1 -1
  110. package/dist/src/intl/he.js +20 -0
  111. package/dist/src/intl/hi.d.ts +24 -1
  112. package/dist/src/intl/hi.d.ts.map +1 -1
  113. package/dist/src/intl/hi.js +20 -0
  114. package/dist/src/intl/hr.d.ts +24 -1
  115. package/dist/src/intl/hr.d.ts.map +1 -1
  116. package/dist/src/intl/hr.js +20 -0
  117. package/dist/src/intl/hu.d.ts +22 -1
  118. package/dist/src/intl/hu.d.ts.map +1 -1
  119. package/dist/src/intl/hu.js +18 -0
  120. package/dist/src/intl/id.d.ts +24 -1
  121. package/dist/src/intl/id.d.ts.map +1 -1
  122. package/dist/src/intl/id.js +20 -0
  123. package/dist/src/intl/it.d.ts +24 -1
  124. package/dist/src/intl/it.d.ts.map +1 -1
  125. package/dist/src/intl/it.js +20 -0
  126. package/dist/src/intl/ja.d.ts +24 -1
  127. package/dist/src/intl/ja.d.ts.map +1 -1
  128. package/dist/src/intl/ja.js +20 -0
  129. package/dist/src/intl/ko.d.ts +24 -1
  130. package/dist/src/intl/ko.d.ts.map +1 -1
  131. package/dist/src/intl/ko.js +20 -0
  132. package/dist/src/intl/ml.d.ts +24 -1
  133. package/dist/src/intl/ml.d.ts.map +1 -1
  134. package/dist/src/intl/ml.js +20 -0
  135. package/dist/src/intl/mr.d.ts +24 -1
  136. package/dist/src/intl/mr.d.ts.map +1 -1
  137. package/dist/src/intl/mr.js +20 -0
  138. package/dist/src/intl/ms.d.ts +24 -1
  139. package/dist/src/intl/ms.d.ts.map +1 -1
  140. package/dist/src/intl/ms.js +20 -0
  141. package/dist/src/intl/nb.d.ts +22 -1
  142. package/dist/src/intl/nb.d.ts.map +1 -1
  143. package/dist/src/intl/nb.js +18 -0
  144. package/dist/src/intl/nl.d.ts +24 -1
  145. package/dist/src/intl/nl.d.ts.map +1 -1
  146. package/dist/src/intl/nl.js +20 -0
  147. package/dist/src/intl/pa.d.ts +24 -1
  148. package/dist/src/intl/pa.d.ts.map +1 -1
  149. package/dist/src/intl/pa.js +20 -0
  150. package/dist/src/intl/pl.d.ts +23 -0
  151. package/dist/src/intl/pl.d.ts.map +1 -1
  152. package/dist/src/intl/pl.js +20 -0
  153. package/dist/src/intl/pt-BR.d.ts +24 -1
  154. package/dist/src/intl/pt-BR.d.ts.map +1 -1
  155. package/dist/src/intl/pt-BR.js +20 -0
  156. package/dist/src/intl/pt.d.ts +24 -1
  157. package/dist/src/intl/pt.d.ts.map +1 -1
  158. package/dist/src/intl/pt.js +20 -0
  159. package/dist/src/intl/ro.d.ts +24 -1
  160. package/dist/src/intl/ro.d.ts.map +1 -1
  161. package/dist/src/intl/ro.js +20 -0
  162. package/dist/src/intl/sk.d.ts +24 -1
  163. package/dist/src/intl/sk.d.ts.map +1 -1
  164. package/dist/src/intl/sk.js +20 -0
  165. package/dist/src/intl/sl.d.ts +24 -1
  166. package/dist/src/intl/sl.d.ts.map +1 -1
  167. package/dist/src/intl/sl.js +20 -0
  168. package/dist/src/intl/sv.d.ts +24 -1
  169. package/dist/src/intl/sv.d.ts.map +1 -1
  170. package/dist/src/intl/sv.js +20 -0
  171. package/dist/src/intl/sw.d.ts +21 -0
  172. package/dist/src/intl/sw.d.ts.map +1 -1
  173. package/dist/src/intl/sw.js +18 -0
  174. package/dist/src/intl/ta.d.ts +24 -1
  175. package/dist/src/intl/ta.d.ts.map +1 -1
  176. package/dist/src/intl/ta.js +20 -0
  177. package/dist/src/intl/te.d.ts +24 -1
  178. package/dist/src/intl/te.d.ts.map +1 -1
  179. package/dist/src/intl/te.js +20 -0
  180. package/dist/src/intl/th.d.ts +24 -1
  181. package/dist/src/intl/th.d.ts.map +1 -1
  182. package/dist/src/intl/th.js +20 -0
  183. package/dist/src/intl/tr.d.ts +24 -1
  184. package/dist/src/intl/tr.d.ts.map +1 -1
  185. package/dist/src/intl/tr.js +20 -0
  186. package/dist/src/intl/uk.d.ts +80 -57
  187. package/dist/src/intl/uk.d.ts.map +1 -1
  188. package/dist/src/intl/uk.js +174 -149
  189. package/dist/src/intl/ur.d.ts +24 -1
  190. package/dist/src/intl/ur.d.ts.map +1 -1
  191. package/dist/src/intl/ur.js +20 -0
  192. package/dist/src/intl/vi.d.ts +24 -1
  193. package/dist/src/intl/vi.d.ts.map +1 -1
  194. package/dist/src/intl/vi.js +20 -0
  195. package/dist/src/intl/zh-CN.d.ts +24 -1
  196. package/dist/src/intl/zh-CN.d.ts.map +1 -1
  197. package/dist/src/intl/zh-CN.js +20 -0
  198. package/dist/src/intl/zh-TW.d.ts +24 -1
  199. package/dist/src/intl/zh-TW.d.ts.map +1 -1
  200. package/dist/src/intl/zh-TW.js +20 -0
  201. package/dist/src/local-first/Db.d.ts +52 -3
  202. package/dist/src/local-first/Db.d.ts.map +1 -1
  203. package/dist/src/local-first/Db.js +412 -137
  204. package/dist/src/local-first/Evolu.d.ts +336 -211
  205. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  206. package/dist/src/local-first/Evolu.js +102 -15
  207. package/dist/src/local-first/Owner.d.ts +13 -30
  208. package/dist/src/local-first/Owner.d.ts.map +1 -1
  209. package/dist/src/local-first/Owner.js +13 -30
  210. package/dist/src/local-first/Protocol.d.ts +95 -17
  211. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  212. package/dist/src/local-first/Protocol.js +119 -39
  213. package/dist/src/local-first/Query.d.ts +8 -15
  214. package/dist/src/local-first/Query.d.ts.map +1 -1
  215. package/dist/src/local-first/Schema.d.ts +345 -21
  216. package/dist/src/local-first/Schema.d.ts.map +1 -1
  217. package/dist/src/local-first/Schema.js +214 -17
  218. package/dist/src/local-first/Shared.d.ts +537 -22
  219. package/dist/src/local-first/Shared.d.ts.map +1 -1
  220. package/dist/src/local-first/Shared.js +1437 -234
  221. package/dist/src/local-first/Storage.d.ts +192 -14
  222. package/dist/src/local-first/Storage.d.ts.map +1 -1
  223. package/dist/src/local-first/Storage.js +82 -21
  224. package/dist/src/local-first/Timestamp.d.ts +392 -41
  225. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  226. package/dist/src/local-first/Timestamp.js +404 -82
  227. package/dist/src/local-first/index.d.ts +0 -1
  228. package/dist/src/local-first/index.d.ts.map +1 -1
  229. package/dist/src/local-first/index.js +0 -1
  230. package/package.json +1 -1
  231. package/src/Assert.test.ts +2 -5
  232. package/src/{Binary.test.ts → Bytes.test.ts} +286 -1
  233. package/src/{Binary.ts → Bytes.ts} +652 -21
  234. package/src/Config.test.ts +668 -0
  235. package/src/Config.ts +410 -0
  236. package/src/Console.ts +62 -7
  237. package/src/Crypto.ts +76 -4
  238. package/src/Eq.test.ts +2 -3
  239. package/src/Error.test.ts +76 -3
  240. package/src/Error.ts +71 -0
  241. package/src/Fs.test.ts +105 -0
  242. package/src/Fs.ts +488 -0
  243. package/src/Identicon.ts +2 -2
  244. package/src/LeakDetector.ts +22 -3
  245. package/src/LockManager.ts +8 -0
  246. package/src/Number.test.ts +82 -18
  247. package/src/Number.ts +76 -8
  248. package/src/Object.test.ts +139 -10
  249. package/src/Object.ts +49 -0
  250. package/src/Platform.ts +50 -8
  251. package/src/Random.ts +25 -2
  252. package/src/Resource.test.ts +837 -0
  253. package/src/Resource.ts +235 -15
  254. package/src/Schedule.test.ts +50 -12
  255. package/src/Schedule.ts +24 -14
  256. package/src/Sqlite.ts +138 -18
  257. package/src/Task.test.ts +189 -8
  258. package/src/Task.ts +56 -17
  259. package/src/Test.ts +9 -0
  260. package/src/Time.test.ts +82 -11
  261. package/src/Time.ts +246 -24
  262. package/src/Type.test.ts +3994 -1119
  263. package/src/Type.ts +7258 -3842
  264. package/src/Types.test.ts +4 -14
  265. package/src/WebSocket.ts +313 -40
  266. package/src/Worker.ts +90 -8
  267. package/src/index.ts +18 -7
  268. package/src/intl/_en.ts +70 -0
  269. package/src/intl/ar.ts +71 -0
  270. package/src/intl/bn.ts +70 -0
  271. package/src/intl/ca.ts +70 -0
  272. package/src/intl/cs.ts +70 -0
  273. package/src/intl/da.ts +70 -0
  274. package/src/intl/de.ts +70 -0
  275. package/src/intl/el.ts +70 -0
  276. package/src/intl/es.ts +70 -0
  277. package/src/intl/fa.ts +70 -0
  278. package/src/intl/fi.ts +70 -0
  279. package/src/intl/fil.ts +70 -0
  280. package/src/intl/fr.ts +70 -0
  281. package/src/intl/he.ts +70 -0
  282. package/src/intl/hi.ts +70 -0
  283. package/src/intl/hr.ts +70 -0
  284. package/src/intl/hu.ts +69 -0
  285. package/src/intl/id.ts +70 -0
  286. package/src/intl/intl.test.ts +819 -1
  287. package/src/intl/it.ts +70 -0
  288. package/src/intl/ja.ts +70 -0
  289. package/src/intl/ko.ts +68 -0
  290. package/src/intl/ml.ts +70 -0
  291. package/src/intl/mr.ts +70 -0
  292. package/src/intl/ms.ts +71 -0
  293. package/src/intl/nb.ts +69 -0
  294. package/src/intl/nl.ts +70 -0
  295. package/src/intl/pa.ts +70 -0
  296. package/src/intl/pl.ts +63 -0
  297. package/src/intl/pt-BR.ts +70 -0
  298. package/src/intl/pt.ts +71 -0
  299. package/src/intl/ro.ts +70 -0
  300. package/src/intl/sk.ts +71 -0
  301. package/src/intl/sl.ts +70 -0
  302. package/src/intl/sv.ts +70 -0
  303. package/src/intl/sw.ts +62 -0
  304. package/src/intl/ta.ts +70 -0
  305. package/src/intl/te.ts +70 -0
  306. package/src/intl/th.ts +68 -0
  307. package/src/intl/tr.ts +70 -0
  308. package/src/intl/uk.ts +228 -155
  309. package/src/intl/ur.ts +70 -0
  310. package/src/intl/vi.ts +70 -0
  311. package/src/intl/zh-CN.ts +68 -0
  312. package/src/intl/zh-TW.ts +68 -0
  313. package/src/local-first/Db.ts +644 -339
  314. package/src/local-first/Evolu.test.ts +686 -21
  315. package/src/local-first/Evolu.ts +450 -228
  316. package/src/local-first/Owner.ts +13 -30
  317. package/src/local-first/Protocol.test.ts +618 -11
  318. package/src/local-first/Protocol.ts +197 -73
  319. package/src/local-first/Query.ts +8 -15
  320. package/src/local-first/Schema.test.ts +143 -0
  321. package/src/local-first/Schema.ts +374 -24
  322. package/src/local-first/Shared.test.ts +7731 -559
  323. package/src/local-first/Shared.ts +2036 -267
  324. package/src/local-first/Storage.ts +219 -33
  325. package/src/local-first/Timestamp.test.ts +344 -70
  326. package/src/local-first/Timestamp.ts +435 -119
  327. package/src/local-first/index.ts +0 -1
  328. package/dist/src/Binary.d.ts +0 -254
  329. package/dist/src/Binary.d.ts.map +0 -1
  330. package/dist/src/local-first/Error.d.ts +0 -12
  331. package/dist/src/local-first/Error.d.ts.map +0 -1
  332. package/dist/src/local-first/Error.js +0 -6
  333. package/dist/src/local-first/LocalAuth.d.ts +0 -150
  334. package/dist/src/local-first/LocalAuth.d.ts.map +0 -1
  335. package/dist/src/local-first/LocalAuth.js +0 -179
  336. package/src/local-first/Error.ts +0 -17
  337. package/src/local-first/LocalAuth.ts +0 -457
@@ -1,6 +1,7 @@
1
- import { test } from "node:test";
1
+ import { describe, test } from "node:test";
2
2
  import {
3
3
  assertEqual,
4
+ assertErr,
4
5
  assertFalse,
5
6
  assertOk,
6
7
  assertSame,
@@ -26,7 +27,16 @@ import {
26
27
  percentageToRatio,
27
28
  } from "./Number.ts";
28
29
  import { err, ok } from "./Result.ts";
29
- import { assertType, NonNegativeInt, PositiveInt, Ratio } from "./Type.ts";
30
+ import {
31
+ localizeTypes,
32
+ object,
33
+ typeErrorToIssues,
34
+ type InferErrors,
35
+ assertType,
36
+ NonNegativeInt,
37
+ PositiveInt,
38
+ Ratio,
39
+ } from "./Type.ts";
30
40
 
31
41
  test("bounded integer literal types", () => {
32
42
  assertType<1 extends Int1To99 ? true : false, true>();
@@ -53,22 +63,76 @@ test("bounded integer literal types", () => {
53
63
  assertType<PositiveInt extends Int1To100OrPositiveInt ? true : false, true>();
54
64
  });
55
65
 
56
- test("Percentage accepts canonical literals or Ratio", () => {
57
- assertType<"0%" extends PercentageLiteral ? true : false, true>();
58
- assertType<"25%" extends PercentageLiteral ? true : false, true>();
59
- assertType<"12.5%" extends PercentageLiteral ? true : false, true>();
60
- assertType<"100%" extends PercentageLiteral ? true : false, true>();
61
- assertType<"01%" extends PercentageLiteral ? true : false, false>();
62
- assertType<"10.0%" extends PercentageLiteral ? true : false, false>();
63
- assertType<"100.1%" extends PercentageLiteral ? true : false, false>();
64
- assertType<Ratio extends Percentage ? true : false, true>();
65
- assertTrue(PercentageLiteral.is("0%"));
66
- assertTrue(PercentageLiteral.is("25%"));
67
- assertTrue(PercentageLiteral.is("12.5%"));
68
- assertTrue(PercentageLiteral.is("100%"));
69
- assertFalse(PercentageLiteral.is("01%"));
70
- assertFalse(PercentageLiteral.is("10.0%"));
71
- assertFalse(PercentageLiteral.is("100.1%"));
66
+ describe("PercentageLiteral", () => {
67
+ test("reports a dedicated error with the original validation failure", () => {
68
+ for (const value of ["101%", "", 1, null, undefined]) {
69
+ const result = PercentageLiteral.fromUnknown(value);
70
+ assertErr(result);
71
+ assertType<typeof result.error.type, "PercentageLiteral">();
72
+ assertSame(result.error.value, value);
73
+ assertEqual(result.error.cause.type, "Union");
74
+ assertEqual(result.error.cause.errors.length, 1);
75
+ }
76
+ const result = PercentageLiteral.fromUnknown("101%", { errors: "all" });
77
+ assertErr(result);
78
+ assertEqual(result.error.cause.errors.length, 4);
79
+ assertEqual(
80
+ PercentageLiteral.formatError(result.error),
81
+ 'The value "101%" is not a percentage literal. Use a value such as "50%" or "12.5%".',
82
+ );
83
+ assertType<
84
+ InferErrors<typeof PercentageLiteral>["type"],
85
+ "PercentageLiteral"
86
+ >();
87
+ assertOk(PercentageLiteral.from("12.5%"), "12.5%");
88
+ assertEqual(PercentageLiteral.to("12.5%"), "12.5%");
89
+ // @ts-expect-error PercentageLiteral Input rejects "101%".
90
+ const _invalidInput: typeof PercentageLiteral.Input = "101%";
91
+ });
92
+
93
+ test("localizes the named error after composition and preserves its path", async () => {
94
+ const { first } = localizeTypes(
95
+ { Value: PercentageLiteral },
96
+ { first: { PercentageLiteral: () => "First message." } },
97
+ );
98
+ const { second } = localizeTypes(
99
+ { Settings: object({ value: first.Value }) },
100
+ {
101
+ second: {
102
+ Object: () => "Object.",
103
+ PercentageLiteral: () => "Second message.",
104
+ },
105
+ },
106
+ );
107
+ const result = second.Settings.fromUnknown({ value: "101%" });
108
+ assertErr(result);
109
+ assertEqual(second.Settings.formatError(result.error), "Second message.");
110
+ assertEqual(typeErrorToIssues(second.Settings, result.error), [
111
+ { path: ["value"], message: "Second message." },
112
+ ]);
113
+ assertEqual(
114
+ await second.Settings["~standard"].validate({ value: "101%" }),
115
+ { issues: [{ path: ["value"], message: "Second message." }] },
116
+ );
117
+ });
118
+
119
+ test("accepts canonical literals or Ratio", () => {
120
+ assertType<"0%" extends PercentageLiteral ? true : false, true>();
121
+ assertType<"25%" extends PercentageLiteral ? true : false, true>();
122
+ assertType<"12.5%" extends PercentageLiteral ? true : false, true>();
123
+ assertType<"100%" extends PercentageLiteral ? true : false, true>();
124
+ assertType<"01%" extends PercentageLiteral ? true : false, false>();
125
+ assertType<"10.0%" extends PercentageLiteral ? true : false, false>();
126
+ assertType<"100.1%" extends PercentageLiteral ? true : false, false>();
127
+ assertType<Ratio extends Percentage ? true : false, true>();
128
+ assertTrue(PercentageLiteral.is("0%"));
129
+ assertTrue(PercentageLiteral.is("25%"));
130
+ assertTrue(PercentageLiteral.is("12.5%"));
131
+ assertTrue(PercentageLiteral.is("100%"));
132
+ assertFalse(PercentageLiteral.is("01%"));
133
+ assertFalse(PercentageLiteral.is("10.0%"));
134
+ assertFalse(PercentageLiteral.is("100.1%"));
135
+ });
72
136
  });
73
137
 
74
138
  test("percentageToRatio converts percentage literals and preserves Ratio", () => {
package/src/Number.ts CHANGED
@@ -7,10 +7,15 @@
7
7
  import type { NonEmptyReadonlyArray } from "./Array.ts";
8
8
  import { assertNonEmptyReadonlyArray } from "./Assert.ts";
9
9
  import type { IsBranded } from "./Brand.ts";
10
+ import { safelyStringifyUnknownValue } from "./String.ts";
10
11
  import type { Result } from "./Result.ts";
11
12
  import { err, ok } from "./Result.ts";
12
13
  import {
13
14
  brand,
15
+ createTypeWithError,
16
+ type Type,
17
+ type TypeError,
18
+ type UnionError,
14
19
  Digit1To9,
15
20
  Digit1To99,
16
21
  lessThanOrEqualTo,
@@ -98,13 +103,10 @@ export type Int1To100OrPositiveInt = Int1To100 | PositiveInt;
98
103
  */
99
104
  export type Percentage = PercentageLiteral | Ratio;
100
105
 
101
- /**
102
- * Percentage literal from `"0%"` to `"100%"`.
103
- *
104
- * Decimal literals support one decimal place. Use {@link Ratio} for computed
105
- * values or greater precision.
106
- */
107
- export const PercentageLiteral = /*#__PURE__*/ union(
106
+ export type PercentageLiteral =
107
+ "0%" | "100%" | `${Digit1To99}%` | `${"0" | Digit1To99}.${Digit1To9}%`;
108
+
109
+ const percentageLiteralSyntax = /*#__PURE__*/ union(
108
110
  "0%",
109
111
  "100%",
110
112
  /*#__PURE__*/ templateLiteral(Digit1To99, "%"),
@@ -115,7 +117,73 @@ export const PercentageLiteral = /*#__PURE__*/ union(
115
117
  "%",
116
118
  ),
117
119
  );
118
- export type PercentageLiteral = typeof PercentageLiteral.Output;
120
+
121
+ /**
122
+ * Percentage literal Type with compile-time and runtime validation.
123
+ *
124
+ * Supported formats:
125
+ *
126
+ * - Integers: `0%`, `50%`, `100%` (0-100)
127
+ * - One decimal place: `0.1%`, `12.5%`, `99.9%` (0.1-99.9)
128
+ *
129
+ * The decimal digit is never zero, so `12.0%` must be written as `"12%"`, and
130
+ * `100%` has no decimal form. For computed values or more precision, use
131
+ * {@link Ratio} directly.
132
+ *
133
+ * See {@link Percentage} for a type that also accepts {@link Ratio}. Use
134
+ * {@link percentageToRatio} to convert.
135
+ *
136
+ * Invalid values produce a {@link PercentageLiteralError}.
137
+ *
138
+ * ### Example
139
+ *
140
+ * ```ts
141
+ * import {
142
+ * assertFalse,
143
+ * assertOk,
144
+ * assertType,
145
+ * PercentageLiteral,
146
+ * } from "@evolu/common";
147
+ *
148
+ * // The TypeScript type accepts valid spellings and rejects the rest.
149
+ * const literal: PercentageLiteral = "12.5%";
150
+ * assertType<
151
+ * Extract<PercentageLiteral, "101%" | "12.0%" | "12.55%">,
152
+ * never
153
+ * >();
154
+ *
155
+ * // The runtime Type validates the same grammar.
156
+ * assertOk(PercentageLiteral.fromUnknown(literal), "12.5%");
157
+ * assertFalse(PercentageLiteral.is("101%"));
158
+ * ```
159
+ */
160
+ export const PercentageLiteral: Type<
161
+ "PercentageLiteral",
162
+ PercentageLiteral,
163
+ PercentageLiteral,
164
+ PercentageLiteralError
165
+ > = /*#__PURE__*/ createTypeWithError(
166
+ "PercentageLiteral",
167
+ percentageLiteralSyntax,
168
+ (cause, value): PercentageLiteralError => ({
169
+ type: "PercentageLiteral",
170
+ value,
171
+ cause,
172
+ }),
173
+ (error) =>
174
+ `The value ${safelyStringifyUnknownValue(error.value)} is not a percentage literal. Use a value such as "50%" or "12.5%".`,
175
+ );
176
+
177
+ /** Error returned when {@link PercentageLiteral} rejects a value. */
178
+ export interface PercentageLiteralError extends TypeError<"PercentageLiteral"> {
179
+ readonly value: unknown;
180
+ /**
181
+ * The underlying union failure, retained for diagnostics.
182
+ *
183
+ * With `{ errors: "all" }`, includes every failed alternative.
184
+ */
185
+ readonly cause: UnionError;
186
+ }
119
187
 
120
188
  /** Converts a {@link Percentage} to its numeric {@link Ratio}. */
121
189
  export const percentageToRatio = (percentage: Percentage): Ratio =>
@@ -1,5 +1,12 @@
1
- import { test } from "node:test";
2
- import { assertEqual, assertFalse, assertSame, assertTrue } from "./Assert.ts";
1
+ import nodeAssert from "node:assert/strict";
2
+ import { describe, it, test } from "node:test";
3
+ import {
4
+ assertEqual,
5
+ assertFalse,
6
+ assertNonNullable,
7
+ assertSame,
8
+ assertTrue,
9
+ } from "./Assert.ts";
3
10
 
4
11
  import type { Brand } from "./Brand.ts";
5
12
  import type { ReadonlyRecord } from "./Object.ts";
@@ -8,6 +15,7 @@ import {
8
15
  createMutableRecord,
9
16
  emptyRecord,
10
17
  excludeProp,
18
+ filterObjectKeys,
11
19
  getObjectKind,
12
20
  getOwnProp,
13
21
  isFunction,
@@ -103,6 +111,22 @@ test("isPlainObject", () => {
103
111
  assertFalse(isPlainObject(Object.create(partialObjectPrototype)));
104
112
  });
105
113
 
114
+ test("isPlainObject checks the root markers of Object.prototype", () => {
115
+ for (const key of ["hasOwnProperty", "isPrototypeOf"]) {
116
+ const descriptor = Object.getOwnPropertyDescriptor(Object.prototype, key);
117
+ assertNonNullable(descriptor);
118
+ Reflect.deleteProperty(Object.prototype, key);
119
+ try {
120
+ assertFalse(isPlainObject({}));
121
+ assertTrue(isPlainObject(Object.create(null)));
122
+ } finally {
123
+ // oxlint-disable-next-line eslint/no-extend-native -- Restores the built-in property this test deleted.
124
+ Object.defineProperty(Object.prototype, key, descriptor);
125
+ }
126
+ }
127
+ assertTrue(isPlainObject({}));
128
+ });
129
+
106
130
  test("isFunction", () => {
107
131
  assertTrue(isFunction(() => {}));
108
132
  assertTrue(isFunction(function () {}));
@@ -205,6 +229,117 @@ test("mapObject", () => {
205
229
  assertEqual(mapped, { u1: "u1:10", u2: "u2:20" });
206
230
  });
207
231
 
232
+ describe("filterObjectKeys", () => {
233
+ it("preserves field types and makes selected fields optional", () => {
234
+ const symbol = Symbol("ignored");
235
+ const source = {
236
+ APP_PORT: "4000",
237
+ other: 1,
238
+ 42: true,
239
+ [symbol]: "ignored",
240
+ };
241
+ const selected = filterObjectKeys(source, (key) => {
242
+ assertType<typeof key, string>();
243
+ return key.startsWith("APP_") || key === "42";
244
+ });
245
+ assertType<
246
+ typeof selected,
247
+ {
248
+ readonly APP_PORT?: string;
249
+ readonly other?: number;
250
+ readonly 42?: boolean;
251
+ }
252
+ >();
253
+ assertEqual(selected, { APP_PORT: "4000", 42: true });
254
+ assertFalse(selected === source);
255
+ assertEqual(
256
+ filterObjectKeys(source, () => false),
257
+ {},
258
+ );
259
+
260
+ type UserId = string & Brand<"UserId">;
261
+ const users: ReadonlyRecord<UserId, number> = { ["u1" as UserId]: 1 };
262
+ const selectedUsers = filterObjectKeys(users, () => true);
263
+ assertType<
264
+ typeof selectedUsers,
265
+ Readonly<Partial<Record<UserId, number>>>
266
+ >();
267
+ assertEqual(selectedUsers, { u1: 1 });
268
+
269
+ void (() => {
270
+ // @ts-expect-error filterObjectKeys requires an object source.
271
+ filterObjectKeys("text", () => true);
272
+ // @ts-expect-error Selected properties are readonly.
273
+ selected.APP_PORT = "5000";
274
+ });
275
+ });
276
+
277
+ it("preserves descriptors without reading getters", () => {
278
+ let reads = 0;
279
+ const getter = () => {
280
+ reads++;
281
+ return "value";
282
+ };
283
+ const source = Object.defineProperties(Object.create({ inherited: 1 }), {
284
+ visible: { value: { nested: true }, enumerable: true, writable: true },
285
+ hidden: { value: undefined },
286
+ accessor: { get: getter, enumerable: true, configurable: true },
287
+ excluded: { get: getter },
288
+ [Symbol("ignored")]: { get: getter },
289
+ });
290
+ const visited: Array<string> = [];
291
+ const selected = filterObjectKeys(source, (key) => {
292
+ visited.push(key);
293
+ return key !== "excluded";
294
+ });
295
+ assertEqual(visited, ["visible", "hidden", "accessor", "excluded"]);
296
+ assertEqual(Object.getOwnPropertyNames(selected), [
297
+ "visible",
298
+ "hidden",
299
+ "accessor",
300
+ ]);
301
+ for (const key of Object.getOwnPropertyNames(selected)) {
302
+ nodeAssert.deepEqual(
303
+ Object.getOwnPropertyDescriptor(selected, key),
304
+ Object.getOwnPropertyDescriptor(source, key),
305
+ );
306
+ }
307
+ assertEqual(reads, 0);
308
+ assertFalse(Object.hasOwn(selected, "inherited"));
309
+ assertEqual(Object.getOwnPropertySymbols(selected), []);
310
+ assertSame(Object.getPrototypeOf(selected), Object.prototype);
311
+ assertSame(
312
+ Reflect.get(selected, "visible"),
313
+ Reflect.get(source, "visible"),
314
+ );
315
+ assertSame(Reflect.get(selected, "accessor"), "value");
316
+ assertEqual(reads, 1);
317
+ });
318
+
319
+ it("safely copies special keys and handles arrays as objects", () => {
320
+ const source = createMutableRecord();
321
+ source.__proto__ = "value";
322
+ Object.defineProperty(source, "constructor", {
323
+ value: "constructor",
324
+ enumerable: true,
325
+ });
326
+ const selected = filterObjectKeys(source, () => true);
327
+ assertTrue(Object.hasOwn(selected, "__proto__"));
328
+ assertSame(Object.getPrototypeOf(selected), Object.prototype);
329
+ assertEqual(selected, {
330
+ ["__proto__"]: "value",
331
+ constructor: "constructor",
332
+ });
333
+
334
+ const values: ReadonlyArray<number> = [10, 20];
335
+ const array = filterObjectKeys(values, (key) => key === "0");
336
+ assertFalse(Array.isArray(array));
337
+ assertEqual(array, { 0: 10 });
338
+ assertType<(typeof array)[0], number | undefined>();
339
+ assertType<typeof array.map, ReadonlyArray<number>["map"] | undefined>();
340
+ });
341
+ });
342
+
208
343
  test("excludeProp", () => {
209
344
  const obj = { a: 1, b: 2, c: 3 };
210
345
 
@@ -246,16 +381,10 @@ test("createMutableRecord", () => {
246
381
  assertEqual(source, { name: "Ada" });
247
382
  assertSame(Object.getPrototypeOf(copy), null);
248
383
 
249
- const compileTimeAssertions = () => {
384
+ void (() => {
250
385
  // @ts-expect-error createMutableRecord source must be an object.
251
386
  createMutableRecord("Ada");
252
- };
253
- assertType<
254
- typeof compileTimeAssertions extends (...args: Array<never>) => unknown
255
- ? true
256
- : false,
257
- true
258
- >();
387
+ });
259
388
  });
260
389
 
261
390
  test("emptyRecord", () => {
package/src/Object.ts CHANGED
@@ -87,6 +87,11 @@ export const isPlainObject = (
87
87
 
88
88
  const prototype = Object.getPrototypeOf(value) as object | null;
89
89
  if (prototype === null) return true;
90
+ // This realm's Object.prototype has an immutable null prototype, so `in`
91
+ // checks the same own properties as the structural test below, faster.
92
+ if (prototype === Object.prototype) {
93
+ return "hasOwnProperty" in prototype && "isPrototypeOf" in prototype;
94
+ }
90
95
  return (
91
96
  Object.getPrototypeOf(prototype) === null &&
92
97
  Object.hasOwn(prototype, "hasOwnProperty") &&
@@ -243,6 +248,50 @@ export const mapObject = <K extends string, V, U>(
243
248
  return out;
244
249
  };
245
250
 
251
+ /**
252
+ * Selects own string-keyed properties while preserving their descriptors.
253
+ *
254
+ * Visits enumerable and non-enumerable properties, ignoring inherited and
255
+ * symbol properties. The predicate receives only the key; property values and
256
+ * getters are never read. The result is a new ordinary object with matching
257
+ * descriptors, including accessor functions and property flags. Declared
258
+ * properties are optional because the predicate may exclude any of them.
259
+ * Numeric property names are visited as strings.
260
+ *
261
+ * ### Example
262
+ *
263
+ * ```ts
264
+ * import {
265
+ * assertEqual,
266
+ * assertType,
267
+ * filterObjectKeys,
268
+ * } from "@evolu/common";
269
+ *
270
+ * const source = { APP_PORT: "4000", HOME: "/home/evolu" };
271
+ * const selected = filterObjectKeys(source, (key) =>
272
+ * key.startsWith("APP_"),
273
+ * );
274
+ *
275
+ * assertEqual(selected, { APP_PORT: "4000" });
276
+ * assertType<
277
+ * typeof selected,
278
+ * { readonly APP_PORT?: string; readonly HOME?: string }
279
+ * >();
280
+ * ```
281
+ */
282
+ export const filterObjectKeys = <T extends object>(
283
+ source: T,
284
+ predicate: (key: string) => boolean,
285
+ ): Readonly<Partial<Pick<T, Exclude<keyof T, symbol>>>> => {
286
+ const selected: Partial<Pick<T, Exclude<keyof T, symbol>>> = {};
287
+ for (const key of Object.getOwnPropertyNames(source)) {
288
+ if (!predicate(key)) continue;
289
+ const descriptor = Object.getOwnPropertyDescriptor(source, key)!;
290
+ Object.defineProperty(selected, key, descriptor);
291
+ }
292
+ return selected;
293
+ };
294
+
246
295
  /** Conditionally excludes a property from an object. */
247
296
  export const excludeProp = <T extends object, K extends keyof T>(
248
297
  obj: T,
package/src/Platform.ts CHANGED
@@ -4,10 +4,18 @@
4
4
  * @module
5
5
  */
6
6
 
7
- /** Returns true if running in React Native with Hermes engine. */
7
+ /**
8
+ * Returns true if running in React Native with Hermes engine.
9
+ *
10
+ * @group Detection
11
+ */
8
12
  export const isHermes = "HermesInternal" in globalThis;
9
13
 
10
- /** Returns true if running in a server environment (no DOM). */
14
+ /**
15
+ * Returns true if running in a server environment (no DOM).
16
+ *
17
+ * @group Detection
18
+ */
11
19
  export const isServer = typeof document === "undefined";
12
20
 
13
21
  /**
@@ -20,6 +28,8 @@ export const isServer = typeof document === "undefined";
20
28
  * where no bundler ran, such as un-bundled browser ESM, where it fails closed
21
29
  * to production behavior. Node.js reads it natively; React Native polyfills it.
22
30
  * A missing `NODE_ENV` counts as development, matching React semantics.
31
+ *
32
+ * @group Detection
23
33
  */
24
34
  export const isDev =
25
35
  typeof process === "undefined"
@@ -35,6 +45,7 @@ export const isDev =
35
45
  * Returns false in React Native even if Buffer is polyfilled, as we prefer
36
46
  * native methods in that environment.
37
47
  *
48
+ * @group Detection
38
49
  * @see https://github.com/craftzdog/react-native-quick-base64#installation
39
50
  */
40
51
  export const hasNodeBuffer =
@@ -49,9 +60,16 @@ export const hasNodeBuffer =
49
60
  * if an onComplete callback is used.
50
61
  *
51
62
  * https://react.dev/reference/react-dom/flushSync
63
+ *
64
+ * @group Integration
52
65
  */
53
66
  export type FlushSync = (callback: () => void) => void;
54
67
 
68
+ /**
69
+ * Dependency wrapper for {@link FlushSync}.
70
+ *
71
+ * @group Integration
72
+ */
55
73
  export interface FlushSyncDep {
56
74
  readonly flushSync: FlushSync;
57
75
  }
@@ -59,18 +77,32 @@ export interface FlushSyncDep {
59
77
  /**
60
78
  * Reload the app in a platform-specific way.
61
79
  *
62
- * Use this after purging persistent storage to clear in-memory state and ensure
63
- * the app starts fresh. It does not purge storage itself.
80
+ * On the web, Evolu reloads tabs of a build when another build of the app waits
81
+ * for the local databases, so they load the build the server now serves. It can
82
+ * also clear in-memory state after persistent storage was purged; it does not
83
+ * purge storage itself.
64
84
  *
65
- * - Web: Redirects to the specified URL (defaults to `/`)
85
+ * - Web: Reloads the page, or loads the specified URL instead
66
86
  * - React Native: Restarts the app (URL ignored)
87
+ *
88
+ * @group Integration
67
89
  */
68
90
  export type ReloadApp = (url?: string) => void;
69
91
 
92
+ /**
93
+ * Dependency wrapper for {@link ReloadApp}.
94
+ *
95
+ * @group Integration
96
+ */
70
97
  export interface ReloadAppDep {
71
98
  readonly reloadApp: ReloadApp;
72
99
  }
73
100
 
101
+ /**
102
+ * Records platform global errors until disposed.
103
+ *
104
+ * @group Testing
105
+ */
74
106
  export interface TestGlobalErrors extends Disposable {
75
107
  readonly errors: ReadonlyArray<unknown>;
76
108
  readonly next: () => Promise<unknown>;
@@ -88,15 +120,25 @@ export interface TestGlobalErrors extends Disposable {
88
120
  readonly settle: () => Promise<ReadonlyArray<unknown>>;
89
121
  }
90
122
 
91
- /** Records platform global uncaught-error reporting until disposed. */
123
+ /**
124
+ * Records platform global uncaught-error reporting until disposed.
125
+ *
126
+ * @group Testing
127
+ */
92
128
  export const testGlobalUncaughtErrors = (): TestGlobalErrors =>
93
129
  createTestGlobalErrors("uncaughtErrors");
94
130
 
95
- /** Records platform global unhandled-rejection reporting until disposed. */
131
+ /**
132
+ * Records platform global unhandled-rejection reporting until disposed.
133
+ *
134
+ * @group Testing
135
+ */
96
136
  export const testGlobalUnhandledRejections = (): TestGlobalErrors =>
97
137
  createTestGlobalErrors("unhandledRejection");
98
138
 
99
- const settleSentinel = new Error("TestGlobalErrors.settle sentinel");
139
+ const settleSentinel = /*#__PURE__*/ new Error(
140
+ "TestGlobalErrors.settle sentinel",
141
+ );
100
142
 
101
143
  const createTestGlobalErrors = (
102
144
  kind: "uncaughtErrors" | "unhandledRejection",
package/src/Random.ts CHANGED
@@ -11,6 +11,8 @@ import type { Brand } from "./Brand.ts";
11
11
  * A random floating point number in [0, 1).
12
12
  *
13
13
  * Branded to distinguish random values from arbitrary numbers.
14
+ *
15
+ * @group Core
14
16
  */
15
17
  export type RandomNumber = number & Brand<"RandomNumber">;
16
18
 
@@ -43,17 +45,28 @@ export type RandomNumber = number & Brand<"RandomNumber">;
43
45
  * const secondTestRandom = testCreateRandom("test");
44
46
  * assertEqual(firstTestRandom.next(), secondTestRandom.next());
45
47
  * ```
48
+ *
49
+ * @group Core
46
50
  */
47
51
  export interface Random {
48
52
  /** Returns a floating point number in [0, 1). Just like Math.random(). */
49
53
  readonly next: () => RandomNumber;
50
54
  }
51
55
 
56
+ /**
57
+ * Dependency wrapper for {@link Random}.
58
+ *
59
+ * @group Core
60
+ */
52
61
  export interface RandomDep {
53
62
  readonly random: Random;
54
63
  }
55
64
 
56
- /** Creates a {@link Random} using Math.random(). */
65
+ /**
66
+ * Creates a {@link Random} using Math.random().
67
+ *
68
+ * @group Core
69
+ */
57
70
  export const createRandom = (): Random => ({
58
71
  next: () => Math.random() as RandomNumber,
59
72
  });
@@ -62,6 +75,8 @@ export const createRandom = (): Random => ({
62
75
  * Creates a seeded {@link Random} for deterministic tests.
63
76
  *
64
77
  * Default seed "evolu".
78
+ *
79
+ * @group Testing
65
80
  */
66
81
  export const testCreateRandom = (seed = "evolu"): Random => {
67
82
  const random = new RandomLib(seed);
@@ -75,12 +90,18 @@ export const testCreateRandom = (seed = "evolu"): Random => {
75
90
  * provided by the NPM `random` package.
76
91
  *
77
92
  * https://github.com/transitive-bullshit/random
93
+ *
94
+ * @group Core
78
95
  */
79
96
  export interface RandomLibDep {
80
97
  readonly randomLib: RandomLib;
81
98
  }
82
99
 
83
- /** Creates a random number generator from the NPM `random` package. */
100
+ /**
101
+ * Creates a random number generator from the NPM `random` package.
102
+ *
103
+ * @group Core
104
+ */
84
105
  export const createRandomLib = (): RandomLib => new RandomLib();
85
106
 
86
107
  /**
@@ -88,6 +109,8 @@ export const createRandomLib = (): RandomLib => new RandomLib();
88
109
  * deterministic tests.
89
110
  *
90
111
  * Default seed "evolu".
112
+ *
113
+ * @group Testing
91
114
  */
92
115
  export const testCreateRandomLib = (seed = "evolu"): RandomLib =>
93
116
  new RandomLib(seed);