detached cb06b35

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 UnverifiedSHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6Tk The key is unknown
3 files · +137 -131
M src/hooks.server.ts +22 -3
@@ -2,13 +2,32 @@ import type { Handle } from "@sveltejs/kit";
22 import { validateSessionToken } from "$lib/api/session.server";
33 import { setSessionTokenCookie, deleteSessionTokenCookie } from "$lib/api/session.cookie";
44 import { initializeDatabase } from "$lib/db/db.server";
5+import db from "$lib/db/db.server";
56 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+}
621
722 // Initialize database when the server starts
823 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+ });
1231
1332 export const handle: Handle = async ({ event, resolve }) => {
1433 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 starts22 // 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 {
155155 }
156156
157157 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,
159162 currentCache: this.userCache[userId],
160163 currentUsers: this.users.find(u => u.id === userId)
161164 });
@@ -171,11 +174,13 @@ class ChatState {
171174 }
172175 }
173176
177+ const now = Date.now();
174178 if (user) {
175179 const updatedUser: User = {
176180 ...user,
177181 status,
178- lastSeen: status === 'offline' ? (lastSeen ?? Date.now()) : user.lastSeen
182+ // Update lastSeen based on status
183+ lastSeen: lastSeen ?? (status === 'offline' ? now : user.lastSeen)
179184 };
180185
181186 // Update cache first
@@ -197,6 +202,7 @@ class ChatState {
197202 console.debug('User status updated:', {
198203 userId,
199204 newStatus: status,
205+ newLastSeen: new Date(updatedUser.lastSeen || now).toISOString(),
200206 updatedInCache: this.userCache[userId].status === status,
201207 updatedInList: this.users.find(u => u.id === userId)?.status === status
202208 });
@@ -210,7 +216,7 @@ class ChatState {
210216 const newUser: User = {
211217 ...data.user,
212218 status,
213- lastSeen: status === 'offline' ? (lastSeen ?? Date.now()) : data.user.lastSeen
219+ lastSeen: lastSeen ?? (status === 'offline' ? now : data.user.lastSeen)
214220 };
215221 // Update both cache and users array
216222 this.userCache[userId] = newUser;
@@ -219,6 +225,7 @@ class ChatState {
219225 console.debug('User fetched and status updated:', {
220226 userId,
221227 status: newUser.status,
228+ lastSeen: new Date(newUser.lastSeen || now).toISOString(),
222229 addedToCache: !!this.userCache[userId],
223230 addedToList: this.users.some(u => u.id === userId)
224231 });
@@ -375,32 +382,39 @@ class ChatState {
375382 this.eventSource = null;
376383 }
377384
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;
381388 }
382389
383- this.isReconnecting = true;
384- console.debug('Connecting to SSE...');
385-
386390 try {
387- // Connect to the unified SSE endpoint
391+ console.debug('Connecting to SSE...');
388392 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+ };
389402
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();
401407 }
402408 };
403409
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+
404418 // Listen for chat messages
405419 this.eventSource.addEventListener('chatMessage', async (event: MessageEvent) => {
406420 try {
@@ -436,83 +450,34 @@ class ChatState {
436450 console.debug('Error processing userStatusUpdate event:', error);
437451 }
438452 });
439-
440- this.eventSource.onerror = (event) => {
441- console.debug('SSE encountered an error:', event);
442- this.handleSSEError();
443- };
444453 } 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();
448456 }
449457 }
450458
451- private async handleSSEError() {
459+ private handleDisconnection() {
460+ if (this.isReconnecting) return;
461+
462+ this.isReconnecting = true;
452463 if (this.eventSource) {
453464 this.eventSource.close();
454465 this.eventSource = null;
455466 }
456467
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
486468 if (this.reconnectAttempts < this.maxReconnectAttempts) {
487- this.sseError = `Connection lost. Attempting to reconnect (${this.reconnectAttempts + 1}/${this.maxReconnectAttempts})...`;
488469 console.debug(`Attempting to reconnect (${this.reconnectAttempts + 1}/${this.maxReconnectAttempts})...`);
489-
490470 setTimeout(() => {
491471 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
494473 this.connectSSE();
495474 }, this.reconnectDelay);
496475 } else {
497- this.isReconnecting = false;
498- this.sseError = 'Unable to connect to chat. Please refresh the page to try again.';
499476 console.debug('Max reconnection attempts reached');
477+ this.sseError = 'Connection lost. Please refresh the page.';
500478 }
501479 }
502480
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-
516481 // Add new method to enrich multiple messages at once
517482 enrichMessages(messages: Message[]): EnrichedMessage[] {
518483 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.lastSeen182+ // Update lastSeen based on status
183+ lastSeen: lastSeen ?? (status === 'offline' ? now : user.lastSeen)
179 };184 };
180 185
181 // Update cache first186 // 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 === status207 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.lastSeen219+ lastSeen: lastSeen ?? (status === 'offline' ? now : data.user.lastSeen)
214 };220 };
215 // Update both cache and users array221 // 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 endpoint391+ 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 handshake403+ 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 messages418 // 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 backoff472+ 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 once481 // 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');
1010 const ONLINE_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
1111 const CHECK_INTERVAL_MS = 30 * 1000; // Check every 30 seconds
1212
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>;
2014
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+ }
4020
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+ });
4827
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({
5533 status: 'offline',
5634 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+ });
6270 });
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();
6879
6980 // Ensure the interval is cleared if the module is reloaded
70-if (typeof process !== 'undefined' && process.on) {
81+if (typeof process !== 'undefined') {
7182 process.on('beforeExit', () => {
7283 if (timeoutInterval) clearInterval(timeoutInterval);
7384 });
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+ });
7496 }
7597
7698 export const GET: RequestHandler = async ({ locals }) => {
@@ -10,67 +10,89 @@ console.log('[SSE] Server initialized');
10 const ONLINE_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes10 const ONLINE_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
11 const CHECK_INTERVAL_MS = 30 * 1000; // Check every 30 seconds11 const CHECK_INTERVAL_MS = 30 * 1000; // Check every 30 seconds
12 12
13-// Initialize periodic check for timed out users13+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 recently16+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 results21+ 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 users28+ 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: now34 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 reloaded80 // 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 }) => {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 8

↑↓ moveenter openesc close