1import type { RequestHandler } from './$types';
2import { sseEmitter } from '$lib/sseEmitter';
3import db from '$lib/db/db.server';
4import { users } from '$lib/db/schema';
5import { eq, and, lt } from 'drizzle-orm';
6
7console.debug('SSE connection initiated');
8
9// Constants for timeout checks
10const ONLINE_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
11const CHECK_INTERVAL_MS = 60 * 1000; // Check every minute
12
13// Initialize periodic check for timed out users
14const timeoutInterval = setInterval(async () => {
15 const timeoutThreshold = Date.now() - ONLINE_TIMEOUT_MS;
16 console.debug('Checking for timed out users, threshold:', new Date(timeoutThreshold));
17
18 try {
19 // Find and update users who are marked as online but haven't been seen recently
20 const now = Date.now();
21 const result = await db.update(users)
22 .set({
23 status: 'offline',
24 lastSeen: now
25 })
26 .where(
27 and(
28 eq(users.status, 'online'),
29 lt(users.lastSeen, timeoutThreshold)
30 )
31 )
32 .returning({ id: users.id });
33
34 // Broadcast status updates for timed out users
35 result.forEach(user => {
36 sseEmitter.emit('sse', {
37 type: 'userStatusUpdate',
38 data: {
39 userId: user.id,
40 status: 'offline',
41 lastSeen: now
42 }
43 });
44 console.debug('User timed out and marked offline:', user.id);
45 });
46 } catch (error) {
47 console.debug('Error during timeout check:', error);
48 }
49}, CHECK_INTERVAL_MS);
50
51// Ensure the interval is cleared if the module is reloaded
52process.on('beforeExit', () => {
53 if (timeoutInterval) clearInterval(timeoutInterval);
54});
55
56export const GET: RequestHandler = async ({ locals }) => {
57 if (!locals.user) {
58 return new Response('Unauthorized', { status: 401 });
59 }
60
61 const userId = locals.user.id;
62 const encoder = new TextEncoder();
63 let keepAliveInterval: ReturnType<typeof setInterval>;
64 let onSSE: ((event: { type: string; data: unknown }) => void) | undefined;
65
66 // Update user status to online and lastSeen timestamp when establishing connection
67 const now = Date.now();
68 await db.update(users)
69 .set({
70 status: 'online',
71 lastSeen: now
72 })
73 .where(eq(users.id, userId));
74
75 // Broadcast the status update
76 sseEmitter.emit('sse', {
77 type: 'userStatusUpdate',
78 data: {
79 userId,
80 status: 'online',
81 lastSeen: now
82 }
83 });
84
85 const stream = new ReadableStream({
86 start(controller) {
87 const sendEvent = (event: { type: string; data: unknown }) => {
88 try {
89 const payload = `event: ${event.type}\n` +
90 `data: ${JSON.stringify(event.data)}\n\n`;
91 controller.enqueue(encoder.encode(payload));
92 console.debug('Sent SSE event:', event);
93 } catch (err) {
94 console.debug('Error sending SSE event:', err);
95 }
96 };
97
98 onSSE = (event: { type: string; data: unknown }) => {
99 sendEvent(event);
100 };
101
102 sseEmitter.addListener('sse', onSSE);
103 console.debug('Added SSE listener');
104
105 // Send initial connection message
106 controller.enqueue(encoder.encode('data: Connected\n\n'));
107
108 // Keep connection alive and update lastSeen: send a comment every 20 seconds
109 keepAliveInterval = setInterval(async () => {
110 controller.enqueue(encoder.encode(':\n\n'));
111 // Update lastSeen timestamp on each keepalive
112 await db.update(users)
113 .set({ lastSeen: Date.now() })
114 .where(eq(users.id, userId));
115 }, 20000);
116 },
117 async cancel(reason) {
118 if (keepAliveInterval) clearInterval(keepAliveInterval);
119 if (onSSE) sseEmitter.removeListener('sse', onSSE);
120 console.debug('SSE connection cancelled:', reason);
121
122 // Update user status to offline in the database
123 const now = Date.now();
124 await db.update(users)
125 .set({ status: 'offline', lastSeen: now })
126 .where(eq(users.id, userId));
127
128 // Broadcast the status update
129 sseEmitter.emit('sse', {
130 type: 'userStatusUpdate',
131 data: {
132 userId,
133 status: 'offline',
134 lastSeen: now
135 }
136 });
137 }
138 });
139
140 return new Response(stream, {
141 headers: {
142 'Content-Type': 'text/event-stream',
143 'Cache-Control': 'no-cache',
144 'Connection': 'keep-alive'
145 }
146 });
147};