@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
@@ -18,6 +18,8 @@ import {
18
18
  import { createCallbacks } from "../Callbacks.ts";
19
19
  import type { ConsoleDep } from "../Console.ts";
20
20
  import { createConsole } from "../Console.ts";
21
+ import type { DecryptWithXChaCha20Poly1305Error } from "../Crypto.ts";
22
+ import type { UnknownError } from "../Error.ts";
21
23
  import { createUnknownError } from "../Error.ts";
22
24
  import { disposable, exhaustiveCheck, todo } from "../Function.ts";
23
25
  import {
@@ -45,11 +47,11 @@ import {
45
47
  UrlSafeString,
46
48
  } from "../Type.ts";
47
49
  import type {
50
+ BroadcastChannel,
48
51
  CreateBroadcastChannelDep,
49
52
  CreateMessageChannelDep,
50
53
  } from "../Worker.ts";
51
- import type { CreateDbWorkerDep } from "./Db.ts";
52
- import type { EvoluError } from "./Error.ts";
54
+ import type { CreateDbWorkerDep, UnsupportedDbVersionError } from "./Db.ts";
53
55
  import type {
54
56
  AppOwner,
55
57
  Owner,
@@ -59,6 +61,7 @@ import type {
59
61
  SyncOwner,
60
62
  } from "./Owner.ts";
61
63
  import { createOwnerWebSocketTransport } from "./Owner.ts";
64
+ import type { ProtocolError, ProtocolQuotaError } from "./Protocol.ts";
62
65
  import type {
63
66
  Queries,
64
67
  QueriesToQueryRowsPromises,
@@ -73,6 +76,7 @@ import type {
73
76
  IndexesConfig,
74
77
  Mutation,
75
78
  MutationChange,
79
+ MutationOptions,
76
80
  ValidateSchema,
77
81
  } from "./Schema.ts";
78
82
  import { evoluSchemaToSqliteSchema } from "./Schema.ts";
@@ -80,23 +84,41 @@ import type {
80
84
  ConsoleEntryOrError,
81
85
  EvoluInput,
82
86
  EvoluOutput,
87
+ OtherBuildRunningError,
83
88
  SharedWorkerDep,
89
+ SyncState,
90
+ syncStateToOwnerSyncStates,
84
91
  } from "./Shared.ts";
85
92
  import { consoleEntryOrErrorBroadcastChannelName } from "./Shared.ts";
86
- import { DbChange } from "./Storage.ts";
93
+ import { DbChange, type StorageQuotaError } from "./Storage.ts";
87
94
  import type { Timestamp } from "./Timestamp.ts";
88
95
 
96
+ /**
97
+ * Configuration for {@link createEvolu}.
98
+ *
99
+ * @group Configuration
100
+ */
89
101
  export interface EvoluConfig {
90
102
  /**
91
- * The app name. Evolu is multitenant - it can run multiple instances
92
- * concurrently. The same app can have multiple instances for different
93
- * accounts.
103
+ * An application name used in logs and local database names.
104
+ *
105
+ * Evolu combines `appName` with the {@link AppOwner} identity to identify the
106
+ * local database. Changing either opens a different database. Keep `appName`
107
+ * stable across ordinary application updates.
94
108
  *
95
- * Evolu derives the final instance name from `appName` and `appOwner` in
96
- * {@link EvoluConfig}. The derived instance name is used as the SQLite
97
- * database filename and as the log prefix. This ensures that each
98
- * {@link Owner} gets a separate local database while preserving a readable app
99
- * prefix.
109
+ * Instances that share an `appName` and AppOwner open the same database and
110
+ * must use the same schema. Evolu applies the schema of the first instance
111
+ * and later instances join it.
112
+ *
113
+ * A different app name lets you create a separate local replica for the same
114
+ * AppOwner—for example, to test a different SQLite implementation or index
115
+ * configuration, or rebuild a replica for debugging while preserving the
116
+ * existing database.
117
+ *
118
+ * Evolu supports running these databases concurrently. Their local separation
119
+ * does not isolate synchronization: replicas synchronizing the same owners
120
+ * through the same relay can still exchange changes. Changing `appName`
121
+ * neither migrates existing local data nor isolates incompatible schemas.
100
122
  *
101
123
  * ### Example
102
124
  *
@@ -132,6 +154,73 @@ export interface EvoluConfig {
132
154
  */
133
155
  readonly appOwner: AppOwner;
134
156
 
157
+ /**
158
+ * Keep the database in memory instead of persisting it on this device.
159
+ *
160
+ * This option controls device persistence independently of synchronization.
161
+ * When synchronization is enabled, data can still be synchronized and
162
+ * persisted remotely.
163
+ *
164
+ * Useful for testing or temporary sessions. Data that exists only in this
165
+ * database is lost when the database closes.
166
+ *
167
+ * The default value is: `false`.
168
+ */
169
+ readonly memoryOnly?: boolean;
170
+
171
+ /**
172
+ * Use the `indexes` option to define SQLite indexes.
173
+ *
174
+ * Table and column names are not typed because Kysely doesn't support it.
175
+ *
176
+ * https://medium.com/@JasonWyatt/squeezing-performance-from-sqlite-indexes-indexes-c4e175f3c346
177
+ *
178
+ * ### Example
179
+ *
180
+ * ```ts
181
+ * import {
182
+ * createEvolu,
183
+ * id,
184
+ * testAppName,
185
+ * testAppOwner,
186
+ * } from "@evolu/common";
187
+ *
188
+ * const Schema = {
189
+ * todo: { id: id("Todo") },
190
+ * todoCategory: { id: id("TodoCategory") },
191
+ * };
192
+ *
193
+ * const _createTodoEvolu = createEvolu(Schema, {
194
+ * appName: testAppName,
195
+ * appOwner: testAppOwner,
196
+ * transports: [],
197
+ * indexes: (create) => [
198
+ * create("todoCreatedAt").on("todo").column("createdAt"),
199
+ * create("todoCategoryCreatedAt")
200
+ * .on("todoCategory")
201
+ * .column("createdAt"),
202
+ * ],
203
+ * });
204
+ * ```
205
+ */
206
+ readonly indexes?: IndexesConfig;
207
+
208
+ /**
209
+ * Called when this instance's local database is deleted.
210
+ *
211
+ * Apps can use this to update UI immediately because the corresponding
212
+ * {@link Evolu} instance becomes unusable after local database deletion.
213
+ */
214
+ readonly onDatabaseDeleted?: () => void;
215
+
216
+ /**
217
+ * Called when local data for an {@link Owner} is deleted.
218
+ *
219
+ * Apps can use this to update UI immediately because that owner stops being
220
+ * used across tabs and instances.
221
+ */
222
+ readonly onOwnerDeleted?: (owner: Owner) => void;
223
+
135
224
  /**
136
225
  * Transport configuration for sync and backup.
137
226
  *
@@ -169,18 +258,11 @@ export interface EvoluConfig {
169
258
  * ```ts
170
259
  * import {
171
260
  * assertEqual,
172
- * createAppOwner,
173
261
  * createOwnerWebSocketTransport,
174
- * createOwnerSecret,
175
- * createRandomBytes,
262
+ * testAppOwner,
176
263
  * type OwnerTransport,
177
264
  * } from "@evolu/common";
178
265
  *
179
- * // Create once, persist the mnemonic securely, and restore it on later runs.
180
- * const appOwner = createAppOwner(
181
- * createOwnerSecret({ randomBytes: createRandomBytes() }),
182
- * );
183
- *
184
266
  * // Use one relay.
185
267
  * const _singleRelay = [
186
268
  * { type: "WebSocket", url: "wss://relay1.example.com" },
@@ -201,91 +283,17 @@ export interface EvoluConfig {
201
283
  * const authenticatedRelay = [
202
284
  * createOwnerWebSocketTransport({
203
285
  * url: "wss://relay.example.com",
204
- * ownerId: appOwner.id,
286
+ * ownerId: testAppOwner.id,
205
287
  * }),
206
288
  * ];
207
289
  *
208
290
  * assertEqual(
209
291
  * authenticatedRelay[0]?.url,
210
- * `wss://relay.example.com?ownerId=${appOwner.id}`,
292
+ * `wss://relay.example.com?ownerId=${testAppOwner.id}`,
211
293
  * );
212
294
  * ```
213
295
  */
214
296
  readonly transports?: ReadonlyArray<OwnerTransport>;
215
-
216
- /**
217
- * Keep local data only in memory instead of persisting it on this device.
218
- * Useful for testing, temporary data, or sensitive data that should not be
219
- * recoverable from local storage after the process ends.
220
- *
221
- * Local data stored in memory is completely destroyed when the process ends.
222
- * Sync can still persist data remotely when transports are enabled.
223
- *
224
- * The default value is: `false`.
225
- */
226
- readonly memoryOnly?: boolean;
227
-
228
- /**
229
- * Use the `indexes` option to define SQLite indexes.
230
- *
231
- * Table and column names are not typed because Kysely doesn't support it.
232
- *
233
- * https://medium.com/@JasonWyatt/squeezing-performance-from-sqlite-indexes-indexes-c4e175f3c346
234
- *
235
- * ### Example
236
- *
237
- * ```ts
238
- * import {
239
- * AppName,
240
- * assertTrue,
241
- * createAppOwner,
242
- * createEvolu,
243
- * createOwnerSecret,
244
- * createRandomBytes,
245
- * id,
246
- * } from "@evolu/common";
247
- *
248
- * const Schema = {
249
- * todo: { id: id("Todo") },
250
- * todoCategory: { id: id("TodoCategory") },
251
- * };
252
- * // Create once, persist the mnemonic securely, and restore it on later runs.
253
- * const appOwner = createAppOwner(
254
- * createOwnerSecret({ randomBytes: createRandomBytes() }),
255
- * );
256
- *
257
- * const createTodoEvolu = createEvolu(Schema, {
258
- * appName: AppName.orThrow("IndexedTodos"),
259
- * appOwner,
260
- * transports: [],
261
- * indexes: (create) => [
262
- * create("todoCreatedAt").on("todo").column("createdAt"),
263
- * create("todoCategoryCreatedAt")
264
- * .on("todoCategory")
265
- * .column("createdAt"),
266
- * ],
267
- * });
268
- *
269
- * assertTrue(typeof createTodoEvolu === "function");
270
- * ```
271
- */
272
- readonly indexes?: IndexesConfig;
273
-
274
- /**
275
- * Called when this instance's local database is deleted.
276
- *
277
- * Apps can use this to update UI immediately because the corresponding
278
- * {@link Evolu} instance becomes unusable after local database deletion.
279
- */
280
- readonly onDatabaseDeleted?: () => void;
281
-
282
- /**
283
- * Called when local data for an {@link Owner} is deleted.
284
- *
285
- * Apps can use this to update UI immediately because that owner stops being
286
- * used across tabs and instances.
287
- */
288
- readonly onOwnerDeleted?: (owner: Owner) => void;
289
297
  }
290
298
 
291
299
  /**
@@ -297,6 +305,8 @@ export interface EvoluConfig {
297
305
  *
298
306
  * Uses the same safe alphabet as {@link UrlSafeString} (letters, digits, `-`,
299
307
  * `_`) and must be between 1 and 41 characters.
308
+ *
309
+ * @group Configuration
300
310
  */
301
311
  export const AppName = /*#__PURE__*/ brand(
302
312
  "AppName",
@@ -309,16 +319,35 @@ export const AppName = /*#__PURE__*/ brand(
309
319
  `The value ${JSON.stringify(error.value)} is not between 1 and 41 characters.`,
310
320
  );
311
321
  export type AppName = typeof AppName.Output;
322
+ /**
323
+ * Error produced when a value is not a valid {@link AppName}.
324
+ *
325
+ * @group Configuration
326
+ */
312
327
  export interface AppNameError extends TypeError<"AppName"> {
313
328
  readonly value: UrlSafeString;
314
329
  }
315
330
 
331
+ /**
332
+ * Stable valid {@link AppName} for tests and examples.
333
+ *
334
+ * @group Testing
335
+ */
316
336
  export const testAppName = /*#__PURE__*/ AppName.orThrow("AppName");
317
337
 
318
338
  /**
319
- * Local-first SQL database with typed queries, mutations, and sync.
339
+ * A local-first SQL database.
340
+ *
341
+ * Stores application data in SQLite on the device, so reads and writes work
342
+ * offline. Provides typed queries, mutations, and reactive subscriptions, with
343
+ * synchronization between devices. Persistent SQLite is encrypted with the
344
+ * {@link AppOwner} key, and synchronized data is end-to-end encrypted.
320
345
  *
321
- * TODO: Better docs.
346
+ * Tables whose names start with `_` stay local, even when the instance
347
+ * synchronizes other data. Use them for device-local data such as application
348
+ * settings or an app-owner registry.
349
+ *
350
+ * @group Core
322
351
  */
323
352
  export interface Evolu<
324
353
  S extends EvoluSchema = EvoluSchema,
@@ -340,32 +369,29 @@ export interface Evolu<
340
369
  * every row has a globally unique, conflict-free identifier without
341
370
  * coordination.
342
371
  *
343
- * Pass `onComplete` when follow-up work must wait until the mutation and its
344
- * query patches have been applied.
372
+ * Pass `onComplete` when follow-up work must wait until the mutation is
373
+ * stored and subscribed queries reflect it. It never runs when the database
374
+ * is unavailable; see {@link Evolu.loadQuery}. A stored change is not always
375
+ * visible; see {@link MutationOptions.onComplete}.
345
376
  *
346
377
  * ### Example
347
378
  *
348
379
  * ```ts
349
380
  * import {
350
381
  * assertType,
351
- * id,
382
+ * type TestEvoluSchema,
352
383
  * NonEmptyTrimmedString100,
353
384
  * type Evolu,
385
+ * type TestTodoId,
354
386
  * } from "@evolu/common";
355
387
  *
356
- * const TodoId = id("Todo");
357
- * type TodoId = typeof TodoId.Output;
358
- * const Schema = {
359
- * todo: { id: TodoId, title: NonEmptyTrimmedString100 },
360
- * };
361
- *
362
- * const insertTodo = (evolu: Evolu<typeof Schema>) =>
388
+ * const insertTodo = (evolu: Evolu<TestEvoluSchema>) =>
363
389
  * evolu.insert("todo", {
364
390
  * title: NonEmptyTrimmedString100.orThrow("Learn Evolu"),
365
391
  * }).id;
366
392
  *
367
393
  * const insertTodoAndNotify = (
368
- * evolu: Evolu<typeof Schema>,
394
+ * evolu: Evolu<TestEvoluSchema>,
369
395
  * onComplete: () => void,
370
396
  * ) =>
371
397
  * evolu.insert(
@@ -379,7 +405,7 @@ export interface Evolu<
379
405
  * ReturnType<typeof insertTodo>,
380
406
  * ReturnType<typeof insertTodoAndNotify>,
381
407
  * ],
382
- * [TodoId, TodoId]
408
+ * [TestTodoId, TestTodoId]
383
409
  * >();
384
410
  * ```
385
411
  *
@@ -397,30 +423,30 @@ export interface Evolu<
397
423
  * ```ts
398
424
  * import {
399
425
  * assertType,
400
- * id,
426
+ * type TestEvoluSchema,
401
427
  * NonEmptyTrimmedString100,
402
428
  * sqliteTrue,
403
429
  * type Evolu,
430
+ * type TestTodoId,
404
431
  * } from "@evolu/common";
405
432
  *
406
- * const TodoId = id("Todo");
407
- * type TodoId = typeof TodoId.Output;
408
- * const Schema = {
409
- * todo: { id: TodoId, title: NonEmptyTrimmedString100 },
410
- * };
411
- *
412
- * const renameTodo = (evolu: Evolu<typeof Schema>, todoId: TodoId) =>
433
+ * const renameTodo = (
434
+ * evolu: Evolu<TestEvoluSchema>,
435
+ * todoId: TestTodoId,
436
+ * ) =>
413
437
  * evolu.update("todo", {
414
438
  * id: todoId,
415
439
  * title: NonEmptyTrimmedString100.orThrow("Updated title"),
416
440
  * }).id;
417
441
  *
418
- * const softDeleteTodo = (evolu: Evolu<typeof Schema>, todoId: TodoId) =>
419
- * evolu.update("todo", { id: todoId, isDeleted: sqliteTrue }).id;
442
+ * const softDeleteTodo = (
443
+ * evolu: Evolu<TestEvoluSchema>,
444
+ * todoId: TestTodoId,
445
+ * ) => evolu.update("todo", { id: todoId, isDeleted: sqliteTrue }).id;
420
446
  *
421
447
  * assertType<
422
448
  * [ReturnType<typeof renameTodo>, ReturnType<typeof softDeleteTodo>],
423
- * [TodoId, TodoId]
449
+ * [TestTodoId, TestTodoId]
424
450
  * >();
425
451
  * ```
426
452
  *
@@ -446,24 +472,20 @@ export interface Evolu<
446
472
  * import {
447
473
  * assertType,
448
474
  * createIdFromString,
449
- * id,
475
+ * type TestEvoluSchema,
450
476
  * NonEmptyTrimmedString100,
451
477
  * type Evolu,
478
+ * TestTodoId,
452
479
  * } from "@evolu/common";
453
480
  *
454
- * const TodoId = id("Todo");
455
- * type TodoId = typeof TodoId.Output;
456
- * const Schema = {
457
- * todo: { id: TodoId, title: NonEmptyTrimmedString100 },
458
- * };
459
- * const stableId = TodoId.orThrow(createIdFromString("my-todo-1"));
460
- * const upsertTodo = (evolu: Evolu<typeof Schema>) =>
481
+ * const stableId = TestTodoId.orThrow(createIdFromString("my-todo-1"));
482
+ * const upsertTodo = (evolu: Evolu<TestEvoluSchema>) =>
461
483
  * evolu.upsert("todo", {
462
484
  * id: stableId,
463
485
  * title: NonEmptyTrimmedString100.orThrow("Learn Evolu"),
464
486
  * }).id;
465
487
  *
466
- * assertType<ReturnType<typeof upsertTodo>, TodoId>();
488
+ * assertType<ReturnType<typeof upsertTodo>, TestTodoId>();
467
489
  * ```
468
490
  *
469
491
  * @see {@link Mutation}
@@ -473,14 +495,19 @@ export interface Evolu<
473
495
  /**
474
496
  * Load {@link Query} and return a promise with {@link QueryRows}.
475
497
  *
476
- * The returned promise always resolves successfully because there is no
477
- * reason why loading should fail. All data are local, and the query is
478
- * typed.
498
+ * The returned promise always resolves successfully because all data are
499
+ * local and the query is typed. If the database refuses startup, unanswered
500
+ * loads stay pending until disposal resolves them with empty rows. Observe
501
+ * {@link EvoluErrorDep.evoluError} to display the refusal independently of
502
+ * query loading.
479
503
  *
480
504
  * Loading is batched. Returned promises are cached while pending and can be
481
- * reused after fulfillment until mutation-driven invalidation, which prevents
482
- * redundant database queries and supports React Suspense (stable references
483
- * while pending).
505
+ * reused after fulfillment, which prevents redundant database queries and
506
+ * supports React Suspense (stable references while pending). A mutation or
507
+ * incoming sync invalidates the cache of an unsubscribed query, so its next
508
+ * load reads again. A subscribed query keeps its cached rows and the
509
+ * subscription refreshes them, so a load can return rows that a pending
510
+ * refresh is about to replace.
484
511
  *
485
512
  * To subscribe a query for automatic updates, use
486
513
  * {@link Evolu.subscribeQuery}.
@@ -491,20 +518,17 @@ export interface Evolu<
491
518
  * import {
492
519
  * assertType,
493
520
  * createQueryBuilder,
494
- * id,
495
- * NonEmptyTrimmedString100,
521
+ * testEvoluSchema,
522
+ * type TestEvoluSchema,
496
523
  * type Evolu,
497
524
  * type QueryRows,
498
525
  * } from "@evolu/common";
499
526
  *
500
- * const Schema = {
501
- * todo: { id: id("Todo"), title: NonEmptyTrimmedString100 },
502
- * };
503
- * const createQuery = createQueryBuilder(Schema);
527
+ * const createQuery = createQueryBuilder(testEvoluSchema);
504
528
  * const allTodos = createQuery((db) =>
505
529
  * db.selectFrom("todo").selectAll(),
506
530
  * );
507
- * const loadTodos = async (evolu: Evolu<typeof Schema>) => {
531
+ * const loadTodos = async (evolu: Evolu<TestEvoluSchema>) => {
508
532
  * const rows = await evolu.loadQuery(allTodos);
509
533
  * return rows;
510
534
  * };
@@ -529,31 +553,22 @@ export interface Evolu<
529
553
  * ```ts
530
554
  * import {
531
555
  * assertType,
532
- * createIdFromString,
533
556
  * createQueryBuilder,
534
- * id,
535
- * NonEmptyTrimmedString100,
557
+ * testEvoluSchema,
558
+ * type TestEvoluSchema,
559
+ * testTodoId,
536
560
  * type Evolu,
537
561
  * type QueryRows,
538
562
  * } from "@evolu/common";
539
563
  *
540
- * const TodoId = id("Todo");
541
- * type TodoId = typeof TodoId.Output;
542
- * const Schema = {
543
- * todo: { id: TodoId, title: NonEmptyTrimmedString100 },
544
- * };
545
- * const createQuery = createQueryBuilder(Schema);
564
+ * const createQuery = createQueryBuilder(testEvoluSchema);
546
565
  * const allTodos = createQuery((db) =>
547
566
  * db.selectFrom("todo").select(["id", "title"]),
548
567
  * );
549
- * const todoById = (todoId: TodoId) =>
550
- * createQuery((db) =>
551
- * db.selectFrom("todo").select("title").where("id", "=", todoId),
552
- * );
553
- * const firstTodo = todoById(
554
- * TodoId.orThrow(createIdFromString("first-todo")),
568
+ * const firstTodo = createQuery((db) =>
569
+ * db.selectFrom("todo").select("title").where("id", "=", testTodoId),
555
570
  * );
556
- * const loadTodoQueries = (evolu: Evolu<typeof Schema>) =>
571
+ * const loadTodoQueries = (evolu: Evolu<TestEvoluSchema>) =>
557
572
  * evolu.loadQueries([allTodos, firstTodo]);
558
573
  *
559
574
  * assertType<
@@ -572,28 +587,28 @@ export interface Evolu<
572
587
  /**
573
588
  * Subscribe to {@link Query} {@link QueryRows} changes.
574
589
  *
590
+ * Cached rows invalidated before subscription are refreshed. Subscribing
591
+ * alone does not load a query that has no cached rows.
592
+ *
575
593
  * ### Example
576
594
  *
577
595
  * ```ts
578
596
  * import {
579
597
  * assertType,
580
598
  * createQueryBuilder,
581
- * id,
582
- * NonEmptyTrimmedString100,
599
+ * testEvoluSchema,
600
+ * type TestEvoluSchema,
583
601
  * type Evolu,
584
602
  * type QueryRows,
585
603
  * type Unsubscribe,
586
604
  * } from "@evolu/common";
587
605
  *
588
- * const Schema = {
589
- * todo: { id: id("Todo"), title: NonEmptyTrimmedString100 },
590
- * };
591
- * const createQuery = createQueryBuilder(Schema);
606
+ * const createQuery = createQueryBuilder(testEvoluSchema);
592
607
  * const allTodos = createQuery((db) =>
593
608
  * db.selectFrom("todo").select("title"),
594
609
  * );
595
610
  * const subscribeToTodos = (
596
- * evolu: Evolu<typeof Schema>,
611
+ * evolu: Evolu<TestEvoluSchema>,
597
612
  * onRows: (rows: QueryRows<typeof allTodos.Row>) => void,
598
613
  * ) =>
599
614
  * evolu.subscribeQuery(allTodos)(() => {
@@ -616,20 +631,17 @@ export interface Evolu<
616
631
  * import {
617
632
  * assertType,
618
633
  * createQueryBuilder,
619
- * id,
620
- * NonEmptyTrimmedString100,
634
+ * testEvoluSchema,
635
+ * type TestEvoluSchema,
621
636
  * type Evolu,
622
637
  * type QueryRows,
623
638
  * } from "@evolu/common";
624
639
  *
625
- * const Schema = {
626
- * todo: { id: id("Todo"), title: NonEmptyTrimmedString100 },
627
- * };
628
- * const createQuery = createQueryBuilder(Schema);
640
+ * const createQuery = createQueryBuilder(testEvoluSchema);
629
641
  * const allTodos = createQuery((db) =>
630
642
  * db.selectFrom("todo").select("title"),
631
643
  * );
632
- * const getTodos = (evolu: Evolu<typeof Schema>) =>
644
+ * const getTodos = (evolu: Evolu<TestEvoluSchema>) =>
633
645
  * evolu.getQueryRows(allTodos);
634
646
  *
635
647
  * assertType<
@@ -647,7 +659,8 @@ export interface Evolu<
647
659
  * of starting parallel exports.
648
660
  *
649
661
  * The pending promise rejects if this {@link Evolu} instance is disposed
650
- * before export completion.
662
+ * before export completion. If the database refuses startup, export stays
663
+ * pending until disposal; see {@link Evolu.loadQuery}.
651
664
  */
652
665
  readonly exportDatabase: () => Promise<Uint8Array<ArrayBuffer>>;
653
666
 
@@ -703,21 +716,15 @@ export interface Evolu<
703
716
  * ```ts
704
717
  * import {
705
718
  * assertEqual,
706
- * createAppOwner,
707
719
  * createOwnerWebSocketTransport,
708
- * createOwnerSecret,
709
- * createRandomBytes,
710
720
  * deriveShardOwner,
721
+ * testAppOwner,
711
722
  * type Evolu,
712
723
  * type ReadonlyOwner,
713
724
  * type UnuseOwner,
714
725
  * } from "@evolu/common";
715
726
  *
716
- * // Create once, persist the mnemonic securely, and restore it on later runs.
717
- * const appOwner = createAppOwner(
718
- * createOwnerSecret({ randomBytes: createRandomBytes() }),
719
- * );
720
- * const shardOwner = deriveShardOwner(appOwner, ["todos", 1]);
727
+ * const shardOwner = deriveShardOwner(testAppOwner, ["todos", 1]);
721
728
  * const shardTransport = createOwnerWebSocketTransport({
722
729
  * url: "wss://relay.example.com",
723
730
  * ownerId: shardOwner.id,
@@ -753,17 +760,90 @@ export interface Evolu<
753
760
  owner: ReadonlyOwner | Owner,
754
761
  transports?: NonEmptyReadonlyArray<OwnerTransport>,
755
762
  ) => UnuseOwner;
763
+
764
+ /**
765
+ * Requests a new synchronization round for an active {@link OwnerId}.
766
+ *
767
+ * Reconciles locally stored changes, including writes previously rejected
768
+ * with {@link ProtocolQuotaError}, through the owner's active transports. Call
769
+ * this after the relay provider confirms additional quota is available.
770
+ * Existing connections and {@link Evolu.useOwner} registrations are retained,
771
+ * including registrations shared by multiple instances or tabs.
772
+ *
773
+ * The owner must have an active writable registration in this database.
774
+ * Unregistered or read-only owners are ignored. Requests are skipped while
775
+ * all of the owner's transports are closed; they synchronize when they
776
+ * reopen. Disposal drops locally buffered requests. Calling a disposed
777
+ * instance throws, like other Evolu operations.
778
+ *
779
+ * Returns immediately, without waiting for synchronization to complete.
780
+ * Errors are reported through {@link EvoluErrorDep.evoluError}.
781
+ *
782
+ * ### Example
783
+ *
784
+ * ```ts
785
+ * import { assertType, type Evolu, type OwnerId } from "@evolu/common";
786
+ *
787
+ * // Call after successfully increasing the affected owner's relay quota.
788
+ * const onQuotaIncreased = (evolu: Evolu, ownerId: OwnerId) => {
789
+ * evolu.requestSync(ownerId);
790
+ * };
791
+ *
792
+ * assertType<
793
+ * typeof onQuotaIncreased,
794
+ * (evolu: Evolu, ownerId: OwnerId) => void
795
+ * >();
796
+ * ```
797
+ */
798
+ readonly requestSync: (ownerId: OwnerId) => void;
756
799
  }
757
800
 
758
- /** Function returned by {@link Evolu.useOwner} to stop using an Owner for sync. */
801
+ /**
802
+ * Function returned by {@link Evolu.useOwner} to stop using an Owner for sync.
803
+ *
804
+ * @group Core
805
+ */
759
806
  export type UnuseOwner = () => void;
760
807
 
808
+ /**
809
+ * Represents errors that can occur in {@link Evolu}.
810
+ *
811
+ * @group Core
812
+ */
813
+ export type EvoluError =
814
+ | DecryptWithXChaCha20Poly1305Error
815
+ | OtherBuildRunningError
816
+ | ProtocolError
817
+ | StorageQuotaError
818
+ | UnknownError
819
+ | UnsupportedDbVersionError;
820
+
821
+ /**
822
+ * Dependency wrapper for the shared {@link EvoluError} store.
823
+ *
824
+ * @group Construction
825
+ */
761
826
  export interface EvoluErrorDep {
762
827
  /**
763
828
  * {@link ReadonlyStore} of {@link EvoluError} shared by all {@link Evolu}
764
829
  * instances created from the same {@link createEvoluDeps} result.
765
830
  *
831
+ * Starts at `null` and otherwise holds the latest reported error until the
832
+ * first {@link UnsupportedDbVersionError}. That refusal remains for the
833
+ * lifetime of these dependencies, even if a tenant is disposed and recreated.
834
+ * Later errors are still logged but do not replace it or notify this store's
835
+ * subscribers. Fresh dependencies start with a fresh error store. An
836
+ * {@link OtherBuildRunningError} reports a wait, so the store returns to
837
+ * `null` when the wait ends, unless another error replaced it.
838
+ *
766
839
  * Subscribe once to show user-facing error messages across all instances.
840
+ * While a refused database's tenant remains alive, the SharedWorker sends the
841
+ * refusal to each tab once, including tabs that connect later, and starts no
842
+ * replacement database workers. After all instances release that tenant and
843
+ * it is disposed when idle, creating another instance retries startup and may
844
+ * send the refusal again. On the web, a refused tab first reloads once
845
+ * instead; see {@link UnsupportedDbVersionError}. Show that blocking message
846
+ * outside any query-loading boundary, so pending queries do not hide it.
767
847
  *
768
848
  * ### Example
769
849
  *
@@ -771,60 +851,113 @@ export interface EvoluErrorDep {
771
851
  * import {
772
852
  * assertEqual,
773
853
  * createStore,
774
- * Millis,
775
854
  * type EvoluError,
776
855
  * } from "@evolu/common";
777
856
  * import type { EvoluErrorDep } from "@evolu/common/local-first";
778
857
  *
858
+ * // The message for the current error, or null for none.
859
+ * const errorMessage = (error: EvoluError | null): string | null => {
860
+ * if (!error) return null;
861
+ * // oxlint-disable-next-line typescript/switch-exhaustiveness-check -- The default handles every other EvoluError.
862
+ * switch (error.type) {
863
+ * case "UnsupportedDbVersionError":
864
+ * return "Your data requires a newer version of this app. Please update it.";
865
+ * case "OtherBuildRunningError":
866
+ * return "This app is open in another tab with a different version. Close that tab to continue.";
867
+ * default:
868
+ * return "Something went wrong. Please try again.";
869
+ * }
870
+ * };
871
+ *
779
872
  * // Stand-in for run.deps.evoluError from createEvoluDeps.
780
873
  * using evoluError = createStore<EvoluError | null>(null);
781
874
  * const deps = { evoluError } satisfies EvoluErrorDep;
782
- * let displayedMessage = "";
783
- * const showMessage = (message: string) => {
784
- * displayedMessage = message;
785
- * };
875
+ * // What the app showed over time; null hides the message.
876
+ * const shown: Array<string | null> = [];
786
877
  *
787
878
  * deps.evoluError.subscribe(() => {
788
- * const error = deps.evoluError.get();
789
- * if (!error) return;
790
- *
791
- * // oxlint-disable-next-line typescript/switch-exhaustiveness-check -- The default intentionally handles every other EvoluError.
792
- * switch (error.type) {
793
- * case "TimestampDriftError":
794
- * // Show guidance specific to the detected error.
795
- * showMessage(
796
- * "Your system clock appears incorrect. Please fix it.",
797
- * );
798
- * break;
799
- * default:
800
- * // Show a generic user message for other operational errors.
801
- * showMessage("Something went wrong. Please try again.");
802
- * }
879
+ * shown.push(errorMessage(deps.evoluError.get()));
803
880
  * });
804
881
  *
805
- * deps.evoluError.set({
806
- * type: "TimestampDriftError",
807
- * next: Millis.orThrow(360000),
808
- * now: Millis.orThrow(0),
809
- * });
810
- * assertEqual(
811
- * displayedMessage,
812
- * "Your system clock appears incorrect. Please fix it.",
813
- * );
882
+ * // Another version keeps this tab waiting, then the wait ends.
883
+ * deps.evoluError.set({ type: "OtherBuildRunningError" });
884
+ * deps.evoluError.set(null);
885
+ *
886
+ * assertEqual(shown, [
887
+ * "This app is open in another tab with a different version. Close that tab to continue.",
888
+ * null,
889
+ * ]);
814
890
  * ```
815
891
  */
816
892
  readonly evoluError: ReadonlyStore<EvoluError | null>;
817
893
  }
818
894
 
895
+ /**
896
+ * Dependency wrapper for the shared {@link SyncState} store.
897
+ *
898
+ * @group Construction
899
+ */
900
+ export interface SyncStateDep {
901
+ /**
902
+ * {@link ReadonlyStore} of the latest {@link SyncState} shared by all
903
+ * {@link Evolu} instances, or null before the shared worker sends its first
904
+ * snapshot. Derive what to show from it, such as one indicator per relay, or
905
+ * use {@link syncStateToOwnerSyncStates} for one state per owner.
906
+ *
907
+ * ### Example
908
+ *
909
+ * ```ts
910
+ * import {
911
+ * assertEqual,
912
+ * createId,
913
+ * createStore,
914
+ * testCreateDeps,
915
+ * } from "@evolu/common";
916
+ * import type {
917
+ * SyncState,
918
+ * SyncStateDep,
919
+ * } from "@evolu/common/local-first";
920
+ *
921
+ * const openRelayLabels = (deps: SyncStateDep): ReadonlyArray<string> =>
922
+ * (deps.syncState.get()?.transports ?? [])
923
+ * .filter(({ readyState }) => readyState === "open")
924
+ * .map(({ label }) => label);
925
+ *
926
+ * using syncState = createStore<SyncState | null>(null);
927
+ * assertEqual(openRelayLabels({ syncState }), []);
928
+ *
929
+ * const deps = testCreateDeps();
930
+ * syncState.set({
931
+ * transports: [
932
+ * {
933
+ * id: createId<"SyncTransport">(deps),
934
+ * label: "wss://relay.example",
935
+ * readyState: "open",
936
+ * openedAt: null,
937
+ * closedAt: null,
938
+ * error: null,
939
+ * },
940
+ * ],
941
+ * tenants: [],
942
+ * });
943
+ * assertEqual(openRelayLabels({ syncState }), ["wss://relay.example"]);
944
+ * ```
945
+ */
946
+ readonly syncState: ReadonlyStore<SyncState | null>;
947
+ }
948
+
819
949
  /**
820
950
  * Shared platform dependencies for creating {@link Evolu} instances.
821
951
  *
822
952
  * Includes platform adapters, the shared {@link EvoluErrorDep.evoluError} store,
823
953
  * and disposal for owned resources.
954
+ *
955
+ * @group Construction
824
956
  */
825
957
  export type EvoluDeps = EvoluPlatformDeps &
826
958
  ConsoleDep &
827
959
  EvoluErrorDep &
960
+ SyncStateDep &
828
961
  Disposable;
829
962
 
830
963
  /**
@@ -832,6 +965,8 @@ export type EvoluDeps = EvoluPlatformDeps &
832
965
  *
833
966
  * Provides worker and channel adapters plus optional platform integrations for
834
967
  * logging and synchronous UI flush.
968
+ *
969
+ * @group Construction
835
970
  */
836
971
  export type EvoluPlatformDeps = CreateDbWorkerDep &
837
972
  CreateBroadcastChannelDep &
@@ -848,36 +983,50 @@ export type EvoluPlatformDeps = CreateDbWorkerDep &
848
983
  *
849
984
  * Call this once per platform and reuse the returned deps when creating
850
985
  * multiple Evolu instances. The returned deps object owns long-lived resources
851
- * such as worker channels and the shared {@link EvoluErrorDep.evoluError}
852
- * store.
986
+ * such as worker channels and the shared {@link EvoluErrorDep.evoluError} and
987
+ * {@link SyncStateDep.syncState} stores.
853
988
  *
854
989
  * Dispose it only during app shutdown.
990
+ *
991
+ * @group Construction
855
992
  */
856
993
  export const createEvoluDeps = (deps: EvoluPlatformDeps): EvoluDeps => {
857
994
  const { createBroadcastChannel, sharedWorker } = deps;
858
995
  const console = deps.console ?? createConsole();
996
+ // Opened when the worker connects.
997
+ let syncStateBroadcastChannel: BroadcastChannel<SyncState> | null = null;
998
+ let tabLeaderElection: Disposable | null = null;
859
999
 
860
1000
  using disposer = new DisposableStack();
861
1001
  disposer.use(sharedWorker);
862
1002
  const evoluError = disposer.use(createStore<EvoluError | null>(null));
1003
+ const syncState = disposer.use(createStore<SyncState | null>(null));
1004
+ disposer.defer(() => {
1005
+ syncStateBroadcastChannel?.[Symbol.dispose]();
1006
+ });
863
1007
 
864
1008
  const consoleEntryOrErrorBroadcastChannel = disposer.use(
865
1009
  createBroadcastChannel<ConsoleEntryOrError>(
866
1010
  consoleEntryOrErrorBroadcastChannelName,
867
1011
  ),
868
1012
  );
1013
+ const setEvoluError = (error: EvoluError): void => {
1014
+ if (evoluError.get()?.type === "UnsupportedDbVersionError") return;
1015
+ evoluError.set(error);
1016
+ };
1017
+
869
1018
  consoleEntryOrErrorBroadcastChannel.onMessage = (message) => {
870
1019
  switch (message.type) {
871
1020
  case "ConsoleEntry":
872
1021
  console.write(message.entry);
873
1022
  // Fallback channel for unexpected errors without EvoluError typing.
874
1023
  if (message.entry.method === "error") {
875
- evoluError.set(createUnknownError(message.entry.args));
1024
+ setEvoluError(createUnknownError(message.entry.args));
876
1025
  }
877
1026
  break;
878
1027
 
879
1028
  case "Error":
880
- evoluError.set(message.error);
1029
+ setEvoluError(message.error);
881
1030
  // Keep typed errors visible in logs as operational failures.
882
1031
  console.error(message.error);
883
1032
  break;
@@ -888,23 +1037,69 @@ export const createEvoluDeps = (deps: EvoluPlatformDeps): EvoluDeps => {
888
1037
  };
889
1038
 
890
1039
  sharedWorker.port.onMessage = (message) => {
891
- deps.createDbWorker().postMessage(message, [message.port]);
1040
+ switch (message.type) {
1041
+ case "DbWorkerInit":
1042
+ deps.createDbWorker().postMessage(message, [message.port]);
1043
+ break;
1044
+
1045
+ case "Error":
1046
+ // Sent to this tab only: its database refused startup, or its worker
1047
+ // still waits for another build.
1048
+ setEvoluError(message.error);
1049
+ console.error(message.error);
1050
+ break;
1051
+
1052
+ case "Waiting":
1053
+ case "StorageUnavailable":
1054
+ // Platform adapters act on these; see Builds and Storage in the Shared
1055
+ // module.
1056
+ break;
1057
+
1058
+ case "Connected": {
1059
+ assert(!syncStateBroadcastChannel, "The shared worker connects once.");
1060
+ // The wait the error reported is over.
1061
+ if (evoluError.get()?.type === "OtherBuildRunningError") {
1062
+ evoluError.set(null);
1063
+ }
1064
+ // The worker is the channel's only sender, and one sender's messages
1065
+ // arrive in order, so the last one is current.
1066
+ syncStateBroadcastChannel = createBroadcastChannel<SyncState>(
1067
+ message.syncStateChannelName,
1068
+ );
1069
+ syncStateBroadcastChannel.onMessage = (state) => {
1070
+ syncState.set(state);
1071
+ };
1072
+ // Asking only after listening misses no snapshot.
1073
+ sharedWorker.port.postMessage({ type: "RequestSyncState" });
1074
+ // Only this worker's tabs compete, so a tab of another worker never
1075
+ // hosts its DbWorkers.
1076
+ tabLeaderElection = acquireLeaderLockCallback(deps)(
1077
+ `tab-${message.workerId}`,
1078
+ () => {
1079
+ sharedWorker.port.postMessage({
1080
+ type: "AnnounceTabLeader",
1081
+ consoleLevel: console.getLevel(),
1082
+ });
1083
+ },
1084
+ );
1085
+ break;
1086
+ }
1087
+
1088
+ default:
1089
+ exhaustiveCheck(message);
1090
+ }
892
1091
  };
893
1092
 
894
- disposer.use(
895
- acquireLeaderLockCallback(deps)("tab", () => {
896
- sharedWorker.port.postMessage({
897
- type: "AnnounceTabLeader",
898
- consoleLevel: console.getLevel(),
899
- });
900
- }),
901
- );
1093
+ disposer.defer(() => {
1094
+ tabLeaderElection?.[Symbol.dispose]();
1095
+ });
902
1096
 
903
1097
  return disposable<EvoluDeps>(
904
1098
  {
905
1099
  ...deps,
906
1100
  console,
907
1101
  evoluError,
1102
+ syncState,
908
1103
  },
909
1104
  disposer,
910
1105
  );
@@ -913,6 +1108,8 @@ export const createEvoluDeps = (deps: EvoluPlatformDeps): EvoluDeps => {
913
1108
  /**
914
1109
  * Creates an {@link Evolu} instance from {@link EvoluSchema} and
915
1110
  * {@link EvoluConfig}.
1111
+ *
1112
+ * @group Construction
916
1113
  */
917
1114
  export const createEvolu =
918
1115
  <S extends EvoluSchema>(
@@ -1091,10 +1288,7 @@ export const createEvolu =
1091
1288
  case "RefreshQueries": {
1092
1289
  releaseUnsubscribedLoadingPromises();
1093
1290
 
1094
- const queries = new Set<Query>([
1095
- ...loadingPromisesByQuery.keys(),
1096
- ...subscribedQueriesRefCount.keys(),
1097
- ]);
1291
+ const queries = new Set<Query>(subscribedQueriesRefCount.keys());
1098
1292
 
1099
1293
  if (isNonEmptySet(queries)) postMessage({ type: "Query", queries });
1100
1294
  break;
@@ -1311,6 +1505,27 @@ export const createEvolu =
1311
1505
  listener();
1312
1506
  });
1313
1507
 
1508
+ // Invalidation can happen after loading but before a framework
1509
+ // subscribes. Register first so subsequent invalidations also
1510
+ // refresh this query.
1511
+ const loadingPromise = loadingPromisesByQuery.get(query);
1512
+ if (loadingPromise?.releaseOnResolve) {
1513
+ // Retain the pending promise for its awaiters, but queue a read
1514
+ // after the mutation or sync that invalidated its result.
1515
+ loadingPromise.releaseOnResolve = false;
1516
+ queryBatch.push(query);
1517
+ } else if (!loadingPromise) {
1518
+ const rows = rowsByQueryMapStore.get().get(query);
1519
+ if (rows) {
1520
+ // Queue a read, but keep cached rows available to React use()
1521
+ // so a render before the worker responds does not suspend.
1522
+ void loadQuery(query);
1523
+ const entry = loadingPromisesByQuery.get(query);
1524
+ assertNotUndefined(entry);
1525
+ fulfillLoadingPromise(entry, rows);
1526
+ }
1527
+ }
1528
+
1314
1529
  return () => {
1315
1530
  assert(
1316
1531
  isSubscribed,
@@ -1347,6 +1562,13 @@ export const createEvolu =
1347
1562
  },
1348
1563
 
1349
1564
  useOwner,
1565
+ requestSync: (ownerId) => {
1566
+ // Do not let the forced mutation flush overtake pending owner actions.
1567
+ useOwnerBatch.flushNow();
1568
+ // Queue preceding mutations before the reconciliation request.
1569
+ mutateBatch.flushNow();
1570
+ useOwnerBatch.push({ action: "sync", ownerId });
1571
+ },
1350
1572
  },
1351
1573
  disposer,
1352
1574
  ),