Skip to content

Release v0.2.38

Choose a tag to compare

@raceychan raceychan released this 30 Dec 13:24
· 7 commits to master since this release

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)