Reaction count shows 2 instead of 1 for own message (optimistic-update vs WS-broadcast race) #31

Closed
opened 2026-04-27 17:01:46 +00:00 by sameh-farouk · 0 comments
Member

Symptom

Reacting to one's own message renders count 2 instead of 1. Clicking again removes both. Server-side state is correct (one reaction row in the DB) — only the client UI miscounts.

Root cause (race)

chat-app.js::toggleReaction does an optimistic local update after await rpc('message.toggle_react') returns:

var result = await rpc('message.toggle_react', ...);
// ... later:
msg.reactions.push({ emoji, user_id: state.currentUser.id });

The server persists the reaction and broadcasts a message.reacted event to all channel members including the sender (multi-tab parity per the WS-refactor design). When the WS event lands faster than the HTTP RPC response — common under any non-trivial server load — the timeline becomes:

  1. Server broadcasts the authoritative reactions list including caller's row.
  2. onMessageReacted arrives → replaces msg.reactions wholesale with the server list (already contains caller's row).
  3. RPC await resolves → optimistic push appends a second copy of the same (emoji, user_id) row.
  4. The badge counts entries → renders 2.

Surfaced in

#10 dogfooding thread — reacting to own message in chat.

Fix in

PR linked below.

### Symptom Reacting to one's own message renders count `2` instead of `1`. Clicking again removes both. Server-side state is correct (one reaction row in the DB) — only the client UI miscounts. ### Root cause (race) `chat-app.js::toggleReaction` does an optimistic local update *after* `await rpc('message.toggle_react')` returns: ```js var result = await rpc('message.toggle_react', ...); // ... later: msg.reactions.push({ emoji, user_id: state.currentUser.id }); ``` The server persists the reaction and broadcasts a `message.reacted` event to **all channel members including the sender** (multi-tab parity per the WS-refactor design). When the WS event lands faster than the HTTP RPC response — common under any non-trivial server load — the timeline becomes: 1. Server broadcasts the authoritative reactions list including caller's row. 2. `onMessageReacted` arrives → replaces `msg.reactions` wholesale with the server list (already contains caller's row). 3. RPC `await` resolves → optimistic `push` appends a *second* copy of the same `(emoji, user_id)` row. 4. The badge counts entries → renders `2`. ### Surfaced in [#10 dogfooding thread](https://forge.ourworld.tf/lhumina_code/hero_collab/issues/10) — reacting to own message in chat. ### Fix in PR linked below.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_collab#31
No description provided.