@actual-app/core 26.3.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 (358) hide show
  1. package/.swcrc +11 -0
  2. package/bin/build-browser +40 -0
  3. package/bin/copy-migrations +9 -0
  4. package/db.sqlite +0 -0
  5. package/default-db.sqlite +0 -0
  6. package/migrations/.force-copy-windows +0 -0
  7. package/migrations/1548957970627_remove-db-version.sql +5 -0
  8. package/migrations/1550601598648_payees.sql +23 -0
  9. package/migrations/1555786194328_remove_category_group_unique.sql +25 -0
  10. package/migrations/1561751833510_indexes.sql +7 -0
  11. package/migrations/1567699552727_budget.sql +38 -0
  12. package/migrations/1582384163573_cleared.sql +6 -0
  13. package/migrations/1597756566448_rules.sql +10 -0
  14. package/migrations/1608652596043_parent_field.sql +13 -0
  15. package/migrations/1608652596044_trans_views.sql +56 -0
  16. package/migrations/1612625548236_optimize.sql +7 -0
  17. package/migrations/1614782639336_trans_views2.sql +33 -0
  18. package/migrations/1615745967948_meta.sql +10 -0
  19. package/migrations/1616167010796_accounts_order.sql +5 -0
  20. package/migrations/1618975177358_schedules.sql +28 -0
  21. package/migrations/1632571489012_remove_cache.js +136 -0
  22. package/migrations/1679728867040_rules_conditions.sql +5 -0
  23. package/migrations/1681115033845_add_schedule_name.sql +5 -0
  24. package/migrations/1682974838138_remove_payee_rules.sql +5 -0
  25. package/migrations/1685007876842_add_category_hidden.sql +6 -0
  26. package/migrations/1686139660866_remove_account_type.sql +5 -0
  27. package/migrations/1688749527273_transaction_filters.sql +10 -0
  28. package/migrations/1688841238000_add_account_type.sql +5 -0
  29. package/migrations/1691233396000_add_schedule_next_date_tombstone.sql +5 -0
  30. package/migrations/1694438752000_add_goal_targets.sql +7 -0
  31. package/migrations/1697046240000_add_reconciled.sql +5 -0
  32. package/migrations/1704572023730_add_account_sync_source.sql +5 -0
  33. package/migrations/1704572023731_add_missing_goCardless_sync_source.sql +9 -0
  34. package/migrations/1707267033000_reports.sql +28 -0
  35. package/migrations/1712784523000_unhide_input_group.sql +8 -0
  36. package/migrations/1716359441000_include_current.sql +5 -0
  37. package/migrations/1720310586000_link_transfer_schedules.sql +19 -0
  38. package/migrations/1720664867241_add_payee_favorite.sql +5 -0
  39. package/migrations/1720665000000_goal_context.sql +6 -0
  40. package/migrations/1722717601000_reports_move_selected_categories.js +55 -0
  41. package/migrations/1722804019000_create_dashboard_table.js +69 -0
  42. package/migrations/1723665565000_prefs.js +59 -0
  43. package/migrations/1730744182000_fix_dashboard_table.sql +7 -0
  44. package/migrations/1736640000000_custom_report_sorting.sql +7 -0
  45. package/migrations/1737158400000_add_learn_categories_to_payees.sql +5 -0
  46. package/migrations/1738491452000_sorting_rename.sql +13 -0
  47. package/migrations/1739139550000_bank_sync_page.sql +7 -0
  48. package/migrations/1740506588539_add_last_reconciled_at.sql +5 -0
  49. package/migrations/1745425408000_update_budgetType_pref.sql +7 -0
  50. package/migrations/1749799110000_add_tags.sql +10 -0
  51. package/migrations/1749799110001_tags_tombstone.sql +5 -0
  52. package/migrations/1754611200000_add_category_template_settings.sql +5 -0
  53. package/migrations/1759260219000_add_trim_interval_report_setting.sql +6 -0
  54. package/migrations/1759842823172_add_isGlobal_to_preferences.sql +1 -0
  55. package/migrations/1762178745667_rename_csv_skip_lines_pref.sql +8 -0
  56. package/migrations/1765518577215_multiple_dashboards.js +30 -0
  57. package/migrations/1768872504000_add_payee_locations.sql +21 -0
  58. package/package.json +128 -0
  59. package/src/mocks/arbitrary-schema.ts +162 -0
  60. package/src/mocks/budget.ts +901 -0
  61. package/src/mocks/files/8859-1.qfx +63 -0
  62. package/src/mocks/files/best.data-ever$.QFX +124 -0
  63. package/src/mocks/files/big.data.QiF +91 -0
  64. package/src/mocks/files/budgets/.commit-to-git +0 -0
  65. package/src/mocks/files/camt/camt.053.payee-memo.xml +127 -0
  66. package/src/mocks/files/camt/camt.053.xml +463 -0
  67. package/src/mocks/files/credit-card.ofx +11 -0
  68. package/src/mocks/files/data-multi-decimal.ofx +64 -0
  69. package/src/mocks/files/data-payee-memo.ofx +75 -0
  70. package/src/mocks/files/data-payee-memo.qif +17 -0
  71. package/src/mocks/files/data.ofx +124 -0
  72. package/src/mocks/files/data.qfx +124 -0
  73. package/src/mocks/files/data.qif +91 -0
  74. package/src/mocks/files/default-budget-template/db.sqlite +0 -0
  75. package/src/mocks/files/default-budget-template/metadata.json +6 -0
  76. package/src/mocks/files/html-vals.qfx +17 -0
  77. package/src/mocks/index.ts +221 -0
  78. package/src/mocks/migrations/1508717984291_up_add-poop.sql +13 -0
  79. package/src/mocks/migrations/1508718036311_up_modify-poop.sql +2 -0
  80. package/src/mocks/migrations/1508727787513_remove-is_income.sql +15 -0
  81. package/src/mocks/random.ts +16 -0
  82. package/src/mocks/setup.ts +180 -0
  83. package/src/mocks/spreadsheet.ts +101 -0
  84. package/src/mocks/util.ts +82 -0
  85. package/src/platform/client/connection/README.md +3 -0
  86. package/src/platform/client/connection/__mocks__/index.ts +67 -0
  87. package/src/platform/client/connection/index-types.ts +95 -0
  88. package/src/platform/client/connection/index.browser.ts +213 -0
  89. package/src/platform/client/connection/index.ts +155 -0
  90. package/src/platform/client/undo/index.ts +59 -0
  91. package/src/platform/exceptions/__mocks__/index.ts +7 -0
  92. package/src/platform/exceptions/index.ts +9 -0
  93. package/src/platform/server/asyncStorage/__mocks__/index.ts +50 -0
  94. package/src/platform/server/asyncStorage/index-types.ts +35 -0
  95. package/src/platform/server/asyncStorage/index.api.ts +2 -0
  96. package/src/platform/server/asyncStorage/index.electron.ts +88 -0
  97. package/src/platform/server/asyncStorage/index.ts +126 -0
  98. package/src/platform/server/connection/README.md +3 -0
  99. package/src/platform/server/connection/__mocks__/index.ts +15 -0
  100. package/src/platform/server/connection/index-types.ts +20 -0
  101. package/src/platform/server/connection/index.api.ts +13 -0
  102. package/src/platform/server/connection/index.electron.ts +102 -0
  103. package/src/platform/server/connection/index.ts +154 -0
  104. package/src/platform/server/fetch/__mocks__/index.ts +3 -0
  105. package/src/platform/server/fetch/index.api.ts +1 -0
  106. package/src/platform/server/fetch/index.electron.ts +18 -0
  107. package/src/platform/server/fetch/index.ts +20 -0
  108. package/src/platform/server/fs/index.api.ts +198 -0
  109. package/src/platform/server/fs/index.electron.ts +208 -0
  110. package/src/platform/server/fs/index.test.ts +117 -0
  111. package/src/platform/server/fs/index.ts +416 -0
  112. package/src/platform/server/fs/path-join.api.ts +1 -0
  113. package/src/platform/server/fs/path-join.electron.ts +1 -0
  114. package/src/platform/server/fs/path-join.ts +97 -0
  115. package/src/platform/server/fs/shared.ts +33 -0
  116. package/src/platform/server/indexeddb/index.ts +115 -0
  117. package/src/platform/server/log/index.ts +43 -0
  118. package/src/platform/server/sqlite/index.api.ts +2 -0
  119. package/src/platform/server/sqlite/index.electron.ts +134 -0
  120. package/src/platform/server/sqlite/index.test.ts +108 -0
  121. package/src/platform/server/sqlite/index.ts +241 -0
  122. package/src/platform/server/sqlite/normalise.ts +9 -0
  123. package/src/platform/server/sqlite/unicodeLike.test.ts +58 -0
  124. package/src/platform/server/sqlite/unicodeLike.ts +31 -0
  125. package/src/server/__mocks__/post.ts +9 -0
  126. package/src/server/__snapshots__/main.test.ts.snap +199 -0
  127. package/src/server/__snapshots__/sheet.test.ts.snap +9 -0
  128. package/src/server/accounts/__snapshots__/sync.test.ts.snap +136 -0
  129. package/src/server/accounts/app-bank-sync.test.ts +136 -0
  130. package/src/server/accounts/app.ts +1294 -0
  131. package/src/server/accounts/link.ts +25 -0
  132. package/src/server/accounts/payees.ts +36 -0
  133. package/src/server/accounts/sync.test.ts +679 -0
  134. package/src/server/accounts/sync.ts +1168 -0
  135. package/src/server/accounts/title/index.ts +60 -0
  136. package/src/server/accounts/title/lower-case.ts +93 -0
  137. package/src/server/accounts/title/specials.ts +21 -0
  138. package/src/server/admin/app.ts +241 -0
  139. package/src/server/api-models.ts +244 -0
  140. package/src/server/api.test.ts +36 -0
  141. package/src/server/api.ts +1030 -0
  142. package/src/server/app.ts +91 -0
  143. package/src/server/aql/compiler.test.ts +966 -0
  144. package/src/server/aql/compiler.ts +1222 -0
  145. package/src/server/aql/exec.test.ts +289 -0
  146. package/src/server/aql/exec.ts +128 -0
  147. package/src/server/aql/index.ts +41 -0
  148. package/src/server/aql/schema/executors.test.ts +420 -0
  149. package/src/server/aql/schema/executors.ts +345 -0
  150. package/src/server/aql/schema/index.test.ts +67 -0
  151. package/src/server/aql/schema/index.ts +409 -0
  152. package/src/server/aql/schema-helpers.test.ts +242 -0
  153. package/src/server/aql/schema-helpers.ts +208 -0
  154. package/src/server/aql/views.test.ts +62 -0
  155. package/src/server/aql/views.ts +57 -0
  156. package/src/server/auth/app.ts +387 -0
  157. package/src/server/bench.ts +29 -0
  158. package/src/server/budget/actions.ts +686 -0
  159. package/src/server/budget/app.ts +469 -0
  160. package/src/server/budget/base.test.ts +340 -0
  161. package/src/server/budget/base.ts +339 -0
  162. package/src/server/budget/category-template-context.test.ts +1658 -0
  163. package/src/server/budget/category-template-context.ts +862 -0
  164. package/src/server/budget/cleanup-template.pegjs +27 -0
  165. package/src/server/budget/cleanup-template.ts +408 -0
  166. package/src/server/budget/envelope.ts +403 -0
  167. package/src/server/budget/goal-template.pegjs +110 -0
  168. package/src/server/budget/goal-template.ts +309 -0
  169. package/src/server/budget/report.ts +308 -0
  170. package/src/server/budget/schedule-template.test.ts +184 -0
  171. package/src/server/budget/schedule-template.ts +351 -0
  172. package/src/server/budget/statements.ts +60 -0
  173. package/src/server/budget/template-notes.test.ts +393 -0
  174. package/src/server/budget/template-notes.ts +323 -0
  175. package/src/server/budget/util.ts +25 -0
  176. package/src/server/budgetfiles/__snapshots__/backups.test.ts.snap +101 -0
  177. package/src/server/budgetfiles/app.ts +672 -0
  178. package/src/server/budgetfiles/backups.test.ts +79 -0
  179. package/src/server/budgetfiles/backups.ts +251 -0
  180. package/src/server/cloud-storage.ts +467 -0
  181. package/src/server/dashboard/app.ts +373 -0
  182. package/src/server/db/__snapshots__/index.test.ts.snap +271 -0
  183. package/src/server/db/index.test.ts +300 -0
  184. package/src/server/db/index.ts +855 -0
  185. package/src/server/db/mappings.ts +59 -0
  186. package/src/server/db/sort.ts +58 -0
  187. package/src/server/db/types/index.ts +342 -0
  188. package/src/server/db/util.ts +36 -0
  189. package/src/server/encryption/app.ts +133 -0
  190. package/src/server/encryption/encryption-internals.api.ts +2 -0
  191. package/src/server/encryption/encryption-internals.electron.ts +89 -0
  192. package/src/server/encryption/encryption-internals.ts +109 -0
  193. package/src/server/encryption/encryption.test.ts +19 -0
  194. package/src/server/encryption/index.test.ts +19 -0
  195. package/src/server/encryption/index.ts +89 -0
  196. package/src/server/errors.ts +110 -0
  197. package/src/server/filters/app.ts +191 -0
  198. package/src/server/importers/actual.ts +49 -0
  199. package/src/server/importers/index.ts +58 -0
  200. package/src/server/importers/ynab4-types.ts +163 -0
  201. package/src/server/importers/ynab4.ts +470 -0
  202. package/src/server/importers/ynab5-types.ts +290 -0
  203. package/src/server/importers/ynab5.ts +1193 -0
  204. package/src/server/main-app.ts +25 -0
  205. package/src/server/main.test.ts +392 -0
  206. package/src/server/main.ts +336 -0
  207. package/src/server/migrate/__snapshots__/migrations.test.ts.snap +17 -0
  208. package/src/server/migrate/cli.ts +100 -0
  209. package/src/server/migrate/migrations.test.ts +81 -0
  210. package/src/server/migrate/migrations.ts +192 -0
  211. package/src/server/models.ts +184 -0
  212. package/src/server/mutators.ts +139 -0
  213. package/src/server/notes/app.ts +18 -0
  214. package/src/server/payees/app.ts +351 -0
  215. package/src/server/polyfills.ts +26 -0
  216. package/src/server/post.ts +219 -0
  217. package/src/server/preferences/app.ts +249 -0
  218. package/src/server/prefs.ts +91 -0
  219. package/src/server/reports/app.ts +187 -0
  220. package/src/server/rules/action.ts +344 -0
  221. package/src/server/rules/app.ts +193 -0
  222. package/src/server/rules/condition.ts +436 -0
  223. package/src/server/rules/customFunctions.ts +61 -0
  224. package/src/server/rules/formula-action.test.ts +175 -0
  225. package/src/server/rules/handlebars-helpers.ts +131 -0
  226. package/src/server/rules/index.test.ts +1095 -0
  227. package/src/server/rules/index.ts +22 -0
  228. package/src/server/rules/rule-indexer.ts +89 -0
  229. package/src/server/rules/rule-utils.ts +274 -0
  230. package/src/server/rules/rule.ts +193 -0
  231. package/src/server/schedules/app.test.ts +502 -0
  232. package/src/server/schedules/app.ts +644 -0
  233. package/src/server/schedules/find-schedules.ts +391 -0
  234. package/src/server/server-config.ts +59 -0
  235. package/src/server/sheet.test.ts +101 -0
  236. package/src/server/sheet.ts +280 -0
  237. package/src/server/spreadsheet/__snapshots__/spreadsheet.test.ts.snap +5 -0
  238. package/src/server/spreadsheet/app.ts +54 -0
  239. package/src/server/spreadsheet/globals.ts +13 -0
  240. package/src/server/spreadsheet/graph-data-structure.ts +165 -0
  241. package/src/server/spreadsheet/scratch +60 -0
  242. package/src/server/spreadsheet/spreadsheet.test.ts +191 -0
  243. package/src/server/spreadsheet/spreadsheet.ts +523 -0
  244. package/src/server/spreadsheet/util.ts +15 -0
  245. package/src/server/sql/init.sql +88 -0
  246. package/src/server/sync/__snapshots__/sync.test.ts.snap +31 -0
  247. package/src/server/sync/app.ts +29 -0
  248. package/src/server/sync/encoder.ts +129 -0
  249. package/src/server/sync/index.ts +820 -0
  250. package/src/server/sync/make-test-message.ts +19 -0
  251. package/src/server/sync/migrate.test.ts +169 -0
  252. package/src/server/sync/migrate.ts +48 -0
  253. package/src/server/sync/repair.ts +39 -0
  254. package/src/server/sync/reset.ts +91 -0
  255. package/src/server/sync/sync.property.test.ts +385 -0
  256. package/src/server/sync/sync.test.ts +349 -0
  257. package/src/server/sync/utils.ts +3 -0
  258. package/src/server/tags/app.ts +101 -0
  259. package/src/server/tests/mockData.json +9352 -0
  260. package/src/server/tests/mockSyncServer.ts +119 -0
  261. package/src/server/tools/app.ts +152 -0
  262. package/src/server/transactions/__snapshots__/transaction-rules.test.ts.snap +173 -0
  263. package/src/server/transactions/__snapshots__/transfer.test.ts.snap +655 -0
  264. package/src/server/transactions/app.ts +136 -0
  265. package/src/server/transactions/export/export-to-csv.ts +132 -0
  266. package/src/server/transactions/import/__snapshots__/parse-file.test.ts.snap +1582 -0
  267. package/src/server/transactions/import/ofx2json.test.ts +33 -0
  268. package/src/server/transactions/import/ofx2json.ts +157 -0
  269. package/src/server/transactions/import/parse-file.test.ts +224 -0
  270. package/src/server/transactions/import/parse-file.ts +286 -0
  271. package/src/server/transactions/import/qif2json.ts +110 -0
  272. package/src/server/transactions/import/xmlcamt2json.ts +168 -0
  273. package/src/server/transactions/index.ts +196 -0
  274. package/src/server/transactions/merge.test.ts +370 -0
  275. package/src/server/transactions/merge.ts +139 -0
  276. package/src/server/transactions/transaction-rules.test.ts +994 -0
  277. package/src/server/transactions/transaction-rules.ts +1038 -0
  278. package/src/server/transactions/transfer.test.ts +221 -0
  279. package/src/server/transactions/transfer.ts +173 -0
  280. package/src/server/undo.ts +271 -0
  281. package/src/server/update.ts +37 -0
  282. package/src/server/util/budget-name.ts +61 -0
  283. package/src/server/util/custom-sync-mapping.ts +48 -0
  284. package/src/server/util/rschedule.ts +9 -0
  285. package/src/shared/__mocks__/platform.ts +7 -0
  286. package/src/shared/__snapshots__/months.test.ts.snap +21 -0
  287. package/src/shared/arithmetic.test.ts +112 -0
  288. package/src/shared/arithmetic.ts +170 -0
  289. package/src/shared/async.test.ts +135 -0
  290. package/src/shared/async.ts +76 -0
  291. package/src/shared/constants.ts +5 -0
  292. package/src/shared/currencies.ts +70 -0
  293. package/src/shared/dashboard.ts +260 -0
  294. package/src/shared/environment.ts +18 -0
  295. package/src/shared/errors.ts +195 -0
  296. package/src/shared/locale.ts +27 -0
  297. package/src/shared/location-utils.test.ts +69 -0
  298. package/src/shared/location-utils.ts +49 -0
  299. package/src/shared/months.test.ts +5 -0
  300. package/src/shared/months.ts +485 -0
  301. package/src/shared/normalisation.ts +6 -0
  302. package/src/shared/platform.electron.ts +21 -0
  303. package/src/shared/platform.ts +20 -0
  304. package/src/shared/query.ts +176 -0
  305. package/src/shared/rules.test.ts +56 -0
  306. package/src/shared/rules.ts +371 -0
  307. package/src/shared/schedules.test.ts +570 -0
  308. package/src/shared/schedules.ts +560 -0
  309. package/src/shared/test-helpers.ts +156 -0
  310. package/src/shared/transactions.test.ts +275 -0
  311. package/src/shared/transactions.ts +433 -0
  312. package/src/shared/transfer.test.ts +75 -0
  313. package/src/shared/transfer.ts +16 -0
  314. package/src/shared/user.ts +4 -0
  315. package/src/shared/util.test.ts +240 -0
  316. package/src/shared/util.ts +633 -0
  317. package/src/types/api-handlers.ts +287 -0
  318. package/src/types/budget.ts +8 -0
  319. package/src/types/file.ts +47 -0
  320. package/src/types/handlers.ts +46 -0
  321. package/src/types/models/account.ts +24 -0
  322. package/src/types/models/bank-sync.ts +23 -0
  323. package/src/types/models/bank.ts +6 -0
  324. package/src/types/models/category-group.ts +11 -0
  325. package/src/types/models/category.ts +13 -0
  326. package/src/types/models/dashboard.ts +199 -0
  327. package/src/types/models/gocardless.ts +84 -0
  328. package/src/types/models/import-transaction.ts +56 -0
  329. package/src/types/models/index.ts +23 -0
  330. package/src/types/models/nearby-payee.ts +7 -0
  331. package/src/types/models/note.ts +4 -0
  332. package/src/types/models/openid.ts +8 -0
  333. package/src/types/models/payee-location.ts +8 -0
  334. package/src/types/models/payee.ts +10 -0
  335. package/src/types/models/pluggyai.ts +19 -0
  336. package/src/types/models/reports.ts +144 -0
  337. package/src/types/models/rule.ts +174 -0
  338. package/src/types/models/schedule.ts +49 -0
  339. package/src/types/models/simplefin.ts +28 -0
  340. package/src/types/models/tags.ts +6 -0
  341. package/src/types/models/templates.ts +135 -0
  342. package/src/types/models/transaction-filter.ts +9 -0
  343. package/src/types/models/transaction.ts +39 -0
  344. package/src/types/models/user-access.ts +10 -0
  345. package/src/types/models/user.ts +25 -0
  346. package/src/types/prefs.ts +167 -0
  347. package/src/types/server-events.ts +86 -0
  348. package/src/types/server-handlers.ts +27 -0
  349. package/src/types/util.ts +26 -0
  350. package/tsconfig.json +34 -0
  351. package/typings/pegjs.ts +1 -0
  352. package/typings/process-worker.ts +12 -0
  353. package/typings/vite-plugin-peggy-loader.ts +1 -0
  354. package/typings/window.ts +62 -0
  355. package/vite.config.ts +109 -0
  356. package/vite.desktop.config.ts +59 -0
  357. package/vitest.config.ts +43 -0
  358. package/vitest.web.config.ts +38 -0
@@ -0,0 +1,820 @@
1
+ // @ts-strict-ignore
2
+ import {
3
+ deserializeClock,
4
+ getClock,
5
+ merkle,
6
+ serializeClock,
7
+ Timestamp,
8
+ } from '@actual-app/crdt';
9
+
10
+ import { captureException } from '../../platform/exceptions';
11
+ import * as asyncStorage from '../../platform/server/asyncStorage';
12
+ import * as connection from '../../platform/server/connection';
13
+ import { logger } from '../../platform/server/log';
14
+ import { once, sequential } from '../../shared/async';
15
+ import { getIn, setIn } from '../../shared/util';
16
+ import type { MetadataPrefs } from '../../types/prefs';
17
+ import { setType as setBudgetType, triggerBudgetChanges } from '../budget/base';
18
+ import * as db from '../db';
19
+ import { PostError, SyncError } from '../errors';
20
+ import { app } from '../main-app';
21
+ import { runMutator } from '../mutators';
22
+ import { postBinary } from '../post';
23
+ import * as prefs from '../prefs';
24
+ import { getServer } from '../server-config';
25
+ import * as sheet from '../sheet';
26
+ import * as undo from '../undo';
27
+
28
+ import * as encoder from './encoder';
29
+ import { rebuildMerkleHash } from './repair';
30
+ import { isError } from './utils';
31
+
32
+ export { makeTestMessage } from './make-test-message';
33
+ export { resetSync } from './reset';
34
+ export { repairSync } from './repair';
35
+
36
+ const FULL_SYNC_DELAY = 1000;
37
+ let SYNCING_MODE = 'enabled';
38
+ type SyncingMode = 'enabled' | 'offline' | 'disabled' | 'import';
39
+
40
+ export function setSyncingMode(mode: SyncingMode) {
41
+ const prevMode = SYNCING_MODE;
42
+ switch (mode) {
43
+ case 'enabled':
44
+ SYNCING_MODE = 'enabled';
45
+ break;
46
+ case 'offline':
47
+ SYNCING_MODE = 'offline';
48
+ break;
49
+ case 'disabled':
50
+ SYNCING_MODE = 'disabled';
51
+ break;
52
+ case 'import':
53
+ SYNCING_MODE = 'import';
54
+ break;
55
+ default:
56
+ throw new Error('setSyncingMode: invalid mode: ' + mode);
57
+ }
58
+ return prevMode;
59
+ }
60
+
61
+ export function checkSyncingMode(mode: SyncingMode): boolean {
62
+ switch (mode) {
63
+ case 'enabled':
64
+ return SYNCING_MODE === 'enabled' || SYNCING_MODE === 'offline';
65
+ case 'disabled':
66
+ return SYNCING_MODE === 'disabled' || SYNCING_MODE === 'import';
67
+ case 'offline':
68
+ return SYNCING_MODE === 'offline';
69
+ case 'import':
70
+ return SYNCING_MODE === 'import';
71
+ default:
72
+ throw new Error('checkSyncingMode: invalid mode: ' + mode);
73
+ }
74
+ }
75
+
76
+ function apply(msg: Message, prev?: boolean) {
77
+ const { dataset, row, column, value } = msg;
78
+
79
+ if (dataset === 'prefs') {
80
+ // Do nothing, it doesn't exist in the db
81
+ } else {
82
+ let query;
83
+ try {
84
+ if (prev) {
85
+ query = {
86
+ sql: `UPDATE ${dataset} SET ${column} = ? WHERE id = ?`,
87
+ params: [value, row],
88
+ };
89
+ } else {
90
+ query = {
91
+ sql: `INSERT INTO ${dataset} (id, ${column}) VALUES (?, ?)`,
92
+ params: [row, value],
93
+ };
94
+ }
95
+
96
+ db.runQuery(db.cache(query.sql), query.params);
97
+ } catch (error) {
98
+ throw new SyncError('invalid-schema', {
99
+ error: { message: error.message, stack: error.stack },
100
+ query,
101
+ });
102
+ }
103
+ }
104
+ }
105
+
106
+ // TODO: convert to `whereIn`
107
+ async function fetchAll(table, ids) {
108
+ let results = [];
109
+
110
+ // was 500, but that caused a stack overflow in Safari
111
+ const batchSize = 100;
112
+
113
+ for (let i = 0; i < ids.length; i += batchSize) {
114
+ const partIds = ids.slice(i, i + batchSize);
115
+ let sql;
116
+ let column = `${table}.id`;
117
+
118
+ // We have to provide *mapped* data so the spreadsheet works. The functions
119
+ // which trigger budget changes based on data changes assumes data has been
120
+ // mapped. The only mapped data that the budget is concerned about is
121
+ // categories. This is kind of annoying, but we manually map it here
122
+ if (table === 'transactions') {
123
+ sql = `
124
+ SELECT t.*, c.transferId AS category
125
+ FROM transactions t
126
+ LEFT JOIN category_mapping c ON c.id = t.category
127
+ `;
128
+ column = 't.id';
129
+ } else {
130
+ sql = `SELECT * FROM ${table}`;
131
+ }
132
+
133
+ sql += ` WHERE `;
134
+ sql += partIds.map(() => `${column} = ?`).join(' OR ');
135
+
136
+ try {
137
+ const rows = db.runQuery(sql, partIds, true);
138
+ results = results.concat(rows);
139
+ } catch (error) {
140
+ throw new SyncError('invalid-schema', {
141
+ error: {
142
+ message: error.message,
143
+ stack: error.stack,
144
+ },
145
+ query: { sql, params: partIds },
146
+ });
147
+ }
148
+ }
149
+
150
+ return results;
151
+ }
152
+
153
+ export function serializeValue(value: string | number | null): string {
154
+ if (value === null) {
155
+ return '0:';
156
+ } else if (typeof value === 'number') {
157
+ return 'N:' + value;
158
+ } else if (typeof value === 'string') {
159
+ return 'S:' + value;
160
+ }
161
+
162
+ throw new Error('Unserializable value type: ' + JSON.stringify(value));
163
+ }
164
+
165
+ export function deserializeValue(value: string): string | number | null {
166
+ const type = value[0];
167
+ switch (type) {
168
+ case '0':
169
+ return null;
170
+ case 'N':
171
+ return parseFloat(value.slice(2));
172
+ case 'S':
173
+ return value.slice(2);
174
+ default:
175
+ }
176
+
177
+ throw new Error('Invalid type key for value: ' + value);
178
+ }
179
+
180
+ // TODO make this type stricter.
181
+ type DataMap = Map<string, unknown>;
182
+ type SyncListener = (oldData: DataMap, newData: DataMap) => unknown;
183
+ let _syncListeners: SyncListener[] = [];
184
+
185
+ export function addSyncListener(func: SyncListener) {
186
+ _syncListeners.push(func);
187
+
188
+ return () => {
189
+ _syncListeners = _syncListeners.filter(f => f !== func);
190
+ };
191
+ }
192
+
193
+ async function compareMessages(messages: Message[]): Promise<Message[]> {
194
+ const newMessages = [];
195
+
196
+ for (let i = 0; i < messages.length; i++) {
197
+ const message = messages[i];
198
+ const { dataset, row, column, timestamp } = message;
199
+ const timestampStr = timestamp.toString();
200
+
201
+ const res = db.runQuery<Pick<db.DbCrdtMessage, 'timestamp'>>(
202
+ db.cache(
203
+ 'SELECT timestamp FROM messages_crdt WHERE dataset = ? AND row = ? AND column = ? AND timestamp >= ?',
204
+ ),
205
+ [dataset, row, column, timestampStr],
206
+ true,
207
+ );
208
+
209
+ // Returned message is any one that is "later" than this message,
210
+ // meaning if the result exists this message is an old one
211
+ if (res.length === 0) {
212
+ newMessages.push(message);
213
+ } else if (res[0].timestamp !== timestampStr) {
214
+ newMessages.push({ ...message, old: true });
215
+ }
216
+ }
217
+
218
+ return newMessages;
219
+ }
220
+
221
+ // This is the fast path `apply` function when in "import" mode.
222
+ // There's no need to run through the whole sync system when
223
+ // importing, but **there is a caveat**: because we don't run sync
224
+ // listeners importers should not rely on any functions that use any
225
+ // projected state (like rules). We can't fire those because they
226
+ // depend on having both old and new data which we don't quere here
227
+ function applyMessagesForImport(messages: Message[]): void {
228
+ db.transaction(() => {
229
+ for (let i = 0; i < messages.length; i++) {
230
+ const msg = messages[i];
231
+ const { dataset } = msg;
232
+
233
+ if (!msg.old) {
234
+ try {
235
+ apply(msg);
236
+ } catch {
237
+ apply(msg, true);
238
+ }
239
+
240
+ if (dataset === 'prefs') {
241
+ throw new Error('Cannot set prefs while importing');
242
+ }
243
+ }
244
+ }
245
+ });
246
+ }
247
+
248
+ export type Message = {
249
+ column: string;
250
+ dataset: string;
251
+ old?: unknown;
252
+ row: string;
253
+ timestamp: Timestamp;
254
+ value: string | number | null;
255
+ };
256
+
257
+ export const applyMessages = sequential(async (messages: Message[]) => {
258
+ if (checkSyncingMode('import')) {
259
+ applyMessagesForImport(messages);
260
+ return undefined;
261
+ } else if (checkSyncingMode('enabled')) {
262
+ // Compare the messages with the existing crdt. This filters out
263
+ // already applied messages and determines if a message is old or
264
+ // not. An "old" message doesn't need to be applied, but it still
265
+ // needs to be put into the merkle trie to maintain the hash.
266
+ messages = await compareMessages(messages);
267
+ }
268
+
269
+ messages = [...messages].sort((m1, m2) => {
270
+ const t1 = m1.timestamp ? m1.timestamp.toString() : '';
271
+ const t2 = m2.timestamp ? m2.timestamp.toString() : '';
272
+ if (t1 < t2) {
273
+ return -1;
274
+ } else if (t1 > t2) {
275
+ return 1;
276
+ }
277
+ return 0;
278
+ });
279
+
280
+ const idsPerTable = {};
281
+ messages.forEach(msg => {
282
+ if (msg.dataset === 'prefs') {
283
+ return;
284
+ }
285
+
286
+ if (idsPerTable[msg.dataset] == null) {
287
+ idsPerTable[msg.dataset] = [];
288
+ }
289
+ idsPerTable[msg.dataset].push(msg.row);
290
+ });
291
+
292
+ async function fetchData(): Promise<DataMap> {
293
+ const data = new Map();
294
+
295
+ for (const table of Object.keys(idsPerTable)) {
296
+ const rows = await fetchAll(table, idsPerTable[table]);
297
+
298
+ for (let i = 0; i < rows.length; i++) {
299
+ const row = rows[i];
300
+ setIn(data, [table, row.id], row);
301
+ }
302
+ }
303
+
304
+ return data;
305
+ }
306
+
307
+ const prefsToSet: MetadataPrefs = {};
308
+ const oldData = await fetchData();
309
+
310
+ undo.appendMessages(messages, oldData);
311
+
312
+ // It's important to not mutate the clock while processing the
313
+ // messages. We only want to mutate it if the transaction succeeds.
314
+ // The merkle variable will be updated while applying the messages and
315
+ // we'll apply it afterwards.
316
+ let clock;
317
+ let currentMerkle;
318
+ if (checkSyncingMode('enabled')) {
319
+ clock = getClock();
320
+ currentMerkle = clock.merkle;
321
+ }
322
+
323
+ if (sheet.get()) {
324
+ sheet.get().startCacheBarrier();
325
+ }
326
+
327
+ // Now that we have all of the data, go through and apply the
328
+ // messages carefully. This transaction is **crucial**: it
329
+ // guarantees that everything is atomically committed to the
330
+ // database, and if any part of it fails everything aborts and
331
+ // nothing is changed. This is critical to maintain consistency. We
332
+ // also avoid any side effects to in-memory objects, and apply them
333
+ // after this succeeds.
334
+ db.transaction(() => {
335
+ const added = new Set();
336
+
337
+ for (const msg of messages) {
338
+ const { dataset, row, column, timestamp, value } = msg;
339
+
340
+ if (!msg.old) {
341
+ apply(msg, getIn(oldData, [dataset, row]) || added.has(dataset + row));
342
+
343
+ if (dataset === 'prefs') {
344
+ prefsToSet[row] = value;
345
+ } else {
346
+ // Keep track of which items have been added it in this sync
347
+ // so it knows whether they already exist in the db or not. We
348
+ // ignore any changes to the spreadsheet.
349
+ added.add(dataset + row);
350
+ }
351
+ }
352
+
353
+ if (checkSyncingMode('enabled')) {
354
+ db.runQuery(
355
+ db.cache(`INSERT INTO messages_crdt (timestamp, dataset, row, column, value)
356
+ VALUES (?, ?, ?, ?, ?)`),
357
+ [timestamp.toString(), dataset, row, column, serializeValue(value)],
358
+ );
359
+
360
+ currentMerkle = merkle.insert(currentMerkle, timestamp);
361
+ }
362
+
363
+ // Special treatment for some synced prefs
364
+ if (dataset === 'preferences' && row === 'budgetType') {
365
+ void setBudgetType(value);
366
+ }
367
+ }
368
+
369
+ if (checkSyncingMode('enabled')) {
370
+ currentMerkle = merkle.prune(currentMerkle);
371
+
372
+ // Save the clock in the db first (queries might throw
373
+ // exceptions)
374
+ db.runQuery(
375
+ db.cache(
376
+ 'INSERT OR REPLACE INTO messages_clock (id, clock) VALUES (1, ?)',
377
+ ),
378
+ [serializeClock({ ...clock, merkle: currentMerkle })],
379
+ );
380
+ }
381
+ });
382
+
383
+ if (checkSyncingMode('enabled')) {
384
+ // The transaction succeeded, so we can update in-memory objects
385
+ // now. Update the in-memory clock.
386
+ clock.merkle = currentMerkle;
387
+ }
388
+
389
+ // Save any synced prefs
390
+ if (Object.keys(prefsToSet).length > 0) {
391
+ void prefs.savePrefs(prefsToSet, { avoidSync: true });
392
+ connection.send('prefs-updated');
393
+ }
394
+
395
+ const newData = await fetchData();
396
+
397
+ // In testing, sometimes the spreadsheet isn't loaded, and that's ok
398
+ if (sheet.get()) {
399
+ // Need to clean up these APIs and make them consistent
400
+ sheet.startTransaction();
401
+ triggerBudgetChanges(oldData, newData);
402
+ sheet.get().triggerDatabaseChanges(oldData, newData);
403
+ sheet.endTransaction();
404
+
405
+ // Allow the cache to be used in the future. At this point it's guaranteed
406
+ // to be up-to-date because we are done mutating any other data
407
+ sheet.get().endCacheBarrier();
408
+ }
409
+
410
+ _syncListeners.forEach(func => func(oldData, newData));
411
+
412
+ const tables = getTablesFromMessages(messages.filter(msg => !msg.old));
413
+ app.events.emit('sync', {
414
+ type: 'applied',
415
+ tables,
416
+ data: newData,
417
+ prevData: oldData,
418
+ });
419
+
420
+ return messages;
421
+ });
422
+
423
+ export function receiveMessages(messages: Message[]): Promise<Message[]> {
424
+ try {
425
+ messages.forEach(msg => {
426
+ Timestamp.recv(msg.timestamp);
427
+ });
428
+ } catch (e) {
429
+ if (e instanceof Timestamp.ClockDriftError) {
430
+ throw new SyncError('clock-drift');
431
+ }
432
+ throw e;
433
+ }
434
+
435
+ return runMutator(() => applyMessages(messages));
436
+ }
437
+
438
+ async function errorHandler(e: Error) {
439
+ captureException(e);
440
+
441
+ if (e instanceof SyncError) {
442
+ if (e.reason === 'invalid-schema') {
443
+ // We know this message came from a local modification, and it
444
+ // couldn't apply, which doesn't make any sense. Must be a bug
445
+ // in the code. Send a specific error type for it for a custom
446
+ // message.
447
+ app.events.emit('sync', {
448
+ type: 'error',
449
+ subtype: 'apply-failure',
450
+ meta: e.meta,
451
+ });
452
+ } else {
453
+ app.events.emit('sync', { type: 'error', meta: e.meta });
454
+ }
455
+ } else if (e instanceof Timestamp.ClockDriftError) {
456
+ app.events.emit('sync', {
457
+ type: 'error',
458
+ subtype: 'clock-drift',
459
+ meta: { message: e.message },
460
+ });
461
+ }
462
+ }
463
+
464
+ async function _sendMessages(messages: Message[]): Promise<void> {
465
+ try {
466
+ await applyMessages(messages);
467
+ } catch (e) {
468
+ void errorHandler(e);
469
+ throw e;
470
+ }
471
+
472
+ await scheduleFullSync();
473
+ }
474
+
475
+ let IS_BATCHING = false;
476
+ let _BATCHED: Message[] = [];
477
+ export async function batchMessages(func: () => Promise<void>): Promise<void> {
478
+ if (IS_BATCHING) {
479
+ await func();
480
+ return;
481
+ }
482
+
483
+ IS_BATCHING = true;
484
+ let batched: Message[] = [];
485
+
486
+ try {
487
+ await func();
488
+ } catch (e) {
489
+ void errorHandler(e);
490
+ throw e;
491
+ } finally {
492
+ IS_BATCHING = false;
493
+ batched = _BATCHED;
494
+ _BATCHED = [];
495
+ }
496
+
497
+ if (batched.length > 0) {
498
+ await _sendMessages(batched);
499
+ }
500
+ }
501
+
502
+ export async function sendMessages(messages: Message[]) {
503
+ if (IS_BATCHING) {
504
+ _BATCHED = _BATCHED.concat(messages);
505
+ } else {
506
+ return _sendMessages(messages);
507
+ }
508
+ }
509
+
510
+ export function getMessagesSince(since: string): Message[] {
511
+ return db.runQuery(
512
+ 'SELECT timestamp, dataset, row, column, value FROM messages_crdt WHERE timestamp > ?',
513
+ [since],
514
+ true,
515
+ );
516
+ }
517
+
518
+ export function clearFullSyncTimeout(): void {
519
+ if (syncTimeout) {
520
+ clearTimeout(syncTimeout);
521
+ syncTimeout = null;
522
+ }
523
+ }
524
+
525
+ let syncTimeout = null;
526
+ export function scheduleFullSync(): Promise<
527
+ { messages: Message[] } | { error: unknown }
528
+ > {
529
+ clearFullSyncTimeout();
530
+
531
+ if (checkSyncingMode('enabled') && !checkSyncingMode('offline')) {
532
+ if (process.env.NODE_ENV === 'test') {
533
+ return fullSync().then(res => {
534
+ if (isError(res)) {
535
+ throw res.error;
536
+ }
537
+ return res;
538
+ });
539
+ } else {
540
+ syncTimeout = setTimeout(fullSync, FULL_SYNC_DELAY);
541
+ }
542
+ }
543
+ }
544
+
545
+ function getTablesFromMessages(messages: Message[]): string[] {
546
+ return messages.reduce((acc, message) => {
547
+ const dataset =
548
+ message.dataset === 'schedules_next_date' ? 'schedules' : message.dataset;
549
+
550
+ if (!acc.includes(dataset)) {
551
+ acc.push(dataset);
552
+ }
553
+ return acc;
554
+ }, []);
555
+ }
556
+
557
+ // This is different than `fullSync` because it waits for the
558
+ // spreadsheet to finish any processing. This is useful if we want to
559
+ // perform a full sync and wait for everything to finish, usually if
560
+ // you're doing an initial sync before working with a file.
561
+ export async function initialFullSync(): Promise<{
562
+ error?: { message: string; reason: string; meta: unknown };
563
+ }> {
564
+ const result = await fullSync();
565
+ if (isError(result)) {
566
+ // Make sure to wait for anything in the spreadsheet to process
567
+ await sheet.waitOnSpreadsheet();
568
+ return result;
569
+ }
570
+ return {};
571
+ }
572
+
573
+ export const fullSync = once(async function (): Promise<
574
+ | { messages: Message[] }
575
+ | { error: { message: string; reason: string; meta: unknown } }
576
+ > {
577
+ app.events.emit('sync', { type: 'start' });
578
+ let messages;
579
+
580
+ try {
581
+ messages = await _fullSync(null, 0, null);
582
+ } catch (e) {
583
+ logger.log(e);
584
+
585
+ if (e instanceof SyncError) {
586
+ if (e.reason === 'out-of-sync') {
587
+ captureException(e);
588
+
589
+ app.events.emit('sync', {
590
+ type: 'error',
591
+ subtype: 'out-of-sync',
592
+ meta: e.meta,
593
+ });
594
+ } else if (e.reason === 'invalid-schema') {
595
+ app.events.emit('sync', {
596
+ type: 'error',
597
+ subtype: 'invalid-schema',
598
+ meta: e.meta,
599
+ });
600
+ } else if (
601
+ e.reason === 'decrypt-failure' ||
602
+ e.reason === 'encrypt-failure'
603
+ ) {
604
+ app.events.emit('sync', {
605
+ type: 'error',
606
+ subtype: e.reason,
607
+ meta: e.meta,
608
+ });
609
+ } else if (e.reason === 'clock-drift') {
610
+ app.events.emit('sync', {
611
+ type: 'error',
612
+ subtype: 'clock-drift',
613
+ meta: e.meta,
614
+ });
615
+ } else {
616
+ app.events.emit('sync', { type: 'error', meta: e.meta });
617
+ }
618
+ } else if (e instanceof PostError) {
619
+ logger.log(e);
620
+ if (e.reason === 'unauthorized') {
621
+ app.events.emit('sync', { type: 'unauthorized' });
622
+
623
+ // Set the user into read-only mode
624
+ void asyncStorage.setItem('readOnly', 'true');
625
+ } else if (e.reason === 'network-failure') {
626
+ app.events.emit('sync', { type: 'error', subtype: 'network' });
627
+ } else {
628
+ app.events.emit('sync', { type: 'error', subtype: e.reason });
629
+ }
630
+ } else {
631
+ captureException(e);
632
+ // TODO: Send the message to the client and allow them to expand & view it
633
+ app.events.emit('sync', { type: 'error' });
634
+ }
635
+
636
+ return { error: { message: e.message, reason: e.reason, meta: e.meta } };
637
+ }
638
+
639
+ const tables = getTablesFromMessages(messages);
640
+
641
+ app.events.emit('sync', {
642
+ type: 'success',
643
+ tables,
644
+ syncDisabled: checkSyncingMode('disabled'),
645
+ });
646
+ return { messages };
647
+ });
648
+
649
+ async function _fullSync(
650
+ sinceTimestamp: string,
651
+ count: number,
652
+ prevDiffTime: number,
653
+ ): Promise<Message[]> {
654
+ const {
655
+ id: currentId,
656
+ cloudFileId,
657
+ groupId,
658
+ lastSyncedTimestamp,
659
+ } = prefs.getPrefs() || {};
660
+
661
+ clearFullSyncTimeout();
662
+
663
+ if (
664
+ checkSyncingMode('disabled') ||
665
+ checkSyncingMode('offline') ||
666
+ !currentId
667
+ ) {
668
+ return [];
669
+ }
670
+
671
+ // Snapshot the point at which we are currently syncing
672
+ const currentTime = getClock().timestamp.toString();
673
+
674
+ const since =
675
+ sinceTimestamp ||
676
+ lastSyncedTimestamp ||
677
+ // Default to 5 minutes ago
678
+ new Timestamp(Date.now() - 5 * 60 * 1000, 0, '0').toString();
679
+
680
+ const messages = getMessagesSince(since);
681
+
682
+ const userToken = await asyncStorage.getItem('user-token');
683
+
684
+ logger.info(
685
+ 'Syncing since',
686
+ since,
687
+ messages.length,
688
+ '(attempt: ' + count + ')',
689
+ );
690
+
691
+ const buffer = await encoder.encode(groupId, cloudFileId, since, messages);
692
+
693
+ // TODO: There a limit on how many messages we can send because of
694
+ // the payload size. Right now it's at 20MB on the server. We should
695
+ // check the worst case here and make multiple requests if it's
696
+ // really large.
697
+ const resBuffer = await postBinary(
698
+ getServer().SYNC_SERVER + '/sync',
699
+ buffer,
700
+ {
701
+ 'X-ACTUAL-TOKEN': userToken,
702
+ },
703
+ );
704
+
705
+ // Abort if the file is either no longer loaded, the group id has
706
+ // changed because of a sync reset
707
+ if (!prefs.getPrefs() || prefs.getPrefs().groupId !== groupId) {
708
+ return [];
709
+ }
710
+
711
+ const res = await encoder.decode(resBuffer);
712
+
713
+ logger.info('Got messages from server', res.messages.length);
714
+
715
+ const localTimeChanged = getClock().timestamp.toString() !== currentTime;
716
+
717
+ // Apply the new messages
718
+ let receivedMessages: Message[] = [];
719
+ if (res.messages.length > 0) {
720
+ receivedMessages = await receiveMessages(
721
+ res.messages.map(msg => ({
722
+ ...msg,
723
+ value: deserializeValue(msg.value as string),
724
+ })),
725
+ );
726
+ }
727
+
728
+ const diffTime = merkle.diff(res.merkle, getClock().merkle);
729
+
730
+ if (diffTime !== null) {
731
+ // This is a bit wonky, but we loop until we are in sync with the
732
+ // server. While syncing, either the client or server could change
733
+ // out from under us, so it might take a couple passes to
734
+ // completely sync up. This is a check that stops the loop in case
735
+ // we are corrupted and can't sync up. We try 10 times if we keep
736
+ // getting the same diff time, and add a upper limit of 300 no
737
+ // matter what (just to stop this from ever being an infinite
738
+ // loop).
739
+ //
740
+ // It's slightly possible for the user to add more messages while we
741
+ // are in `receiveMessages`, but `localTimeChanged` would still be
742
+ // false. In that case, we don't reset the counter but it should be
743
+ // very unlikely that this happens enough to hit the loop limit.
744
+
745
+ if ((count >= 10 && diffTime === prevDiffTime) || count >= 100) {
746
+ logger.info('SENT -------');
747
+ logger.info(JSON.stringify(messages));
748
+ logger.info('RECEIVED -------');
749
+ logger.info(JSON.stringify(res.messages));
750
+
751
+ const rebuiltMerkle = rebuildMerkleHash();
752
+
753
+ logger.log(
754
+ count,
755
+ 'messages:',
756
+ messages.length,
757
+ messages.length > 0 ? messages[0] : null,
758
+ 'res.messages:',
759
+ res.messages.length,
760
+ res.messages.length > 0 ? res.messages[0] : null,
761
+ 'clientId',
762
+ getClock().timestamp.node(),
763
+ 'groupId',
764
+ groupId,
765
+ 'diffTime:',
766
+ diffTime,
767
+ diffTime === prevDiffTime,
768
+ 'local clock:',
769
+ getClock().timestamp.toString(),
770
+ getClock().merkle.hash,
771
+ 'rebuilt hash:',
772
+ rebuiltMerkle.numMessages,
773
+ rebuiltMerkle.trie.hash,
774
+ 'server hash:',
775
+ res.merkle.hash,
776
+ 'localTimeChanged:',
777
+ localTimeChanged,
778
+ );
779
+
780
+ if (rebuiltMerkle.trie.hash === res.merkle.hash) {
781
+ // Rebuilding the merkle worked... but why?
782
+ const clocks = await db.all<db.DbClockMessage>(
783
+ 'SELECT * FROM messages_clock',
784
+ );
785
+ if (clocks.length !== 1) {
786
+ logger.log('Bad number of clocks:', clocks.length);
787
+ }
788
+ const hash = deserializeClock(clocks[0].clock).merkle.hash;
789
+ logger.log('Merkle hash in db:', hash);
790
+ }
791
+
792
+ throw new SyncError('out-of-sync');
793
+ }
794
+
795
+ receivedMessages = receivedMessages.concat(
796
+ await _fullSync(
797
+ new Timestamp(diffTime, 0, '0').toString(),
798
+ // If something local changed while we were syncing, always
799
+ // reset, token the counter. We never want to think syncing failed
800
+ // because we tried to syncing many times and couldn't sync,
801
+ // but it was because the user kept changing stuff in the
802
+ // middle of syncing.
803
+ localTimeChanged ? 0 : count + 1,
804
+ diffTime,
805
+ ),
806
+ );
807
+ } else {
808
+ // All synced up, store the current time as a simple optimization for the next sync
809
+ const requiresUpdate =
810
+ getClock().timestamp.toString() !== lastSyncedTimestamp;
811
+
812
+ if (requiresUpdate) {
813
+ await prefs.savePrefs({
814
+ lastSyncedTimestamp: getClock().timestamp.toString(),
815
+ });
816
+ }
817
+ }
818
+
819
+ return receivedMessages;
820
+ }