How to Use Socket.IO for Real-Time Events in RomM: Backend Architecture and Client Implementation
RomM implements real-time communication using Socket.IO with Redis-backed pub/sub to broadcast scan progress, activity updates, admin logs, and WebRTC net-play signals from FastAPI workers to connected web clients.
RomM leverages Socket.IO to push live data from the backend to the web UI without requiring polling. The architecture is built around a single AsyncServer instance backed by AsyncRedisManager, which allows multiple FastAPI workers to share the same socket namespace across a distributed deployment. This implementation supports horizontal scaling while maintaining event consistency for all connected clients.
Architecture Overview: Redis-Backed Socket.IO Server
The foundation of RomM's real-time system lies in backend/handler/socket_handler.py, which initializes an AsyncServer using Redis as the client manager. This design ensures that every emitted event is written to Redis, and all workers subscribe to the same channel, guaranteeing that any client connected to any worker receives identical events.
The server exposes two distinct endpoints:
/ws/socket.io– Handles general UI events including scan progress, library activity, and administrative log streams./netplay/socket.io– Manages peer-to-peer WebRTC signaling for multiplayer gameplay.
Server Configuration and Initialization
The SocketHandler class in backend/handler/socket_handler.py creates the server instance and mounts it as an ASGI application. The configuration uses AsyncRedisManager to enable cross-worker communication and sets specific ping parameters to maintain connection stability.
# backend/handler/socket_handler.py
class SocketHandler:
def __init__(self, path: str) -> None:
self.socket_server = socketio.AsyncServer(
cors_allowed_origins="*",
async_mode="asgi",
json=json_module,
client_manager=socketio.AsyncRedisManager(REDIS_URL),
ping_timeout=60,
ping_interval=25,
)
self.socket_app = socketio.ASGIApp(
self.socket_server,
socketio_path=path
)
This configuration allows the server to run behind multiple FastAPI workers while maintaining synchronized state through Redis.
Emitting Real-Time Events
Throughout the codebase, handlers emit events using the emit() method of the socket server instance. Events are broadcast either globally or to specific rooms depending on the use case.
Scan Progress Updates
During library scans, backend/endpoints/sockets/scan.py emits granular progress updates to the frontend. The handler broadcasts statistics and individual ROM processing status:
# backend/endpoints/sockets/scan.py (lines 98-106, 77-80)
await socket_manager.emit("scan:update_stats", self.to_dict())
await socket_manager.emit("scan:scanning_rom", rom_payload)
Activity Notifications
The activity socket handler in backend/endpoints/sockets/activity.py sends updates when library changes occur, using the activity:update event to notify clients of new or modified content.
Admin Log Streaming
Administrators receive real-time log entries through the logs:entry event. The backend/endpoints/sockets/logs.py file implements authentication in the connect handler, resolving the user from the request and placing authenticated admins into the "admin" room:
# backend/endpoints/sockets/logs.py
@socket_server.event
async def connect(sid, environ):
user = await resolve_user(environ)
await socket_server.save_session(sid, {"user_id": user.id})
if user.role == "admin":
await socket_server.enter_room(sid, "admin")
A background task subscribes to the "romm:logs" Redis channel and emits lines to the "admin" room, ensuring all admin clients receive live logs regardless of which worker they connect to.
Net-Play Signaling and Room Management
For multiplayer functionality, RomM uses Socket.IO rooms to isolate WebRTC signaling between peers. The backend/endpoints/sockets/netplay.py handler manages room lifecycle and session state.
Creating and Managing Rooms
When a client opens a net-play room, the server creates a room identifier and stores session data in Redis:
# backend/endpoints/sockets/netplay.py (lines 56-58)
await netplay_socket_handler.socket_server.enter_room(sid, session_id)
await netplay_socket_handler.socket_server.save_session(sid, {
"room_id": room_id,
"peer_id": peer_id
})
Broadcasting to Specific Peers
To route WebRTC signals between specific users, the server emits to individual socket IDs or rooms while excluding the sender:
# backend/endpoints/sockets/netplay.py (lines 4-9)
await socket_server.emit(
"webrtc-signal",
{"sender": sid, "candidate": data["candidate"]},
to=target_sid,
skip_sid=sid
)
This pattern ensures signaling data reaches only the intended recipient during peer connection establishment.
Client-Side Implementation
The RomM frontend uses the Socket.IO JavaScript client to establish connections and listen for events. Authentication relies on cookie forwarding via the withCredentials: true option.
Connecting to General UI Events
For scan progress and activity updates, connect to the /ws/socket.io endpoint:
import { io } from "socket.io-client";
const socket = io("/ws/socket.io", { withCredentials: true });
socket.on("connect", () => console.log("Socket connected"));
socket.on("scan:update_stats", data => {
// Update progress bar with scan statistics
console.log("Scan stats:", data);
});
socket.on("scan:scanning_rom", rom => {
// Display currently scanning ROM
console.log("Scanning ROM:", rom);
});
socket.on("logs:entry", logEntry => {
// Append to admin log viewer
console.log("Log:", logEntry);
});
Connecting to Net-Play Signaling
For WebRTC coordination, use the netplay namespace:
const netplay = io("/netplay/socket.io", { withCredentials: true });
// Open a room for multiplayer
netplay.emit("open-room", { gameId: "super_mario_64", roomName: "Player1's Game" });
// Handle incoming WebRTC signals
netplay.on("webrtc-signal", payload => {
handleSignal(payload);
});
The withCredentials: true flag is essential as it forwards the authentication cookie, allowing the backend connect handlers to resolve the user session and enforce authorization rules.
Horizontal Scaling with Redis Pub/Sub
Because RomM uses AsyncRedisManager as the client manager, the system scales horizontally without additional code. When a worker emits an event via socket_server.emit(), the data is written to a Redis pub/sub channel. All other workers subscribe to this channel and broadcast the message to their connected clients.
This architecture is demonstrated in the log forwarding mechanism, where a single worker holds a lock to subscribe to the "romm:logs" channel, yet all workers can emit the log entries to their local admin clients. This ensures that even with multiple FastAPI workers behind a load balancer, all clients receive consistent real-time updates.
Summary
- Redis-backed architecture: The
AsyncRedisManagerinbackend/handler/socket_handler.pyenables multiple FastAPI workers to share socket state through Redis pub/sub. - Dual endpoint design: Separate namespaces at
/ws/socket.io(UI events) and/netplay/socket.io(WebRTC signaling) isolate concerns. - Event emission pattern: Use
socket_server.emit(event, data)for global broadcasts or specifyroomandtoparameters for targeted delivery. - Room-based access control: The
"admin"room restricts log streaming to authorized users, while net-play rooms isolate peer-to-peer signaling. - Client authentication: The frontend must set
withCredentials: trueto forward cookies for user resolution inconnecthandlers.
Frequently Asked Questions
What is the difference between the two Socket.IO endpoints in RomM?
RomM exposes two distinct Socket.IO endpoints to separate concerns. The /ws/socket.io endpoint handles general application events including scan progress (scan:update_stats), activity updates (activity:update), and admin log streams (logs:entry). The /netplay/socket.io endpoint is dedicated to WebRTC signaling for multiplayer functionality, managing rooms and routing ICE candidates between peers. Both endpoints use the same Redis backend but maintain separate connection namespaces.
How does RomM handle authentication for Socket.IO connections?
Authentication occurs in the connect event handlers, such as in backend/endpoints/sockets/logs.py. The handler inspects the incoming request cookies to resolve the user, then stores the user ID in the socket session using socket_server.save_session(). For protected resources like admin logs, the server places the socket into the "admin" room only if the user has admin privileges. The frontend must include withCredentials: true in the client options to ensure cookies are forwarded during the WebSocket handshake.
Can RomM's Socket.IO implementation scale across multiple servers?
Yes. By configuring the server with client_manager=AsyncRedisManager(REDIS_URL) in backend/handler/socket_handler.py, RomM uses Redis as a message broker between workers. When one worker emits an event, it publishes to Redis, and all other workers receive the message and broadcast it to their local clients. This design allows horizontal scaling across multiple FastAPI processes or servers without requiring sticky sessions or complex coordination code.
How do I listen for scan progress updates in the frontend?
Connect to the /ws/socket.io namespace and register listeners for the specific scan events. The scan:update_stats event provides overall progress statistics, while scan:scanning_rom fires for each individual ROM being processed. Example implementation:
const socket = io("/ws/socket.io", { withCredentials: true });
socket.on("scan:update_stats", stats => updateProgressBar(stats));
socket.on("scan:scanning_rom", rom => updateCurrentRomDisplay(rom));
These events are emitted from backend/endpoints/sockets/scan.py during the library scan process.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →