import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; import Redis from 'ioredis'; import { env } from '../../../config/env.js'; /** * SSE Chat Stream Routes * * Provides real-time updates for video chat using Server-Sent Events (SSE) * and Redis pub/sub for broadcasting. */ // Redis client for pub/sub let redisPubClient: Redis | null = null; function getRedisClient(): Redis { if (!redisPubClient) { redisPubClient = new Redis(env.REDIS_URL); } return redisPubClient; } export async function chatStreamRoutes(fastify: FastifyInstance) { /** * GET /public/:id/stream * SSE endpoint for real-time chat updates */ fastify.get( '/public/:id/chat-stream', async ( request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply ) => { const videoId = parseInt(request.params.id, 10); if (isNaN(videoId)) { return reply.code(400).send({ message: 'Invalid video ID' }); } // Set SSE headers reply.raw.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no', // Disable nginx buffering }); // Create dedicated Redis subscriber for this connection const subscriber = new Redis(env.REDIS_URL); const channel = `video:${videoId}:chat`; // Set up message handler subscriber.on('message', (receivedChannel, message) => { if (receivedChannel === channel) { // Send message to client via SSE reply.raw.write(`data: ${message}\n\n`); } }); // Subscribe to channel await subscriber.subscribe(channel); // Send initial connection confirmation reply.raw.write(`data: ${JSON.stringify({ type: 'connected', videoId })}\n\n`); // Keep-alive ping every 30 seconds const pingInterval = setInterval(() => { reply.raw.write(': ping\n\n'); }, 30000); // Cleanup on disconnect request.raw.on('close', async () => { clearInterval(pingInterval); await subscriber.unsubscribe(channel); subscriber.disconnect(); }); } ); } /** * Broadcast a new comment to all connected clients for a video */ export function broadcastCommentToVideo( videoId: number, comment: any ): void { try { const redis = getRedisClient(); const channel = `video:${videoId}:chat`; const message = JSON.stringify({ type: 'new_comment', comment, }); redis.publish(channel, message); } catch (error) { console.error('Failed to broadcast comment:', error); } } /** * Broadcast a new reaction to all connected clients for a video */ export function broadcastReactionToVideo( videoId: number, reaction: any ): void { try { const redis = getRedisClient(); const channel = `video:${videoId}:chat`; const message = JSON.stringify({ type: 'new_reaction', reaction, }); redis.publish(channel, message); } catch (error) { console.error('Failed to broadcast reaction:', error); } } /** * Cleanup Redis connections on shutdown */ export function closeRedisConnections(): void { if (redisPubClient) { redisPubClient.disconnect(); redisPubClient = null; } }