You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Settings sync: sync engine with revision conflict handling #986
Part of cross-device settings sync for Switchify PC.
Scope
Download on sign-in and app start. Upload 3 s after any settings save, waiting until changes stop.
Avoid lost writes with revision checks: PATCH ...?revision=eq.N, and if no row matches, download again and resolve.
On first sign-in, if both this PC and the cloud have customised settings, ask whether to keep this PC's settings or use the cloud copy. Never overwrite silently.
Keep working offline, retry with backoff, and show sync status in the UI.
Never log the contents of the settings document.
Acceptance
Tests against a fake back end covering conflicts, offline retry, the first-sign-in prompt and sign-out.
Notes from the Supabase review (switchifyapp/switchify-supabase#2), for the client contract:
Create the row with a plain POST. A 409/23505 means another device created it first; fall back to GET then PATCH.
Update with PATCH ?revision=eq.N and Prefer: return=representation. An empty result means a conflict.
Never upsert (merge-duplicates / on_conflict), because an upsert bypasses the revision check. Clients have no DELETE permission.
Key the cached revision and last-synced document by user_id. After an account is deleted and recreated, the new row starts at revision 1 under a new uid, so a cache from the old account must never be applied to the new one.
The 256 KiB payload limit is measured on jsonb storage size, so treat it as approximate and keep the document well under it.
Call portable_settings::apply on the main thread (run_on_main_thread, like configure_point_scan).
apply can partly succeed, for example when a file write fails or a switch capture is active. Treat an error as retryable: re-running it with the same document converges.
Profile changes aren't part of AppState, so emit_state won't refresh the Profiles screen. Emit a profiles-changed event, or have the UI refetch list_switch_profiles, after a sync applies profiles.
ParseError::Newer means the cloud copy came from a newer app. Show the "update Switchify PC" message and don't upload over it.
One more note from the #989 review: parsing requires an exact JSON round trip, and the document contains explicit nulls. Never strip nulls on the way to or from storage (no jsonb_strip_nulls, and no client that drops null fields), or every document will fail to parse. Add a test that round-trips through the real column.
Activity
enaboapps commentedon Oct 5, 2026
Notes from the Supabase review (switchifyapp/switchify-supabase#2), for the client contract:
POST. A409/23505means another device created it first; fall back to GET thenPATCH.PATCH ?revision=eq.NandPrefer: return=representation. An empty result means a conflict.merge-duplicates/on_conflict), because an upsert bypasses the revision check. Clients have no DELETE permission.user_id. After an account is deleted and recreated, the new row starts at revision 1 under a new uid, so a cache from the old account must never be applied to the new one.enaboapps commentedon Oct 5, 2026
Notes from the #989 review, for the sync engine:
portable_settings::applyon the main thread (run_on_main_thread, likeconfigure_point_scan).applycan partly succeed, for example when a file write fails or a switch capture is active. Treat an error as retryable: re-running it with the same document converges.AppState, soemit_statewon't refresh the Profiles screen. Emit a profiles-changed event, or have the UI refetchlist_switch_profiles, after a sync applies profiles.ParseError::Newermeans the cloud copy came from a newer app. Show the "update Switchify PC" message and don't upload over it.enaboapps commentedon Oct 5, 2026
One more note from the #989 review: parsing requires an exact JSON round trip, and the document contains explicit
nulls. Never strip nulls on the way to or from storage (nojsonb_strip_nulls, and no client that drops null fields), or every document will fail to parse. Add a test that round-trips through the real column.