Repository navigation
Sync settings between computers through the account - #991
Merged
Merged
Conversation
Add the settings sync engine. When signed in, the portable settings document is kept in the account's desktop_preferences row: - Every write is conditional on the revision last seen (POST to create, PATCH revision=eq.N to update, never upsert), so a lost race re-reads and merges instead of overwriting another computer's change. - The last document synced with each account is kept locally as the merge base. Changes on one side apply to the other; sections changed on both merge by section, with the account's copy winning a section changed on both sides. - The first sync on a computer whose settings differ from the account asks which to keep; an untouched install takes the account's settings. - A copy from a newer app is never overwritten; this install asks to be updated. An invalid copy is reported, not replaced. - Sync runs at start, after sign-in, when local settings have settled after a change, every five minutes for other computers' changes, and on demand, with backoff after failures. The Account tab shows sync status, Sync now, and the first-sync choice. Profiles and remote switches screens refresh when sync changes them. Refs #986 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Address review on the settings sync engine: - Keep the account's copy and this computer's copy as separate merge bases, so a value normalized on apply (e.g. a profile version) never looks like an account change that silently overrides a later local edit. - Apply only if local settings still match what the merge was computed from, checked on the main thread; otherwise re-read and merge again, so an edit made during a sync is not reverted. - When an apply is followed by an upload, record an interim base first so a failed upload neither re-uploads the account's sections nor forgets this computer's changes. - Merge profiles by id: profiles added on two computers are both kept, an edit survives a deletion on the other side, and merged names stay unique. - Keep sync state in memory, stay idle while signed out or waiting for the user, and retry failed syncs on backoff even without a local change. - Report a deleted account (409 foreign key) instead of retrying a create. - forget() waits for a running sync; opening the Account tab wakes sync if a locked keychain hid the session at startup. - Sync panel moves focus to the choice when it replaces the focused button; the remote switches reload handles errors. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Address re-review on the settings sync engine: - Profile changes are judged on name, provider and bindings, not version, since versions drift between computers; a merged profile keeps the higher version. A version-only difference no longer beats a real edit or brings back a deleted profile. - A merge that would exceed 32 custom profiles stops with a clear message instead of failing every retry with a generic error. - The applied result is read on the main thread right after applying, so a settings change made just afterwards is not mistaken for synced. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Address re-review: applying a profile with unchanged content kept the lower local version, so after lost same-profile races two computers could upload their own versions to each other forever. Same-content profiles now keep the higher version, so both converge. The reserved-name rename no longer bumps the version during a merge (only once at load, for connected phones). The sync tests' fake host now stores profiles with the real merge rules, and a two-computer test replays the review sequence and checks it settles. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.
Closes #986
Part of #988.
What
Adds
src-tauri/src/settings_sync.rs. When an account is signed in, the portable settings document (#985) is kept in the account'sdesktop_preferencesrow, using the client contract from switchifyapp/switchify-supabase#2:Conditional writes only:
POST(409means another computer created it first, so re-read).PATCH ?user_id=eq.…&revision=eq.NandPrefer: return=representation(an empty result means another computer wrote first, so re-read and merge).Merge base: the last document synced with each account is stored in
settings-sync.json, keyed byuser_id, so a base from another account (e.g. one deleted and recreated) is never used.First sync on a computer:
Newer and invalid copies: a copy written by a newer app (
schema_versionabove ours) is never overwritten; this install shows an "update Switchify PC" message. An invalid copy is reported and left alone.When it runs:
Failures back off from 3 s up to 5 min.
Main thread: incoming changes are applied through
portable_settings::applyon the main thread.Screen refreshes: after applying,
switch-profiles-changedandremote-switches-changedare emitted, and the Switch Forwarding and Remote switch screens refresh. A refresh is skipped while an edit is unsaved, so the edit wins and syncs afterwards.Install id:
updated_byis a random per-install id, never the BLE desktop id.Size limit: documents over 200 KB are refused before upload (the server allows 256 KiB).
Account integration: sign-in wakes the engine, sign-out turns sync off, and deleting the account forgets the merge base.
Account::endpoint()exposes the project URL and key, andAuthorized.user_idis now used.UI: when signed in, the Account tab shows the sync status (up to date with the last sync time, syncing, error or update-required message), a Sync now button, and the first-sync choice. Focus moves to Sync now after the choice.
Review fixes
Two merge bases: the base keeps the account's copy (
cloud) and this computer's copy (local) separately. Local changes are measured againstlocaland account changes againstcloud, so a value normalized on apply (e.g. a profile version bump) never looks like an account change that overrides a later local edit. The review's repro is now a test.Edits during a sync:
Host::apply(expected, document)applies only if local settings still match what the merge was computed from. The check runs on the main thread, where the settings commands run, so no edit can slip in between. Otherwise the sync re-reads and merges again, and an edit made mid-sync is never reverted.Apply, then upload: an interim base (the account's copy on both sides) is recorded before the upload. If the upload fails, sections taken from the account don't look like local changes, and this computer's own changes still upload next time.
Profiles merge by id: profiles added on two computers at the same time are both kept, and an edit survives a deletion on the other side. Names made equal by the merge get a
(2)suffix.Loop behaviour:
Deleted account: a 409 with code
23503(account deleted elsewhere) is reported as "This account no longer exists" instead of being treated as a lost create race.forget()waits for a running sync.Opening the Account tab wakes sync if a locked keychain hid the session at startup.
UI: the sync panel moves focus to the first choice button when it replaces a focused Sync now, and the remote switches reload catches errors.
Re-review round 2:
Re-review round 3:
portable_settings::merge_profiles), so computers converge on one version instead of uploading their own versions to each other forever after lost same-profile races.Validation
npm run lint,npm test(279 passed) andnpm run buildcargo fmt --checkandcargo clippy --all-targets -D warnings: cleancargo test: 708 passed, 5 ignored, including 29 newsettings_syncengine tests with a fake remote and host. They cover:Against a real local Supabase stack, the ignored test
settings_sync::tests::local_stack_round_trips_the_document_and_enforces_revisionspassed. It shows:nulls survive thejsonbcolumn and parse strictlyuser_idThe account local-stack test also still passes.
UI tests cover the sync panel (status, Sync now, both choices, backend messages, live updates, hidden while confirming deletion), the remote switch reload (and that it skips while edits are saving), and the Switch Forwarding refresh.
🤖 Generated with Claude Code