Skip to content

Stabilize room host ownership across rejoins - #195

Merged
coder13 merged 6 commits into
masterfrom
agent/issue-153-room-owner-rejoin
Jul 14, 2026
Merged

Stabilize room host ownership across rejoins#195
coder13 merged 6 commits into
masterfrom
agent/issue-153-room-owner-rejoin

Conversation

@coder13

@coder13 coder13 commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary

  • make owner/admin selection deterministic across leave, rejoin, handoff, and empty-room transitions
  • preserve the established non-null UPDATE_ADMIN Socket.IO contract while persisting admin: null for empty rooms
  • select and persist the next admin for every membership removal, including moderation paths with no notification callback
  • reject raw self-kick and self-ban requests so normal departure remains the sole self-removal path
  • make simultaneous explicit leaves from two tabs finalize exactly one last-socket departure
  • claim the persisted departure with a MongoDB compare-and-set, preventing duplicate cross-process leave side effects
  • bind departures to room membership and per-user presence generations; stale claims cannot remove later reconnects or duplicate tabs
  • document reconnect, multi-tab, Grand Prix, and downstream invitation authorization semantics

Root cause

The ordinary creator leave/rejoin path already attempted to restore ownership. Its selector was brittle, though: owner restoration required a non-null current admin, while the fallback always chose the first stored active participant. That could select the wrong host for a null-admin room and erase a legitimate active handoff while the owner was absent. The original correction also made empty rooms invoke a callback with admin: null, but established clients dereference admin.id. Explicit leave checked for peer tabs before removing the current socket from the per-user Socket.IO room, allowing two simultaneous leaves to each see the other and strand membership. Process-local departure queues also could not serialize competing Socket.IO processes. Finally, a duplicate socket could arrive while persisted membership was still active, leaving an old departure generation valid unless the new socket durably fenced it.

Authorization, protocol, and concurrency contract

Owner is permanent creator/deletion authority. Admin is the sole active control holder. The active owner reclaims admin; otherwise the active current admin remains, then the first active participant is promoted. Empty rooms persist no admin but never broadcast a null UPDATE_ADMIN payload. Raw self-kick and self-ban are rejected; users leave through the ordinary departure path.

Each actual join or removal advances the room membership revision. Each authenticated duplicate join atomically advances only that user's presenceRevision before acknowledgment; it does not alter membership or admin. Reconnect cleanup captures both revisions. Departure uses a MongoDB conditional update requiring the same active membership and both generations. Exactly one process can claim that departure and emit its metrics/events. A failed claim is terminal and cannot remove a later session. Reconnect grace remains unchanged.

For #187, private-room invitation authority must combine canonical active membership with owner-or-current-admin role checks. Delete authorization alone is intentionally insufficient because an absent owner may still delete a room.

Verification

  • focused server Jest suites: 2 suites / 31 tests passed
  • full server Jest suite: 15 suites / 100 tests passed
  • changed JavaScript files pass ESLint
  • git diff --check passes
  • full server ESLint remains blocked by a pre-existing server/api.js import-extension violation, outside this PR

Grand Prix is disabled in current runtime configuration; the rejection/no-mutation path is covered, while enabled Grand Prix uses the same model selector but was not browser-tested.

Fixes #153

coder13 added 6 commits July 12, 2026 20:52
Keep creator ownership separate from active admin handoffs and restore the creator deterministically on rejoin. Centralize role checks and document the authorization contract needed by room invitations.\n\nRefs #153
Preserve the non-null admin socket contract, always select and persist a successor on removal, and reject self-removal moderation requests. Make explicit two-tab leaves remove their own socket membership before checking for remaining tabs so the last departure cannot strand a room.\n\nRefs #153
Claim persisted room departures with a MongoDB compare-and-set on the active membership revision. This prevents competing Socket.IO processes from duplicating leave side effects or overwriting a rejoin, while retaining the reconnect grace behavior.\n\nRefs #153
Carry the membership revision captured when a socket leaves through reconnect cleanup and reject a departure when that generation no longer matches. This prevents a losing old finalizer from removing a later rejoin.\n\nRefs #153
Advance a durable per-user presence revision before acknowledging an already-active room membership. Departure claims now require that generation, so an old leave cannot remove a newly opened tab.\n\nRefs #153
Validate the current room state after a departure wins the duplicate-join race, preventing bans or changed access rules from being bypassed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Host ownership should survive disconnects and rejoins

1 participant