-
Notifications
You must be signed in to change notification settings - Fork 9
Description
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
- Connect normal client A and stealth client S to the same room
- Disconnect normal client A (let it time out)
- 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].