Presence avatar count differs between owner view and shared-link view; fixed on reload #109
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
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_whiteboard#109
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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
The presence indicator (active-user avatars in the top toolbar) shows a
different number of connected users depending on which entry point is
used to view the same board:
for the same board at the same moment.
The two should agree because they are displaying the same set of active
sessions.
Steps to reproduce
Expected
Both views show the same set of active-user avatars at any given moment,
and the list updates live as users join or leave — without requiring a
reload.
Actual
The two views disagree on the number of avatars. Reloading either tab

brings it in sync with the actual session count.
Implementation Spec for Issue #109
Objective
Make the presence avatar list converge on every join and leave, without requiring a reload, regardless of which entry point a tab was opened from.
Root cause
Three gaps in
sync.js:{type: 'join', ...}. Existing tabs add the new tab to their_remoteUsers. But existing tabs do not reply with their own identity. So the new tab only learns about an existing tab when that existing tab happens to broadcast a cursor or edit.leaveonbeforeunload. Closing a tab doesn't tell other tabs to prune the user.The user sees the divergence in the screenshot: owner tab shows fewer avatars than the shared-link tab because each one independently accumulated only the users it happened to "discover" via secondary broadcasts.
Approach
Fix #1 and #2 client-side. Both are minimal changes to
sync.jsand produce convergent presence on every connect/disconnect through user action. #3 (server-side disconnect detection without explicit leave) is more invasive and out of scope — the clientbeforeunloadcovers normal close; abnormal disconnects converge on the next join handshake.Files to Modify
crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.js— extend thejoinhandler to reply with own identity for unknown users; add abeforeunloadleavebroadcast.Implementation Plan
Step 1: Client — handshake on join + leave on unload
File:
crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.js1a. On receiving
join, reply with own identity if the joiner is new.In
handleWsMessage, the existingjoinbranch is:Replace with:
1b. Send a
leavebroadcast on page unload.The current
beforeunloadhandler only flushes pending updates:Append a leave broadcast (best-effort — not all browsers reliably deliver after
beforeunload, but most do for the synchronous WebSocket send):The receiver path for
leave(line ~216) already prunes the sender viadelete _remoteUsers[msg.sender]and callsupdatePresenceDisplay(). ThewsSendhelper stampssender: localUserIdon outgoing messages, so the receiver knows who left.Dependencies: none.
Acceptance Criteria
cargo fmt,cargo clippy --workspace --all-targets -- -D warnings,cargo test --workspace --libclean.Notes
alreadyKnownrather than a_reply: trueflag: simpler, idempotent, doesn't require the sender to know about the protocol marker. The firstjoinfor an unknown user always triggers exactly one reply per existing tab; the reply is itself ajoin, but by the time it arrives the original sender has already added the replier (soalreadyKnownis true) and the chain stops.leave:beforeunloaddoesn't guarantee delivery (browser may close the WebSocket before the message flushes). Crashes / network drops still leave stale entries — a future fix could add server-side disconnect detection (server tracks each connection's localUserId and broadcasts aleavewhen the WS closes). Out of scope here._localColoris already defined at module scope (line ~928) and stable across the lifetime of the tab — safe to use in the handshake reply.Test Results
cargo fmt --all -- --check— cleancargo clippy --workspace --all-targets -- -D warnings— cleancargo test --workspace --lib— 0 failednode --check sync.js— cleanImplementation Summary
Two client-only changes in
sync.jsto make presence converge on every join and leave.crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.jsHandshake on join. The
handleWsMessagebranch fortype: 'join'now replies with our own identity when we see an unknown joiner, so the joiner discovers us without waiting for an unrelated cursor / edit broadcast. AnalreadyKnowncheck stops the reply chain after one round-trip:Leave on
beforeunload. Best-effortwsSend({ type: 'leave' })appended to the existing unload handler. The receiver path forleavealready prunes the sender from_remoteUsersand refreshes the presence display.Files Changed
crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.js—+18 / -3Test Results
cargo fmt --all -- --check— cleancargo clippy --workspace --all-targets -- -D warnings— cleancargo test --workspace --lib— 0 failednode --check sync.js— cleanManual smoke
alreadyKnownguard caps each pair at one handshake).Notes
leave:beforeunloaddoesn't guarantee delivery if the browser closes the WebSocket too early. Crashes / network drops still leave stale entries until the next join handshake from the affected pair, at which point the unknown-user reply path re-syncs everyone.localUserId. Out of scope here; the client-only fix resolves the reported symptom (join-time divergence).