import { NextRequest, NextResponse } from 'next/server'; import { spawn } from 'child_process'; // Prevent static generation of this route export const dynamic = 'force-dynamic'; export const runtime = 'nodejs'; // Store connected clients const clients = new Set(); // Handle CORS preflight requests export async function OPTIONS(request: NextRequest) { return new NextResponse(null, { status: 200, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, OPTIONS', 'Access-Control-Allow-Headers': 'Cache-Control, Content-Type', 'Access-Control-Max-Age': '86400', }, }); } export async function GET(request: NextRequest) { try { const stream = new ReadableStream({ start(controller) { // Add this client to the set clients.add(controller); // Send initial connection message try { controller.enqueue(`data: ${JSON.stringify({ type: 'connected', message: 'SSE connection established' })}\n\n`); } catch (error) { console.error('Error sending initial message:', error); clients.delete(controller); return; } // Set up Rust file watcher if not already set up if (clients.size === 1) { try { const rustWatcher = spawn( process.cwd() + '/markdown_backend/target/release/markdown_backend', ['watch'], { stdio: ['pipe', 'pipe', 'pipe'] } ); rustWatcher.stdout.on('data', (data) => { const message = data.toString().trim(); console.log('Rust watcher output:', message); if (message.includes('Posts directory changed!')) { // Notify all connected clients about the update const updateMessage = JSON.stringify({ type: 'update', timestamp: new Date().toISOString() }); const clientsToRemove: ReadableStreamDefaultController[] = []; clients.forEach(client => { try { client.enqueue(`data: ${updateMessage}\n\n`); } catch (error) { // Mark client for removal clientsToRemove.push(client); } }); // Remove disconnected clients clientsToRemove.forEach(client => { clients.delete(client); }); // Stop watching if no clients are connected if (clients.size === 0) { console.log('No clients connected, stopping watcher'); rustWatcher.kill(); } } }); rustWatcher.stderr.on('data', (data) => { const errorMessage = data.toString().trim(); console.error('Rust watcher error:', errorMessage); // Don't treat RecvError as a real error - it's expected when the process is terminated if (!errorMessage.includes('RecvError')) { // Send error to clients const errorData = JSON.stringify({ type: 'error', message: errorMessage }); const clientsToRemove: ReadableStreamDefaultController[] = []; clients.forEach(client => { try { client.enqueue(`data: ${errorData}\n\n`); } catch (error) { clientsToRemove.push(client); } }); clientsToRemove.forEach(client => { clients.delete(client); }); } }); rustWatcher.on('error', (error) => { console.error('Rust watcher spawn error:', error); }); rustWatcher.on('close', (code) => { console.log('Rust watcher closed with code:', code); // Only restart if we still have clients if (clients.size > 0) { console.log('Restarting watcher due to unexpected close'); // The watcher will be restarted when the next client connects } }); // Store the watcher process for cleanup (controller as any).rustWatcher = rustWatcher; } catch (error) { console.error('Error setting up Rust file watcher:', error); } } // Clean up when client disconnects request.signal.addEventListener('abort', () => { clients.delete(controller); // Stop watching if no clients are connected if (clients.size === 0) { const rustWatcher = (controller as any).rustWatcher; if (rustWatcher) { console.log('Last client disconnected, stopping watcher'); rustWatcher.kill(); } } }); }, cancel() { // Handle stream cancellation - this is called when the stream is cancelled // We can't access the specific controller here, so we'll handle cleanup in the abort event } }); return new NextResponse(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Cache-Control, Content-Type', 'X-Accel-Buffering': 'no', // Disable nginx buffering }, }); } catch (error) { console.error('SSE route error:', error); return new NextResponse( JSON.stringify({ error: 'Internal server error' }), { status: 500, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', }, } ); } }