import { db } from '~~/server/lib/db'; import { topicEvents } from '~~/server/utils/events'; import { createTopicSnapshotStream } from '~~/server/utils/topic-snapshot-stream'; export default defineEventHandler(async (event) => { await protectRoute(event); const topicId = getRouterParam(event, 'topicId')!; const userId = event.context.user!.id as string; const isSSE = getHeader(event, 'accept')?.includes('text/event-stream') ?? false; const readTopic = () => db.query.topics.findFirst({ where: { id: topicId, userId }, with: { messages: { orderBy: { createdAt: 'asc' }, with: { parts: { orderBy: { createdAt: 'asc' }, with: { toolCall: true }, }, attachments: { with: { file: true } }, generation: true, }, }, }, }); if (!isSSE) { const topic = await readTopic(); if (!topic) { setResponseStatus(event, 404); return { ok: false, error: 'Topic not found' }; } topicEvents.applyLiveText(topicId, topic.messages); return topic; } // Authorize before subscribing; no topic data is exposed before this check. const ownedTopic = await db.query.topics.findFirst({ where: { id: topicId, userId }, columns: { id: true }, }); if (!ownedTopic) { setResponseStatus(event, 404); return { ok: false, error: 'Topic not found' }; } const subscription = createTopicSnapshotStream(topicId, getQuery(event).snapshot); event.node.res.once('close', subscription.close); const stream = await subscription.open(readTopic); if (!stream.ok) { setResponseStatus(event, stream.error === 'read-failed' ? 500 : 404); return { ok: false, error: stream.error }; } setHeader(event, 'Content-Type', 'text/event-stream'); setHeader(event, 'Cache-Control', 'no-cache'); setHeader(event, 'X-Accel-Buffering', 'no'); return sendStream(event, stream.data); });