My-Weekly-ToDo-List/src/app/api/events/stream/route.ts
mARTin 876bd1047b feat: real-time sync across devices via Server-Sent Events
Adds SSE-based push notifications so changes made on one device
appear instantly on all other connected devices for the same user.

- SSE notification hub (src/lib/sse.ts) with per-user client tracking
- Stream endpoint (/api/events/stream) with 30s heartbeat
- Task and list API routes notify connected clients on mutations
- Client auto-connects with 5s reconnect on connection loss
- Existing 2-minute polling sync remains as fallback

v1.53.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 07:44:00 +01:00

51 lines
1.7 KiB
TypeScript

import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { addClient, removeClient } from '@/lib/sse';
export const dynamic = 'force-dynamic';
export const runtime = 'nodejs';
export async function GET() {
const session = await getServerSession(authOptions);
const userId = (session?.user as any)?.id;
if (!userId) {
return new Response('Unauthorized', { status: 401 });
}
const encoder = new TextEncoder();
let sseClient: ReturnType<typeof addClient> | null = null;
let heartbeatInterval: NodeJS.Timeout | null = null;
const stream = new ReadableStream({
start(controller) {
sseClient = addClient(userId, controller);
// Send initial connected event
controller.enqueue(encoder.encode(`event: connected\ndata: ${JSON.stringify({ clientId: sseClient.id })}\n\n`));
// Heartbeat every 30s to keep connection alive
heartbeatInterval = setInterval(() => {
try {
controller.enqueue(encoder.encode(`: heartbeat\n\n`));
} catch {
if (heartbeatInterval) clearInterval(heartbeatInterval);
if (sseClient) removeClient(sseClient);
}
}, 30000);
},
cancel() {
if (heartbeatInterval) clearInterval(heartbeatInterval);
if (sseClient) removeClient(sseClient);
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no',
},
});
}