Skip to content

Stealth client does not receive disconnect notification when the last normal client leaves a room #349

@hacha

Description

@hacha

Summary

When the last normal client disconnects from a room, stealth clients still connected to the same room do not receive an updated ID mapping reflecting the disconnection. The stealth client continues to see the disconnected client in its ID mapping, with no way to detect that the client has left.

Reproduction Steps

  1. Connect normal client A and stealth client S to the same room
  2. Disconnect normal client A (let it time out)
  3. Observe the ID mapping received by stealth client S

Expected Behavior

Stealth client S receives an updated ID mapping that no longer includes client A, allowing it to detect the disconnection.

Actual Behavior

After client A times out, stealth client S still receives an ID mapping that includes client A. The stealth client has no way to distinguish a connected client from a disconnected one.

This issue is not limited to the "last normal client" scenario — it likely affects any client disconnection — but it is most visible when the last normal client leaves, as there is no other signal (such as missing transform data from other clients) to infer the disconnection.

Probable Cause

_cleanup_clients (server.py:1854-1871) removes timed-out clients from self.rooms[room_id] but intentionally leaves their entry in room_device_id_to_client_no, deferring cleanup to DEVICE_ID_EXPIRY_TIME:

# Note: We don't remove device ID->clientNo mapping here
# It will be cleaned up after DEVICE_ID_EXPIRY_TIME

_broadcast_id_mappings (server.py:1597-1606) builds the mapping list from room_device_id_to_client_no without checking whether the client still exists in self.rooms:

for device_id, client_no in self.room_device_id_to_client_no[room_id].items():
    client_data = self.rooms.get(room_id, {}).get(device_id, {})
    is_stealth = client_data.get("is_stealth", False)
    mappings.append((client_no, device_id, is_stealth))

When a device_id has already been removed from self.rooms, client_data resolves to an empty dict, and the disconnected client is still included in the broadcast with is_stealth=False.

Proposed Fix

Filter the mapping list in _broadcast_id_mappings to only include clients that still exist in self.rooms[room_id].

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions