fix(sync): recover desktop syncing after a session goes stale - #517
Open
plusmobileapps wants to merge 4 commits into
Open
fix(sync): recover desktop syncing after a session goes stale#517plusmobileapps wants to merge 4 commits into
plusmobileapps wants to merge 4 commits into
Conversation
The desktop app can stay open for days, and outside Android the SDK's token refresh is a single in-process timer with no lifecycle backstop — after the machine sleeps it can miss its window, leaving every request failing on an expired JWT until the app is restarted. Two additions make that recoverable: - `authenticatedSessions` emits on every usable session, not just the first sign-in. `state` can't carry this: a refresh produces an equal `AuthState.Authenticated`, which StateFlow drops, so nothing downstream ever learns the token came back. - `refreshSessionIfNeeded()` refreshes an expired (or nearly expired) token inline, which also re-arms the SDK's auto-refresh job. `RefreshFailure` now logs instead of passing silently, and the session collector runs under a SupervisorJob so a throw can't freeze the auth state for the rest of the process' life. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sync only ever ran when `AuthState` *changed* to Authenticated, on a realtime Postgres event, or on a manual sync tap. A recovered token produces no state change, so anything stranded while it was dead stayed unsynced until the process restarted — which is what made the desktop app look permanently stuck. The four syncing repositories now hang off `authenticatedSessions` instead, so the initial sign-in and every later refresh both trigger a reconcile, and each `syncWithRemote` first calls `refreshSessionIfNeeded()` so a sync started with a dead token repairs it rather than failing every call silently. Grocery keeps a separate `state` collector, since sign-out still has to tear the realtime subscription down. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mobile gets a free recovery path: the OS kills the process and the next launch reconciles. A desktop window left open for days never gets one, so a sync that stopped stays stopped. `SyncCoordinator` reconciles every repository in dependency order, reviving the session first and throttling to one automatic run a minute so a burst of focus changes costs nothing. Desktop drives it from window focus — the strongest hint the machine just woke, which is when the refresh timer has usually slipped — plus a 15-minute heartbeat for a window left focused and untouched. The per-screen sync buttons still bypass the throttle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A sync that can't authenticate looked exactly like a sync that had nothing to do: repositories log the failure and move on, so the app just appeared to stop working. Desktop now surfaces `SessionExpired` from an automatic sync as a snackbar. Only the outcome the app is sure about is surfaced. Individual push failures stay as they were — those already show up per item as NOT_SYNCED, and promoting each one to a snackbar would be noise. Co-Authored-By: Claude Opus 5 <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.
The problem
On desktop (reproduced on macOS, but the mechanism isn't Mac-specific), leaving the app open for a while ends with it silently syncing nothing to Supabase until it's force-quit and reopened.
Four things combine:
delay()job. The "stop auto-refresh on background, restart on foreground" safety net is Android-only (AuthConfig.enableLifecycleCallbacks— "Currently only supported on Android";androidMain/setupPlatform.ktinstalls theProcessLifecycleOwnerobservers, and there is no desktop equivalent). After a sleep, an App Nap, or a Wi-Fi flap, nothing re-arms that timer from outside.catch { Logger.e },catch (_: Exception) {},.catch {}), so the failure mode was silence.AuthStatechanged to Authenticated. A refresh produces an equalAuthState.Authenticated, andStateFlowdrops equal values — so the collector never re-fired. A process restart was the only thing that re-ran the initial sync, which is exactly the workaround that was being used.The fix
Four commits, one concern each:
feat(auth)—authenticatedSessionsemits on every usable session (initial sign-in and every silent refresh), andrefreshSessionIfNeeded()renews an expired/expiring token inline, which also re-arms the SDK's refresh job.RefreshFailurenow logs instead of passing silently, and the session collector runs under aSupervisorJob.feat(sync)— the four syncing repositories hang offauthenticatedSessionsinstead ofstate, so a recovered token retries whatever was stranded; eachsyncWithRemoterevives the session first.feat(desktop)—SyncCoordinatorreconciles every repository in dependency order, throttled to one automatic run a minute. Desktop drives it from window focus (the strongest hint the machine just woke) plus a 15-minute heartbeat.feat(sync)— a blocked sync now surfaces as a snackbar instead of looking like the app doing nothing.Testing
SyncCoordinatorTest(7 cases — ordering, sign-out, session revival, throttle, force).AuthStatechange. Both fail without the repository change../gradlew jvmTestgreen across the project.Not covered by tests: the desktop focus/heartbeat wiring itself and the snackbar — those need the running app. Worth confirming by leaving a desktop build open overnight and checking that syncing resumes on focus without a restart.
🤖 Generated with Claude Code