To demonstrate the practical application and ergonomics of the proposed Falcon-Pachinko extension, this section outlines its use in building a real-time chat application.
4.1. Scenario Overview
The chat application will support multiple chat rooms. Users can connect to a
specific room via a WebSocket URL (e.g., /ws/chat/{room_name}). Once
connected, they can send messages, and these messages will be broadcast to all
other users in the same room. The application will also support presence
indications (users joining/leaving) and typing indicators.
An AsyncAPI document (used as a design artifact) would define the message structures. For example:
-
Client to Server:
-
{"type": "clientSendMessage", "payload": {"text": "Hello everyone!"}} -
{"type": "clientStartTyping"} -
{"type": "clientStopTyping"} -
Server to Client:
-
{"type": "serverNewMessage", "payload": {"user": "Alice", "text": "Hello everyone!"}} -
{"type": "serverUserJoined", "payload": {"user": "Bob"}} -
{"type": "serverUserLeft", "payload": {"user": "Alice"}} -
{"type": "serverUserTyping", "payload": {"user": "Charlie", "isTyping": true}}
4.2. Defining the Chat WebSocket Resource
A ChatRoomResource class would be defined, inheriting from
falcon_pachinko.WebSocketResource:
import falcon.asgi
from falcon_pachinko import WebSocketLike, WebSocketResource, handles_message
class ChatRoomResource(WebSocketResource):
async def on_connect(self, req: falcon.Request, ws: WebSocketLike, room_name: str) -> bool:
# Assume authentication middleware has set req.context.user
self.user = req.context.get("user")
if not self.user:
return False # Reject connection
self.room_name = room_name
await self.join_room(self.room_name)
await ws.send_media({
"type": "serverSystemMessage",
"payload": {"text": f"Welcome {self.user.name} to room '{room_name}'!"}
})
await self.broadcast_to_room(
self.room_name,
{"type": "serverUserJoined", "payload": {"user": self.user.name}},
exclude_self=True
)
return True
@handles_message("clientSendMessage")
async def handle_send_message(self, ws: WebSocketLike, payload: dict):
message_text = payload.get("text", "")
# Add validation/sanitization as needed
await self.broadcast_to_room(
self.room_name,
{"type": "serverNewMessage", "payload": {"user": self.user.name, "text": message_text}}
)
@handles_message("clientStartTyping")
async def handle_start_typing(self, ws: WebSocketLike, payload: dict):
await self.broadcast_to_room(
self.room_name,
{"type": "serverUserTyping", "payload": {"user": self.user.name, "isTyping": True}},
exclude_self=True
)
@handles_message("clientStopTyping")
async def handle_stop_typing(self, ws: WebSocketLike, payload: dict):
await self.broadcast_to_room(
self.room_name,
{"type": "serverUserTyping", "payload": {"user": self.user.name, "isTyping": False}},
exclude_self=True
)
async def on_disconnect(self, ws: WebSocketLike, close_code: int):
if hasattr(self, 'room_name') and hasattr(self, 'user'):
await self.broadcast_to_room(
self.room_name,
{"type": "serverUserLeft", "payload": {"user": self.user.name}},
exclude_self=True
)
await self.leave_room(self.room_name)
print(
f"User {getattr(self.user, 'name', 'Unknown')} disconnected from room"
f" {getattr(self, 'room_name', 'N/A')} with code {close_code}"
)
async def on_unhandled(self, ws: WebSocketLike, message: Union[str, bytes]):
# Fallback for messages not matching handled types or non-JSON
print(f"Received unhandled message from {self.user.name} in {self.room_name}: {message}")
await ws.send_media({
"type": "serverError",
"payload": {"error": "Unrecognized message format or type."}
})
This example demonstrates how the WebSocketResource streamlines the
development of a feature-rich chat application. The abstractions for room
management and typed message handling significantly reduce boilerplate code.
4.3. Routing and Application Setup
In the main application file:
import falcon.asgi
import falcon_pachinko
import asyncio
from falcon_pachinko.workers import WorkerController
# Assuming ChatRoomResource is defined as above
# Assuming an authentication middleware `AuthMiddleware` that sets `req.context.user`
app = falcon.asgi.App(middleware=[AuthMiddleware()])
falcon_pachinko.install(app)
conn_mgr = app.ws_connection_manager
# Add the WebSocket route
app.add_websocket_route('/ws/chat/{room_name}', ChatRoomResource)
# Define a background worker
async def system_announcement_worker(conn_mgr: falcon_pachinko.WebSocketConnectionManager) -> None:
while True:
await asyncio.sleep(3600) # Every hour
announcement_text = "System maintenance is scheduled for 2 AM UTC."
chat_room_ids = await conn_mgr.get_rooms_by_prefix("chat_")
for room_id in chat_room_ids:
await conn_mgr.broadcast_to_room(
room_id,
{"type": "serverSystemAnnouncement", "payload": {"text": announcement_text}}
)
# Manage the worker with the ASGI lifespan
controller = WorkerController()
@app.lifespan
async def lifespan(app_instance):
await controller.start(system_announcement_worker, conn_mgr=conn_mgr)
yield
await controller.stop()
4.4. Client-Side Interaction (Conceptual)
A JavaScript client would interact as follows:
-
Connect:
const socket = new WebSocket("wss://example.com/ws/chat/general"); -
Send Messages:
socket.send(JSON.stringify({type: "clientSendMessage", payload: {text: "Hi there!"}}));
socket.send(JSON.stringify({type: "clientStartTyping"}));
- Receive Messages:
socket.onmessage = function(event) {
const message = JSON.parse(event.data);
switch(message.type) {
case "serverNewMessage":
// Display message.payload.user and message.payload.text
break;
case "serverUserJoined":
// Update user list with message.payload.user
break;
// Handle other message types...
}
};
This illustrative use case shows how Falcon-Pachinko can provide a comprehensive and developer-friendly solution for building real-time applications on Falcon.