| 7 | |
| 8 | |
| 9 | class WSManager: |
| 10 | MAX_CONNECTIONS: ClassVar[int] = 1000 |
| 11 | def __init__( |
| 12 | self, |
| 13 | log: Log, |
| 14 | ) -> None: |
| 15 | self._connections: dict[str, list[tuple[WebSocket, asyncio.Lock]]] = {} |
| 16 | self._lock = asyncio.Lock() |
| 17 | self.log = log |
| 18 | |
| 19 | async def connect(self, user_id: str, websocket: WebSocket) -> None: |
| 20 | if len(self._connections) >= self.MAX_CONNECTIONS: |
| 21 | await websocket.close(code=status.WS_1008_POLICY_VIOLATION) # Connection limit exceeded |
| 22 | self.log.error(f"🥕 WebSocket connect: user {user_id}, connection limit exceeded") |
| 23 | return |
| 24 | await websocket.accept() |
| 25 | async with self._lock: |
| 26 | lock = asyncio.Lock() |
| 27 | self._connections.setdefault(user_id, []).append((websocket, lock)) |
| 28 | self.log.info(f"🍏 WebSocket connected: {user_id} (total {len(self._connections.get(user_id, []))})") |
| 29 | |
| 30 | async def disconnect(self, user_id: str, websocket: WebSocket) -> None: |
| 31 | async with self._lock: |
| 32 | conns = self._connections.get(user_id, []) |
| 33 | conns = [c for c in conns if c[0] is not websocket] |
| 34 | if not conns: |
| 35 | self._connections.pop(user_id, None) |
| 36 | try: |
| 37 | await websocket.close(code=status.WS_1000_NORMAL_CLOSURE) |
| 38 | except Exception as e: |
| 39 | self.log.warning(f" WebSocket failed to close to {user_id}: {e}") |
| 40 | self.log.info(f"🍎 WebSocket disconnected: {user_id}") |
| 41 | |
| 42 | async def send_to_user(self, user_id: str, data: dict[str, Any], websocket: WebSocket | None = None) -> int: |
| 43 | conns = self._connections.get(user_id, []) |
| 44 | if not conns: |
| 45 | self.log.debug(f"🍊 WebSocket No active ws for user {user_id}") |
| 46 | return 0 |
| 47 | if websocket is not None: |
| 48 | wss = [c for c in conns if c[0] is websocket] |
| 49 | if not wss: |
| 50 | self.log.debug(f"🍊 WebSocket No active ws for user {user_id}") |
| 51 | return 0 |
| 52 | success = int(await self._safe_send(wss[0], data, user_id)) |
| 53 | else: |
| 54 | coros = [self._safe_send(ws, data, user_id) for ws in conns] |
| 55 | results = await asyncio.gather(*coros, return_exceptions=True) |
| 56 | success = sum(1 for r in results if r is True) |
| 57 | self.log.info(f"🍐 WebSocket sent message to {success}/{len(conns)} connections for user {user_id}") |
| 58 | return success |
| 59 | |
| 60 | async def broadcast(self, data: dict[str, Any], exclude_user_id: list[str] | None = None) -> int: |
| 61 | if exclude_user_id is None: |
| 62 | exclude_user_id = [] |
| 63 | success = 0 |
| 64 | user_ids = [] |
| 65 | for user_id in self._connections.keys(): |
| 66 | if user_id in exclude_user_id: |
nothing calls this directly
no outgoing calls
no test coverage detected