version 0.2.38
Added
- Managed WebSocket hub:
SocketHub now drives class-based channels (ChannelBase) with regex topic patterns via Topic("room:{id}"), lifecycle hooks (on_join/on_message/on_leave), and bus fanout (publish/broadcast).
- Bus factories:
SocketHub(bus_factory=...) resolves a per-connection SocketBus through the DI graph (supports nested factories), enabling custom backends while keeping the default in-memory bus.
- Tests & demo: new hub tests cover join/leave/chat, bus failure cleanup, and DI-resolved bus factories;
demo/ws.py/demo/chat.html updated to show room switching and broadcasts.
from pathlib import Path
from typing import Any
from msgspec import Struct
from lihil import ChannelBase, ISocket, Lihil, MessageEnvelope, Route, SocketHub, Topic
from lihil.vendors import Response
chat_hub = SocketHub("/ws/chat")
class Message(Struct, kw_only=True):
name: str = "anonymous"
text: str = ""
sentAt: str
def to_payload(self) -> dict[str, Any]:
return {"name": self.name, "text": self.text, "sentAt": self.sentAt}
@chat_hub.on_connect
async def on_connect(sock: ISocket) -> None:
# Simple per-socket bookkeeping example; not required by the hub.
sock.assigns.setdefault("rooms", set())
@chat_hub.on_disconnect
async def on_disconnect(sock: ISocket) -> None:
sock.assigns.get("rooms", set()).clear()
class RoomChannel(ChannelBase):
topic = Topic("room:{room_id}")
async def on_join(self) -> None:
await super().on_join()
room_id = self.params.get("room_id", "lobby")
self.socket.assigns.setdefault("rooms", set()).add(room_id)
await self.socket.emit({"room": room_id, "event": "joined"}, event="system")
async def on_leave(self) -> None:
await super().on_leave()
room_id = self.params.get("room_id", "lobby")
self.socket.assigns.get("rooms", set()).discard(room_id)
await self.socket.emit({"room": room_id, "event": "left"}, event="system")
async def on_message(self, env: MessageEnvelope) -> dict[str, Any] | None:
if env.event != "chat":
return {"code": 4404, "reason": "Event not found"}
message = Message(**env.payload)
# Broadcast to everyone in the room via the bus; uses incoming event verbatim.
await self.publish(message.to_payload(), event=env.event)
chat_hub.channel(RoomChannel)
app = Lihil(chat_hub)