Build1 publisher3 min readPublished
Sync pill flips offline on an explicit OS false or a lost PowerSync connection
A debounce added to stop a sync indicator flickering on every app switch sat on top of a socket that needs tens of seconds to notice a dead mobile link, so the rewrite takes expo-network's reachability flags as a second input.
The Engineer · Build desk

What happened
- An item added on one phone never arrived on the second phone sharing the list, and both phones showed a green pill that said synced.
- The app is offline-first on PowerSync, and the pill was reading PowerSync's own connection state, which is derived from the socket.
- On a dying mobile link nothing sends a FIN, so the client keeps the connection open until the library's heartbeat times out, tens of seconds later.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint Any client that gates its retry path on the library's own connected flag inherits the same dead interval: no sync, no reconnect, and a confident indicator throughout.
- decision Battery cost rules out simply shortening the keepalive, so the choice is between a second, independent signal and an indicator that is honest only after the heartbeat expires.
- exposure Treat unknown as offline and users on transports the developer never tested see offline permanently.
- capability Two signals that can disagree give the app something it did not have with one: a test for a wedged socket, which is worth a forced bounce rather than a wait.
Inside that window the app also stopped trying. The reconnect logic only fired once the sync engine admitted it was disconnected, so nothing was retried while the pill said synced [7].
The cases that produce the window are ordinary phone behaviour: mobile data toggled off while the radio stays registered, a handoff to a cell that never completes, or a socket coming back from a long background period holding a file descriptor that points at nothing [4]. None of those closes the connection, and no FIN arrives [5]. A shorter keepalive would catch them sooner and burn battery reconnecting every time somebody walks into a lift [6].
Two weeks earlier the same developer had fixed a real complaint. Bringing the app to the foreground briefly drops and re-establishes the socket, so the pill flashed offline on every app switch [8]. The fix holds the current state and admits offline only if the socket is still down after a grace window, with a reconnect inside the window cancelling the pending flip, while connections still reflect immediately [8]. The debounce fixes the flicker, and it puts a deliberate delay on top of a signal that was already slow, so the worst case before the pill tells the truth is the grace window plus the heartbeat timeout [1]. The post does not say how long the grace window is.
The second signal is the operating system. expo-network exposes isConnected and isInternetReachable, and on the transports that populate them they flip well before a heartbeat gives up [11]. Both are typed boolean | null, and some transports never populate them at all [12]. The comment above the helper reads "// Only an EXPLICIT false means the OS is sure there is no internet." [19] So osOffline returns true only when osReachable === false or osConnected === false [13], and resolveSyncPill checks that first: the OS wins, then a false psConnected means offline, then psFlowing decides between syncing and synced [14].
The same disagreement drives the reconnect. needsForcedReconnect fires only when PowerSync believes its socket is up and the OS says there is no internet [15]. When PowerSync already admits it is disconnected, ordinary retry handles the case and a forced bounce would fight the backoff [16]. The bounce is rate-limited, and ten seconds between forced bounces was enough, which caps a flapping link at six forced reconnects a minute [17][2]. Clearing the local database to fix a connection is tempting and wrong, according to the post [18].
For any of this to transfer, the transports you actually ship on have to populate those two fields. Where they return null the resolver falls back to psConnected, and you are back to the heartbeat plus the debounce [13][14]. Treating null as false goes the other way and shows offline forever on hardware the developer does not own [12]. In an offline-first app I would take the extra input, because the failure it removes is a green pill reading synced on both phones while the item sits unsent [1].
What to watch
- Whether isConnected and isInternetReachable actually populate on every transport and OS version in a given device matrix, since the fallback is the heartbeat.
- Whether PowerSync exposes a supported forced-reconnect call, so the bounce does not have to be driven from outside the engine.
- Whether the ten-second bounce interval holds on links that flap faster than that, or needs backoff of its own.