fix(sync): reconcile the cache instead of rebuilding it - #236
Merged
Merged
Conversation
store_snapshot deleted every row for the backend and re-inserted with a fresh Uuid::new_v4(), so the on_conflict clauses in the store_*_batch functions never fired and every local UUID churned on each sync. Anything the UI keys on one goes stale under the user, now on a 5-minute timer. Delete only the rows the remote stopped returning, and let the batches update the survivors in place. Two things fall out of that: Task parent links are detached before the delete pass, alongside the project ones that already were. Todoist never returns completed tasks, so completing a parent makes it vanish from the fetch while its still-open subtasks keep coming. The task hierarchy FK cascades, so deleting the stale parent row would take a live subtask with it. An empty section list no longer reconciles anything. sync turns a failed section fetch into an empty slice and carries on, so a backend hiccup used to wipe every cached section. The other three fetches abort the sync on failure, where empty honestly means empty.
This was referenced Sep 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR #209 item 1. Groundwork for keying UI state on local UUIDs.
The bug
store_snapshotdeleted every row for the backend and then re-inserted with a freshUuid::new_v4(), so theon_conflictclauses in thestore_*_batchfunctions never had anything to conflict with. Every local UUID churned on every sync, which since the move to a 5-minute timer means anything the UI keys on one goes stale under the user every five minutes.The change
Delete only the rows the remote stopped returning (
remote_id NOT INthe snapshot), and let the existing batches update the survivors in place. The unique indexes on(backend_uuid, remote_id)were already there, so the upserts just start working.Two things fall out of it:
Task parent links are detached before the delete pass, alongside the project ones that already were. Todoist never returns completed tasks, so completing a parent makes it vanish from the fetch while its still-open subtasks keep coming.
task.parent_uuidisON DELETE CASCADE, so deleting the stale parent row would take a live subtask with it. The batches relink from the snapshot afterwards.An empty section list no longer reconciles anything.
sync(sync/mod.rs:258-268) turns a failed section fetch into an empty slice and carries on, so a backend hiccup used to wipe every cached section. The other three fetches return early on failure, so empty honestly means empty there and they reconcile unguarded.No entity definition changed, so
SCHEMA_VERSIONstays put.Tests
Two new ones, both verified to fail against the old wipe-and-reinsert:
snapshot_keeps_uuids_and_orphans_subtasks_of_vanished_parentscovers both halves: a renamed project keeps its UUID across a sync, and a subtask survives its parent being completed away withparent_uuidcleared.empty_section_list_leaves_cached_sections_alonepins the ambiguous-empty branch.The two existing snapshot tests are unchanged in intent, just moved onto a shared
test_servicehelper since the third would have been a third copy of the same 25 lines of setup.cargo fmt,cargo clippy --all-targets,cargo testall clean.Next
This unblocks the rest of the family, none of it in this PR:
SidebarSelection::Project(usize)is positional, so a project deleted from another client slides your selection onto a neighbour. Wants a uuid-keyed selection, which wanted this first.SelectionPolicy).