@evolu/common 8.9.0 → 8.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/dist/src/Bytes.d.ts +647 -0
  2. package/dist/src/Bytes.d.ts.map +1 -0
  3. package/dist/src/{Binary.js → Bytes.js} +266 -16
  4. package/dist/src/Config.d.ts +142 -0
  5. package/dist/src/Config.d.ts.map +1 -0
  6. package/dist/src/Config.js +181 -0
  7. package/dist/src/Console.d.ts +62 -7
  8. package/dist/src/Console.d.ts.map +1 -1
  9. package/dist/src/Console.js +20 -4
  10. package/dist/src/Crypto.d.ts +76 -4
  11. package/dist/src/Crypto.d.ts.map +1 -1
  12. package/dist/src/Crypto.js +55 -4
  13. package/dist/src/Error.d.ts +45 -0
  14. package/dist/src/Error.d.ts.map +1 -1
  15. package/dist/src/Error.js +69 -0
  16. package/dist/src/Fs.d.ts +376 -0
  17. package/dist/src/Fs.d.ts.map +1 -0
  18. package/dist/src/Fs.js +113 -0
  19. package/dist/src/Identicon.d.ts +2 -2
  20. package/dist/src/Identicon.js +2 -2
  21. package/dist/src/LeakDetector.d.ts +22 -3
  22. package/dist/src/LeakDetector.d.ts.map +1 -1
  23. package/dist/src/LeakDetector.js +12 -2
  24. package/dist/src/LockManager.d.ts +8 -0
  25. package/dist/src/LockManager.d.ts.map +1 -1
  26. package/dist/src/LockManager.js +6 -0
  27. package/dist/src/Number.d.ts +50 -7
  28. package/dist/src/Number.d.ts.map +1 -1
  29. package/dist/src/Number.js +47 -8
  30. package/dist/src/Object.d.ts +32 -0
  31. package/dist/src/Object.d.ts.map +1 -1
  32. package/dist/src/Object.js +46 -0
  33. package/dist/src/Platform.d.ts +47 -7
  34. package/dist/src/Platform.d.ts.map +1 -1
  35. package/dist/src/Platform.js +24 -5
  36. package/dist/src/Random.d.ts +25 -2
  37. package/dist/src/Random.d.ts.map +1 -1
  38. package/dist/src/Random.js +14 -2
  39. package/dist/src/Resource.d.ts +156 -1
  40. package/dist/src/Resource.d.ts.map +1 -1
  41. package/dist/src/Resource.js +201 -72
  42. package/dist/src/Schedule.d.ts +11 -10
  43. package/dist/src/Schedule.d.ts.map +1 -1
  44. package/dist/src/Schedule.js +1 -1
  45. package/dist/src/Sqlite.d.ts +132 -16
  46. package/dist/src/Sqlite.d.ts.map +1 -1
  47. package/dist/src/Sqlite.js +64 -10
  48. package/dist/src/Task.d.ts +15 -4
  49. package/dist/src/Task.d.ts.map +1 -1
  50. package/dist/src/Task.js +41 -15
  51. package/dist/src/Test.d.ts +9 -0
  52. package/dist/src/Test.d.ts.map +1 -1
  53. package/dist/src/Test.js +4 -0
  54. package/dist/src/Time.d.ts +179 -20
  55. package/dist/src/Time.d.ts.map +1 -1
  56. package/dist/src/Time.js +95 -6
  57. package/dist/src/Type.d.ts +3056 -1539
  58. package/dist/src/Type.d.ts.map +1 -1
  59. package/dist/src/Type.js +2548 -584
  60. package/dist/src/WebSocket.d.ts +164 -13
  61. package/dist/src/WebSocket.d.ts.map +1 -1
  62. package/dist/src/WebSocket.js +133 -24
  63. package/dist/src/Worker.d.ts +90 -8
  64. package/dist/src/Worker.d.ts.map +1 -1
  65. package/dist/src/Worker.js +28 -2
  66. package/dist/src/index.d.ts +9 -8
  67. package/dist/src/index.d.ts.map +1 -1
  68. package/dist/src/index.js +5 -4
  69. package/dist/src/intl/_en.d.ts +24 -1
  70. package/dist/src/intl/_en.d.ts.map +1 -1
  71. package/dist/src/intl/_en.js +20 -0
  72. package/dist/src/intl/ar.d.ts +24 -1
  73. package/dist/src/intl/ar.d.ts.map +1 -1
  74. package/dist/src/intl/ar.js +20 -0
  75. package/dist/src/intl/bn.d.ts +24 -1
  76. package/dist/src/intl/bn.d.ts.map +1 -1
  77. package/dist/src/intl/bn.js +20 -0
  78. package/dist/src/intl/ca.d.ts +24 -1
  79. package/dist/src/intl/ca.d.ts.map +1 -1
  80. package/dist/src/intl/ca.js +20 -0
  81. package/dist/src/intl/cs.d.ts +24 -1
  82. package/dist/src/intl/cs.d.ts.map +1 -1
  83. package/dist/src/intl/cs.js +20 -0
  84. package/dist/src/intl/da.d.ts +24 -1
  85. package/dist/src/intl/da.d.ts.map +1 -1
  86. package/dist/src/intl/da.js +20 -0
  87. package/dist/src/intl/de.d.ts +24 -1
  88. package/dist/src/intl/de.d.ts.map +1 -1
  89. package/dist/src/intl/de.js +20 -0
  90. package/dist/src/intl/el.d.ts +24 -1
  91. package/dist/src/intl/el.d.ts.map +1 -1
  92. package/dist/src/intl/el.js +20 -0
  93. package/dist/src/intl/es.d.ts +24 -1
  94. package/dist/src/intl/es.d.ts.map +1 -1
  95. package/dist/src/intl/es.js +20 -0
  96. package/dist/src/intl/fa.d.ts +24 -1
  97. package/dist/src/intl/fa.d.ts.map +1 -1
  98. package/dist/src/intl/fa.js +20 -0
  99. package/dist/src/intl/fi.d.ts +24 -1
  100. package/dist/src/intl/fi.d.ts.map +1 -1
  101. package/dist/src/intl/fi.js +20 -0
  102. package/dist/src/intl/fil.d.ts +24 -1
  103. package/dist/src/intl/fil.d.ts.map +1 -1
  104. package/dist/src/intl/fil.js +20 -0
  105. package/dist/src/intl/fr.d.ts +24 -1
  106. package/dist/src/intl/fr.d.ts.map +1 -1
  107. package/dist/src/intl/fr.js +20 -0
  108. package/dist/src/intl/he.d.ts +24 -1
  109. package/dist/src/intl/he.d.ts.map +1 -1
  110. package/dist/src/intl/he.js +20 -0
  111. package/dist/src/intl/hi.d.ts +24 -1
  112. package/dist/src/intl/hi.d.ts.map +1 -1
  113. package/dist/src/intl/hi.js +20 -0
  114. package/dist/src/intl/hr.d.ts +24 -1
  115. package/dist/src/intl/hr.d.ts.map +1 -1
  116. package/dist/src/intl/hr.js +20 -0
  117. package/dist/src/intl/hu.d.ts +22 -1
  118. package/dist/src/intl/hu.d.ts.map +1 -1
  119. package/dist/src/intl/hu.js +18 -0
  120. package/dist/src/intl/id.d.ts +24 -1
  121. package/dist/src/intl/id.d.ts.map +1 -1
  122. package/dist/src/intl/id.js +20 -0
  123. package/dist/src/intl/it.d.ts +24 -1
  124. package/dist/src/intl/it.d.ts.map +1 -1
  125. package/dist/src/intl/it.js +20 -0
  126. package/dist/src/intl/ja.d.ts +24 -1
  127. package/dist/src/intl/ja.d.ts.map +1 -1
  128. package/dist/src/intl/ja.js +20 -0
  129. package/dist/src/intl/ko.d.ts +24 -1
  130. package/dist/src/intl/ko.d.ts.map +1 -1
  131. package/dist/src/intl/ko.js +20 -0
  132. package/dist/src/intl/ml.d.ts +24 -1
  133. package/dist/src/intl/ml.d.ts.map +1 -1
  134. package/dist/src/intl/ml.js +20 -0
  135. package/dist/src/intl/mr.d.ts +24 -1
  136. package/dist/src/intl/mr.d.ts.map +1 -1
  137. package/dist/src/intl/mr.js +20 -0
  138. package/dist/src/intl/ms.d.ts +24 -1
  139. package/dist/src/intl/ms.d.ts.map +1 -1
  140. package/dist/src/intl/ms.js +20 -0
  141. package/dist/src/intl/nb.d.ts +22 -1
  142. package/dist/src/intl/nb.d.ts.map +1 -1
  143. package/dist/src/intl/nb.js +18 -0
  144. package/dist/src/intl/nl.d.ts +24 -1
  145. package/dist/src/intl/nl.d.ts.map +1 -1
  146. package/dist/src/intl/nl.js +20 -0
  147. package/dist/src/intl/pa.d.ts +24 -1
  148. package/dist/src/intl/pa.d.ts.map +1 -1
  149. package/dist/src/intl/pa.js +20 -0
  150. package/dist/src/intl/pl.d.ts +23 -0
  151. package/dist/src/intl/pl.d.ts.map +1 -1
  152. package/dist/src/intl/pl.js +20 -0
  153. package/dist/src/intl/pt-BR.d.ts +24 -1
  154. package/dist/src/intl/pt-BR.d.ts.map +1 -1
  155. package/dist/src/intl/pt-BR.js +20 -0
  156. package/dist/src/intl/pt.d.ts +24 -1
  157. package/dist/src/intl/pt.d.ts.map +1 -1
  158. package/dist/src/intl/pt.js +20 -0
  159. package/dist/src/intl/ro.d.ts +24 -1
  160. package/dist/src/intl/ro.d.ts.map +1 -1
  161. package/dist/src/intl/ro.js +20 -0
  162. package/dist/src/intl/sk.d.ts +24 -1
  163. package/dist/src/intl/sk.d.ts.map +1 -1
  164. package/dist/src/intl/sk.js +20 -0
  165. package/dist/src/intl/sl.d.ts +24 -1
  166. package/dist/src/intl/sl.d.ts.map +1 -1
  167. package/dist/src/intl/sl.js +20 -0
  168. package/dist/src/intl/sv.d.ts +24 -1
  169. package/dist/src/intl/sv.d.ts.map +1 -1
  170. package/dist/src/intl/sv.js +20 -0
  171. package/dist/src/intl/sw.d.ts +21 -0
  172. package/dist/src/intl/sw.d.ts.map +1 -1
  173. package/dist/src/intl/sw.js +18 -0
  174. package/dist/src/intl/ta.d.ts +24 -1
  175. package/dist/src/intl/ta.d.ts.map +1 -1
  176. package/dist/src/intl/ta.js +20 -0
  177. package/dist/src/intl/te.d.ts +24 -1
  178. package/dist/src/intl/te.d.ts.map +1 -1
  179. package/dist/src/intl/te.js +20 -0
  180. package/dist/src/intl/th.d.ts +24 -1
  181. package/dist/src/intl/th.d.ts.map +1 -1
  182. package/dist/src/intl/th.js +20 -0
  183. package/dist/src/intl/tr.d.ts +24 -1
  184. package/dist/src/intl/tr.d.ts.map +1 -1
  185. package/dist/src/intl/tr.js +20 -0
  186. package/dist/src/intl/uk.d.ts +80 -57
  187. package/dist/src/intl/uk.d.ts.map +1 -1
  188. package/dist/src/intl/uk.js +174 -149
  189. package/dist/src/intl/ur.d.ts +24 -1
  190. package/dist/src/intl/ur.d.ts.map +1 -1
  191. package/dist/src/intl/ur.js +20 -0
  192. package/dist/src/intl/vi.d.ts +24 -1
  193. package/dist/src/intl/vi.d.ts.map +1 -1
  194. package/dist/src/intl/vi.js +20 -0
  195. package/dist/src/intl/zh-CN.d.ts +24 -1
  196. package/dist/src/intl/zh-CN.d.ts.map +1 -1
  197. package/dist/src/intl/zh-CN.js +20 -0
  198. package/dist/src/intl/zh-TW.d.ts +24 -1
  199. package/dist/src/intl/zh-TW.d.ts.map +1 -1
  200. package/dist/src/intl/zh-TW.js +20 -0
  201. package/dist/src/local-first/Db.d.ts +52 -3
  202. package/dist/src/local-first/Db.d.ts.map +1 -1
  203. package/dist/src/local-first/Db.js +412 -137
  204. package/dist/src/local-first/Evolu.d.ts +336 -211
  205. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  206. package/dist/src/local-first/Evolu.js +102 -15
  207. package/dist/src/local-first/Owner.d.ts +13 -30
  208. package/dist/src/local-first/Owner.d.ts.map +1 -1
  209. package/dist/src/local-first/Owner.js +13 -30
  210. package/dist/src/local-first/Protocol.d.ts +95 -17
  211. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  212. package/dist/src/local-first/Protocol.js +119 -39
  213. package/dist/src/local-first/Query.d.ts +8 -15
  214. package/dist/src/local-first/Query.d.ts.map +1 -1
  215. package/dist/src/local-first/Schema.d.ts +345 -21
  216. package/dist/src/local-first/Schema.d.ts.map +1 -1
  217. package/dist/src/local-first/Schema.js +214 -17
  218. package/dist/src/local-first/Shared.d.ts +537 -22
  219. package/dist/src/local-first/Shared.d.ts.map +1 -1
  220. package/dist/src/local-first/Shared.js +1437 -234
  221. package/dist/src/local-first/Storage.d.ts +192 -14
  222. package/dist/src/local-first/Storage.d.ts.map +1 -1
  223. package/dist/src/local-first/Storage.js +82 -21
  224. package/dist/src/local-first/Timestamp.d.ts +392 -41
  225. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  226. package/dist/src/local-first/Timestamp.js +404 -82
  227. package/dist/src/local-first/index.d.ts +0 -1
  228. package/dist/src/local-first/index.d.ts.map +1 -1
  229. package/dist/src/local-first/index.js +0 -1
  230. package/package.json +1 -1
  231. package/src/Assert.test.ts +2 -5
  232. package/src/{Binary.test.ts → Bytes.test.ts} +286 -1
  233. package/src/{Binary.ts → Bytes.ts} +652 -21
  234. package/src/Config.test.ts +668 -0
  235. package/src/Config.ts +410 -0
  236. package/src/Console.ts +62 -7
  237. package/src/Crypto.ts +76 -4
  238. package/src/Eq.test.ts +2 -3
  239. package/src/Error.test.ts +76 -3
  240. package/src/Error.ts +71 -0
  241. package/src/Fs.test.ts +105 -0
  242. package/src/Fs.ts +488 -0
  243. package/src/Identicon.ts +2 -2
  244. package/src/LeakDetector.ts +22 -3
  245. package/src/LockManager.ts +8 -0
  246. package/src/Number.test.ts +82 -18
  247. package/src/Number.ts +76 -8
  248. package/src/Object.test.ts +139 -10
  249. package/src/Object.ts +49 -0
  250. package/src/Platform.ts +50 -8
  251. package/src/Random.ts +25 -2
  252. package/src/Resource.test.ts +837 -0
  253. package/src/Resource.ts +235 -15
  254. package/src/Schedule.test.ts +50 -12
  255. package/src/Schedule.ts +24 -14
  256. package/src/Sqlite.ts +138 -18
  257. package/src/Task.test.ts +189 -8
  258. package/src/Task.ts +56 -17
  259. package/src/Test.ts +9 -0
  260. package/src/Time.test.ts +82 -11
  261. package/src/Time.ts +246 -24
  262. package/src/Type.test.ts +3994 -1119
  263. package/src/Type.ts +7258 -3842
  264. package/src/Types.test.ts +4 -14
  265. package/src/WebSocket.ts +313 -40
  266. package/src/Worker.ts +90 -8
  267. package/src/index.ts +18 -7
  268. package/src/intl/_en.ts +70 -0
  269. package/src/intl/ar.ts +71 -0
  270. package/src/intl/bn.ts +70 -0
  271. package/src/intl/ca.ts +70 -0
  272. package/src/intl/cs.ts +70 -0
  273. package/src/intl/da.ts +70 -0
  274. package/src/intl/de.ts +70 -0
  275. package/src/intl/el.ts +70 -0
  276. package/src/intl/es.ts +70 -0
  277. package/src/intl/fa.ts +70 -0
  278. package/src/intl/fi.ts +70 -0
  279. package/src/intl/fil.ts +70 -0
  280. package/src/intl/fr.ts +70 -0
  281. package/src/intl/he.ts +70 -0
  282. package/src/intl/hi.ts +70 -0
  283. package/src/intl/hr.ts +70 -0
  284. package/src/intl/hu.ts +69 -0
  285. package/src/intl/id.ts +70 -0
  286. package/src/intl/intl.test.ts +819 -1
  287. package/src/intl/it.ts +70 -0
  288. package/src/intl/ja.ts +70 -0
  289. package/src/intl/ko.ts +68 -0
  290. package/src/intl/ml.ts +70 -0
  291. package/src/intl/mr.ts +70 -0
  292. package/src/intl/ms.ts +71 -0
  293. package/src/intl/nb.ts +69 -0
  294. package/src/intl/nl.ts +70 -0
  295. package/src/intl/pa.ts +70 -0
  296. package/src/intl/pl.ts +63 -0
  297. package/src/intl/pt-BR.ts +70 -0
  298. package/src/intl/pt.ts +71 -0
  299. package/src/intl/ro.ts +70 -0
  300. package/src/intl/sk.ts +71 -0
  301. package/src/intl/sl.ts +70 -0
  302. package/src/intl/sv.ts +70 -0
  303. package/src/intl/sw.ts +62 -0
  304. package/src/intl/ta.ts +70 -0
  305. package/src/intl/te.ts +70 -0
  306. package/src/intl/th.ts +68 -0
  307. package/src/intl/tr.ts +70 -0
  308. package/src/intl/uk.ts +228 -155
  309. package/src/intl/ur.ts +70 -0
  310. package/src/intl/vi.ts +70 -0
  311. package/src/intl/zh-CN.ts +68 -0
  312. package/src/intl/zh-TW.ts +68 -0
  313. package/src/local-first/Db.ts +644 -339
  314. package/src/local-first/Evolu.test.ts +686 -21
  315. package/src/local-first/Evolu.ts +450 -228
  316. package/src/local-first/Owner.ts +13 -30
  317. package/src/local-first/Protocol.test.ts +618 -11
  318. package/src/local-first/Protocol.ts +197 -73
  319. package/src/local-first/Query.ts +8 -15
  320. package/src/local-first/Schema.test.ts +143 -0
  321. package/src/local-first/Schema.ts +374 -24
  322. package/src/local-first/Shared.test.ts +7731 -559
  323. package/src/local-first/Shared.ts +2036 -267
  324. package/src/local-first/Storage.ts +219 -33
  325. package/src/local-first/Timestamp.test.ts +344 -70
  326. package/src/local-first/Timestamp.ts +435 -119
  327. package/src/local-first/index.ts +0 -1
  328. package/dist/src/Binary.d.ts +0 -254
  329. package/dist/src/Binary.d.ts.map +0 -1
  330. package/dist/src/local-first/Error.d.ts +0 -12
  331. package/dist/src/local-first/Error.d.ts.map +0 -1
  332. package/dist/src/local-first/Error.js +0 -6
  333. package/dist/src/local-first/LocalAuth.d.ts +0 -150
  334. package/dist/src/local-first/LocalAuth.d.ts.map +0 -1
  335. package/dist/src/local-first/LocalAuth.js +0 -179
  336. package/src/local-first/Error.ts +0 -17
  337. package/src/local-first/LocalAuth.ts +0 -457
@@ -1,18 +1,257 @@
1
1
  /**
2
2
  * Hybrid logical clock timestamps for CRDT ordering.
3
3
  *
4
+ * Every change to a synced table becomes a CRDT message stamped with a
5
+ * {@link Timestamp}. The timestamp is the message's identity for sync, which
6
+ * reconciles sets of timestamps between devices and relays, and its order for
7
+ * conflicts, which last-writer-wins resolves per column by comparing
8
+ * timestamps. Each database keeps one clock. It advances when stamping local
9
+ * changes to synced tables and accepting incoming messages, so later writes
10
+ * sort after earlier local writes and accepted messages.
11
+ *
12
+ * A device whose system clock is ahead produces future timestamps. They can
13
+ * override edits made later in real time on devices that have not yet accepted
14
+ * them. Accepting one advances the receiver's logical clock, so its later
15
+ * writes sort after the accepted message and carry future timestamps too, even
16
+ * before system time catches up. That propagation preserves ordering, and it
17
+ * does not compound: each device checks an incoming timestamp against its own
18
+ * system time. Counter rollover or a backwards system-clock adjustment can
19
+ * still put such a write in quarantine.
20
+ *
21
+ * ### Clock drift
22
+ *
23
+ * {@link sendTimestamp} and {@link receiveTimestamp} check the resulting clock
24
+ * against {@link TimestampConfig.maxDrift}, returning {@link TimestampDriftError}
25
+ * when it exceeds the limit. Receipt also checks the remote timestamp before
26
+ * arithmetic. The database uses the same {@link isTimestampBeyondMaxDrift}
27
+ * predicate to decide whether a message can be applied. Two situations matter
28
+ * here:
29
+ *
30
+ * - An incoming message has a timestamp too far ahead of the receiving device's
31
+ * system time. It can come from another device of the same owner or from a
32
+ * collaborator, and the check cannot tell whether the sender is ahead or the
33
+ * receiver behind.
34
+ * - The device's own system time is ahead and a write advances its logical clock.
35
+ * Drift is measured against the device's own system time, so the device
36
+ * accepts such writes as normal and stamps them ahead; other devices
37
+ * quarantine them. Only if system time then moves back far enough does the
38
+ * logical clock remain ahead and subsequent local changes go to quarantine.
39
+ * Ordinary incoming messages are still applied when their own timestamps are
40
+ * within the limit, even when the local clock is ahead. The clock is shared
41
+ * by all owners in the database.
42
+ *
43
+ * System time is usually wrong by hours, rarely by years. A person sets the
44
+ * clock by hand, often misreading daylight saving time or the year, or picks a
45
+ * wrong time zone while the clock is set manually. A virtual machine resumes
46
+ * from a snapshot. Time synchronization corrects a clock that ran fast. A
47
+ * device with a dead clock battery boots into the past. Daylight saving time
48
+ * itself changes nothing, because timestamps use epoch milliseconds. Drift of
49
+ * years comes from a clock set to the wrong year, a bug, or, in collaboration,
50
+ * a vandalizing collaborator.
51
+ *
52
+ * The database uses a five-minute limit. It tolerates a few minutes of
53
+ * difference between device clocks and quarantines messages hours or days ahead
54
+ * of system time. The limit is a trade-off: a smaller one quarantines more; a
55
+ * larger one admits more future skew and releases sooner. No limit orders
56
+ * independent offline edits by real time. Five minutes is a policy choice: the
57
+ * [HLC paper, Section 4.2](https://cse.buffalo.edu/tech-reports/2014-04.pdf)
58
+ * leaves the tolerance to application semantics and suggests at most seconds
59
+ * for NTP-synchronized servers, which user devices are not. [Actual
60
+ * Budget](https://github.com/actualbudget/actual/blob/master/packages/crdt/src/crdt/timestamp.ts)
61
+ * uses the same default, a precedent rather than proof.
62
+ *
63
+ * ### Quarantine
64
+ *
65
+ * When a message's own timestamp exceeds the limit, Evolu stores the message in
66
+ * quarantine without applying it to application tables. The database queue and
67
+ * sync continue; other messages and requests are processed normally. Completing
68
+ * a mutation means its changes are stored; some may be quarantined rather than
69
+ * visible in application queries.
70
+ *
71
+ * Quarantine is state, not an error. Nothing is reported through the error
72
+ * channel; the quarantine table records each unapplied message with its reason,
73
+ * whether this database stamped it for a local mutation or received it, and the
74
+ * system time when it was quarantined. Applications watch that table through
75
+ * queries, and a subscribed query reflects a local mutation's quarantined rows
76
+ * before its completion callback runs. Quarantine works offline, independently
77
+ * of sync state, so the application can explain why the user's change is not
78
+ * visible.
79
+ *
80
+ * A local change still receives the next logical timestamp, and that clock
81
+ * advance is persisted with the quarantined message. Further local changes also
82
+ * go to quarantine while their timestamps exceed the drift limit. New mutations
83
+ * apply normally once their timestamps fall within it; existing drift
84
+ * quarantine still waits for database worker startup. An incoming message is
85
+ * quarantined only when its own timestamp exceeds system time by more than the
86
+ * limit. `receiveTimestamp` rejects that timestamp before calculating the next
87
+ * clock, so the message keeps its original timestamp, quarantining it does not
88
+ * advance the local clock. For a message within the limit, the database applies
89
+ * the message and persists the next clock even if an already-ahead local clock
90
+ * or counter rollover produces a timestamp beyond the limit. An ahead local
91
+ * clock surfaces through local changes. A message whose timestamp is already in
92
+ * the owner's set is not written again: it was applied or quarantined before,
93
+ * and a duplicate cannot change that decision. Quarantined messages count as
94
+ * stored for sync and can be forwarded normally; each receiving device decides
95
+ * whether to apply or quarantine them. A quarantined timestamp far in the
96
+ * future also becomes the owner's last stored timestamp on every device and
97
+ * relay that stores it, so their later timestamps take the slower insert path
98
+ * of the timestamp skiplist instead of append until system time passes it. One
99
+ * device whose clock is set ahead is enough to cause this for the whole owner.
100
+ * The cost is a constant factor per stored message, the `insert` versus
101
+ * `append` workloads of the storage benchmark; ordering and sync are
102
+ * unaffected. Relays store and forward messages without checking clock drift:
103
+ * acceptance belongs to clients and must not depend on an honest relay or its
104
+ * system clock.
105
+ *
106
+ * Quarantine does not itself make sync fail: completing sync does not mean
107
+ * every stored message has been applied to application tables.
108
+ *
109
+ * ### Release
110
+ *
111
+ * Drift quarantine is checked only when the database worker starts, as schema
112
+ * quarantine is. At that point, messages are released if their timestamps are
113
+ * no more than the drift limit ahead of system time, matching acceptance on a
114
+ * fresh receipt. Devices can therefore converge on the same visible state after
115
+ * their workers restart, regardless of delivery timing.
116
+ *
117
+ * On the web, the tab holding the leader lock hosts the database worker. When
118
+ * that tab closes, reloads, or enters the browser's back-forward cache, a tab
119
+ * taking over leadership starts a replacement, and other open tabs refresh
120
+ * their subscribed queries. Reloading a non-leader tab does not restart the
121
+ * database worker.
122
+ *
123
+ * Release checks use one captured system time. The logical clock advances over
124
+ * distinct released timestamps in timestamp order, as receipts do, so later
125
+ * local changes sort after them even before system time catches up. Released
126
+ * columns use last-writer-wins, so a future-stamped message overrides edits any
127
+ * device stamped before accepting or releasing it. Columns the schema does not
128
+ * define move to schema quarantine and are applied after a schema update.
129
+ * Duplicate delivery does not release: the timestamp is already in the owner's
130
+ * set. Release during a running database worker's session is not implemented.
131
+ * Correcting system time does not trigger release; eligible messages are
132
+ * released when the database worker next starts. Until then, a later message
133
+ * from one author can be visible while an earlier one is not, so an application
134
+ * can see a row that refers to one still in quarantine. Devices each within the
135
+ * limit can also be up to twice the limit apart, so a message one of them
136
+ * accepted can be quarantined by the other.
137
+ *
138
+ * Release runs before the database worker reports its clock, so fresh requests
139
+ * start from the clock advanced by release. The stored clock never moves
140
+ * backwards. A replacement database worker's clock is adopted only if newer, so
141
+ * an empty `memoryOnly` replacement does not reset the session clock. Pending
142
+ * writes keep their captured inputs, so a replay reproduces the timestamps of
143
+ * the original attempt. Responses report their computed clock, which the
144
+ * SharedWorker adopts only if newer. Acquiring the replacement refreshes
145
+ * subscribed queries, because a startup release or a committed write whose
146
+ * response was lost would otherwise stay invisible.
147
+ *
148
+ * ### Recovery
149
+ *
150
+ * Recovery for messages further ahead than the drift limit is not implemented
151
+ * yet. These constraints shape it. Deleting quarantine rows is not a safe
152
+ * primitive: the timestamp stays in the owner's set and on relays, and a stored
153
+ * timestamp must be able to produce its message. Recovery therefore applies the
154
+ * rows early, re-authors them as a new mutation with a fresh timestamp, or
155
+ * marks them discarded. Re-authoring is only right for a local origin. All
156
+ * three leave the future-stamped message stored for sync with its original
157
+ * timestamp, so on other devices it still overrides edits stamped before they
158
+ * accept it. Applying early is acceptable while the rows are a short time
159
+ * ahead, as after a manual clock change; how short is an application decision,
160
+ * measured as the row's timestamp minus current time. Rows far ahead, from a
161
+ * clock set to the wrong year, a bug, or a vandalizing collaborator, require
162
+ * migrating the owner's visible state to a new owner with fresh timestamps; the
163
+ * old owner is abandoned. How relays treat an abandoned owner is not specified
164
+ * yet.
165
+ *
166
+ * On the device whose clock ran ahead, fresh timestamps first need a clock
167
+ * reset. The database clock is shared by all owners and never moves backwards,
168
+ * so after that device's system time is corrected, every later timestamp is
169
+ * still at least as far ahead, for every owner, including a new one.
170
+ * Re-authored rows and a migration to a new owner would be quarantined too.
171
+ * Recovery there therefore starts by resetting the clock to system time with a
172
+ * fresh random node ID, which keeps timestamps stamped after the reset distinct
173
+ * from the future ones. The SharedWorker must adopt the reset clock although it
174
+ * is older than its session clock, and, like node ID rotation, the reset runs
175
+ * as its own request, never inside a replayed write. The reset is never
176
+ * automatic, because a clock that fell back, as after a dead clock battery,
177
+ * looks the same, and resetting then would stamp changes in the past. Nor is it
178
+ * a standalone action: after it, local edits to columns holding future-stamped
179
+ * values are stored but lose last-writer-wins without being quarantined, so the
180
+ * reset belongs only inside the migration to a new owner. Such a device can be
181
+ * recognized by local-origin drift quarantine whose timestamps exceed their
182
+ * quarantine time by about the skew.
183
+ *
184
+ * ### Range ceiling
185
+ *
186
+ * Counter rollover past the {@link Millis} ceiling, in August 10889, throws. The
187
+ * clock can get there only if the system clock came within the drift limit of
188
+ * the ceiling or the stored clock was tampered with, because `receiveTimestamp`
189
+ * checks remote drift before clock arithmetic. Such a clock is as broken as one
190
+ * past the ceiling, which {@link Millis} already rejects by throwing.
191
+ *
192
+ * ### Duplicate node IDs
193
+ *
194
+ * Detection and recovery are deliberately deferred to separate work. This
195
+ * includes the receive-first collision below, where a local mutation can
196
+ * complete without being stored.
197
+ *
198
+ * The node ID is random per database and persisted in the clock. A database
199
+ * copied to another device, as when an operating system backup is restored to a
200
+ * new phone while the old one stays in use, leaves both independent copies
201
+ * stamping from the same node ID and clock. Tabs and Evolu instances sharing
202
+ * one database coordinate their writes through its shared clock.
203
+ *
204
+ * For the same owner, two changes stamped in the same logical millisecond with
205
+ * the same counter get identical timestamps. If both copies write before
206
+ * receiving the other's change, each keeps its own version, while relays and
207
+ * third devices keep the first arrival. The copies can diverge with no error.
208
+ * This is likely while the copied clock is ahead of both devices' system time,
209
+ * because both stamp counters 1, 2, 3 in the same millisecond.
210
+ *
211
+ * Receiving first can instead lose a local change. A copy quarantines an
212
+ * incoming timestamp beyond the drift limit without advancing its clock. Its
213
+ * next local mutation can then produce that same timestamp. The timestamp is
214
+ * already in the owner's set, so the local change is skipped: it is stored in
215
+ * neither application tables, history, nor quarantine, but `onComplete` still
216
+ * runs. The previously received change remains stored under that timestamp.
217
+ *
218
+ * Detection: a received message whose timestamp is new to the owner's set but
219
+ * carries the local node ID cannot be ours, because every timestamp authored
220
+ * locally is already in the set before it can be sent, and a restored or
221
+ * recreated database mints a fresh node ID. `applyMessages` knows both facts
222
+ * when `insertTimestamp` reports a new timestamp. Messages whose timestamps are
223
+ * already stored are invisible to this rule; a new timestamp from the copy
224
+ * trips it.
225
+ *
226
+ * An empty `memoryOnly` replacement is an exception: it can retain the
227
+ * SharedWorker's previous clock and node ID while losing the timestamp set.
228
+ * Detection must account for this before rotation, or our own earlier messages
229
+ * could be mistaken for another database's changes.
230
+ *
231
+ * Handling: rotate the local node ID to a fresh random one, which changes only
232
+ * future timestamps, and report the copy through sync state or the error store
233
+ * so the application can warn that edits before detection may have diverged or
234
+ * been lost. Both copies detect each other and rotate, after which detection
235
+ * stops because messages with the old ID are no longer ours. Rotation cannot
236
+ * heal past collisions. Deleting and resyncing the database automatically is
237
+ * rejected: it drops unsynced changes and local-only tables, needs a relay, and
238
+ * does not recover the dropped half of a collision; restore from mnemonic is
239
+ * its manual form. The rotation must not happen inside the replayed write: a
240
+ * replay sees nothing new and would save the input clock with the old node ID
241
+ * again. The write's response flags the detection and the SharedWorker enqueues
242
+ * a separate rotation request, which is harmless to replay.
243
+ *
4
244
  * @module
5
245
  */
6
246
 
7
- import { bytesToHex, hexToBytes } from "../Binary.ts";
247
+ import { bytesToHex, hexToBytes } from "../Bytes.ts";
8
248
  import type { RandomBytesDep } from "../Crypto.ts";
9
249
  import { createEqObject, eqNumber, eqString } from "../Eq.ts";
10
250
  import { increment } from "../Number.ts";
11
251
  import type { Order } from "../Order.ts";
12
- import { orderUint8Array } from "../Order.ts";
252
+ import { orderNumber, orderString, orderUint8Array } from "../Order.ts";
13
253
  import type { Result } from "../Result.ts";
14
254
  import { err, ok } from "../Result.ts";
15
- import type { TimeDep } from "../Time.ts";
16
255
  import { Millis, minMillis } from "../Time.ts";
17
256
  import {
18
257
  brand,
@@ -31,9 +270,10 @@ import {
31
270
 
32
271
  export interface TimestampConfig {
33
272
  /**
34
- * Maximum physical clock drift allowed in ms.
273
+ * How far in ms a timestamp may be ahead of system time.
35
274
  *
36
- * The default value is 5 * 60 * 1000 (5 minutes).
275
+ * The database uses {@link defaultTimestampMaxDrift}; it is not configurable.
276
+ * A timestamp behind system time is never drift.
37
277
  */
38
278
  readonly maxDrift: number;
39
279
  }
@@ -45,20 +285,28 @@ export interface TimestampConfigDep {
45
285
  readonly timestampConfig: TimestampConfig;
46
286
  }
47
287
 
48
- export type TimestampError =
49
- | TimestampDriftError
50
- | TimestampCounterOverflowError
51
- | TimestampTimeOutOfRangeError;
288
+ /** Errors from advancing a {@link Timestamp}. */
289
+ export type TimestampError = TimestampDriftError;
52
290
 
291
+ /**
292
+ * A timestamp exceeds {@link TimestampConfig.maxDrift}.
293
+ *
294
+ * For local drift, the failed operation includes its candidate for explicit
295
+ * recovery by the database. For remote drift, it includes the rejected input;
296
+ * the local clock must not advance from it.
297
+ *
298
+ * Local drift stays an error even when the database recovers: callers can rely
299
+ * on successful timestamp operations satisfying the drift limit without a
300
+ * separate check.
301
+ */
53
302
  export interface TimestampDriftError extends Typed<"TimestampDriftError"> {
54
- readonly next: Millis;
303
+ /** The computed candidate for local drift, or the rejected remote input. */
304
+ readonly timestamp: Timestamp;
305
+ readonly cause: "local" | "remote";
306
+ /** Captured system time used for the drift check. */
55
307
  readonly now: Millis;
56
308
  }
57
309
 
58
- export interface TimestampCounterOverflowError extends Typed<"TimestampCounterOverflowError"> {}
59
-
60
- export interface TimestampTimeOutOfRangeError extends Typed<"TimestampTimeOutOfRangeError"> {}
61
-
62
310
  export const Counter = /*#__PURE__*/ brand(
63
311
  "Counter",
64
312
  /*#__PURE__*/ lessThanOrEqualTo(65535)(NonNegativeInt),
@@ -69,29 +317,14 @@ export const minCounter = 0 as Counter;
69
317
  export const maxCounter = 65535 as Counter;
70
318
 
71
319
  /**
72
- * A NodeId uniquely identifies an owner's device. Generated once per device
73
- * using cryptographic randomness.
74
- *
75
- * Collision probability (birthday paradox):
76
- *
77
- * - 1,000 devices: ~0.00000000000271% (negligible).
78
- * - 1M devices: ~0.00000271% (1 in 37M chance).
79
- * - 135M devices: ~1% chance.
80
- * - 4.29B devices: ~50% chance.
81
- *
82
- * https://lemire.me/blog/2019/12/12/are-64-bit-random-identifiers-free-from-collision
320
+ * A NodeId identifies the database that stamped a timestamp. It is 64 random
321
+ * bits, generated when the database is created and persisted in its clock.
83
322
  *
84
- * What happens if different devices generate the same NodeId?
85
- *
86
- * If devices with the same NodeId use different owners, no issues occur.
87
- *
88
- * If devices with the same NodeId use the same owner, problems only arise when
89
- * they generate CRDT messages with identical timestamps (same millis, counter,
90
- * and NodeId). In this case, the protocol sync algorithm treats them as the
91
- * same message: the first will be synced with the relay, while the affected
92
- * message will not be delivered. The affected devices will see different data
93
- * yet they will think they are synced. This is extremely rare and can be
94
- * resolved by resetting one device to generate a new NodeId.
323
+ * Only databases that share an owner can collide, so random collisions are
324
+ * negligible. Identical timestamps (same millis, counter, and NodeId) are the
325
+ * same message to sync: one of the changes is lost, and the affected devices
326
+ * see different data while they appear synced. The realistic case is a copied
327
+ * database, described in the Timestamp module's Duplicate node IDs section.
95
328
  */
96
329
  export const NodeId = /*#__PURE__*/ regex("NodeId", /^[a-f0-9]{16}$/u)(String);
97
330
  export type NodeId = typeof NodeId.Output;
@@ -130,11 +363,20 @@ export const nodeIdBytesToNodeId = (nodeIdBytes: NodeIdBytes): NodeId =>
130
363
  * clocks while staying close to physical time for better human
131
364
  * interpretability.
132
365
  *
133
- * The counter component ensures causality is maintained even when physical
134
- * clocks are imperfect. When clocks drift or operations occur concurrently, the
135
- * counter increments to establish a total order. This means Evolu achieves
136
- * well-defined, eventually-consistent behavior regardless of physical clock
137
- * accuracy.
366
+ * The counter gives a total order and preserves causality for accepted
367
+ * messages: when clocks differ within the drift limit or operations occur
368
+ * concurrently, it increments so later writes sort after what the device has
369
+ * seen. Messages beyond the drift limit wait in quarantine, and devices
370
+ * converge once system time is within the limit of those messages and the
371
+ * database worker restarts. See the Timestamp module's Clock drift section.
372
+ *
373
+ * When the 16-bit counter is exhausted, the logical millisecond advances by one
374
+ * and the counter resets to zero. The resulting timestamp must still fit within
375
+ * {@link Millis}. If rollover exceeds {@link TimestampConfig.maxDrift},
376
+ * {@link sendTimestamp} and {@link receiveTimestamp} return
377
+ * {@link TimestampDriftError} with the candidate timestamp for the database to
378
+ * handle. Rollover preserves deterministic ordering even when a batch uses one
379
+ * captured wall time.
138
380
  *
139
381
  * Vector clocks can accurately track causality and detect concurrent
140
382
  * operations, but they require unbounded space in peer-to-peer systems and
@@ -144,12 +386,22 @@ export const nodeIdBytesToNodeId = (nodeIdBytes: NodeIdBytes): NodeId =>
144
386
  * malicious actors.
145
387
  *
146
388
  * HLC timestamps work well in practice because modern device clocks accurately
147
- * reflect the order of sequential edits in the common case. Evolu's `maxDrift`
148
- * configuration protects against buggy clocks and prevents problematic
149
- * future-dated entries from propagating through the network.
389
+ * reflect the order of sequential edits in the common case. The database uses
390
+ * the drift limit to quarantine messages whose own timestamps are too far ahead
391
+ * of its system time without applying them to application tables. Quarantined
392
+ * messages remain stored and synchronized; each receiving device checks drift
393
+ * against its own system time.
150
394
  *
151
395
  * ## References
152
396
  *
397
+ * - Kulkarni, Demirbas, Madeppa, Avva, Leone: [Logical Physical Clocks and
398
+ * Consistent Snapshots in Globally Distributed
399
+ * Databases](https://cse.buffalo.edu/tech-reports/2014-04.pdf) (OPODIS 2014,
400
+ * [doi:10.1007/978-3-319-14472-6_2](https://doi.org/10.1007/978-3-319-14472-6_2)).
401
+ * The paper proposes 48 significant bits of an NTP timestamp plus a 16-bit
402
+ * counter and argues that the counter is sufficient under its assumptions.
403
+ * Evolu uses 48-bit milliseconds and rolls counter exhaustion into the next
404
+ * logical millisecond, subject to the timestamp range.
153
405
  * - https://muratbuffalo.blogspot.com/2014/07/hybrid-logical-clocks.html
154
406
  * - https://sergeiturukin.com/2017/06/26/hybrid-logical-clocks.html
155
407
  * - https://jaredforsyth.com/posts/hybrid-logical-clocks/
@@ -195,6 +447,30 @@ export const eqTimestamp = /*#__PURE__*/ createEqObject<Timestamp>({
195
447
  nodeId: eqString,
196
448
  });
197
449
 
450
+ /**
451
+ * Orders {@link Timestamp} by milliseconds, counter, then node ID.
452
+ *
453
+ * Matches {@link orderTimestampBytes} without encoding timestamps. Distinct
454
+ * objects with identical fields compare as equal.
455
+ *
456
+ * ### Example
457
+ *
458
+ * ```ts
459
+ * import { assertEqual } from "@evolu/common";
460
+ * import {
461
+ * createTimestamp,
462
+ * orderTimestamp,
463
+ * } from "@evolu/common/local-first";
464
+ *
465
+ * const timestamp = createTimestamp();
466
+ * assertEqual(orderTimestamp(timestamp, { ...timestamp }), 0);
467
+ * ```
468
+ */
469
+ export const orderTimestamp: Order<Timestamp> = (a, b) =>
470
+ orderNumber(a.millis, b.millis) ||
471
+ orderNumber(a.counter, b.counter) ||
472
+ orderString(a.nodeId, b.nodeId);
473
+
198
474
  export const createTimestamp = ({
199
475
  millis = minMillis,
200
476
  counter = minCounter,
@@ -206,91 +482,131 @@ export const createInitialTimestamp = (deps: RandomBytesDep): Timestamp => {
206
482
  return createTimestamp({ nodeId });
207
483
  };
208
484
 
209
- const getNextMillis =
210
- (deps: TimeDep & TimestampConfigDep) =>
211
- (
212
- millis: ReadonlyArray<Millis>,
213
- ): Result<Millis, TimestampTimeOutOfRangeError | TimestampDriftError> => {
214
- const now = Millis.fromUnknown(deps.time.now());
215
- if (!now.ok) {
216
- return err({ type: "TimestampTimeOutOfRangeError" });
217
- }
218
- const next = Math.max(now.value, ...millis) as Millis;
219
- return next - now.value > deps.timestampConfig.maxDrift
220
- ? err<TimestampDriftError>({
221
- type: "TimestampDriftError",
222
- now: now.value,
223
- next,
224
- })
225
- : ok(next);
226
- };
227
-
228
- const incrementCounter = (
229
- counter: Counter,
230
- ): Result<Counter, TimestampCounterOverflowError> => {
231
- const next = Counter.fromUnknown(increment(counter));
232
- if (!next.ok) return err({ type: "TimestampCounterOverflowError" });
233
- return ok(next.value);
234
- };
235
-
485
+ /**
486
+ * Advances a {@link Timestamp} for a local event.
487
+ *
488
+ * Counter exhaustion rolls into the next logical millisecond, and rollover past
489
+ * the {@link Millis} ceiling throws; see the module documentation. The resulting
490
+ * timestamp is checked for drift, including after rollover; a failure returns
491
+ * {@link TimestampDriftError} with the candidate and `cause: "local"`. Pass the
492
+ * request's captured system time so replay produces the same result.
493
+ *
494
+ * ### Example
495
+ *
496
+ * ```ts
497
+ * import { assertOk, Millis } from "@evolu/common";
498
+ * import {
499
+ * createTimestamp,
500
+ * maxCounter,
501
+ * sendTimestamp,
502
+ * } from "@evolu/common/local-first";
503
+ *
504
+ * const before = createTimestamp({ counter: maxCounter });
505
+ * const result = sendTimestamp({ timestampConfig: { maxDrift: 1 } })(
506
+ * before,
507
+ * Millis.orThrow(0),
508
+ * );
509
+ * assertOk(result, { ...before, millis: Millis.orThrow(1), counter: 0 });
510
+ * ```
511
+ */
236
512
  export const sendTimestamp =
237
- (deps: TimeDep & TimestampConfigDep) =>
238
- (
239
- timestamp: Timestamp,
240
- ): Result<
241
- Timestamp,
242
- | TimestampDriftError
243
- | TimestampCounterOverflowError
244
- | TimestampTimeOutOfRangeError
245
- > => {
246
- const millis = getNextMillis(deps)([timestamp.millis]);
247
- if (!millis.ok) return millis;
248
-
249
- const counter =
250
- millis.value === timestamp.millis
251
- ? incrementCounter(timestamp.counter)
252
- : ok(minCounter);
253
- if (!counter.ok) return counter;
254
-
255
- return ok({
256
- millis: millis.value,
257
- counter: counter.value,
513
+ (deps: TimestampConfigDep) =>
514
+ (timestamp: Timestamp, now: Millis): Result<Timestamp, TimestampError> => {
515
+ let millis = Math.max(now, timestamp.millis) as Millis;
516
+ let counter =
517
+ millis === timestamp.millis ? increment(timestamp.counter) : minCounter;
518
+ if (counter > maxCounter) {
519
+ millis = Millis.orThrow(increment(millis));
520
+ counter = minCounter;
521
+ }
522
+ const nextTimestamp: Timestamp = {
523
+ millis,
524
+ counter: counter as Counter,
258
525
  nodeId: timestamp.nodeId,
259
- });
526
+ };
527
+ if (isTimestampBeyondMaxDrift(deps)(millis, now)) {
528
+ return err({
529
+ type: "TimestampDriftError",
530
+ timestamp: nextTimestamp,
531
+ cause: "local",
532
+ now,
533
+ });
534
+ }
535
+ return ok(nextTimestamp);
260
536
  };
261
537
 
538
+ /**
539
+ * Advances a {@link Timestamp} for a received one.
540
+ *
541
+ * Rejects remote drift with {@link TimestampDriftError} and `cause: "remote"`
542
+ * before clock arithmetic. Merges the later clock state, preserves the local
543
+ * node ID, and uses {@link sendTimestamp} to advance and validate the result. An
544
+ * already-ahead local clock or rollover beyond the drift limit returns local
545
+ * drift. Pass one captured time for a batch or replay.
546
+ *
547
+ * ### Example
548
+ *
549
+ * ```ts
550
+ * import { assertErr, Millis } from "@evolu/common";
551
+ * import {
552
+ * createTimestamp,
553
+ * receiveTimestamp,
554
+ * } from "@evolu/common/local-first";
555
+ *
556
+ * const receive = receiveTimestamp({ timestampConfig: { maxDrift: 10 } });
557
+ * const local = createTimestamp();
558
+ * const remote = createTimestamp({ millis: Millis.orThrow(111) });
559
+ * assertErr(receive(local, remote, Millis.orThrow(100)), {
560
+ * type: "TimestampDriftError",
561
+ * timestamp: remote,
562
+ * cause: "remote",
563
+ * now: Millis.orThrow(100),
564
+ * });
565
+ * ```
566
+ */
262
567
  export const receiveTimestamp =
263
- (deps: TimeDep & TimestampConfigDep) =>
568
+ (deps: TimestampConfigDep) =>
264
569
  (
265
570
  local: Timestamp,
266
571
  remote: Timestamp,
267
- ): Result<
268
- Timestamp,
269
- | TimestampDriftError
270
- | TimestampCounterOverflowError
271
- | TimestampTimeOutOfRangeError
272
- > => {
273
- const millis = getNextMillis(deps)([local.millis, remote.millis]);
274
- if (!millis.ok) return millis;
275
-
276
- const counter =
277
- millis.value === local.millis && millis.value === remote.millis
278
- ? incrementCounter(Math.max(local.counter, remote.counter) as Counter)
279
- : millis.value === local.millis
280
- ? incrementCounter(local.counter)
281
- : millis.value === remote.millis
282
- ? incrementCounter(remote.counter)
283
- : ok(minCounter);
284
-
285
- if (!counter.ok) return counter;
286
-
287
- return ok({
288
- millis: millis.value,
289
- counter: counter.value,
290
- nodeId: local.nodeId,
291
- });
572
+ now: Millis,
573
+ ): Result<Timestamp, TimestampError> => {
574
+ if (isTimestampBeyondMaxDrift(deps)(remote.millis, now)) {
575
+ return err({
576
+ type: "TimestampDriftError",
577
+ timestamp: remote,
578
+ cause: "remote",
579
+ now,
580
+ });
581
+ }
582
+ const latest = orderTimestamp(local, remote) >= 0 ? local : remote;
583
+ return sendTimestamp(deps)({ ...latest, nodeId: local.nodeId }, now);
292
584
  };
293
585
 
586
+ /**
587
+ * Whether timestamp milliseconds exceed {@link TimestampConfig.maxDrift} ahead
588
+ * of the supplied reference time. The exact limit and past timestamps are
589
+ * accepted.
590
+ *
591
+ * ### Example
592
+ *
593
+ * ```ts
594
+ * import { assertFalse, assertTrue, Millis } from "@evolu/common";
595
+ * import { isTimestampBeyondMaxDrift } from "@evolu/common/local-first";
596
+ *
597
+ * const isBeyondMaxDrift = isTimestampBeyondMaxDrift({
598
+ * timestampConfig: { maxDrift: 10 },
599
+ * });
600
+ * const now = Millis.orThrow(100);
601
+ * assertFalse(isBeyondMaxDrift(Millis.orThrow(110), now));
602
+ * assertTrue(isBeyondMaxDrift(Millis.orThrow(111), now));
603
+ * ```
604
+ */
605
+ export const isTimestampBeyondMaxDrift =
606
+ (deps: TimestampConfigDep) =>
607
+ (millis: Millis, now: Millis): boolean =>
608
+ millis - now > deps.timestampConfig.maxDrift;
609
+
294
610
  /** Sortable bytes representation of {@link Timestamp}. */
295
611
  export const TimestampBytes = /*#__PURE__*/ brand(
296
612
  "TimestampBytes",
@@ -1,5 +1,4 @@
1
1
  export * from "./Db.ts";
2
- export * from "./Error.ts";
3
2
  export * from "./Evolu.ts";
4
3
  export * from "./Owner.ts";
5
4
  export * from "./Protocol.ts";