Files
drinkmarble/app/api/rooms/[roomId]/events/route.ts

86 lines
2.3 KiB
TypeScript

import {
cancelScheduledDisconnectRoomChat,
scheduleDisconnectRoomChat,
} from '@/lib/chzzk/chat-session';
import { assertChannelRoomAccess } from '@/lib/auth/room-access';
import {
addRoomViewer,
getRoom,
removeRoomViewer,
subscribeRoom,
toPublicRoom,
type PublicRoomState,
} from '@/lib/rooms/store';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
type Params = { params: Promise<{ roomId: string }> };
export async function GET(request: Request, { params }: Params) {
const { roomId } = await params;
const room = getRoom(roomId);
if (!room) {
return new Response(JSON.stringify({ error: '방을 찾을 수 없습니다.' }), {
status: 404,
headers: { 'Content-Type': 'application/json' },
});
}
const access = await assertChannelRoomAccess(room, request);
if (!access.ok) return access.response;
const clientId =
new URL(request.url).searchParams.get('clientId')?.trim() ||
crypto.randomUUID();
const encoder = new TextEncoder();
let unsubscribe: (() => void) | null = null;
let heartbeat: ReturnType<typeof setInterval> | null = null;
let presenceHeld = false;
const release = () => {
if (!presenceHeld) return;
presenceHeld = false;
const { emptied } = removeRoomViewer(roomId, clientId);
if (emptied) {
scheduleDisconnectRoomChat(roomId);
}
};
const stream = new ReadableStream({
start(controller) {
cancelScheduledDisconnectRoomChat(roomId);
addRoomViewer(roomId, clientId);
presenceHeld = true;
const send = (event: string, data: unknown) => {
controller.enqueue(
encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`),
);
};
send('room', toPublicRoom(room));
unsubscribe = subscribeRoom(roomId, (state: PublicRoomState) => {
send('room', state);
});
heartbeat = setInterval(() => {
controller.enqueue(encoder.encode(`: ping\n\n`));
}, 15000);
},
cancel() {
if (unsubscribe) unsubscribe();
if (heartbeat) clearInterval(heartbeat);
release();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream; charset=utf-8',
'Cache-Control': 'no-cache, no-transform',
Connection: 'keep-alive',
},
});
}