@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
@@ -182,6 +182,7 @@ import {
182
182
  } from "../Array.ts";
183
183
  import {
184
184
  assert,
185
+ assertNonEmptyArray,
185
186
  assertNonNullable,
186
187
  assertNotUndefined,
187
188
  assertSame,
@@ -204,7 +205,7 @@ import {
204
205
  encodeNonNegativeInt,
205
206
  encodeNumber,
206
207
  encodeString,
207
- } from "../Binary.ts";
208
+ } from "../Bytes.ts";
208
209
  import type { ConsoleDep } from "../Console.ts";
209
210
  import {
210
211
  createPadmePadding,
@@ -247,6 +248,7 @@ import {
247
248
  zeroNonNegativeInt,
248
249
  } from "../Type.ts";
249
250
  import type { Predicate } from "../Types.ts";
251
+ import type { Evolu } from "./Evolu.ts";
250
252
  import {
251
253
  type Owner,
252
254
  type OwnerError,
@@ -271,6 +273,7 @@ import {
271
273
  type RangeUpperBound,
272
274
  type SkipRange,
273
275
  type StorageDep,
276
+ type StorageWriteMessagesError,
274
277
  type TimestampsRange,
275
278
  } from "./Storage.ts";
276
279
  import {
@@ -367,18 +370,27 @@ export const MessageType = {
367
370
 
368
371
  export type MessageType = (typeof MessageType)[keyof typeof MessageType];
369
372
 
370
- /** Parsed protocol header for supported protocol messages. */
373
+ /**
374
+ * The routing prefix of a {@link ProtocolMessage}.
375
+ *
376
+ * The version and the {@link OwnerId} begin every message of every protocol
377
+ * version. The message type follows them only in messages of this peer's
378
+ * version; the layout after the prefix is unknown for any other version.
379
+ */
371
380
  export interface ProtocolHeader extends Typed<"ProtocolHeader"> {
372
- readonly version: 1;
381
+ readonly version: NonNegativeInt;
373
382
  readonly ownerId: OwnerId;
374
- readonly messageType: MessageType;
383
+ /** Absent when `version` differs from {@link protocolVersion}. */
384
+ readonly messageType?: MessageType;
375
385
  }
376
386
 
377
387
  /**
378
- * Parses the protocol header needed for routing.
388
+ * Parses the {@link ProtocolHeader} a transport needs to route a message.
379
389
  *
380
- * Every protocol message begins with `protocolVersion`, `ownerId`, and
381
- * `messageType`.
390
+ * Parsing succeeds for versions this peer cannot process, because a
391
+ * non-initiator answers a version mismatch with only its version and the owner
392
+ * ID. Rejecting that reply here would drop it before
393
+ * {@link applyProtocolMessageAsClient} can report {@link ProtocolVersionError}.
382
394
  */
383
395
  export const parseProtocolHeader = (
384
396
  inputMessage: Uint8Array,
@@ -468,8 +480,9 @@ export interface ProtocolWriteError
468
480
  * still sync normally.
469
481
  *
470
482
  * Clients should prompt the user to contact the relay provider or upgrade their
471
- * plan. Quota monitoring and management is the relay provider's
472
- * responsibility.
483
+ * plan. Quota monitoring and management is the relay provider's responsibility.
484
+ * After additional quota is available, call {@link Evolu.requestSync} with the
485
+ * affected owner's ID to retry locally stored changes.
473
486
  */
474
487
  export interface ProtocolQuotaError
475
488
  extends OwnerError, Typed<"ProtocolQuotaError"> {}
@@ -558,9 +571,87 @@ export const createProtocolMessageFromCrdtMessages =
558
571
  return buffer.unwrap();
559
572
  };
560
573
 
574
+ /**
575
+ * Creates size-limited broadcast {@link ProtocolMessage}s containing every
576
+ * supplied {@link CrdtMessage}.
577
+ *
578
+ * Broadcasts contain no synchronization ranges or write key. Unlike
579
+ * {@link createProtocolMessageFromCrdtMessages}, this function splits all
580
+ * messages into complete frames rather than relying on later synchronization
581
+ * rounds to deliver messages that do not fit. Each individual message must fit
582
+ * the configured size limit.
583
+ *
584
+ * ### Example
585
+ *
586
+ * ```ts
587
+ * import {
588
+ * assertSame,
589
+ * createId,
590
+ * getOrThrow,
591
+ * testCreateDeps,
592
+ * } from "@evolu/common";
593
+ * import {
594
+ * createProtocolBroadcastMessagesFromCrdtMessages,
595
+ * MessageType,
596
+ * parseProtocolHeader,
597
+ * testAppOwner,
598
+ * testCreateCrdtMessage,
599
+ * } from "@evolu/common/local-first";
600
+ *
601
+ * const deps = testCreateDeps();
602
+ * const broadcasts = createProtocolBroadcastMessagesFromCrdtMessages(deps)(
603
+ * testAppOwner,
604
+ * [testCreateCrdtMessage(createId(deps), 1, "Ada")],
605
+ * );
606
+ * assertSame(broadcasts.length, 1);
607
+ * assertSame(
608
+ * getOrThrow(parseProtocolHeader(broadcasts[0])).messageType,
609
+ * MessageType.Broadcast,
610
+ * );
611
+ * ```
612
+ */
613
+ export const createProtocolBroadcastMessagesFromCrdtMessages =
614
+ (deps: RandomBytesDep) =>
615
+ (
616
+ owner: Owner,
617
+ messages: NonEmptyReadonlyArray<CrdtMessage>,
618
+ maxSize: ProtocolMessageMaxSize = defaultProtocolMessageMaxSize,
619
+ ): NonEmptyReadonlyArray<ProtocolMessage> => {
620
+ const broadcasts: Array<ProtocolMessage> = [];
621
+ let buffer = createProtocolMessageBuffer(owner.id, {
622
+ messageType: MessageType.Broadcast,
623
+ totalMaxSize: maxSize,
624
+ });
625
+ for (const message of messages) {
626
+ const encryptedMessage = {
627
+ timestamp: message.timestamp,
628
+ change: encodeAndEncryptDbChange(deps)(message, owner.encryptionKey),
629
+ };
630
+
631
+ if (!buffer.canAddMessage(encryptedMessage)) {
632
+ const nextBuffer = createProtocolMessageBuffer(owner.id, {
633
+ messageType: MessageType.Broadcast,
634
+ totalMaxSize: maxSize,
635
+ });
636
+ assert(
637
+ nextBuffer.canAddMessage(encryptedMessage),
638
+ "the message is too big",
639
+ );
640
+ broadcasts.push(buffer.unwrap());
641
+ buffer = nextBuffer;
642
+ }
643
+
644
+ buffer.addMessage(encryptedMessage);
645
+ }
646
+
647
+ broadcasts.push(buffer.unwrap());
648
+ assertNonEmptyArray(broadcasts);
649
+ return broadcasts;
650
+ };
651
+
561
652
  /** Creates a {@link ProtocolMessage} for sync. */
562
653
  export const createProtocolMessageForSync =
563
- (deps: StorageDep & ConsoleDep) =>
654
+ (deps: StorageDep) =>
564
655
  (ownerId: OwnerId, subscriptionFlag?: SubscriptionFlag): ProtocolMessage => {
565
656
  const buffer = createProtocolMessageBuffer(ownerId, {
566
657
  messageType: MessageType.Request,
@@ -902,21 +993,53 @@ export interface ApplyProtocolMessageAsClientOptions {
902
993
  }
903
994
 
904
995
  /**
905
- * Result type for {@link applyProtocolMessageAsClient} that distinguishes
906
- * between responses to client requests and broadcast messages.
996
+ * Result of {@link applyProtocolMessageAsClient}: a continuation for the
997
+ * non-initiator.
907
998
  */
908
999
  export interface ApplyProtocolMessageAsClientResponse extends Typed<"Response"> {
909
1000
  readonly message: ProtocolMessage;
1001
+ /** The uploaded messages, if any, for delivery to other local databases. */
1002
+ readonly broadcast?: ProtocolMessage;
910
1003
  }
911
1004
 
912
- export interface ApplyProtocolMessageAsClientNoResponse extends Typed<"NoResponse"> {}
1005
+ /**
1006
+ * Result of {@link applyProtocolMessageAsClient}: the response needed nothing
1007
+ * more, because its ranges all matched or it carried none.
1008
+ */
1009
+ export interface ApplyProtocolMessageAsClientConverged extends Typed<"Converged"> {}
913
1010
 
1011
+ /**
1012
+ * Result of {@link applyProtocolMessageAsClient}: a Broadcast message whose
1013
+ * messages were written.
1014
+ */
914
1015
  export interface ApplyProtocolMessageAsClientBroadcast extends Typed<"Broadcast"> {}
915
1016
 
1017
+ /**
1018
+ * Result of {@link applyProtocolMessageAsClient}: the messages were written, but
1019
+ * without a write key no ranges are reconciled.
1020
+ */
1021
+ export interface ApplyProtocolMessageAsClientReadonly extends Typed<"Readonly"> {}
1022
+
1023
+ /**
1024
+ * Result of {@link applyProtocolMessageAsClient}: the protocol logged an
1025
+ * exception thrown by calling the storage's `writeMessages` (`Write`) or a
1026
+ * failed range reconciliation (`Sync`) to the console. An exception while the
1027
+ * returned Task runs, as the built-in storages throw, is a defect that aborts
1028
+ * the Run instead. Expected write rejections return the original
1029
+ * {@link StorageWriteMessagesError} through {@link Result}. Reconciliation can
1030
+ * fail after messages have been committed; that failure does not roll back the
1031
+ * write.
1032
+ */
1033
+ export interface ApplyProtocolMessageAsClientFailed extends Typed<"Failed"> {
1034
+ readonly cause: "Write" | "Sync";
1035
+ }
1036
+
916
1037
  export type ApplyProtocolMessageAsClientResult =
917
1038
  | ApplyProtocolMessageAsClientResponse
918
- | ApplyProtocolMessageAsClientNoResponse
919
- | ApplyProtocolMessageAsClientBroadcast;
1039
+ | ApplyProtocolMessageAsClientConverged
1040
+ | ApplyProtocolMessageAsClientBroadcast
1041
+ | ApplyProtocolMessageAsClientReadonly
1042
+ | ApplyProtocolMessageAsClientFailed;
920
1043
 
921
1044
  export const applyProtocolMessageAsClient =
922
1045
  (
@@ -924,12 +1047,7 @@ export const applyProtocolMessageAsClient =
924
1047
  options: ApplyProtocolMessageAsClientOptions = {},
925
1048
  ): Task<
926
1049
  ApplyProtocolMessageAsClientResult,
927
- | ProtocolInvalidDataError
928
- | ProtocolSyncError
929
- | ProtocolVersionError
930
- | ProtocolWriteError
931
- | ProtocolWriteKeyError
932
- | ProtocolQuotaError,
1050
+ ProtocolError | StorageWriteMessagesError,
933
1051
  StorageDep
934
1052
  > =>
935
1053
  async (run) => {
@@ -995,15 +1113,18 @@ export const applyProtocolMessageAsClient =
995
1113
  const result = await run(
996
1114
  storage.writeMessages(ownerIdBytes, messages),
997
1115
  );
998
- // Quota errors are handled by Storage; protocol just stops syncing.
999
- if (!result.ok) return ok({ type: "NoResponse" });
1116
+ if (!result.ok) return result;
1000
1117
  } catch (error) {
1001
1118
  if (AbortError.is(error)) throw error;
1002
1119
  run.deps.console.error(error);
1003
- return ok({ type: "NoResponse" });
1120
+ return ok({ type: "Failed", cause: "Write" });
1004
1121
  }
1005
1122
  }
1006
1123
 
1124
+ if (messageType === MessageType.Broadcast) {
1125
+ return ok({ type: "Broadcast" });
1126
+ }
1127
+
1007
1128
  // Now: No writeKey, no sync.
1008
1129
  // TODO: Allow to sync SharedReadonlyOwner
1009
1130
  // Without local changes, writeKey will not be required.
@@ -1011,17 +1132,13 @@ export const applyProtocolMessageAsClient =
1011
1132
  // the sync will stop.
1012
1133
  const writeKey = options.writeKey;
1013
1134
  if (writeKey == null) {
1014
- return ok({ type: "NoResponse" });
1015
- }
1016
-
1017
- if (messageType === MessageType.Broadcast) {
1018
- return ok({ type: "Broadcast" });
1135
+ return ok({ type: "Readonly" });
1019
1136
  }
1020
1137
 
1021
1138
  const ranges = decodeRanges(input);
1022
1139
 
1023
1140
  if (!isNonEmptyArray(ranges)) {
1024
- return ok({ type: "NoResponse" });
1141
+ return ok({ type: "Converged" });
1025
1142
  }
1026
1143
 
1027
1144
  const output = createProtocolMessageBuffer(ownerId, {
@@ -1030,14 +1147,23 @@ export const applyProtocolMessageAsClient =
1030
1147
  rangesMaxSize: options.rangesMaxSize,
1031
1148
  });
1032
1149
 
1033
- const result = sync(run.deps)(ranges, output, ownerIdBytes);
1150
+ let broadcast: ProtocolMessageBuffer | undefined;
1151
+ const result = sync(run.deps)(ranges, output, ownerIdBytes, (message) => {
1152
+ broadcast ??= createProtocolMessageBuffer(ownerId, {
1153
+ messageType: MessageType.Broadcast,
1154
+ });
1155
+ broadcast.addMessage(message);
1156
+ });
1034
1157
 
1035
- // Client sync error (handled via Storage) or no changes.
1036
- if (!result.ok || !result.value) {
1037
- return ok({ type: "NoResponse" });
1038
- }
1158
+ // A failure was logged by sync.
1159
+ if (!result.ok) return ok({ type: "Failed", cause: "Sync" });
1160
+ if (!result.value) return ok({ type: "Converged" });
1039
1161
 
1040
- return ok({ type: "Response", message: output.unwrap() });
1162
+ return ok({
1163
+ type: "Response",
1164
+ message: output.unwrap(),
1165
+ ...(broadcast && { broadcast: broadcast.unwrap() }),
1166
+ });
1041
1167
  } catch (error) {
1042
1168
  if (AbortError.is(error)) throw error;
1043
1169
  return err<ProtocolInvalidDataError>({
@@ -1159,9 +1285,13 @@ export const applyProtocolMessageAsRelay =
1159
1285
  );
1160
1286
 
1161
1287
  if (!result.ok) {
1288
+ const isQuotaError = result.error.type === "StorageQuotaError";
1289
+ if (!isQuotaError) run.deps.console.error(result.error);
1162
1290
  const message = createProtocolMessageBuffer(ownerId, {
1163
1291
  messageType: MessageType.Response,
1164
- errorCode: ProtocolErrorCode.QuotaError,
1292
+ errorCode: isQuotaError
1293
+ ? ProtocolErrorCode.QuotaError
1294
+ : ProtocolErrorCode.WriteError,
1165
1295
  }).unwrap();
1166
1296
  return ok({ type: "Response", message });
1167
1297
  }
@@ -1256,7 +1386,7 @@ const parseProtocolHeaderFromBuffer = (input: Buffer): ProtocolHeader => {
1256
1386
  const [version, ownerId] = decodeVersionAndOwner(input);
1257
1387
 
1258
1388
  if (version !== protocolVersion) {
1259
- throw new ProtocolDecodeError(`Unsupported protocol version: ${version}`);
1389
+ return { type: "ProtocolHeader", version, ownerId };
1260
1390
  }
1261
1391
 
1262
1392
  const messageTypeValue = input.shift();
@@ -1276,12 +1406,7 @@ const parseProtocolHeaderFromBuffer = (input: Buffer): ProtocolHeader => {
1276
1406
  throw new ProtocolDecodeError("Invalid MessageType");
1277
1407
  }
1278
1408
 
1279
- return {
1280
- type: "ProtocolHeader",
1281
- version: 1,
1282
- ownerId,
1283
- messageType,
1284
- };
1409
+ return { type: "ProtocolHeader", version, ownerId, messageType };
1285
1410
  };
1286
1411
 
1287
1412
  /**
@@ -1319,6 +1444,7 @@ const sync =
1319
1444
  ranges: NonEmptyReadonlyArray<Range>,
1320
1445
  output: ProtocolMessageBuffer,
1321
1446
  ownerIdBytes: OwnerIdBytes,
1447
+ onMessage?: (message: EncryptedCrdtMessage) => void,
1322
1448
  ): Result<boolean, typeof ProtocolErrorCode.SyncError> => {
1323
1449
  const outputInitialSize = output.getSize();
1324
1450
  let storageSize: NonNegativeInt;
@@ -1427,13 +1553,19 @@ const sync =
1427
1553
  skipRange(range);
1428
1554
  } else if (output.canSplitRange()) {
1429
1555
  coalesceSkipsBeforeAdd();
1430
- splitRange(deps)(
1431
- ownerIdBytes,
1432
- lower,
1433
- upper,
1434
- currentUpperBound,
1435
- output,
1436
- );
1556
+ try {
1557
+ splitRange(deps)(
1558
+ ownerIdBytes,
1559
+ lower,
1560
+ upper,
1561
+ currentUpperBound,
1562
+ output,
1563
+ );
1564
+ } catch (error) {
1565
+ if (AbortError.is(error)) throw error;
1566
+ deps.console.error(error);
1567
+ return err(ProtocolErrorCode.SyncError);
1568
+ }
1437
1569
  } else {
1438
1570
  return addFingerprintForRemainingRange(upper)
1439
1571
  ? ok(true)
@@ -1495,7 +1627,10 @@ const sync =
1495
1627
  }
1496
1628
 
1497
1629
  ourTimestamps.add(timestampBinary);
1498
- if (message) output.addMessage(message);
1630
+ if (message) {
1631
+ output.addMessage(message);
1632
+ onMessage?.(message);
1633
+ }
1499
1634
  return true;
1500
1635
  },
1501
1636
  );
@@ -1547,7 +1682,7 @@ const sync =
1547
1682
  };
1548
1683
 
1549
1684
  const splitRange =
1550
- (deps: StorageDep & ConsoleDep) =>
1685
+ (deps: StorageDep) =>
1551
1686
  (
1552
1687
  ownerId: OwnerIdBytes,
1553
1688
  lower: NonNegativeInt,
@@ -1565,15 +1700,10 @@ const splitRange =
1565
1700
  timestamps: createTimestampsBuffer(),
1566
1701
  };
1567
1702
 
1568
- deps.storage.iterate(
1569
- ownerId,
1570
- zeroNonNegativeInt,
1571
- itemCount,
1572
- (timestamp) => {
1573
- range.timestamps.add(timestampBytesToTimestamp(timestamp));
1574
- return true;
1575
- },
1576
- );
1703
+ deps.storage.iterate(ownerId, lower, upper, (timestamp) => {
1704
+ range.timestamps.add(timestampBytesToTimestamp(timestamp));
1705
+ return true;
1706
+ });
1577
1707
 
1578
1708
  buffer.addRange(range);
1579
1709
  return;
@@ -1588,17 +1718,11 @@ const splitRange =
1588
1718
  ...buckets.value.map((b) => NonNegativeInt.orThrow(b + lower)),
1589
1719
  ];
1590
1720
 
1591
- let fingerprintRanges: ReadonlyArray<FingerprintRange>;
1592
- try {
1593
- fingerprintRanges = deps.storage.fingerprintRanges(
1594
- ownerId,
1595
- fingerprintRangesBuckets,
1596
- upperBound,
1597
- );
1598
- } catch (error) {
1599
- deps.console.error(error);
1600
- return;
1601
- }
1721
+ const fingerprintRanges = deps.storage.fingerprintRanges(
1722
+ ownerId,
1723
+ fingerprintRangesBuckets,
1724
+ upperBound,
1725
+ );
1602
1726
 
1603
1727
  const rangesToUse =
1604
1728
  lower > 0 ? fingerprintRanges.slice(1) : fingerprintRanges;
@@ -43,25 +43,22 @@ export type { NotNull as KyselyNotNull } from "kysely";
43
43
  * import {
44
44
  * assertType,
45
45
  * createQueryBuilder,
46
- * id,
46
+ * testEvoluSchema,
47
+ * type TestEvoluSchema,
47
48
  * NonEmptyTrimmedString100,
48
49
  * type Query,
50
+ * type TestTodoId,
49
51
  * } from "@evolu/common";
50
52
  *
51
- * const TodoId = id("Todo");
52
- * type TodoId = typeof TodoId.Output;
53
- * const Schema = {
54
- * todo: { id: TodoId, title: NonEmptyTrimmedString100 },
55
- * };
56
- * const createQuery = createQueryBuilder(Schema);
53
+ * const createQuery = createQueryBuilder(testEvoluSchema);
57
54
  * const allTodos = createQuery((db) => db.selectFrom("todo").selectAll());
58
55
  *
59
56
  * type AllTodosRow = typeof allTodos.Row;
60
57
  * assertType<
61
- * typeof allTodos extends Query<typeof Schema> ? true : false,
58
+ * typeof allTodos extends Query<TestEvoluSchema> ? true : false,
62
59
  * true
63
60
  * >();
64
- * assertType<AllTodosRow["id"], TodoId>();
61
+ * assertType<AllTodosRow["id"], TestTodoId>();
65
62
  * assertType<AllTodosRow["title"], NonEmptyTrimmedString100 | null>();
66
63
  * ```
67
64
  */
@@ -82,15 +79,11 @@ export type Query<
82
79
  * import {
83
80
  * assertType,
84
81
  * createQueryBuilder,
85
- * id,
82
+ * testEvoluSchema,
86
83
  * type InferRow,
87
- * NonEmptyTrimmedString100,
88
84
  * } from "@evolu/common";
89
85
  *
90
- * const Schema = {
91
- * todo: { id: id("Todo"), title: NonEmptyTrimmedString100 },
92
- * };
93
- * const createQuery = createQueryBuilder(Schema);
86
+ * const createQuery = createQueryBuilder(testEvoluSchema);
94
87
  * const allTodos = createQuery((db) =>
95
88
  * db.selectFrom("todo").selectAll(),
96
89
  * );
@@ -0,0 +1,143 @@
1
+ import { test } from "node:test";
2
+ import {
3
+ assertEqual,
4
+ assertErr,
5
+ assertOk,
6
+ assertSame,
7
+ assertType,
8
+ createEvolu,
9
+ createQueryBuilder,
10
+ type Evolu,
11
+ NonEmptyTrimmedString100,
12
+ object,
13
+ type OwnerIdBytes,
14
+ ownerIdToOwnerIdBytes,
15
+ sqliteQueryStringToSqliteQuery,
16
+ type Task,
17
+ testAppName,
18
+ testAppOwner,
19
+ type TestEvoluSchema,
20
+ testEvoluSchema,
21
+ testLocalOnlyEvoluSchema,
22
+ testOwnerSecret,
23
+ TestProjectId,
24
+ testProjectId,
25
+ TestTodoId,
26
+ testTodoId,
27
+ type TimestampBytes,
28
+ } from "../index.ts";
29
+ import type { EvoluPlatformDeps } from "./Evolu.ts";
30
+ import type { ValidateSchema } from "./Schema.ts";
31
+
32
+ test("history queries select and filter by ownerId bytes", () => {
33
+ const createQuery = createQueryBuilder(testEvoluSchema);
34
+ const query = createQuery((db) =>
35
+ db
36
+ .selectFrom("evolu_history")
37
+ .select(["ownerId", "timestamp"])
38
+ .where("ownerId", "=", ownerIdToOwnerIdBytes(testAppOwner.id)),
39
+ );
40
+
41
+ assertType<
42
+ typeof query.Row,
43
+ { ownerId: OwnerIdBytes; timestamp: TimestampBytes }
44
+ >();
45
+ });
46
+
47
+ test("testEvoluSchema supports project-linked and independent todos", () => {
48
+ assertType<TestEvoluSchema, typeof testEvoluSchema>();
49
+ assertType<ValidateSchema<TestEvoluSchema>, TestEvoluSchema>();
50
+ assertType<typeof testTodoId, TestTodoId>();
51
+ assertType<typeof testProjectId, TestProjectId>();
52
+ assertSame(testEvoluSchema.todo.id, TestTodoId);
53
+ assertSame(testEvoluSchema.project.id, TestProjectId);
54
+ assertEqual(new Set<string>([testTodoId, testProjectId]).size, 2);
55
+ const Todo = object(testEvoluSchema.todo);
56
+ const todo = {
57
+ id: testTodoId,
58
+ title: "Write documentation",
59
+ isCompleted: null,
60
+ projectId: testProjectId,
61
+ };
62
+ assertOk(Todo.fromUnknown(todo), todo);
63
+ const independentTodo = { ...todo, projectId: null };
64
+ assertOk(Todo.fromUnknown(independentTodo), independentTodo);
65
+ assertOk(testEvoluSchema.todo.projectId.from(testProjectId), testProjectId);
66
+ void (() => {
67
+ // @ts-expect-error A todo ID is not a project ID.
68
+ testEvoluSchema.todo.projectId.from(testTodoId);
69
+ });
70
+
71
+ const createQuery = createQueryBuilder(testEvoluSchema);
72
+ const query = createQuery((db) =>
73
+ db
74
+ .selectFrom("todo")
75
+ .leftJoin("project", "project.id", "todo.projectId")
76
+ .select(["todo.title", "project.name as projectName"]),
77
+ );
78
+ assertType<
79
+ typeof query.Row,
80
+ {
81
+ title: NonEmptyTrimmedString100 | null;
82
+ projectName: NonEmptyTrimmedString100 | null;
83
+ }
84
+ >();
85
+ assertEqual(
86
+ sqliteQueryStringToSqliteQuery(query).sql,
87
+ 'select "todo"."title", "project"."name" as "projectName" from "todo" left join "project" on "project"."id" = "todo"."projectId"',
88
+ );
89
+
90
+ const createTodos = createEvolu(testEvoluSchema, {
91
+ appName: testAppName,
92
+ appOwner: testAppOwner,
93
+ transports: [],
94
+ });
95
+ assertType<
96
+ typeof createTodos,
97
+ Task<Evolu<TestEvoluSchema>, never, EvoluPlatformDeps>
98
+ >();
99
+ });
100
+
101
+ test("testLocalOnlyEvoluSchema supports keys with or without recovery material", () => {
102
+ assertType<
103
+ ValidateSchema<typeof testLocalOnlyEvoluSchema>,
104
+ typeof testLocalOnlyEvoluSchema
105
+ >();
106
+ const AppOwnerRow = object(testLocalOnlyEvoluSchema._appOwner);
107
+ const recoverableOwner = {
108
+ id: testAppOwner.id,
109
+ encryptionKey: testAppOwner.encryptionKey,
110
+ writeKey: testAppOwner.writeKey,
111
+ secret: testOwnerSecret,
112
+ name: "Personal",
113
+ };
114
+ assertOk(AppOwnerRow.fromUnknown(recoverableOwner), recoverableOwner);
115
+ const keyOnlyOwner = {
116
+ ...recoverableOwner,
117
+ secret: null,
118
+ name: null,
119
+ };
120
+ assertOk(AppOwnerRow.fromUnknown(keyOnlyOwner), keyOnlyOwner);
121
+ assertErr(
122
+ AppOwnerRow.fromUnknown({
123
+ ...keyOnlyOwner,
124
+ encryptionKey: new Uint8Array(1),
125
+ }),
126
+ );
127
+ assertErr(
128
+ AppOwnerRow.fromUnknown({
129
+ ...recoverableOwner,
130
+ secret: new Uint8Array(1),
131
+ }),
132
+ );
133
+
134
+ const createAccounts = createEvolu(testLocalOnlyEvoluSchema, {
135
+ appName: testAppName,
136
+ appOwner: testAppOwner,
137
+ transports: [],
138
+ });
139
+ assertType<
140
+ typeof createAccounts,
141
+ Task<Evolu<typeof testLocalOnlyEvoluSchema>, never, EvoluPlatformDeps>
142
+ >();
143
+ });