Architecture of the WebSocket Notification System for Real-Time Updates
The WebSocket notification system in davila7/claude-code-templates employs a modular, event-driven architecture centered on two core classes—WebSocketServer for low-level socket operations and NotificationManager for high-level notification orchestration—wired together by the Analytics core to deliver scalable real-time updates.
This WebSocket notification system for real-time updates powers the analytics platform in the davila7/claude-code-templates repository. The design isolates connection lifecycle management from business logic, providing throttling, history tracking, and channel-based subscription filtering through a clean, extensible API.
Core Components
The architecture consists of four primary layers that handle distinct responsibilities:
| Component | File Path | Responsibility | Key Methods |
|---|---|---|---|
| WebSocketServer | src/analytics/notifications/WebSocketServer.js |
Low-level WebSocket handling, connection lifecycle, heartbeat, broadcast, and per-client message queuing. | initialize(), handleConnection(), broadcast(), sendToClient(), queueMessage(), startHeartbeat(), getStats() |
| NotificationManager | src/analytics/notifications/NotificationManager.js |
High-level notification orchestration, type-specific helpers, throttling, subscriber registry, and history management. | initialize(), notifyConversationStateChange(), notifyDataRefresh(), notifyNewMessage(), subscribe(), isThrottled(), getStats() |
| Analytics Core | src/analytics.js |
Server bootstrap that instantiates and connects the WebSocket and notification layers to other subsystems. | initializeWebSocket(), setupNotificationSubscriptions(), setupConsoleBridgeIntegration() |
| Console Bridge | src/console-bridge.js |
Optional secondary WebSocket endpoint for local web UI integration that forwards console interactions to the main server. | setupWebSocketServer(), initialize() |
System Interaction Flow
The system follows a precise lifecycle from server startup through message delivery to graceful shutdown.
Server Initialization
The Analytics core boots the stack in src/analytics.js through the initializeWebSocket() method. It first instantiates WebSocketServer with the HTTP server, configuration options including path: '/ws' and heartbeatInterval: 30000, and a PerformanceMonitor instance for metrics collection. After awaiting webSocketServer.initialize(), which opens the WS listener, it creates a NotificationManager instance and registers event handlers for client messages like refresh_requested.
Client Connection Lifecycle
When a browser or service connects, WebSocketServer.handleConnection() generates a unique client ID and stores connection metadata including IP address, user agent, and an empty subscription Set. The server immediately transmits a welcome payload with type: 'connection' and starts a ping/pong heartbeat via startHeartbeat() to detect and terminate dead connections.
Message Routing and Channel Subscriptions
Incoming client messages trigger WebSocketServer.handleClientMessage(), which parses JSON payloads and dispatches to specific handlers. Valid message types include subscribe, unsubscribe, ping, and refresh_request. Subscription requests add channel names to the client's subscriptions Set, enabling fine-grained filtering where broadcast() only delivers messages to clients that have explicitly joined that channel.
Broadcasting and Message Queuing
The broadcast() method in WebSocketServer serializes payloads, adds timestamps, and iterates over active connections. If a channel argument is provided, the system checks client.subscriptions.has(channel) before transmission. When no clients are currently connected, messages are not dropped but instead persisted via queueMessage() for delivery to the first client that connects, ensuring no data loss during intermittent connectivity.
High-Level Notification Pipeline
NotificationManager provides domain-specific convenience methods such as notifyConversationStateChange(), notifyDataRefresh(), and notifyNewMessage(). Each method constructs a notification object containing type, payload, timestamp, and unique ID, then:
- Validates throttling state via
isThrottled(key, interval)to prevent spam. - Persists to in-memory history using
addToHistory(). - Invokes
WebSocketServer.broadcast()or targeted send methods. - Notifies local subscribers registered through the
subscribe()API.
External modules like file watchers register callbacks using notificationManager.subscribe(type, callback), which stores functions in a Map<string, Set<Function>>. When notifications fire, all callbacks execute safely with errors logged without breaking the pipeline.
Graceful Shutdown
Both classes expose shutdown methods to ensure clean resource disposal. WebSocketServer.close() stops the heartbeat timer, terminates all client sockets, and clears internal maps. NotificationManager.shutdown() clears the subscriber registry and stops any periodic throttle-map cleanup intervals.
Implementation Examples
Bootstrapping the WebSocket Layer
In src/analytics.js, the Analytics class initializes the infrastructure:
// Inside Analytics.initializeWebSocket()
this.webSocketServer = new WebSocketServer(this.httpServer, {
path: '/ws',
heartbeatInterval: 30000
}, this.performanceMonitor);
await this.webSocketServer.initialize();
this.notificationManager = new NotificationManager(this.webSocketServer);
await this.notificationManager.initialize();
Sending Domain-Specific Notifications
Any module can trigger updates through high-level methods:
// After loading fresh analytics data
this.notificationManager.notifyDataRefresh(this.data, 'websocket_request');
// When conversation state changes
this.notificationManager.notifyConversationStateChange(conversationId, newState);
Subscribing to Custom Events
Internal modules register for notifications without WebSocket complexity:
// In the file-watcher module
const unsub = this.notificationManager.subscribe('file_change', ({filePath, changeType}) => {
console.log(`File ${changeType}: ${filePath}`);
});
// Cleanup when no longer needed
unsub();
Client-Side Connection
Browser clients connect to ws://localhost:3333/ws and subscribe to channels:
const ws = new WebSocket('ws://localhost:3333/ws');
ws.onopen = () => {
ws.send(JSON.stringify({
type: 'subscribe',
channel: 'conversation_updates'
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'new_message') {
// Update UI with msg.data.message
}
};
Extensibility Points
The architecture supports several extension vectors without modifying core classes:
- Custom Notification Types: Add methods to
NotificationManagerthat build payloads and callwebSocketServer.broadcast()with new channel identifiers. - Persistence Layer: Replace
addToHistory()andgetHistory()implementations inNotificationManagerwith database-backed storage (e.g., SQLite, Supabase) while maintaining the same public API. - Authentication: Wrap
WebSocketServer.handleConnection()with JWT verification middleware, leveraging existingsrc/lib/api/auth.tslogic used for HTTP routes. - Channel Architecture: Clients can subscribe to arbitrary string identifiers; the server already filters broadcasts using
client.subscriptions.has(channel).
Summary
- The WebSocket notification system for real-time updates relies on two primary classes:
WebSocketServerfor transport andNotificationManagerfor business logic. - Channel-based filtering ensures clients only receive messages for subscribed topics via
Setstorage per connection. - Message queuing prevents data loss when no clients are connected by holding broadcasts in memory until the next connection.
- Throttling and history management in
NotificationManagerprotect against notification spam while maintaining recent context. - Graceful shutdown procedures ensure clean termination of heartbeats, sockets, and subscriber registries.
Frequently Asked Questions
How does the WebSocket server detect and handle disconnected clients?
The WebSocketServer class implements a heartbeat mechanism via startHeartbeat() that sends periodic ping frames. If a client fails to respond with a pong within the configured interval, the server terminates the connection and removes the client from the active connections Map, preventing resource leaks.
Can notification history persist across server restarts?
Currently, notification history is stored in-memory within NotificationManager via addToHistory(). To enable persistence, you can extend the class to replace the in-memory store with a database implementation (such as SQLite or Supabase) without changing the public getHistory() or addToHistory() method signatures.
How are notification channels secured?
The base implementation does not enforce authentication on WebSocket connections. To secure channels, wrap the handleConnection() method in src/analytics/notifications/WebSocketServer.js with JWT verification using the existing authentication logic from src/lib/api/auth.ts, rejecting unauthorized connections before they subscribe to sensitive channels.
What happens if a notification is sent while no clients are connected?
The broadcast() method automatically detects when the client list is empty and routes the message to queueMessage(). This system maintains an internal queue of pending messages and delivers them to the first client that establishes a connection, ensuring no real-time updates are lost during periods of zero connectivity.
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 →