@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.
- package/.swcrc +11 -0
- package/bin/build-browser +40 -0
- package/bin/copy-migrations +9 -0
- package/db.sqlite +0 -0
- package/default-db.sqlite +0 -0
- package/migrations/.force-copy-windows +0 -0
- package/migrations/1548957970627_remove-db-version.sql +5 -0
- package/migrations/1550601598648_payees.sql +23 -0
- package/migrations/1555786194328_remove_category_group_unique.sql +25 -0
- package/migrations/1561751833510_indexes.sql +7 -0
- package/migrations/1567699552727_budget.sql +38 -0
- package/migrations/1582384163573_cleared.sql +6 -0
- package/migrations/1597756566448_rules.sql +10 -0
- package/migrations/1608652596043_parent_field.sql +13 -0
- package/migrations/1608652596044_trans_views.sql +56 -0
- package/migrations/1612625548236_optimize.sql +7 -0
- package/migrations/1614782639336_trans_views2.sql +33 -0
- package/migrations/1615745967948_meta.sql +10 -0
- package/migrations/1616167010796_accounts_order.sql +5 -0
- package/migrations/1618975177358_schedules.sql +28 -0
- package/migrations/1632571489012_remove_cache.js +136 -0
- package/migrations/1679728867040_rules_conditions.sql +5 -0
- package/migrations/1681115033845_add_schedule_name.sql +5 -0
- package/migrations/1682974838138_remove_payee_rules.sql +5 -0
- package/migrations/1685007876842_add_category_hidden.sql +6 -0
- package/migrations/1686139660866_remove_account_type.sql +5 -0
- package/migrations/1688749527273_transaction_filters.sql +10 -0
- package/migrations/1688841238000_add_account_type.sql +5 -0
- package/migrations/1691233396000_add_schedule_next_date_tombstone.sql +5 -0
- package/migrations/1694438752000_add_goal_targets.sql +7 -0
- package/migrations/1697046240000_add_reconciled.sql +5 -0
- package/migrations/1704572023730_add_account_sync_source.sql +5 -0
- package/migrations/1704572023731_add_missing_goCardless_sync_source.sql +9 -0
- package/migrations/1707267033000_reports.sql +28 -0
- package/migrations/1712784523000_unhide_input_group.sql +8 -0
- package/migrations/1716359441000_include_current.sql +5 -0
- package/migrations/1720310586000_link_transfer_schedules.sql +19 -0
- package/migrations/1720664867241_add_payee_favorite.sql +5 -0
- package/migrations/1720665000000_goal_context.sql +6 -0
- package/migrations/1722717601000_reports_move_selected_categories.js +55 -0
- package/migrations/1722804019000_create_dashboard_table.js +69 -0
- package/migrations/1723665565000_prefs.js +59 -0
- package/migrations/1730744182000_fix_dashboard_table.sql +7 -0
- package/migrations/1736640000000_custom_report_sorting.sql +7 -0
- package/migrations/1737158400000_add_learn_categories_to_payees.sql +5 -0
- package/migrations/1738491452000_sorting_rename.sql +13 -0
- package/migrations/1739139550000_bank_sync_page.sql +7 -0
- package/migrations/1740506588539_add_last_reconciled_at.sql +5 -0
- package/migrations/1745425408000_update_budgetType_pref.sql +7 -0
- package/migrations/1749799110000_add_tags.sql +10 -0
- package/migrations/1749799110001_tags_tombstone.sql +5 -0
- package/migrations/1754611200000_add_category_template_settings.sql +5 -0
- package/migrations/1759260219000_add_trim_interval_report_setting.sql +6 -0
- package/migrations/1759842823172_add_isGlobal_to_preferences.sql +1 -0
- package/migrations/1762178745667_rename_csv_skip_lines_pref.sql +8 -0
- package/migrations/1765518577215_multiple_dashboards.js +30 -0
- package/migrations/1768872504000_add_payee_locations.sql +21 -0
- package/package.json +128 -0
- package/src/mocks/arbitrary-schema.ts +162 -0
- package/src/mocks/budget.ts +901 -0
- package/src/mocks/files/8859-1.qfx +63 -0
- package/src/mocks/files/best.data-ever$.QFX +124 -0
- package/src/mocks/files/big.data.QiF +91 -0
- package/src/mocks/files/budgets/.commit-to-git +0 -0
- package/src/mocks/files/camt/camt.053.payee-memo.xml +127 -0
- package/src/mocks/files/camt/camt.053.xml +463 -0
- package/src/mocks/files/credit-card.ofx +11 -0
- package/src/mocks/files/data-multi-decimal.ofx +64 -0
- package/src/mocks/files/data-payee-memo.ofx +75 -0
- package/src/mocks/files/data-payee-memo.qif +17 -0
- package/src/mocks/files/data.ofx +124 -0
- package/src/mocks/files/data.qfx +124 -0
- package/src/mocks/files/data.qif +91 -0
- package/src/mocks/files/default-budget-template/db.sqlite +0 -0
- package/src/mocks/files/default-budget-template/metadata.json +6 -0
- package/src/mocks/files/html-vals.qfx +17 -0
- package/src/mocks/index.ts +221 -0
- package/src/mocks/migrations/1508717984291_up_add-poop.sql +13 -0
- package/src/mocks/migrations/1508718036311_up_modify-poop.sql +2 -0
- package/src/mocks/migrations/1508727787513_remove-is_income.sql +15 -0
- package/src/mocks/random.ts +16 -0
- package/src/mocks/setup.ts +180 -0
- package/src/mocks/spreadsheet.ts +101 -0
- package/src/mocks/util.ts +82 -0
- package/src/platform/client/connection/README.md +3 -0
- package/src/platform/client/connection/__mocks__/index.ts +67 -0
- package/src/platform/client/connection/index-types.ts +95 -0
- package/src/platform/client/connection/index.browser.ts +213 -0
- package/src/platform/client/connection/index.ts +155 -0
- package/src/platform/client/undo/index.ts +59 -0
- package/src/platform/exceptions/__mocks__/index.ts +7 -0
- package/src/platform/exceptions/index.ts +9 -0
- package/src/platform/server/asyncStorage/__mocks__/index.ts +50 -0
- package/src/platform/server/asyncStorage/index-types.ts +35 -0
- package/src/platform/server/asyncStorage/index.api.ts +2 -0
- package/src/platform/server/asyncStorage/index.electron.ts +88 -0
- package/src/platform/server/asyncStorage/index.ts +126 -0
- package/src/platform/server/connection/README.md +3 -0
- package/src/platform/server/connection/__mocks__/index.ts +15 -0
- package/src/platform/server/connection/index-types.ts +20 -0
- package/src/platform/server/connection/index.api.ts +13 -0
- package/src/platform/server/connection/index.electron.ts +102 -0
- package/src/platform/server/connection/index.ts +154 -0
- package/src/platform/server/fetch/__mocks__/index.ts +3 -0
- package/src/platform/server/fetch/index.api.ts +1 -0
- package/src/platform/server/fetch/index.electron.ts +18 -0
- package/src/platform/server/fetch/index.ts +20 -0
- package/src/platform/server/fs/index.api.ts +198 -0
- package/src/platform/server/fs/index.electron.ts +208 -0
- package/src/platform/server/fs/index.test.ts +117 -0
- package/src/platform/server/fs/index.ts +416 -0
- package/src/platform/server/fs/path-join.api.ts +1 -0
- package/src/platform/server/fs/path-join.electron.ts +1 -0
- package/src/platform/server/fs/path-join.ts +97 -0
- package/src/platform/server/fs/shared.ts +33 -0
- package/src/platform/server/indexeddb/index.ts +115 -0
- package/src/platform/server/log/index.ts +43 -0
- package/src/platform/server/sqlite/index.api.ts +2 -0
- package/src/platform/server/sqlite/index.electron.ts +134 -0
- package/src/platform/server/sqlite/index.test.ts +108 -0
- package/src/platform/server/sqlite/index.ts +241 -0
- package/src/platform/server/sqlite/normalise.ts +9 -0
- package/src/platform/server/sqlite/unicodeLike.test.ts +58 -0
- package/src/platform/server/sqlite/unicodeLike.ts +31 -0
- package/src/server/__mocks__/post.ts +9 -0
- package/src/server/__snapshots__/main.test.ts.snap +199 -0
- package/src/server/__snapshots__/sheet.test.ts.snap +9 -0
- package/src/server/accounts/__snapshots__/sync.test.ts.snap +136 -0
- package/src/server/accounts/app-bank-sync.test.ts +136 -0
- package/src/server/accounts/app.ts +1294 -0
- package/src/server/accounts/link.ts +25 -0
- package/src/server/accounts/payees.ts +36 -0
- package/src/server/accounts/sync.test.ts +679 -0
- package/src/server/accounts/sync.ts +1168 -0
- package/src/server/accounts/title/index.ts +60 -0
- package/src/server/accounts/title/lower-case.ts +93 -0
- package/src/server/accounts/title/specials.ts +21 -0
- package/src/server/admin/app.ts +241 -0
- package/src/server/api-models.ts +244 -0
- package/src/server/api.test.ts +36 -0
- package/src/server/api.ts +1030 -0
- package/src/server/app.ts +91 -0
- package/src/server/aql/compiler.test.ts +966 -0
- package/src/server/aql/compiler.ts +1222 -0
- package/src/server/aql/exec.test.ts +289 -0
- package/src/server/aql/exec.ts +128 -0
- package/src/server/aql/index.ts +41 -0
- package/src/server/aql/schema/executors.test.ts +420 -0
- package/src/server/aql/schema/executors.ts +345 -0
- package/src/server/aql/schema/index.test.ts +67 -0
- package/src/server/aql/schema/index.ts +409 -0
- package/src/server/aql/schema-helpers.test.ts +242 -0
- package/src/server/aql/schema-helpers.ts +208 -0
- package/src/server/aql/views.test.ts +62 -0
- package/src/server/aql/views.ts +57 -0
- package/src/server/auth/app.ts +387 -0
- package/src/server/bench.ts +29 -0
- package/src/server/budget/actions.ts +686 -0
- package/src/server/budget/app.ts +469 -0
- package/src/server/budget/base.test.ts +340 -0
- package/src/server/budget/base.ts +339 -0
- package/src/server/budget/category-template-context.test.ts +1658 -0
- package/src/server/budget/category-template-context.ts +862 -0
- package/src/server/budget/cleanup-template.pegjs +27 -0
- package/src/server/budget/cleanup-template.ts +408 -0
- package/src/server/budget/envelope.ts +403 -0
- package/src/server/budget/goal-template.pegjs +110 -0
- package/src/server/budget/goal-template.ts +309 -0
- package/src/server/budget/report.ts +308 -0
- package/src/server/budget/schedule-template.test.ts +184 -0
- package/src/server/budget/schedule-template.ts +351 -0
- package/src/server/budget/statements.ts +60 -0
- package/src/server/budget/template-notes.test.ts +393 -0
- package/src/server/budget/template-notes.ts +323 -0
- package/src/server/budget/util.ts +25 -0
- package/src/server/budgetfiles/__snapshots__/backups.test.ts.snap +101 -0
- package/src/server/budgetfiles/app.ts +672 -0
- package/src/server/budgetfiles/backups.test.ts +79 -0
- package/src/server/budgetfiles/backups.ts +251 -0
- package/src/server/cloud-storage.ts +467 -0
- package/src/server/dashboard/app.ts +373 -0
- package/src/server/db/__snapshots__/index.test.ts.snap +271 -0
- package/src/server/db/index.test.ts +300 -0
- package/src/server/db/index.ts +855 -0
- package/src/server/db/mappings.ts +59 -0
- package/src/server/db/sort.ts +58 -0
- package/src/server/db/types/index.ts +342 -0
- package/src/server/db/util.ts +36 -0
- package/src/server/encryption/app.ts +133 -0
- package/src/server/encryption/encryption-internals.api.ts +2 -0
- package/src/server/encryption/encryption-internals.electron.ts +89 -0
- package/src/server/encryption/encryption-internals.ts +109 -0
- package/src/server/encryption/encryption.test.ts +19 -0
- package/src/server/encryption/index.test.ts +19 -0
- package/src/server/encryption/index.ts +89 -0
- package/src/server/errors.ts +110 -0
- package/src/server/filters/app.ts +191 -0
- package/src/server/importers/actual.ts +49 -0
- package/src/server/importers/index.ts +58 -0
- package/src/server/importers/ynab4-types.ts +163 -0
- package/src/server/importers/ynab4.ts +470 -0
- package/src/server/importers/ynab5-types.ts +290 -0
- package/src/server/importers/ynab5.ts +1193 -0
- package/src/server/main-app.ts +25 -0
- package/src/server/main.test.ts +392 -0
- package/src/server/main.ts +336 -0
- package/src/server/migrate/__snapshots__/migrations.test.ts.snap +17 -0
- package/src/server/migrate/cli.ts +100 -0
- package/src/server/migrate/migrations.test.ts +81 -0
- package/src/server/migrate/migrations.ts +192 -0
- package/src/server/models.ts +184 -0
- package/src/server/mutators.ts +139 -0
- package/src/server/notes/app.ts +18 -0
- package/src/server/payees/app.ts +351 -0
- package/src/server/polyfills.ts +26 -0
- package/src/server/post.ts +219 -0
- package/src/server/preferences/app.ts +249 -0
- package/src/server/prefs.ts +91 -0
- package/src/server/reports/app.ts +187 -0
- package/src/server/rules/action.ts +344 -0
- package/src/server/rules/app.ts +193 -0
- package/src/server/rules/condition.ts +436 -0
- package/src/server/rules/customFunctions.ts +61 -0
- package/src/server/rules/formula-action.test.ts +175 -0
- package/src/server/rules/handlebars-helpers.ts +131 -0
- package/src/server/rules/index.test.ts +1095 -0
- package/src/server/rules/index.ts +22 -0
- package/src/server/rules/rule-indexer.ts +89 -0
- package/src/server/rules/rule-utils.ts +274 -0
- package/src/server/rules/rule.ts +193 -0
- package/src/server/schedules/app.test.ts +502 -0
- package/src/server/schedules/app.ts +644 -0
- package/src/server/schedules/find-schedules.ts +391 -0
- package/src/server/server-config.ts +59 -0
- package/src/server/sheet.test.ts +101 -0
- package/src/server/sheet.ts +280 -0
- package/src/server/spreadsheet/__snapshots__/spreadsheet.test.ts.snap +5 -0
- package/src/server/spreadsheet/app.ts +54 -0
- package/src/server/spreadsheet/globals.ts +13 -0
- package/src/server/spreadsheet/graph-data-structure.ts +165 -0
- package/src/server/spreadsheet/scratch +60 -0
- package/src/server/spreadsheet/spreadsheet.test.ts +191 -0
- package/src/server/spreadsheet/spreadsheet.ts +523 -0
- package/src/server/spreadsheet/util.ts +15 -0
- package/src/server/sql/init.sql +88 -0
- package/src/server/sync/__snapshots__/sync.test.ts.snap +31 -0
- package/src/server/sync/app.ts +29 -0
- package/src/server/sync/encoder.ts +129 -0
- package/src/server/sync/index.ts +820 -0
- package/src/server/sync/make-test-message.ts +19 -0
- package/src/server/sync/migrate.test.ts +169 -0
- package/src/server/sync/migrate.ts +48 -0
- package/src/server/sync/repair.ts +39 -0
- package/src/server/sync/reset.ts +91 -0
- package/src/server/sync/sync.property.test.ts +385 -0
- package/src/server/sync/sync.test.ts +349 -0
- package/src/server/sync/utils.ts +3 -0
- package/src/server/tags/app.ts +101 -0
- package/src/server/tests/mockData.json +9352 -0
- package/src/server/tests/mockSyncServer.ts +119 -0
- package/src/server/tools/app.ts +152 -0
- package/src/server/transactions/__snapshots__/transaction-rules.test.ts.snap +173 -0
- package/src/server/transactions/__snapshots__/transfer.test.ts.snap +655 -0
- package/src/server/transactions/app.ts +136 -0
- package/src/server/transactions/export/export-to-csv.ts +132 -0
- package/src/server/transactions/import/__snapshots__/parse-file.test.ts.snap +1582 -0
- package/src/server/transactions/import/ofx2json.test.ts +33 -0
- package/src/server/transactions/import/ofx2json.ts +157 -0
- package/src/server/transactions/import/parse-file.test.ts +224 -0
- package/src/server/transactions/import/parse-file.ts +286 -0
- package/src/server/transactions/import/qif2json.ts +110 -0
- package/src/server/transactions/import/xmlcamt2json.ts +168 -0
- package/src/server/transactions/index.ts +196 -0
- package/src/server/transactions/merge.test.ts +370 -0
- package/src/server/transactions/merge.ts +139 -0
- package/src/server/transactions/transaction-rules.test.ts +994 -0
- package/src/server/transactions/transaction-rules.ts +1038 -0
- package/src/server/transactions/transfer.test.ts +221 -0
- package/src/server/transactions/transfer.ts +173 -0
- package/src/server/undo.ts +271 -0
- package/src/server/update.ts +37 -0
- package/src/server/util/budget-name.ts +61 -0
- package/src/server/util/custom-sync-mapping.ts +48 -0
- package/src/server/util/rschedule.ts +9 -0
- package/src/shared/__mocks__/platform.ts +7 -0
- package/src/shared/__snapshots__/months.test.ts.snap +21 -0
- package/src/shared/arithmetic.test.ts +112 -0
- package/src/shared/arithmetic.ts +170 -0
- package/src/shared/async.test.ts +135 -0
- package/src/shared/async.ts +76 -0
- package/src/shared/constants.ts +5 -0
- package/src/shared/currencies.ts +70 -0
- package/src/shared/dashboard.ts +260 -0
- package/src/shared/environment.ts +18 -0
- package/src/shared/errors.ts +195 -0
- package/src/shared/locale.ts +27 -0
- package/src/shared/location-utils.test.ts +69 -0
- package/src/shared/location-utils.ts +49 -0
- package/src/shared/months.test.ts +5 -0
- package/src/shared/months.ts +485 -0
- package/src/shared/normalisation.ts +6 -0
- package/src/shared/platform.electron.ts +21 -0
- package/src/shared/platform.ts +20 -0
- package/src/shared/query.ts +176 -0
- package/src/shared/rules.test.ts +56 -0
- package/src/shared/rules.ts +371 -0
- package/src/shared/schedules.test.ts +570 -0
- package/src/shared/schedules.ts +560 -0
- package/src/shared/test-helpers.ts +156 -0
- package/src/shared/transactions.test.ts +275 -0
- package/src/shared/transactions.ts +433 -0
- package/src/shared/transfer.test.ts +75 -0
- package/src/shared/transfer.ts +16 -0
- package/src/shared/user.ts +4 -0
- package/src/shared/util.test.ts +240 -0
- package/src/shared/util.ts +633 -0
- package/src/types/api-handlers.ts +287 -0
- package/src/types/budget.ts +8 -0
- package/src/types/file.ts +47 -0
- package/src/types/handlers.ts +46 -0
- package/src/types/models/account.ts +24 -0
- package/src/types/models/bank-sync.ts +23 -0
- package/src/types/models/bank.ts +6 -0
- package/src/types/models/category-group.ts +11 -0
- package/src/types/models/category.ts +13 -0
- package/src/types/models/dashboard.ts +199 -0
- package/src/types/models/gocardless.ts +84 -0
- package/src/types/models/import-transaction.ts +56 -0
- package/src/types/models/index.ts +23 -0
- package/src/types/models/nearby-payee.ts +7 -0
- package/src/types/models/note.ts +4 -0
- package/src/types/models/openid.ts +8 -0
- package/src/types/models/payee-location.ts +8 -0
- package/src/types/models/payee.ts +10 -0
- package/src/types/models/pluggyai.ts +19 -0
- package/src/types/models/reports.ts +144 -0
- package/src/types/models/rule.ts +174 -0
- package/src/types/models/schedule.ts +49 -0
- package/src/types/models/simplefin.ts +28 -0
- package/src/types/models/tags.ts +6 -0
- package/src/types/models/templates.ts +135 -0
- package/src/types/models/transaction-filter.ts +9 -0
- package/src/types/models/transaction.ts +39 -0
- package/src/types/models/user-access.ts +10 -0
- package/src/types/models/user.ts +25 -0
- package/src/types/prefs.ts +167 -0
- package/src/types/server-events.ts +86 -0
- package/src/types/server-handlers.ts +27 -0
- package/src/types/util.ts +26 -0
- package/tsconfig.json +34 -0
- package/typings/pegjs.ts +1 -0
- package/typings/process-worker.ts +12 -0
- package/typings/vite-plugin-peggy-loader.ts +1 -0
- package/typings/window.ts +62 -0
- package/vite.config.ts +109 -0
- package/vite.desktop.config.ts +59 -0
- package/vitest.config.ts +43 -0
- 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
|
+
}
|