changemaker.lite/api/src/modules/media/routes/chat-stream.routes.ts

130 lines
3.2 KiB
TypeScript

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;
}
}