Commit Graph
2 Commits
Author SHA1 Message Date
Linus Rath 029eba4562 fix: keep the last sync's result when a sync after it finds nothing to do #34
The settings show the stored status of the last sync. A sync that
finds nothing to do still replaced it, so a change the server refused
and the phone put back, or a conflict, was shown for a second or two
and then gone. The previous fix covered only the upload sync Android
starts right after a manual one. When changes arrive during a sync,
the adapter runs a full sync right after it, which finds nothing new
on the server and replaced the status 1-2 s after the real one; a
manual sync right after another does the same.

Now what counts is what a run did, not how it started. A run that
wrote nothing either way and reported no conflict, item error, note or
failure keeps the details of the run before it and moves only the time
on. After a failed run it shows the sync as working again, without the
failure's message. Any other run replaces the status, so a problem
stays only until the next run that does something.
2026-09-27 10:20:54 +02:00
Linus Rath 96a6b38321 feat: sync the device's contacts and calendars with the server in the background #34
The sync adapters from ed2525e start a headless task for every sync, but
the task only reported "disabled". This adds the engine it runs
(`src/device-sync/engine`, `src/device-sync/jmap`): for one Android
account and authority it downloads the server's changes, uploads what
the device changed, and hands a report back to SyncManager.

- Downloads follow `/changes` from the state kept in the provider's
  SyncState. Rows and the state they reach are written in one batch, so
  a killed run resumes where it stopped. Without a state, after
  `cannotCalculateChanges` or after the user discarded deletions, a full
  reconcile compares ids instead.
- Uploads go in three phases across accounts (creates, updates,
  deletions), so moving an item to another account never destroys it
  before the copy exists. A create carries a uid written to the device
  first, and the server is asked for that uid before creating, so a lost
  response or a killed run never makes a duplicate. Updates are patches
  guarded by `ifInState`; nothing is replaced whole.
- More than 50 deletions that are also more than a fifth of the synced
  items hold every deletion back and report `tooManyDeletions`, so
  Android asks the user; "undo" restores the items from the server.
- An item the server refuses is marked and not sent again until it
  changes or its back-off (1 hour, doubling to a day) ends. Such
  refusals and planner errors stay with their item; the rest of the run
  goes on.
- Runs of one account and authority take a lock, stop at checkpoints
  when cancelled or close to the deadline, and never throw: every
  failure becomes an outcome, and a rejected sign-in posts one "Sign in
  again" notification.
- The JMAP side uses a client of its own per app account, never the
  UI's, since a run can share the JS runtime with the app.

Turning sync off or signing out runs the same upload first
(`teardownAuthority`) and deletes the rows only once nothing is waiting,
or when the user agreed to lose it.

The planners that map items to rows land in the next commits; until
then a run fails before it touches a row.

Checked: 94 engine tests against the fake provider and JMAP server
(sync flows, failures, crashes and retries, teardown), and `npm run
typecheck`, `npm test` and `npm run i18n:check` on this commit.
2026-09-27 04:19:21 +02:00