Skip to content

[Mobile] Offline support and sync - #36

Merged
wak327 merged 1 commit into
mobile-site-adminfrom
mobile-offline
Sep 11, 2026
Merged

wak327 merged 1 commit into
mobile-site-adminfrom
mobile-offline

Conversation

@wak327

@wak327 wak327 commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Closes #18.

React Query already knows how to pause a mutation and replay it, so the work is telling it the truth about a phone. Its online default listens for a browser event that does not exist here, so NetInfo drives onlineManager — without that, a phone in a tunnel fires requests that time out one by one rather than queueing anything. It watches isInternetReachable rather than isConnected, because a captive portal is "connected" and is exactly the case that hangs.

Queued mutations are registered by key rather than passed inline. A persisted mutation is only data, and setMutationDefaults is what turns it back into something runnable after the app has been killed — the difference between a queue and a list of regrets.

Conflicts

A queued move captures the issue's updated_at when it is made and refuses on replay if somebody else moved the card meanwhile; applying it blindly would undo their work. Moving it to where it was already going is not a conflict, so that case still applies.

Conflicts are surfaced, not resolved — the server's version and the queued one are both somebody's intent, and choosing between them is not a decision to make on their behalf.

The banner stays up while queued work is still going out after a connection returns. "Back online" with three unsent changes is not the same as up to date.

Only three mutations queue: moving a card, commenting, creating an issue. Those are the ones worth doing without a connection; everything else asks for one.

Verification

6 unit tests covering the conflict rule in both directions. The offline behaviour itself is unverified on hardware — nobody has watched a phone lose signal, move a card and come back. That needs a device.

Closes #18.

React Query already knows how to pause a mutation and replay it, so the work is
telling it the truth about a phone. Its online default listens for a browser
event that does not exist here, so NetInfo drives onlineManager instead --
without that, a phone in a tunnel fires requests that time out one by one rather
than queueing anything. It watches isInternetReachable rather than isConnected,
because a captive portal is "connected" and is exactly the case that hangs.

Queued mutations are registered by key rather than passed inline. A persisted
mutation is only data, and setMutationDefaults is what turns it back into
something runnable after the app has been killed -- which is the difference
between a queue and a list of regrets.

A queued move captures the issue's updated_at when it is made, and refuses on
replay if somebody else moved the card meanwhile. Applying it blindly would undo
their work. Moving it to where it was already going is not a conflict, so that
case still applies.

Conflicts are surfaced, not resolved: the server's version and the queued one
are both somebody's intent, and choosing between them is not a decision to make
on their behalf.

The banner stays up while queued work is still going out after a connection
returns. "Back online" with three unsent changes is not the same as up to date.

Only three mutations queue -- moving a card, commenting, creating an issue.
They are the ones worth doing without a connection; everything else asks for one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wak327
wak327 merged commit 77e9846 into mobile-site-admin Sep 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant