Private
Public Access
Previously GET /api/rooms/mine was only ever fetched once at app mount, so a room added mid-session stayed invisible until a full page reload -- add_member had no way to reach an already-open client at all. Backend: ConnectionManager and Broadcaster (renamed from RoomBroadcaster) now support per-user channels alongside the existing per-room ones, so a signal can reach a user's socket even for a room they haven't joined (and by definition can't have, until this fires). add_member publishes a room_added event on the target user's channel. Frontend: the WebSocket connection is no longer scoped to whichever room is open -- ChatShellPage now owns one persistent connection for the whole session (including while no room is open, which is exactly when this bug showed), and ChatPane joins/leaves rooms on top of it. A room_added event triggers a room-list refetch with no reload needed. Verified end-to-end in the browser: a user sitting on the empty room list saw a newly-added room appear live, then chatted in it normally. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
51 lines
1.9 KiB
Python
51 lines
1.9 KiB
Python
import uuid
|
|
from collections import defaultdict
|
|
|
|
from fastapi import WebSocket
|
|
|
|
|
|
class ConnectionManager:
|
|
"""Local, single-process WebSocket socket registry.
|
|
|
|
Purely about delivering to sockets connected to *this* process --
|
|
cross-instance fan-out lives in Broadcaster, and cross-instance "who's
|
|
connected" for push lives in Presence, both backed by Redis
|
|
(ARCHITECTURE.md phase 5).
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
self._rooms: dict[uuid.UUID, set[WebSocket]] = defaultdict(set)
|
|
self._users: dict[uuid.UUID, set[WebSocket]] = defaultdict(set)
|
|
|
|
def join(self, room_id: uuid.UUID, websocket: WebSocket) -> None:
|
|
self._rooms[room_id].add(websocket)
|
|
|
|
def leave(self, room_id: uuid.UUID, websocket: WebSocket) -> None:
|
|
self._rooms[room_id].discard(websocket)
|
|
if not self._rooms[room_id]:
|
|
del self._rooms[room_id]
|
|
|
|
def leave_all(self, websocket: WebSocket) -> None:
|
|
for room_id in list(self._rooms.keys()):
|
|
self.leave(room_id, websocket)
|
|
|
|
def register_user(self, user_id: uuid.UUID, websocket: WebSocket) -> None:
|
|
"""Ties a socket to the authenticated user who owns it, independent
|
|
of which (if any) room it has joined -- lets a user be reached the
|
|
instant they're added to a room, before they've ever joined that
|
|
room's channel."""
|
|
self._users[user_id].add(websocket)
|
|
|
|
def unregister_user(self, user_id: uuid.UUID, websocket: WebSocket) -> None:
|
|
self._users[user_id].discard(websocket)
|
|
if not self._users[user_id]:
|
|
del self._users[user_id]
|
|
|
|
async def broadcast(self, room_id: uuid.UUID, payload: dict) -> None:
|
|
for websocket in list(self._rooms.get(room_id, ())):
|
|
await websocket.send_json(payload)
|
|
|
|
async def send_to_user(self, user_id: uuid.UUID, payload: dict) -> None:
|
|
for websocket in list(self._users.get(user_id, ())):
|
|
await websocket.send_json(payload)
|