First-class presence (eliminate two-producer race) (B2 — WS refactor follow-up) #17
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#17
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?
Follow-up to #13. Closes gap B2 from the post-refactor architectural assessment.
Problem: the refactor's spec documented an accepted race in presence handling — two concurrent producers (
handle_user_ws's first-tab/last-tab hooks AND client-invokedpresence.updateRPC) can emit events out of order. Recipients seeofflineafteronlineflicker for up to ~60s until the next client heartbeat reconciles.Solution: server is the sole producer of the
online/offlinebit. Split into two narrow RPCs:presence.set_status_text— client sets status text ("In a meeting"); cannot flip online/offlinepresence.mark_connection— server-authoritative, called only byhandle_user_ws's lifecycle hooksKeep
presence.updatefor one release as a deprecated alias routing toset_status_text(ignores anystatusfield). Client-side presence-flipping RPCs removed.Docs:
plan/feature-first-class-presence.mdplan/impl-first-class-presence.mdSize: ~120 lines production + ~150 lines tests. 3 tasks.
Branch: lands on
feat/ws-refactor. Must run BEFORE #A2 heartbeat and #B1 session resume — both extendhandle_user_wsand reference the newpresence.mark_connectioncall site.Implementation landed
Three commits on
feat/ws-refactor:505d7effeat(server): first-class presence — server-authoritative online/offlineSplits
handlers::presence::updateinto three handlers:set_status_text(caller_id, user_id, status_text)— client-facing. Persistsusers.data.status_text+last_seen. Never writesusers.data.status(verified by grep:obj["status"] = ...only appears inmark_connection).mark_connection(caller_id, user_id, online: bool)— server-authoritative WS-lifecycle path. Only way to flip the online bit.update(...)— deprecated alias routing toset_status_text. Emitstracing::warn!; explicitly ignores anystatusfield. Kept for one release to tolerate stray callers during cutover.listunchanged. 4 new integration tests + P7.12 test migrated to usemark_connection.ab29528docs(openrpc): register presence.set_status_text + presence.mark_connectionSchema entries for both new methods;
presence.updatedescription updated with DEPRECATED note. SDK proc-macro regenerates typedPresenceSetStatusTextInput/PresenceMarkConnectionInputstructs at build time.15bd2e7refactor(ui): presence lifecycle via presence.mark_connection (server-authoritative)Both
handle_user_wsfirst-tab/last-tab RPC call sites swapped frompresence.updatetopresence.mark_connection. Deleted client-side 60s presence heartbeat (setIntervalfiringpresence.updatewithstatus: 'online') andbeforeunloadoffline beacon (sendBeaconfiringstatus: 'offline'). Both were client-side workarounds for the race that B2 eliminates structurally.Architectural notes
beforeunloadbeacon removal is a net improvement, not just simplification.beforeunloadis unreliable on mobile Safari and background-killed tabs — offline signal could be lost. The server's last-tabdrop(_tx)+receiver_count == 0check operates on TCP close, which IS reliable. B2 trades an unreliable signal for a reliable one.RpcError::Validation(code-32602).mark_connectionrequirescaller_id == user_idin proxy-auth mode viaresolve_self_user_id. Dev mode allows mismatch (matches existing dev-mode semantics elsewhere). Malicious self-mark is harmless — flipping your own presence is self-DoS at worst.presence.updatewithstatus:"online"degrades gracefully (status_text still writes; status bit untouched). Remove in a future chore commit once log-grep shows no callers.Tests impact
cargo test -p hero_collab_server: integration 68 (+4 new presence tests, P7.12 migrated).Minor debt noted
Two orphaned state fields on chat-app.js (
state._heartbeatInterval,state._unloadRegistered) are now unreferenced after the client-side workaround deletion. Noise-level; not worth a dedicated commit.