Fixing Timer Sync Between Web and Mobile
A focus timer showed 12 minutes' discrepancy between web and mobile. One component had WebSocket sync, the other was running on stale data from mount time.
A user started a focus session on mobile, then opened the web app. The FocusWidget showed the correct remaining time. But the ActiveFocusSessionBar -- a sticky bar at the top of the planner -- was off by 12 minutes.
The Drift
The bar only fetched timer state on mount and browser focus. It ran a local countdown from that initial value. Meanwhile, the actual session was paused and resumed on mobile, accumulating drift:
- User starts 25-min session on mobile
- Web loads -- bar fetches "20 min remaining," starts local countdown
- User pauses on mobile for 10 minutes
- User resumes on mobile
- Bar keeps counting down from its stale value
- Bar shows 8 min remaining. Actual: 20 min remaining. 12-minute drift.
The Fix
The FocusWidget already had WebSocket sync -- it subscribed to a Laravel Echo channel and updated on every state change. The bar didn't. I added the same subscription:
useEffect(() => {
const channel = echo.private(`focus-session.${userId}`);
channel.listen('.focus-session.updated', (data: FocusSessionData) => {
setSessionData(data);
setRemainingSeconds(data.remaining_seconds);
});
return () => channel.stopListening('.focus-session.updated');
}, [userId]);
Plus a 30-second periodic sync as fallback for missed WebSocket messages:
useEffect(() => {
const interval = setInterval(async () => {
const fresh = await fetchActiveSession();
if (fresh) setRemainingSeconds(fresh.remaining_seconds);
}, 30000);
return () => clearInterval(interval);
}, []);
Defense in Depth
Three layers: WebSocket for instant sync (pause/resume reflects immediately), polling every 30 seconds as fallback (catches missed events), and screen focus refetch (catches drift when the tab was backgrounded). Any single layer can fail and the timer stays within 30 seconds of accurate.
Built while working on PlanPal.
Back to danielships ->