-
-
Notifications
You must be signed in to change notification settings - Fork 73
iOS Native App: Offline‐First & Sync
Offline-first. The local JSON file is always the working copy — every tap updates memory + UI + file immediately, with no network in the path. Sync then runs in two lanes:
| Lane | Carries | Transport | When |
|---|---|---|---|
| Realtime tick | tick done / note only |
WebSocket wss://…/ws/sync
|
while app is foreground |
| Full sync | everything else + reconciliation | HTTP export / import
|
first connect · foreground · pull-to-refresh · periodic |
Three methods share this engine; the two API methods are the same code, differing only in endpoint:
| Method | Transport | Endpoint |
|---|---|---|
| iCloud | file coordination (existing) | ubiquity container |
| API — self-host | HTTPS + wss | user-entered URL |
| API — cloud | HTTPS + wss | https://beaverhabits.com |
Each device opens one authenticated WebSocket while foregrounded. A tick applies locally first (memory + file), then goes out on the socket; the server persists it and broadcasts it to the user's other connections, which apply it directly — the tick payload is tiny and self-contained, so no follow-up pull is needed.
sequenceDiagram
autonumber
actor U as User (iPhone)
participant P as iPhone
participant S as Server /ws/sync
participant Q as iPad (same user)
Note over P,Q: both foreground -> each holds one wss connection
U->>P: toggle(habit, day) %% or add/edit note
P->>P: mutate memory + file (instant)
P->>S: ws send ( type:tick, habit_id, day, done, text, hlc )
S->>S: persist tick (reuse habit.tick), LWW by hlc
S-->>Q: ws broadcast ( same tick )
Q->>Q: apply via HabitMerger LWW -> UI updates live
Note over S,Q: broadcast skips the origin connection
Reconnection: on drop / return-to-foreground the client reopens the socket and runs one full sync (Flow B) to catch any ticks missed while disconnected.
Non-tick edits and the drift backstop go through a whole-dict merge round-trip: pull the
remote dict, merge with local via HabitMerger, push the merged result back. Runs on
first connect, foreground, pull-to-refresh, and periodically.
sequenceDiagram
autonumber
participant VM as HabitListViewModel
participant ST as HabitStore (local file)
participant SE as SyncEngine
participant S as Server
Note over VM: first connect / foreground / pull-to-refresh / periodic
VM->>ST: load() -> localHabits
SE->>S: GET /api/v1/habits/export
S-->>SE: remoteDict (full)
SE->>SE: merged = HabitMerger.merge(local, remote)
alt merged != last pushed
SE->>S: POST /api/v1/habits/import ( merged )
S-->>SE: 200
else unchanged
Note over SE,S: skip POST (hash equal) -> no server write
end
SE->>ST: save(merged)
SE->>VM: publish merged
Realtime scope. Local edits are instant. Cross-device tick is live only while both apps are foreground (iOS tears down sockets in background). When an app returns to foreground it reconnects + full-syncs. True background delivery (app closed) would need APNs — out of scope; "latest on next open" is acceptable for a habit tracker.