ca-grow-ops-manager/backend/src/plugins/websocket.ts
fullsizemalt 58d38aef8a chore: Reduce WebSocket logging from info to debug
Connect/disconnect messages were flooding production logs.
Changed to log.debug level for cleaner output.
2026-01-12 22:57:19 -08:00

117 lines
3.1 KiB
TypeScript

/**
* WebSocket Plugin for Real-time Alerts
*
* Broadcasts environment alerts to connected clients.
*/
import { FastifyInstance } from 'fastify';
import websocket from '@fastify/websocket';
interface AlertMessage {
type: 'ALERT' | 'READING' | 'HEARTBEAT';
data: any;
timestamp: string;
}
// Connected clients (storing the raw WebSocket)
const clients: Map<string, any> = new Map();
export async function websocketPlugin(fastify: FastifyInstance) {
await fastify.register(websocket);
/**
* WebSocket endpoint for real-time alerts
*/
fastify.get('/api/ws/alerts', { websocket: true }, (connection, request) => {
const clientId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
// Get the raw WebSocket from the SocketStream
const socket = connection.socket;
clients.set(clientId, socket);
fastify.log.debug(`WebSocket client connected: ${clientId}`);
// Send welcome message
socket.send(JSON.stringify({
type: 'CONNECTED',
clientId,
timestamp: new Date().toISOString()
}));
socket.on('message', (message: Buffer) => {
try {
const data = JSON.parse(message.toString());
// Handle ping/pong for keepalive
if (data.type === 'PING') {
socket.send(JSON.stringify({ type: 'PONG', timestamp: new Date().toISOString() }));
}
} catch {
// Ignore invalid messages
}
});
socket.on('close', () => {
clients.delete(clientId);
fastify.log.debug(`WebSocket client disconnected: ${clientId}`);
});
socket.on('error', (error: Error) => {
fastify.log.error(`WebSocket error for ${clientId}: ${error.message}`);
clients.delete(clientId);
});
});
}
/**
* Broadcast an alert to all connected clients
*/
export function broadcastAlert(alert: any): void {
const message: AlertMessage = {
type: 'ALERT',
data: alert,
timestamp: new Date().toISOString()
};
const payload = JSON.stringify(message);
clients.forEach((socket, clientId) => {
try {
if (socket.readyState === 1) { // OPEN
socket.send(payload);
}
} catch (error) {
console.error(`Failed to broadcast to ${clientId}:`, error);
}
});
}
/**
* Broadcast a sensor reading update
*/
export function broadcastReading(reading: any): void {
const message: AlertMessage = {
type: 'READING',
data: reading,
timestamp: new Date().toISOString()
};
const payload = JSON.stringify(message);
clients.forEach((socket, clientId) => {
try {
if (socket.readyState === 1) {
socket.send(payload);
}
} catch (error) {
console.error(`Failed to broadcast reading to ${clientId}:`, error);
}
});
}
/**
* Get count of connected clients
*/
export function getConnectedClientCount(): number {
return clients.size;
}