Connect/disconnect messages were flooding production logs. Changed to log.debug level for cleaner output.
117 lines
3.1 KiB
TypeScript
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;
|
|
}
|