import type { NextRequest } from "next/server";
import { redisClient } from "@/repo/RedisClient";
import { FlagsService } from "@/services/FlagsService";
const subscriber = redisClient.createSubscriber();
// Map: topic -> Set<{controller, connectionId}> (still pod-local)
const connections = new Map<
string,
Set<{ controller: ReadableStreamDefaultController; connectionId: string }>
>();
function findConnectionByController(
controller: ReadableStreamDefaultController,
) {
for (const [topic, connectionSet] of connections) {
for (const connection of connectionSet) {
if (connection.controller === controller) {
return { connection, topic };
}
}
}
return null;
}
subscriber.subscribe("sse-broadcast");
subscriber.on("message", (channel, message) => {
if (channel === "sse-broadcast") {
try {
const event = JSON.parse(message);
broadcastToLocalConnections(event);
} catch (error) {
console.error("Error processing Redis message:", error);
}
}
});
// biome-ignore lint/suspicious/noExplicitAny: SSE data can contain anything
function broadcastToLocalConnections(event: any) {
const {
topic,
broadcastToAll,
broadcastToOtherUsers,
excludeConnectionId,
targetConnectionId,
} = event;
const eventString = `data: ${JSON.stringify(event)}\n\n`;
const encoder = new TextEncoder();
const eventData = encoder.encode(eventString);
const connectionsToRemove: Array<{
connection: {
controller: ReadableStreamDefaultController;
connectionId: string;
};
topic: string;
}> = [];
if (targetConnectionId) {
// Send to specific connection
connections.forEach((connectionSet, currentTopic) => {
if (currentTopic === topic || topic === "*") {
connectionSet.forEach((connection) => {
if (connection.connectionId === targetConnectionId) {
try {
connection.controller.enqueue(eventData);
} catch {
connectionsToRemove.push({ connection, topic: currentTopic });
}
}
});
}
});
} else if (broadcastToOtherUsers && excludeConnectionId) {
// Broadcast to all connections except the one with excludeConnectionId
connections.forEach((connectionSet, currentTopic) => {
if (currentTopic === topic || topic === "*") {
connectionSet.forEach((connection) => {
if (connection.connectionId !== excludeConnectionId) {
try {
connection.controller.enqueue(eventData);
} catch {
connectionsToRemove.push({ connection, topic: currentTopic });
}
}
});
}
});
} else if (broadcastToAll) {
// Broadcast to all connections in the topic
connections.forEach((connectionSet, currentTopic) => {
if (currentTopic === topic || topic === "*") {
connectionSet.forEach((connection) => {
try {
connection.controller.enqueue(eventData);
} catch {
connectionsToRemove.push({ connection, topic: currentTopic });
}
});
}
});
} else {
// Broadcast to specific topic connections
const connectionSet = connections.get(topic);
if (connectionSet) {
connectionSet.forEach((connection) => {
try {
connection.controller.enqueue(eventData);
} catch {
connectionsToRemove.push({ connection, topic });
}
});
}
}
// Clean up dead connections
cleanupConnections(connectionsToRemove);
}
function cleanupConnections(
connectionsToRemove: Array<{
connection: {
controller: ReadableStreamDefaultController;
connectionId: string;
};
topic: string;
}>,
) {
connectionsToRemove.forEach(({ connection, topic: topicToClean }) => {
const connectionSet = connections.get(topicToClean);
if (connectionSet) {
connectionSet.delete(connection);
if (connectionSet.size === 0) {
connections.delete(topicToClean);
}
}
});
}
export async function GET(request: NextRequest) {
// Check if SSE feature is enabled
const isEnabled = await FlagsService.isFeatureEnabled("enable_sse");
if (!isEnabled) {
return new Response(
JSON.stringify({ error: "SSE is currently disabled" }),
{
status: 418, // I'm a teapot - indicates SSE feature is disabled
headers: { "Content-Type": "application/json" },
},
);
}
const { searchParams } = new URL(request.url);
const topic = searchParams.get("topic") || "*";
const stream = new ReadableStream({
start(controller) {
if (!connections.has(topic)) {
connections.set(topic, new Set());
}
const connectionSet = connections.get(topic);
const connectionId = crypto.randomUUID();
const connection = { controller, connectionId };
connectionSet?.add(connection);
const connectionEvent = {
id: crypto.randomUUID(),
topic: "connection",
data: {
message: "Connected to SSE",
connectionId,
},
timestamp: Date.now(),
};
const eventString = `data: ${JSON.stringify(connectionEvent)}\n\n`;
const encoder = new TextEncoder();
const eventData = encoder.encode(eventString);
try {
controller.enqueue(eventData);
} catch (err) {
console.error("Failed to send connection event:", err);
}
},
cancel(controller) {
// Find and remove the connection
const found = findConnectionByController(controller);
if (found) {
const connectionSet = connections.get(found.topic);
if (connectionSet) {
connectionSet.delete(found.connection);
if (connectionSet.size === 0) {
connections.delete(found.topic);
}
}
}
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}
export async function POST(request: NextRequest) {
try {
// Check if SSE feature is enabled
const isEnabled = await FlagsService.isFeatureEnabled("enable_sse");
if (!isEnabled) {
return Response.json(
{ error: "SSE is currently disabled" },
{ status: 418 }, // I'm a teapot - indicates SSE feature is disabled
);
}
const {
topic,
data,
targetConnectionId,
excludeConnectionId,
broadcastToAll,
broadcastToOtherUsers,
} = await request.json();
if (!topic) {
return Response.json({ error: "topic is required" }, { status: 400 });
}
const event = {
id: crypto.randomUUID(),
topic,
data: data || {},
timestamp: Date.now(),
targetConnectionId,
excludeConnectionId,
broadcastToAll,
broadcastToOtherUsers,
};
await redisClient.publish("sse-broadcast", JSON.stringify(event));
return Response.json({
success: true,
eventId: event.id,
message: "Event published to all connections",
});
} catch (error) {
console.error("Error processing SSE event:", error);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
}