fix(reactions): idempotent optimistic update to prevent double-count #33
No reviewers
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_collab!33
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/reaction-double-count-race"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Closes #31.
toggleReactiondoes optimistic UI afterawait rpc('message.toggle_react'). The server broadcasts the authoritative reactions list to all channel members (including sender, per the multi-tab WS contract). When the WS event lands faster than the RPC response (common under any non-trivial fanout latency),onMessageReactedreplacesmsg.reactionswith the server-authoritative list — already containing the caller's row — and then the optimisticpushadds a second copy. Badge renders2for one reaction.Fix
Make the 'added' branch idempotent. Before pushing, check whether
(emoji, user_id)is already present:The 'removed' branch's
.filteris naturally idempotent (filtering an already-filtered array is a no-op), so no change there.Why optimistic update is kept
Removing the optimistic update entirely would also fix the bug, but reintroduces a latency-sensitive feedback gap (badge would lag visibly behind the click under packet loss / slow fanout). Idempotent push is the cheap fix that preserves both perceived latency and correctness.
Test plan
findMessageAnywherelookup unchanged).node --checkon chat-app.js passes.🤖 Generated with Claude Code
`toggleReaction` does an optimistic local update after `await rpc( 'message.toggle_react')` returns, then `onMessageReacted` later replaces `msg.reactions` wholesale when the WS broadcast lands. With `Audience::ChannelMembers` including the sender (multi-tab parity), the WS event can arrive before the RPC `await` resolves — interleaving: 1. RPC fires; server persists + broadcasts to channel members (including sender) → onMessageReacted sets msg.reactions to the authoritative server list (already contains caller's row). 2. RPC await resolves on the original click handler → optimistic `msg.reactions.push(...)` appends a second copy of the same (emoji, user_id) row. 3. The badge counts entries → renders "2" for what is in fact one reaction. Symptom from dogfooding: reacting to one's own message visibly counts 2 instead of 1. Fix: make the 'added' branch idempotent. Before pushing, check whether an entry with the same (emoji, user_id) is already present. The 'removed' branch's `.filter` is naturally idempotent (filtering an already-filtered array is a no-op), so no change needed there. Removing the optimistic update entirely would also fix the bug, but it would re-introduce the latency-sensitive feedback gap that prompted the optimistic path in the first place — under packet loss / slow fanout the badge would lag visibly behind the click. Idempotent push is the cheap fix that preserves both perceived latency and correctness. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>