feat: Improve server startup and user status management
- Add function to set all users offline on server initialization - Enhance SSE timeout check with more robust interval management - Improve error handling and logging for server startup processes - Add process signal handlers for graceful interval cleanup - Refactor user status update logic with more precise timestamp handling
- commit
- cb06b355ecf185e21fdca6ddc4189990552c1e44
- author
- Kit · · 2025-02-14 14:13 UTC
- parent
- d242971
- signature
- SSH Unverified
SHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6TkThe key is unknown
3 files · +137 -131
M src/hooks.server.ts +22 -3
| @@ -2,13 +2,32 @@ import type { Handle } from "@sveltejs/kit"; | |||
| 2 | 2 | import { validateSessionToken } from "$lib/api/session.server"; | |
| 3 | 3 | import { setSessionTokenCookie, deleteSessionTokenCookie } from "$lib/api/session.cookie"; | |
| 4 | 4 | import { initializeDatabase } from "$lib/db/db.server"; | |
| 5 | + | import db from "$lib/db/db.server"; | |
| 5 | 6 | import { handleRateLimit } from "$lib/api/rate-limiter"; | |
| 7 | + | import { users } from "$lib/db/schema"; | |
| 8 | + | ||
| 9 | + | async function setAllUsersOffline() { | |
| 10 | + | try { | |
| 11 | + | console.log("Setting all users to offline status..."); | |
| 12 | + | await db.update(users) | |
| 13 | + | .set({ | |
| 14 | + | status: "offline" | |
| 15 | + | }); | |
| 16 | + | console.log("Successfully set all users to offline status"); | |
| 17 | + | } catch (error) { | |
| 18 | + | console.error("Failed to set users offline:", error); | |
| 19 | + | } | |
| 20 | + | } | |
| 6 | 21 | ||
| 7 | 22 | // Initialize database when the server starts | |
| 8 | 23 | console.debug("Initializing database on server start..."); | |
| 9 | - | initializeDatabase().catch(error => { | |
| 10 | - | console.error('Failed to initialize database:', error); | |
| 11 | - | }); | |
| 24 | + | initializeDatabase() | |
| 25 | + | .then(async () => { | |
| 26 | + | await setAllUsersOffline(); | |
| 27 | + | }) | |
| 28 | + | .catch(error => { | |
| 29 | + | console.error('Failed to initialize database:', error); | |
| 30 | + | }); | |
| 12 | 31 | ||
| 13 | 32 | export const handle: Handle = async ({ event, resolve }) => { | |
| 14 | 33 | console.debug("Handling request:", event.url.pathname); | |
| @@ -2,13 +2,32 @@ import type { Handle } from "@sveltejs/kit"; | |||||
| 2 | import { validateSessionToken } from "$lib/api/session.server"; | 2 | import { validateSessionToken } from "$lib/api/session.server"; | ||
| 3 | import { setSessionTokenCookie, deleteSessionTokenCookie } from "$lib/api/session.cookie"; | 3 | import { setSessionTokenCookie, deleteSessionTokenCookie } from "$lib/api/session.cookie"; | ||
| 4 | import { initializeDatabase } from "$lib/db/db.server"; | 4 | import { initializeDatabase } from "$lib/db/db.server"; | ||
| 5 | + | import db from "$lib/db/db.server"; | |||
| 5 | import { handleRateLimit } from "$lib/api/rate-limiter"; | 6 | import { handleRateLimit } from "$lib/api/rate-limiter"; | ||
| 7 | + | import { users } from "$lib/db/schema"; | |||
| 8 | + | ||||
| 9 | + | async function setAllUsersOffline() { | |||
| 10 | + | try { | |||
| 11 | + | console.log("Setting all users to offline status..."); | |||
| 12 | + | await db.update(users) | |||
| 13 | + | .set({ | |||
| 14 | + | status: "offline" | |||
| 15 | + | }); | |||
| 16 | + | console.log("Successfully set all users to offline status"); | |||
| 17 | + | } catch (error) { | |||
| 18 | + | console.error("Failed to set users offline:", error); | |||
| 19 | + | } | |||
| 20 | + | } | |||
| 6 | 21 | ||||
| 7 | // Initialize database when the server starts | 22 | // Initialize database when the server starts | ||
| 8 | console.debug("Initializing database on server start..."); | 23 | console.debug("Initializing database on server start..."); | ||
| 9 | - | initializeDatabase().catch(error => { | 24 | + | initializeDatabase() |
| 10 | - | console.error('Failed to initialize database:', error); | 25 | + | .then(async () => { |
| 11 | - | }); | 26 | + | await setAllUsersOffline(); |
| 27 | + | }) | |||
| 28 | + | .catch(error => { | |||
| 29 | + | console.error('Failed to initialize database:', error); | |||
| 30 | + | }); | |||
| 12 | 31 | ||||
| 13 | export const handle: Handle = async ({ event, resolve }) => { | 32 | export const handle: Handle = async ({ event, resolve }) => { | ||
| 14 | console.debug("Handling request:", event.url.pathname); | 33 | console.debug("Handling request:", event.url.pathname); | ||
M src/lib/states/chat.svelte.ts +43 -78
| @@ -155,7 +155,10 @@ class ChatState { | |||
| 155 | 155 | } | |
| 156 | 156 | ||
| 157 | 157 | updateUserStatus(userId: string, status: User['status'], lastSeen?: number) { | |
| 158 | - | console.debug('Updating user status with:', { userId, status, lastSeen, | |
| 158 | + | console.debug('Updating user status with:', { | |
| 159 | + | userId, | |
| 160 | + | status, | |
| 161 | + | lastSeen, | |
| 159 | 162 | currentCache: this.userCache[userId], | |
| 160 | 163 | currentUsers: this.users.find(u => u.id === userId) | |
| 161 | 164 | }); | |
| @@ -171,11 +174,13 @@ class ChatState { | |||
| 171 | 174 | } | |
| 172 | 175 | } | |
| 173 | 176 | ||
| 177 | + | const now = Date.now(); | |
| 174 | 178 | if (user) { | |
| 175 | 179 | const updatedUser: User = { | |
| 176 | 180 | ...user, | |
| 177 | 181 | status, | |
| 178 | - | lastSeen: status === 'offline' ? (lastSeen ?? Date.now()) : user.lastSeen | |
| 182 | + | // Update lastSeen based on status | |
| 183 | + | lastSeen: lastSeen ?? (status === 'offline' ? now : user.lastSeen) | |
| 179 | 184 | }; | |
| 180 | 185 | ||
| 181 | 186 | // Update cache first | |
| @@ -197,6 +202,7 @@ class ChatState { | |||
| 197 | 202 | console.debug('User status updated:', { | |
| 198 | 203 | userId, | |
| 199 | 204 | newStatus: status, | |
| 205 | + | newLastSeen: new Date(updatedUser.lastSeen || now).toISOString(), | |
| 200 | 206 | updatedInCache: this.userCache[userId].status === status, | |
| 201 | 207 | updatedInList: this.users.find(u => u.id === userId)?.status === status | |
| 202 | 208 | }); | |
| @@ -210,7 +216,7 @@ class ChatState { | |||
| 210 | 216 | const newUser: User = { | |
| 211 | 217 | ...data.user, | |
| 212 | 218 | status, | |
| 213 | - | lastSeen: status === 'offline' ? (lastSeen ?? Date.now()) : data.user.lastSeen | |
| 219 | + | lastSeen: lastSeen ?? (status === 'offline' ? now : data.user.lastSeen) | |
| 214 | 220 | }; | |
| 215 | 221 | // Update both cache and users array | |
| 216 | 222 | this.userCache[userId] = newUser; | |
| @@ -219,6 +225,7 @@ class ChatState { | |||
| 219 | 225 | console.debug('User fetched and status updated:', { | |
| 220 | 226 | userId, | |
| 221 | 227 | status: newUser.status, | |
| 228 | + | lastSeen: new Date(newUser.lastSeen || now).toISOString(), | |
| 222 | 229 | addedToCache: !!this.userCache[userId], | |
| 223 | 230 | addedToList: this.users.some(u => u.id === userId) | |
| 224 | 231 | }); | |
| @@ -375,32 +382,39 @@ class ChatState { | |||
| 375 | 382 | this.eventSource = null; | |
| 376 | 383 | } | |
| 377 | 384 | ||
| 378 | - | if (this.isReconnecting) { | |
| 379 | - | console.debug('Already attempting to reconnect, skipping new connection attempt'); | |
| 380 | - | return; | |
| 385 | + | if (this.connectionTimeout) { | |
| 386 | + | clearTimeout(this.connectionTimeout); | |
| 387 | + | this.connectionTimeout = null; | |
| 381 | 388 | } | |
| 382 | 389 | ||
| 383 | - | this.isReconnecting = true; | |
| 384 | - | console.debug('Connecting to SSE...'); | |
| 385 | - | ||
| 386 | 390 | try { | |
| 387 | - | // Connect to the unified SSE endpoint | |
| 391 | + | console.debug('Connecting to SSE...'); | |
| 388 | 392 | this.eventSource = new EventSource('/api/sse', { withCredentials: true }); | |
| 393 | + | this.isReconnecting = false; | |
| 394 | + | ||
| 395 | + | this.eventSource.onopen = () => { | |
| 396 | + | console.debug('SSE connection established'); | |
| 397 | + | this.reconnectAttempts = 0; | |
| 398 | + | this.reconnectDelay = 1000; | |
| 399 | + | this.sseError = null; | |
| 400 | + | this.sseRetryAfter = null; | |
| 401 | + | }; | |
| 389 | 402 | ||
| 390 | - | // Default message event for handshake | |
| 391 | - | this.eventSource.onmessage = (event) => { | |
| 392 | - | if (event.data === 'Connected') { | |
| 393 | - | console.debug('SSE connection established'); | |
| 394 | - | this.reconnectAttempts = 0; | |
| 395 | - | this.reconnectDelay = 1000; | |
| 396 | - | this.isReconnecting = false; | |
| 397 | - | this.sseError = null; | |
| 398 | - | this.sseRetryAfter = null; | |
| 399 | - | } else { | |
| 400 | - | console.debug('Received unexpected default message:', event.data); | |
| 403 | + | this.eventSource.onerror = (error) => { | |
| 404 | + | console.debug('SSE connection error:', error); | |
| 405 | + | if (this.eventSource?.readyState === EventSource.CLOSED) { | |
| 406 | + | this.handleDisconnection(); | |
| 401 | 407 | } | |
| 402 | 408 | }; | |
| 403 | 409 | ||
| 410 | + | // Set a connection timeout | |
| 411 | + | this.connectionTimeout = setTimeout(() => { | |
| 412 | + | if (this.eventSource?.readyState !== EventSource.OPEN) { | |
| 413 | + | console.debug('SSE connection timeout'); | |
| 414 | + | this.handleDisconnection(); | |
| 415 | + | } | |
| 416 | + | }, 10000); // 10 second timeout | |
| 417 | + | ||
| 404 | 418 | // Listen for chat messages | |
| 405 | 419 | this.eventSource.addEventListener('chatMessage', async (event: MessageEvent) => { | |
| 406 | 420 | try { | |
| @@ -436,83 +450,34 @@ class ChatState { | |||
| 436 | 450 | console.debug('Error processing userStatusUpdate event:', error); | |
| 437 | 451 | } | |
| 438 | 452 | }); | |
| 439 | - | ||
| 440 | - | this.eventSource.onerror = (event) => { | |
| 441 | - | console.debug('SSE encountered an error:', event); | |
| 442 | - | this.handleSSEError(); | |
| 443 | - | }; | |
| 444 | 453 | } catch (error) { | |
| 445 | - | console.debug('Error setting up SSE connection:', error); | |
| 446 | - | this.isReconnecting = false; | |
| 447 | - | this.handleSSEError(); | |
| 454 | + | console.error('Error setting up SSE connection:', error); | |
| 455 | + | this.handleDisconnection(); | |
| 448 | 456 | } | |
| 449 | 457 | } | |
| 450 | 458 | ||
| 451 | - | private async handleSSEError() { | |
| 459 | + | private handleDisconnection() { | |
| 460 | + | if (this.isReconnecting) return; | |
| 461 | + | ||
| 462 | + | this.isReconnecting = true; | |
| 452 | 463 | if (this.eventSource) { | |
| 453 | 464 | this.eventSource.close(); | |
| 454 | 465 | this.eventSource = null; | |
| 455 | 466 | } | |
| 456 | 467 | ||
| 457 | - | // If we're already reconnecting, don't start another reconnection attempt | |
| 458 | - | if (this.isReconnecting) { | |
| 459 | - | console.debug('Already attempting to reconnect, skipping error handler'); | |
| 460 | - | return; | |
| 461 | - | } | |
| 462 | - | ||
| 463 | - | // Check if the error was due to rate limiting | |
| 464 | - | try { | |
| 465 | - | const response = await fetch('/api/sse'); | |
| 466 | - | if (response.status === 429) { | |
| 467 | - | const data = await response.json(); | |
| 468 | - | this.sseError = data.error; | |
| 469 | - | this.sseRetryAfter = data.retryAfter; | |
| 470 | - | this.isReconnecting = false; | |
| 471 | - | ||
| 472 | - | // Set up automatic retry after the rate limit expires | |
| 473 | - | setTimeout(() => { | |
| 474 | - | this.sseError = null; | |
| 475 | - | this.sseRetryAfter = null; | |
| 476 | - | this.reconnectSSE(); | |
| 477 | - | }, (data.retryAfter + 1) * 1000); // Add 1 second buffer | |
| 478 | - | ||
| 479 | - | return; | |
| 480 | - | } | |
| 481 | - | } catch (error) { | |
| 482 | - | console.debug('Error checking SSE status:', error); | |
| 483 | - | } | |
| 484 | - | ||
| 485 | - | // Handle other types of errors with exponential backoff | |
| 486 | 468 | if (this.reconnectAttempts < this.maxReconnectAttempts) { | |
| 487 | - | this.sseError = `Connection lost. Attempting to reconnect (${this.reconnectAttempts + 1}/${this.maxReconnectAttempts})...`; | |
| 488 | 469 | console.debug(`Attempting to reconnect (${this.reconnectAttempts + 1}/${this.maxReconnectAttempts})...`); | |
| 489 | - | ||
| 490 | 470 | setTimeout(() => { | |
| 491 | 471 | this.reconnectAttempts++; | |
| 492 | - | this.reconnectDelay *= 2; // Exponential backoff | |
| 493 | - | this.isReconnecting = false; | |
| 472 | + | this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000); // Max 30 second delay | |
| 494 | 473 | this.connectSSE(); | |
| 495 | 474 | }, this.reconnectDelay); | |
| 496 | 475 | } else { | |
| 497 | - | this.isReconnecting = false; | |
| 498 | - | this.sseError = 'Unable to connect to chat. Please refresh the page to try again.'; | |
| 499 | 476 | console.debug('Max reconnection attempts reached'); | |
| 477 | + | this.sseError = 'Connection lost. Please refresh the page.'; | |
| 500 | 478 | } | |
| 501 | 479 | } | |
| 502 | 480 | ||
| 503 | - | private reconnectSSE() { | |
| 504 | - | if (this.isReconnecting) { | |
| 505 | - | console.debug('Already attempting to reconnect, skipping reconnection request'); | |
| 506 | - | return; | |
| 507 | - | } | |
| 508 | - | ||
| 509 | - | this.reconnectAttempts = 0; | |
| 510 | - | this.reconnectDelay = 1000; | |
| 511 | - | this.sseError = null; | |
| 512 | - | this.sseRetryAfter = null; | |
| 513 | - | this.connectSSE(); | |
| 514 | - | } | |
| 515 | - | ||
| 516 | 481 | // Add new method to enrich multiple messages at once | |
| 517 | 482 | enrichMessages(messages: Message[]): EnrichedMessage[] { | |
| 518 | 483 | return messages.map(msg => this.enrichMessageWithUser(msg)); | |
| @@ -155,7 +155,10 @@ class ChatState { | |||||
| 155 | } | 155 | } | ||
| 156 | 156 | ||||
| 157 | updateUserStatus(userId: string, status: User['status'], lastSeen?: number) { | 157 | updateUserStatus(userId: string, status: User['status'], lastSeen?: number) { | ||
| 158 | - | console.debug('Updating user status with:', { userId, status, lastSeen, | 158 | + | console.debug('Updating user status with:', { |
| 159 | + | userId, | |||
| 160 | + | status, | |||
| 161 | + | lastSeen, | |||
| 159 | currentCache: this.userCache[userId], | 162 | currentCache: this.userCache[userId], | ||
| 160 | currentUsers: this.users.find(u => u.id === userId) | 163 | currentUsers: this.users.find(u => u.id === userId) | ||
| 161 | }); | 164 | }); | ||
| @@ -171,11 +174,13 @@ class ChatState { | |||||
| 171 | } | 174 | } | ||
| 172 | } | 175 | } | ||
| 173 | 176 | ||||
| 177 | + | const now = Date.now(); | |||
| 174 | if (user) { | 178 | if (user) { | ||
| 175 | const updatedUser: User = { | 179 | const updatedUser: User = { | ||
| 176 | ...user, | 180 | ...user, | ||
| 177 | status, | 181 | status, | ||
| 178 | - | lastSeen: status === 'offline' ? (lastSeen ?? Date.now()) : user.lastSeen | 182 | + | // Update lastSeen based on status |
| 183 | + | lastSeen: lastSeen ?? (status === 'offline' ? now : user.lastSeen) | |||
| 179 | }; | 184 | }; | ||
| 180 | 185 | ||||
| 181 | // Update cache first | 186 | // Update cache first | ||
| @@ -197,6 +202,7 @@ class ChatState { | |||||
| 197 | console.debug('User status updated:', { | 202 | console.debug('User status updated:', { | ||
| 198 | userId, | 203 | userId, | ||
| 199 | newStatus: status, | 204 | newStatus: status, | ||
| 205 | + | newLastSeen: new Date(updatedUser.lastSeen || now).toISOString(), | |||
| 200 | updatedInCache: this.userCache[userId].status === status, | 206 | updatedInCache: this.userCache[userId].status === status, | ||
| 201 | updatedInList: this.users.find(u => u.id === userId)?.status === status | 207 | updatedInList: this.users.find(u => u.id === userId)?.status === status | ||
| 202 | }); | 208 | }); | ||
| @@ -210,7 +216,7 @@ class ChatState { | |||||
| 210 | const newUser: User = { | 216 | const newUser: User = { | ||
| 211 | ...data.user, | 217 | ...data.user, | ||
| 212 | status, | 218 | status, | ||
| 213 | - | lastSeen: status === 'offline' ? (lastSeen ?? Date.now()) : data.user.lastSeen | 219 | + | lastSeen: lastSeen ?? (status === 'offline' ? now : data.user.lastSeen) |
| 214 | }; | 220 | }; | ||
| 215 | // Update both cache and users array | 221 | // Update both cache and users array | ||
| 216 | this.userCache[userId] = newUser; | 222 | this.userCache[userId] = newUser; | ||
| @@ -219,6 +225,7 @@ class ChatState { | |||||
| 219 | console.debug('User fetched and status updated:', { | 225 | console.debug('User fetched and status updated:', { | ||
| 220 | userId, | 226 | userId, | ||
| 221 | status: newUser.status, | 227 | status: newUser.status, | ||
| 228 | + | lastSeen: new Date(newUser.lastSeen || now).toISOString(), | |||
| 222 | addedToCache: !!this.userCache[userId], | 229 | addedToCache: !!this.userCache[userId], | ||
| 223 | addedToList: this.users.some(u => u.id === userId) | 230 | addedToList: this.users.some(u => u.id === userId) | ||
| 224 | }); | 231 | }); | ||
| @@ -375,32 +382,39 @@ class ChatState { | |||||
| 375 | this.eventSource = null; | 382 | this.eventSource = null; | ||
| 376 | } | 383 | } | ||
| 377 | 384 | ||||
| 378 | - | if (this.isReconnecting) { | 385 | + | if (this.connectionTimeout) { |
| 379 | - | console.debug('Already attempting to reconnect, skipping new connection attempt'); | 386 | + | clearTimeout(this.connectionTimeout); |
| 380 | - | return; | 387 | + | this.connectionTimeout = null; |
| 381 | } | 388 | } | ||
| 382 | 389 | ||||
| 383 | - | this.isReconnecting = true; | |||
| 384 | - | console.debug('Connecting to SSE...'); | |||
| 385 | - | ||||
| 386 | try { | 390 | try { | ||
| 387 | - | // Connect to the unified SSE endpoint | 391 | + | console.debug('Connecting to SSE...'); |
| 388 | this.eventSource = new EventSource('/api/sse', { withCredentials: true }); | 392 | this.eventSource = new EventSource('/api/sse', { withCredentials: true }); | ||
| 393 | + | this.isReconnecting = false; | |||
| 394 | + | ||||
| 395 | + | this.eventSource.onopen = () => { | |||
| 396 | + | console.debug('SSE connection established'); | |||
| 397 | + | this.reconnectAttempts = 0; | |||
| 398 | + | this.reconnectDelay = 1000; | |||
| 399 | + | this.sseError = null; | |||
| 400 | + | this.sseRetryAfter = null; | |||
| 401 | + | }; | |||
| 389 | 402 | ||||
| 390 | - | // Default message event for handshake | 403 | + | this.eventSource.onerror = (error) => { |
| 391 | - | this.eventSource.onmessage = (event) => { | 404 | + | console.debug('SSE connection error:', error); |
| 392 | - | if (event.data === 'Connected') { | 405 | + | if (this.eventSource?.readyState === EventSource.CLOSED) { |
| 393 | - | console.debug('SSE connection established'); | 406 | + | this.handleDisconnection(); |
| 394 | - | this.reconnectAttempts = 0; | |||
| 395 | - | this.reconnectDelay = 1000; | |||
| 396 | - | this.isReconnecting = false; | |||
| 397 | - | this.sseError = null; | |||
| 398 | - | this.sseRetryAfter = null; | |||
| 399 | - | } else { | |||
| 400 | - | console.debug('Received unexpected default message:', event.data); | |||
| 401 | } | 407 | } | ||
| 402 | }; | 408 | }; | ||
| 403 | 409 | ||||
| 410 | + | // Set a connection timeout | |||
| 411 | + | this.connectionTimeout = setTimeout(() => { | |||
| 412 | + | if (this.eventSource?.readyState !== EventSource.OPEN) { | |||
| 413 | + | console.debug('SSE connection timeout'); | |||
| 414 | + | this.handleDisconnection(); | |||
| 415 | + | } | |||
| 416 | + | }, 10000); // 10 second timeout | |||
| 417 | + | ||||
| 404 | // Listen for chat messages | 418 | // Listen for chat messages | ||
| 405 | this.eventSource.addEventListener('chatMessage', async (event: MessageEvent) => { | 419 | this.eventSource.addEventListener('chatMessage', async (event: MessageEvent) => { | ||
| 406 | try { | 420 | try { | ||
| @@ -436,83 +450,34 @@ class ChatState { | |||||
| 436 | console.debug('Error processing userStatusUpdate event:', error); | 450 | console.debug('Error processing userStatusUpdate event:', error); | ||
| 437 | } | 451 | } | ||
| 438 | }); | 452 | }); | ||
| 439 | - | ||||
| 440 | - | this.eventSource.onerror = (event) => { | |||
| 441 | - | console.debug('SSE encountered an error:', event); | |||
| 442 | - | this.handleSSEError(); | |||
| 443 | - | }; | |||
| 444 | } catch (error) { | 453 | } catch (error) { | ||
| 445 | - | console.debug('Error setting up SSE connection:', error); | 454 | + | console.error('Error setting up SSE connection:', error); |
| 446 | - | this.isReconnecting = false; | 455 | + | this.handleDisconnection(); |
| 447 | - | this.handleSSEError(); | |||
| 448 | } | 456 | } | ||
| 449 | } | 457 | } | ||
| 450 | 458 | ||||
| 451 | - | private async handleSSEError() { | 459 | + | private handleDisconnection() { |
| 460 | + | if (this.isReconnecting) return; | |||
| 461 | + | ||||
| 462 | + | this.isReconnecting = true; | |||
| 452 | if (this.eventSource) { | 463 | if (this.eventSource) { | ||
| 453 | this.eventSource.close(); | 464 | this.eventSource.close(); | ||
| 454 | this.eventSource = null; | 465 | this.eventSource = null; | ||
| 455 | } | 466 | } | ||
| 456 | 467 | ||||
| 457 | - | // If we're already reconnecting, don't start another reconnection attempt | |||
| 458 | - | if (this.isReconnecting) { | |||
| 459 | - | console.debug('Already attempting to reconnect, skipping error handler'); | |||
| 460 | - | return; | |||
| 461 | - | } | |||
| 462 | - | ||||
| 463 | - | // Check if the error was due to rate limiting | |||
| 464 | - | try { | |||
| 465 | - | const response = await fetch('/api/sse'); | |||
| 466 | - | if (response.status === 429) { | |||
| 467 | - | const data = await response.json(); | |||
| 468 | - | this.sseError = data.error; | |||
| 469 | - | this.sseRetryAfter = data.retryAfter; | |||
| 470 | - | this.isReconnecting = false; | |||
| 471 | - | ||||
| 472 | - | // Set up automatic retry after the rate limit expires | |||
| 473 | - | setTimeout(() => { | |||
| 474 | - | this.sseError = null; | |||
| 475 | - | this.sseRetryAfter = null; | |||
| 476 | - | this.reconnectSSE(); | |||
| 477 | - | }, (data.retryAfter + 1) * 1000); // Add 1 second buffer | |||
| 478 | - | ||||
| 479 | - | return; | |||
| 480 | - | } | |||
| 481 | - | } catch (error) { | |||
| 482 | - | console.debug('Error checking SSE status:', error); | |||
| 483 | - | } | |||
| 484 | - | ||||
| 485 | - | // Handle other types of errors with exponential backoff | |||
| 486 | if (this.reconnectAttempts < this.maxReconnectAttempts) { | 468 | if (this.reconnectAttempts < this.maxReconnectAttempts) { | ||
| 487 | - | this.sseError = `Connection lost. Attempting to reconnect (${this.reconnectAttempts + 1}/${this.maxReconnectAttempts})...`; | |||
| 488 | console.debug(`Attempting to reconnect (${this.reconnectAttempts + 1}/${this.maxReconnectAttempts})...`); | 469 | console.debug(`Attempting to reconnect (${this.reconnectAttempts + 1}/${this.maxReconnectAttempts})...`); | ||
| 489 | - | ||||
| 490 | setTimeout(() => { | 470 | setTimeout(() => { | ||
| 491 | this.reconnectAttempts++; | 471 | this.reconnectAttempts++; | ||
| 492 | - | this.reconnectDelay *= 2; // Exponential backoff | 472 | + | this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000); // Max 30 second delay |
| 493 | - | this.isReconnecting = false; | |||
| 494 | this.connectSSE(); | 473 | this.connectSSE(); | ||
| 495 | }, this.reconnectDelay); | 474 | }, this.reconnectDelay); | ||
| 496 | } else { | 475 | } else { | ||
| 497 | - | this.isReconnecting = false; | |||
| 498 | - | this.sseError = 'Unable to connect to chat. Please refresh the page to try again.'; | |||
| 499 | console.debug('Max reconnection attempts reached'); | 476 | console.debug('Max reconnection attempts reached'); | ||
| 477 | + | this.sseError = 'Connection lost. Please refresh the page.'; | |||
| 500 | } | 478 | } | ||
| 501 | } | 479 | } | ||
| 502 | 480 | ||||
| 503 | - | private reconnectSSE() { | |||
| 504 | - | if (this.isReconnecting) { | |||
| 505 | - | console.debug('Already attempting to reconnect, skipping reconnection request'); | |||
| 506 | - | return; | |||
| 507 | - | } | |||
| 508 | - | ||||
| 509 | - | this.reconnectAttempts = 0; | |||
| 510 | - | this.reconnectDelay = 1000; | |||
| 511 | - | this.sseError = null; | |||
| 512 | - | this.sseRetryAfter = null; | |||
| 513 | - | this.connectSSE(); | |||
| 514 | - | } | |||
| 515 | - | ||||
| 516 | // Add new method to enrich multiple messages at once | 481 | // Add new method to enrich multiple messages at once | ||
| 517 | enrichMessages(messages: Message[]): EnrichedMessage[] { | 482 | enrichMessages(messages: Message[]): EnrichedMessage[] { | ||
| 518 | return messages.map(msg => this.enrichMessageWithUser(msg)); | 483 | return messages.map(msg => this.enrichMessageWithUser(msg)); | ||
M src/routes/api/sse/+server.ts +72 -50
| @@ -10,67 +10,89 @@ console.log('[SSE] Server initialized'); | |||
| 10 | 10 | const ONLINE_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes | |
| 11 | 11 | const CHECK_INTERVAL_MS = 30 * 1000; // Check every 30 seconds | |
| 12 | 12 | ||
| 13 | - | // Initialize periodic check for timed out users | |
| 14 | - | const timeoutInterval = setInterval(async () => { | |
| 15 | - | const timeoutThreshold = Date.now() - ONLINE_TIMEOUT_MS; | |
| 16 | - | console.log('[SSE] Checking for timed out users', { | |
| 17 | - | checkTime: new Date().toISOString(), | |
| 18 | - | timeoutThreshold: new Date(timeoutThreshold).toISOString() | |
| 19 | - | }); | |
| 13 | + | let timeoutInterval: ReturnType<typeof setInterval>; | |
| 20 | 14 | ||
| 21 | - | try { | |
| 22 | - | // Find and update users who haven't been seen recently | |
| 23 | - | const now = Date.now(); | |
| 24 | - | const result = await db.update(users) | |
| 25 | - | .set({ | |
| 26 | - | status: 'offline', | |
| 27 | - | lastSeen: now | |
| 28 | - | }) | |
| 29 | - | .where( | |
| 30 | - | and( | |
| 31 | - | eq(users.status, 'online'), // Only update online users | |
| 32 | - | lt(users.lastSeen, timeoutThreshold) // Who haven't been seen recently | |
| 33 | - | ) | |
| 34 | - | ) | |
| 35 | - | .returning({ | |
| 36 | - | id: users.id, | |
| 37 | - | nickname: users.nickname, | |
| 38 | - | lastSeen: users.lastSeen | |
| 39 | - | }); | |
| 15 | + | // Initialize periodic check for timed out users | |
| 16 | + | function startTimeoutCheck() { | |
| 17 | + | if (timeoutInterval) { | |
| 18 | + | clearInterval(timeoutInterval); | |
| 19 | + | } | |
| 40 | 20 | ||
| 41 | - | // Log timeout results | |
| 42 | - | if (result.length > 0) { | |
| 43 | - | console.log('[SSE] Found timed out users:', result.map(user => ({ | |
| 44 | - | nickname: user.nickname, | |
| 45 | - | lastSeen: new Date(user.lastSeen || 0).toISOString() | |
| 46 | - | }))); | |
| 47 | - | } | |
| 21 | + | timeoutInterval = setInterval(async () => { | |
| 22 | + | const timeoutThreshold = Date.now() - ONLINE_TIMEOUT_MS; | |
| 23 | + | console.log('[SSE] Checking for timed out users', { | |
| 24 | + | checkTime: new Date().toISOString(), | |
| 25 | + | timeoutThreshold: new Date(timeoutThreshold).toISOString() | |
| 26 | + | }); | |
| 48 | 27 | ||
| 49 | - | // Broadcast status updates for timed out users | |
| 50 | - | result.forEach(user => { | |
| 51 | - | sseEmitter.emit('sse', { | |
| 52 | - | type: 'userStatusUpdate', | |
| 53 | - | data: { | |
| 54 | - | userId: user.id, | |
| 28 | + | try { | |
| 29 | + | // Find and update users who haven't been seen recently | |
| 30 | + | const now = Date.now(); | |
| 31 | + | const result = await db.update(users) | |
| 32 | + | .set({ | |
| 55 | 33 | status: 'offline', | |
| 56 | 34 | lastSeen: now | |
| 57 | - | } | |
| 58 | - | }); | |
| 59 | - | console.log('[SSE] User timed out and marked offline:', { | |
| 60 | - | nickname: user.nickname, | |
| 61 | - | lastSeen: new Date(now).toISOString() | |
| 35 | + | }) | |
| 36 | + | .where( | |
| 37 | + | and( | |
| 38 | + | eq(users.status, 'online'), // Only update online users | |
| 39 | + | lt(users.lastSeen, timeoutThreshold) // Who haven't been seen recently | |
| 40 | + | ) | |
| 41 | + | ) | |
| 42 | + | .returning({ | |
| 43 | + | id: users.id, | |
| 44 | + | nickname: users.nickname, | |
| 45 | + | lastSeen: users.lastSeen | |
| 46 | + | }); | |
| 47 | + | ||
| 48 | + | // Log timeout results | |
| 49 | + | if (result.length > 0) { | |
| 50 | + | console.log('[SSE] Found timed out users:', result.map(user => ({ | |
| 51 | + | nickname: user.nickname, | |
| 52 | + | lastSeen: new Date(user.lastSeen || 0).toISOString() | |
| 53 | + | }))); | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | // Broadcast status updates for timed out users | |
| 57 | + | result.forEach(user => { | |
| 58 | + | sseEmitter.emit('sse', { | |
| 59 | + | type: 'userStatusUpdate', | |
| 60 | + | data: { | |
| 61 | + | userId: user.id, | |
| 62 | + | status: 'offline', | |
| 63 | + | lastSeen: now | |
| 64 | + | } | |
| 65 | + | }); | |
| 66 | + | console.log('[SSE] User timed out and marked offline:', { | |
| 67 | + | nickname: user.nickname, | |
| 68 | + | lastSeen: new Date(now).toISOString() | |
| 69 | + | }); | |
| 62 | 70 | }); | |
| 63 | - | }); | |
| 64 | - | } catch (error) { | |
| 65 | - | console.log('[SSE] Error during timeout check:', error); | |
| 66 | - | } | |
| 67 | - | }, CHECK_INTERVAL_MS); | |
| 71 | + | } catch (error) { | |
| 72 | + | console.error('[SSE] Error during timeout check:', error); | |
| 73 | + | } | |
| 74 | + | }, CHECK_INTERVAL_MS); | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | // Start the timeout check when this module loads | |
| 78 | + | startTimeoutCheck(); | |
| 68 | 79 | ||
| 69 | 80 | // Ensure the interval is cleared if the module is reloaded | |
| 70 | - | if (typeof process !== 'undefined' && process.on) { | |
| 81 | + | if (typeof process !== 'undefined') { | |
| 71 | 82 | process.on('beforeExit', () => { | |
| 72 | 83 | if (timeoutInterval) clearInterval(timeoutInterval); | |
| 73 | 84 | }); | |
| 85 | + | ||
| 86 | + | // Also handle SIGTERM and SIGINT | |
| 87 | + | process.on('SIGTERM', () => { | |
| 88 | + | if (timeoutInterval) clearInterval(timeoutInterval); | |
| 89 | + | process.exit(0); | |
| 90 | + | }); | |
| 91 | + | ||
| 92 | + | process.on('SIGINT', () => { | |
| 93 | + | if (timeoutInterval) clearInterval(timeoutInterval); | |
| 94 | + | process.exit(0); | |
| 95 | + | }); | |
| 74 | 96 | } | |
| 75 | 97 | ||
| 76 | 98 | export const GET: RequestHandler = async ({ locals }) => { | |
| @@ -10,67 +10,89 @@ console.log('[SSE] Server initialized'); | |||||
| 10 | const ONLINE_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes | 10 | const ONLINE_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes | ||
| 11 | const CHECK_INTERVAL_MS = 30 * 1000; // Check every 30 seconds | 11 | const CHECK_INTERVAL_MS = 30 * 1000; // Check every 30 seconds | ||
| 12 | 12 | ||||
| 13 | - | // Initialize periodic check for timed out users | 13 | + | let timeoutInterval: ReturnType<typeof setInterval>; |
| 14 | - | const timeoutInterval = setInterval(async () => { | |||
| 15 | - | const timeoutThreshold = Date.now() - ONLINE_TIMEOUT_MS; | |||
| 16 | - | console.log('[SSE] Checking for timed out users', { | |||
| 17 | - | checkTime: new Date().toISOString(), | |||
| 18 | - | timeoutThreshold: new Date(timeoutThreshold).toISOString() | |||
| 19 | - | }); | |||
| 20 | 14 | ||||
| 21 | - | try { | 15 | + | // Initialize periodic check for timed out users |
| 22 | - | // Find and update users who haven't been seen recently | 16 | + | function startTimeoutCheck() { |
| 23 | - | const now = Date.now(); | 17 | + | if (timeoutInterval) { |
| 24 | - | const result = await db.update(users) | 18 | + | clearInterval(timeoutInterval); |
| 25 | - | .set({ | 19 | + | } |
| 26 | - | status: 'offline', | |||
| 27 | - | lastSeen: now | |||
| 28 | - | }) | |||
| 29 | - | .where( | |||
| 30 | - | and( | |||
| 31 | - | eq(users.status, 'online'), // Only update online users | |||
| 32 | - | lt(users.lastSeen, timeoutThreshold) // Who haven't been seen recently | |||
| 33 | - | ) | |||
| 34 | - | ) | |||
| 35 | - | .returning({ | |||
| 36 | - | id: users.id, | |||
| 37 | - | nickname: users.nickname, | |||
| 38 | - | lastSeen: users.lastSeen | |||
| 39 | - | }); | |||
| 40 | 20 | ||||
| 41 | - | // Log timeout results | 21 | + | timeoutInterval = setInterval(async () => { |
| 42 | - | if (result.length > 0) { | 22 | + | const timeoutThreshold = Date.now() - ONLINE_TIMEOUT_MS; |
| 43 | - | console.log('[SSE] Found timed out users:', result.map(user => ({ | 23 | + | console.log('[SSE] Checking for timed out users', { |
| 44 | - | nickname: user.nickname, | 24 | + | checkTime: new Date().toISOString(), |
| 45 | - | lastSeen: new Date(user.lastSeen || 0).toISOString() | 25 | + | timeoutThreshold: new Date(timeoutThreshold).toISOString() |
| 46 | - | }))); | 26 | + | }); |
| 47 | - | } | |||
| 48 | 27 | ||||
| 49 | - | // Broadcast status updates for timed out users | 28 | + | try { |
| 50 | - | result.forEach(user => { | 29 | + | // Find and update users who haven't been seen recently |
| 51 | - | sseEmitter.emit('sse', { | 30 | + | const now = Date.now(); |
| 52 | - | type: 'userStatusUpdate', | 31 | + | const result = await db.update(users) |
| 53 | - | data: { | 32 | + | .set({ |
| 54 | - | userId: user.id, | |||
| 55 | status: 'offline', | 33 | status: 'offline', | ||
| 56 | lastSeen: now | 34 | lastSeen: now | ||
| 57 | - | } | 35 | + | }) |
| 58 | - | }); | 36 | + | .where( |
| 59 | - | console.log('[SSE] User timed out and marked offline:', { | 37 | + | and( |
| 60 | - | nickname: user.nickname, | 38 | + | eq(users.status, 'online'), // Only update online users |
| 61 | - | lastSeen: new Date(now).toISOString() | 39 | + | lt(users.lastSeen, timeoutThreshold) // Who haven't been seen recently |
| 40 | + | ) | |||
| 41 | + | ) | |||
| 42 | + | .returning({ | |||
| 43 | + | id: users.id, | |||
| 44 | + | nickname: users.nickname, | |||
| 45 | + | lastSeen: users.lastSeen | |||
| 46 | + | }); | |||
| 47 | + | ||||
| 48 | + | // Log timeout results | |||
| 49 | + | if (result.length > 0) { | |||
| 50 | + | console.log('[SSE] Found timed out users:', result.map(user => ({ | |||
| 51 | + | nickname: user.nickname, | |||
| 52 | + | lastSeen: new Date(user.lastSeen || 0).toISOString() | |||
| 53 | + | }))); | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | // Broadcast status updates for timed out users | |||
| 57 | + | result.forEach(user => { | |||
| 58 | + | sseEmitter.emit('sse', { | |||
| 59 | + | type: 'userStatusUpdate', | |||
| 60 | + | data: { | |||
| 61 | + | userId: user.id, | |||
| 62 | + | status: 'offline', | |||
| 63 | + | lastSeen: now | |||
| 64 | + | } | |||
| 65 | + | }); | |||
| 66 | + | console.log('[SSE] User timed out and marked offline:', { | |||
| 67 | + | nickname: user.nickname, | |||
| 68 | + | lastSeen: new Date(now).toISOString() | |||
| 69 | + | }); | |||
| 62 | }); | 70 | }); | ||
| 63 | - | }); | 71 | + | } catch (error) { |
| 64 | - | } catch (error) { | 72 | + | console.error('[SSE] Error during timeout check:', error); |
| 65 | - | console.log('[SSE] Error during timeout check:', error); | 73 | + | } |
| 66 | - | } | 74 | + | }, CHECK_INTERVAL_MS); |
| 67 | - | }, CHECK_INTERVAL_MS); | 75 | + | } |
| 76 | + | ||||
| 77 | + | // Start the timeout check when this module loads | |||
| 78 | + | startTimeoutCheck(); | |||
| 68 | 79 | ||||
| 69 | // Ensure the interval is cleared if the module is reloaded | 80 | // Ensure the interval is cleared if the module is reloaded | ||
| 70 | - | if (typeof process !== 'undefined' && process.on) { | 81 | + | if (typeof process !== 'undefined') { |
| 71 | process.on('beforeExit', () => { | 82 | process.on('beforeExit', () => { | ||
| 72 | if (timeoutInterval) clearInterval(timeoutInterval); | 83 | if (timeoutInterval) clearInterval(timeoutInterval); | ||
| 73 | }); | 84 | }); | ||
| 85 | + | ||||
| 86 | + | // Also handle SIGTERM and SIGINT | |||
| 87 | + | process.on('SIGTERM', () => { | |||
| 88 | + | if (timeoutInterval) clearInterval(timeoutInterval); | |||
| 89 | + | process.exit(0); | |||
| 90 | + | }); | |||
| 91 | + | ||||
| 92 | + | process.on('SIGINT', () => { | |||
| 93 | + | if (timeoutInterval) clearInterval(timeoutInterval); | |||
| 94 | + | process.exit(0); | |||
| 95 | + | }); | |||
| 74 | } | 96 | } | ||
| 75 | 97 | ||||
| 76 | export const GET: RequestHandler = async ({ locals }) => { | 98 | export const GET: RequestHandler = async ({ locals }) => { | ||