feat: sync activity timer across a player's open sessions - #770
Merged
Conversation
Fixes multi-session desync (#759): if a player has more than one session open (two tabs, phone + laptop), starting, labelling, or submitting an activity in one had no effect on the others, since each session's timer state was purely local React state. Backend: - Add gameplay.utils.broadcast_activity_timer(timer), reusing the existing player_{id} channel group and send_group_message/"action" message pattern from gameplay/signals.py and utils.control_timers. - Call it after every timer mutation: start/pause/reset/complete (BaseTimerViewSet), and set_activity/label_activity/complete (ActivityTimerViewSet), plus both auto-complete paths in tasks.py (disconnect grace period and the stale-connection sweep) so a tab left open elsewhere is told the timer already closed out. - No consumer changes needed: TimerConsumer's existing generic "action" handler already relays any {"type": "action", ...} group message straight to the socket. Frontend: - Extend WebSocketActionMessage with the new "activity_timer_update" action and its data payload. - Wire the previously-stubbed handleGlobalWebSocketEvent dispatcher with an onActivityTimerUpdate option. - WebSocketContext calls activityTimer.loadFromServer on receipt, mirroring GameContext's existing bootstrap load (same limitSeconds/is_premium resolution). loadFromServer just overwrites local state, so a session echoing its own update is a harmless no-op. Tests: broadcast_activity_timer call verified for start/complete/ set_activity/label_activity and both auto-complete Celery tasks.
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.
Summary
Fixes #759. If a player has more than one session open (two browser tabs, or a phone + a laptop), starting, labelling, or submitting an activity in one session had no effect on the others — each session's
useActivityTimerstate was purely local, seeded once from the server and never invalidated by another session's writes.Changes
Backend
gameplay.utils.broadcast_activity_timer(timer), reusing the existing per-playerplayer_{id}channel group and thesend_group_message/{"type": "action"}pattern already used bygameplay/signals.pyandutils.control_timers.start/pause/reset/completeinBaseTimerViewSet, andset_activity/label_activity/completeinActivityTimerViewSet.gameplay/tasks.py(the 30s disconnect grace period, and the stale-connection sweep) — these complete a timer server-side without any originating request, so a tab left open elsewhere needs telling too.TimerConsumerchanges needed: its existing genericactionhandler already relays any{"type": "action", ...}group message straight to the socket.Frontend
WebSocketActionMessagewith the new"activity_timer_update"action and itsdata.activity_timerpayload.handleGlobalWebSocketEventdispatcher with anonActivityTimerUpdateoption (theload-gamestub is left as-is for future use).WebSocketContextcallsactivityTimer.loadFromServeron receipt, mirroringGameContext's existing bootstrap load (samelimitSeconds/is_premiumresolution).loadFromServerjust overwrites local state, so a session echoing its own update back to itself is a harmless no-op.Testing
broadcast_activity_timeris called fromstart/complete/set_activity/label_activityand from both Celery auto-complete tasks.🤖 Generated with Claude Code