fix(sse): Fix status bug and optimize performance with caching
- commit
- 6e58ca3538e7c00da9c2c1acd2ef26a375b176fa
- author
- Kit · · 2025-07-03 20:08 UTC
- committer
- Kit · · 2025-07-03 20:11 UTC
- parent
- 64ad42e
- signature
- SSH Unverified
SHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6TkThe key is unknown
6 files · +275 -52
M package.json +1 -1
| @@ -19,7 +19,7 @@ | |||
| 19 | 19 | "test": "npm run test:unit -- --run", | |
| 20 | 20 | "db:generate": "npx drizzle-kit generate", | |
| 21 | 21 | "db:push": "npx drizzle-kit push", | |
| 22 | - | "db:push-prod": "TURSO_DATABASE_URL=\"$TURSO_DATABASE_URL_PROD\" TURSO_AUTH_TOKEN=\"$TURSO_AUTH_TOKEN_PROD\" npx drizzle-kit push", | |
| 22 | + | "db:push-prod": "bash -c 'source .env && TURSO_DATABASE_URL=\"$TURSO_DATABASE_URL_PROD\" TURSO_AUTH_TOKEN=\"$TURSO_AUTH_TOKEN_PROD\" npx drizzle-kit push'", | |
| 23 | 23 | "db:flush": "tsx utils/db-flush.ts" | |
| 24 | 24 | }, | |
| 25 | 25 | "devDependencies": { | |
| @@ -19,7 +19,7 @@ | |||||
| 19 | "test": "npm run test:unit -- --run", | 19 | "test": "npm run test:unit -- --run", | ||
| 20 | "db:generate": "npx drizzle-kit generate", | 20 | "db:generate": "npx drizzle-kit generate", | ||
| 21 | "db:push": "npx drizzle-kit push", | 21 | "db:push": "npx drizzle-kit push", | ||
| 22 | - | "db:push-prod": "TURSO_DATABASE_URL=\"$TURSO_DATABASE_URL_PROD\" TURSO_AUTH_TOKEN=\"$TURSO_AUTH_TOKEN_PROD\" npx drizzle-kit push", | 22 | + | "db:push-prod": "bash -c 'source .env && TURSO_DATABASE_URL=\"$TURSO_DATABASE_URL_PROD\" TURSO_AUTH_TOKEN=\"$TURSO_AUTH_TOKEN_PROD\" npx drizzle-kit push'", |
| 23 | "db:flush": "tsx utils/db-flush.ts" | 23 | "db:flush": "tsx utils/db-flush.ts" | ||
| 24 | }, | 24 | }, | ||
| 25 | "devDependencies": { | 25 | "devDependencies": { | ||
A src/lib/buddyListCache.ts +55 -0
| @@ -0,0 +1,55 @@ | |||
| 1 | + | import { createLogger } from './utils/logger.server'; | |
| 2 | + | import type { SafeUser } from './types/chat'; | |
| 3 | + | ||
| 4 | + | const log = createLogger('buddy-list-cache'); | |
| 5 | + | ||
| 6 | + | class BuddyListCache { | |
| 7 | + | private cache: SafeUser[] | null = null; | |
| 8 | + | private lastHash: string = ''; | |
| 9 | + | private lastFetch: number = 0; | |
| 10 | + | private isDirty: boolean = true; // Force initial fetch | |
| 11 | + | ||
| 12 | + | // Mark cache as needing refresh | |
| 13 | + | invalidate(): void { | |
| 14 | + | this.isDirty = true; | |
| 15 | + | log.debug('Buddy list cache invalidated'); | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | // Check if we need to fetch new data | |
| 19 | + | needsRefresh(): boolean { | |
| 20 | + | return this.isDirty || this.cache === null; | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | // Update cache with new data | |
| 24 | + | update(users: SafeUser[]): boolean { | |
| 25 | + | const newHash = JSON.stringify(users); | |
| 26 | + | const hasChanged = newHash !== this.lastHash; | |
| 27 | + | ||
| 28 | + | if (hasChanged) { | |
| 29 | + | this.cache = users; | |
| 30 | + | this.lastHash = newHash; | |
| 31 | + | this.lastFetch = Date.now(); | |
| 32 | + | this.isDirty = false; | |
| 33 | + | ||
| 34 | + | log.debug('Buddy list cache updated', { | |
| 35 | + | userCount: users.length, | |
| 36 | + | changed: hasChanged | |
| 37 | + | }); | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | return hasChanged; | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | // Get cached data | |
| 44 | + | get(): SafeUser[] | null { | |
| 45 | + | return this.cache; | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | // Get cache age in milliseconds | |
| 49 | + | getAge(): number { | |
| 50 | + | return this.cache ? Date.now() - this.lastFetch : Infinity; | |
| 51 | + | } | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | // Export singleton instance | |
| 55 | + | export const buddyListCache = new BuddyListCache();⏎̸ | |
| @@ -0,0 +1,55 @@ | |||||
| 1 | + | import { createLogger } from './utils/logger.server'; | |||
| 2 | + | import type { SafeUser } from './types/chat'; | |||
| 3 | + | ||||
| 4 | + | const log = createLogger('buddy-list-cache'); | |||
| 5 | + | ||||
| 6 | + | class BuddyListCache { | |||
| 7 | + | private cache: SafeUser[] | null = null; | |||
| 8 | + | private lastHash: string = ''; | |||
| 9 | + | private lastFetch: number = 0; | |||
| 10 | + | private isDirty: boolean = true; // Force initial fetch | |||
| 11 | + | ||||
| 12 | + | // Mark cache as needing refresh | |||
| 13 | + | invalidate(): void { | |||
| 14 | + | this.isDirty = true; | |||
| 15 | + | log.debug('Buddy list cache invalidated'); | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | // Check if we need to fetch new data | |||
| 19 | + | needsRefresh(): boolean { | |||
| 20 | + | return this.isDirty || this.cache === null; | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | // Update cache with new data | |||
| 24 | + | update(users: SafeUser[]): boolean { | |||
| 25 | + | const newHash = JSON.stringify(users); | |||
| 26 | + | const hasChanged = newHash !== this.lastHash; | |||
| 27 | + | ||||
| 28 | + | if (hasChanged) { | |||
| 29 | + | this.cache = users; | |||
| 30 | + | this.lastHash = newHash; | |||
| 31 | + | this.lastFetch = Date.now(); | |||
| 32 | + | this.isDirty = false; | |||
| 33 | + | ||||
| 34 | + | log.debug('Buddy list cache updated', { | |||
| 35 | + | userCount: users.length, | |||
| 36 | + | changed: hasChanged | |||
| 37 | + | }); | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | return hasChanged; | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | // Get cached data | |||
| 44 | + | get(): SafeUser[] | null { | |||
| 45 | + | return this.cache; | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | // Get cache age in milliseconds | |||
| 49 | + | getAge(): number { | |||
| 50 | + | return this.cache ? Date.now() - this.lastFetch : Infinity; | |||
| 51 | + | } | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | // Export singleton instance | |||
| 55 | + | export const buddyListCache = new BuddyListCache();⏎̸ | |||
A src/lib/sseConnectionTracker.ts +90 -0
| @@ -0,0 +1,90 @@ | |||
| 1 | + | import { createLogger } from './utils/logger.server'; | |
| 2 | + | ||
| 3 | + | const log = createLogger('sse-connection-tracker'); | |
| 4 | + | ||
| 5 | + | interface ConnectionInfo { | |
| 6 | + | userId: string; | |
| 7 | + | connectedAt: number; | |
| 8 | + | lastActivity: number; | |
| 9 | + | } | |
| 10 | + | ||
| 11 | + | class SSEConnectionTracker { | |
| 12 | + | private connections = new Map<string, ConnectionInfo>(); | |
| 13 | + | ||
| 14 | + | addConnection(userId: string): boolean { | |
| 15 | + | const existing = this.connections.get(userId); | |
| 16 | + | if (existing) { | |
| 17 | + | log.debug('User already has active connection', { userId }); | |
| 18 | + | // Update last activity but don't trigger status update | |
| 19 | + | existing.lastActivity = Date.now(); | |
| 20 | + | return false; // Connection already exists | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | this.connections.set(userId, { | |
| 24 | + | userId, | |
| 25 | + | connectedAt: Date.now(), | |
| 26 | + | lastActivity: Date.now() | |
| 27 | + | }); | |
| 28 | + | ||
| 29 | + | log.debug('New SSE connection tracked', { | |
| 30 | + | userId, | |
| 31 | + | totalConnections: this.connections.size | |
| 32 | + | }); | |
| 33 | + | return true; // New connection | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | removeConnection(userId: string): boolean { | |
| 37 | + | const deleted = this.connections.delete(userId); | |
| 38 | + | if (deleted) { | |
| 39 | + | log.debug('SSE connection removed', { | |
| 40 | + | userId, | |
| 41 | + | remainingConnections: this.connections.size | |
| 42 | + | }); | |
| 43 | + | } | |
| 44 | + | return deleted; | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | hasConnection(userId: string): boolean { | |
| 48 | + | return this.connections.has(userId); | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | updateActivity(userId: string): void { | |
| 52 | + | const connection = this.connections.get(userId); | |
| 53 | + | if (connection) { | |
| 54 | + | connection.lastActivity = Date.now(); | |
| 55 | + | } | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | getActiveUserIds(): string[] { | |
| 59 | + | return Array.from(this.connections.keys()); | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | getConnectionCount(): number { | |
| 63 | + | return this.connections.size; | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // Clean up stale connections (e.g., if server didn't properly close them) | |
| 67 | + | cleanupStaleConnections(maxInactivityMs: number = 5 * 60 * 1000): string[] { | |
| 68 | + | const now = Date.now(); | |
| 69 | + | const staleUserIds: string[] = []; | |
| 70 | + | ||
| 71 | + | for (const [userId, info] of this.connections.entries()) { | |
| 72 | + | if (now - info.lastActivity > maxInactivityMs) { | |
| 73 | + | this.connections.delete(userId); | |
| 74 | + | staleUserIds.push(userId); | |
| 75 | + | } | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | if (staleUserIds.length > 0) { | |
| 79 | + | log.info('Cleaned up stale connections', { | |
| 80 | + | count: staleUserIds.length, | |
| 81 | + | userIds: staleUserIds | |
| 82 | + | }); | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | return staleUserIds; | |
| 86 | + | } | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | // Export singleton instance | |
| 90 | + | export const sseConnectionTracker = new SSEConnectionTracker();⏎̸ | |
| @@ -0,0 +1,90 @@ | |||||
| 1 | + | import { createLogger } from './utils/logger.server'; | |||
| 2 | + | ||||
| 3 | + | const log = createLogger('sse-connection-tracker'); | |||
| 4 | + | ||||
| 5 | + | interface ConnectionInfo { | |||
| 6 | + | userId: string; | |||
| 7 | + | connectedAt: number; | |||
| 8 | + | lastActivity: number; | |||
| 9 | + | } | |||
| 10 | + | ||||
| 11 | + | class SSEConnectionTracker { | |||
| 12 | + | private connections = new Map<string, ConnectionInfo>(); | |||
| 13 | + | ||||
| 14 | + | addConnection(userId: string): boolean { | |||
| 15 | + | const existing = this.connections.get(userId); | |||
| 16 | + | if (existing) { | |||
| 17 | + | log.debug('User already has active connection', { userId }); | |||
| 18 | + | // Update last activity but don't trigger status update | |||
| 19 | + | existing.lastActivity = Date.now(); | |||
| 20 | + | return false; // Connection already exists | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | this.connections.set(userId, { | |||
| 24 | + | userId, | |||
| 25 | + | connectedAt: Date.now(), | |||
| 26 | + | lastActivity: Date.now() | |||
| 27 | + | }); | |||
| 28 | + | ||||
| 29 | + | log.debug('New SSE connection tracked', { | |||
| 30 | + | userId, | |||
| 31 | + | totalConnections: this.connections.size | |||
| 32 | + | }); | |||
| 33 | + | return true; // New connection | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | removeConnection(userId: string): boolean { | |||
| 37 | + | const deleted = this.connections.delete(userId); | |||
| 38 | + | if (deleted) { | |||
| 39 | + | log.debug('SSE connection removed', { | |||
| 40 | + | userId, | |||
| 41 | + | remainingConnections: this.connections.size | |||
| 42 | + | }); | |||
| 43 | + | } | |||
| 44 | + | return deleted; | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | hasConnection(userId: string): boolean { | |||
| 48 | + | return this.connections.has(userId); | |||
| 49 | + | } | |||
| 50 | + | ||||
| 51 | + | updateActivity(userId: string): void { | |||
| 52 | + | const connection = this.connections.get(userId); | |||
| 53 | + | if (connection) { | |||
| 54 | + | connection.lastActivity = Date.now(); | |||
| 55 | + | } | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | getActiveUserIds(): string[] { | |||
| 59 | + | return Array.from(this.connections.keys()); | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | getConnectionCount(): number { | |||
| 63 | + | return this.connections.size; | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | // Clean up stale connections (e.g., if server didn't properly close them) | |||
| 67 | + | cleanupStaleConnections(maxInactivityMs: number = 5 * 60 * 1000): string[] { | |||
| 68 | + | const now = Date.now(); | |||
| 69 | + | const staleUserIds: string[] = []; | |||
| 70 | + | ||||
| 71 | + | for (const [userId, info] of this.connections.entries()) { | |||
| 72 | + | if (now - info.lastActivity > maxInactivityMs) { | |||
| 73 | + | this.connections.delete(userId); | |||
| 74 | + | staleUserIds.push(userId); | |||
| 75 | + | } | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | if (staleUserIds.length > 0) { | |||
| 79 | + | log.info('Cleaned up stale connections', { | |||
| 80 | + | count: staleUserIds.length, | |||
| 81 | + | userIds: staleUserIds | |||
| 82 | + | }); | |||
| 83 | + | } | |||
| 84 | + | ||||
| 85 | + | return staleUserIds; | |||
| 86 | + | } | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | // Export singleton instance | |||
| 90 | + | export const sseConnectionTracker = new SSEConnectionTracker();⏎̸ | |||
M src/routes/api/rooms/[roomId]/+server.ts +3 -3
| @@ -40,7 +40,7 @@ export async function GET({ params, url, locals }): Promise<Response> { | |||
| 40 | 40 | // Process users to mark them as offline if they've timed out | |
| 41 | 41 | const processedUsers = normalizedUsers.map( | |
| 42 | 42 | (user: SafeUser & { lastSeen: number }) => { | |
| 43 | - | const shouldBeOffline = user.status === 'online' && user.lastSeen < timeoutThreshold; | |
| 43 | + | const shouldBeOffline = user.status !== 'offline' && user.lastSeen < timeoutThreshold; | |
| 44 | 44 | return { | |
| 45 | 45 | ...user, | |
| 46 | 46 | status: shouldBeOffline ? 'offline' : user.status | |
| @@ -48,13 +48,13 @@ export async function GET({ params, url, locals }): Promise<Response> { | |||
| 48 | 48 | } | |
| 49 | 49 | ); | |
| 50 | 50 | ||
| 51 | - | // Find users who were originally online but now marked offline | |
| 51 | + | // Find users who were originally non-offline but now marked offline | |
| 52 | 52 | const usersToUpdate = processedUsers.filter( | |
| 53 | 53 | (user: SafeUser & { lastSeen: number }) => | |
| 54 | 54 | user.status === 'offline' && | |
| 55 | 55 | normalizedUsers.find( | |
| 56 | 56 | (u: SafeUser & { lastSeen: number }) => u.id === user.id | |
| 57 | - | )?.status === 'online' | |
| 57 | + | )?.status !== 'offline' | |
| 58 | 58 | ); | |
| 59 | 59 | ||
| 60 | 60 | if (usersToUpdate.length > 0) { | |
| @@ -40,7 +40,7 @@ export async function GET({ params, url, locals }): Promise<Response> { | |||||
| 40 | // Process users to mark them as offline if they've timed out | 40 | // Process users to mark them as offline if they've timed out | ||
| 41 | const processedUsers = normalizedUsers.map( | 41 | const processedUsers = normalizedUsers.map( | ||
| 42 | (user: SafeUser & { lastSeen: number }) => { | 42 | (user: SafeUser & { lastSeen: number }) => { | ||
| 43 | - | const shouldBeOffline = user.status === 'online' && user.lastSeen < timeoutThreshold; | 43 | + | const shouldBeOffline = user.status !== 'offline' && user.lastSeen < timeoutThreshold; |
| 44 | return { | 44 | return { | ||
| 45 | ...user, | 45 | ...user, | ||
| 46 | status: shouldBeOffline ? 'offline' : user.status | 46 | status: shouldBeOffline ? 'offline' : user.status | ||
| @@ -48,13 +48,13 @@ export async function GET({ params, url, locals }): Promise<Response> { | |||||
| 48 | } | 48 | } | ||
| 49 | ); | 49 | ); | ||
| 50 | 50 | ||||
| 51 | - | // Find users who were originally online but now marked offline | 51 | + | // Find users who were originally non-offline but now marked offline |
| 52 | const usersToUpdate = processedUsers.filter( | 52 | const usersToUpdate = processedUsers.filter( | ||
| 53 | (user: SafeUser & { lastSeen: number }) => | 53 | (user: SafeUser & { lastSeen: number }) => | ||
| 54 | user.status === 'offline' && | 54 | user.status === 'offline' && | ||
| 55 | normalizedUsers.find( | 55 | normalizedUsers.find( | ||
| 56 | (u: SafeUser & { lastSeen: number }) => u.id === user.id | 56 | (u: SafeUser & { lastSeen: number }) => u.id === user.id | ||
| 57 | - | )?.status === 'online' | 57 | + | )?.status !== 'offline' |
| 58 | ); | 58 | ); | ||
| 59 | 59 | ||||
| 60 | if (usersToUpdate.length > 0) { | 60 | if (usersToUpdate.length > 0) { | ||
M src/routes/api/sse/+server.ts +122 -48
| @@ -8,9 +8,11 @@ import type { RequestHandler } from './$types'; | |||
| 8 | 8 | import { sseEmitter } from '$lib/sseEmitter'; | |
| 9 | 9 | import db from '$lib/db/db.server'; | |
| 10 | 10 | import { users } from '$lib/db/schema'; | |
| 11 | - | import { eq, and, lt } from 'drizzle-orm'; | |
| 11 | + | import { eq, and, lt, ne } from 'drizzle-orm'; | |
| 12 | 12 | import { createSafeUser } from '$lib/types/chat'; | |
| 13 | 13 | import { createLogger } from '$lib/utils/logger.server'; | |
| 14 | + | import { sseConnectionTracker } from '$lib/sseConnectionTracker'; | |
| 15 | + | import { buddyListCache } from '$lib/buddyListCache'; | |
| 14 | 16 | ||
| 15 | 17 | const log = createLogger('sse-server'); | |
| 16 | 18 | ||
| @@ -52,7 +54,7 @@ function startTimeoutCheck() { | |||
| 52 | 54 | }) | |
| 53 | 55 | .where( | |
| 54 | 56 | and( | |
| 55 | - | eq(users.status, 'online'), // Only update online users | |
| 57 | + | ne(users.status, 'offline'), // Update any non-offline users | |
| 56 | 58 | lt(users.lastSeen, timeoutThreshold) // Who haven't been seen recently | |
| 57 | 59 | ) | |
| 58 | 60 | ) | |
| @@ -67,6 +69,9 @@ function startTimeoutCheck() { | |||
| 67 | 69 | usersUpdated: result.length, | |
| 68 | 70 | timeoutThreshold: new Date(timeoutThreshold).toISOString() | |
| 69 | 71 | }); | |
| 72 | + | ||
| 73 | + | // Invalidate buddy list cache when users timeout | |
| 74 | + | buddyListCache.invalidate(); | |
| 70 | 75 | } | |
| 71 | 76 | } catch (error) { | |
| 72 | 77 | log.error('Error during timeout check', { error }); | |
| @@ -84,19 +89,35 @@ function startBuddyListUpdateInterval() { | |||
| 84 | 89 | const now = Date.now(); | |
| 85 | 90 | // Only fetch and broadcast if 10 seconds have passed since last update | |
| 86 | 91 | if (now - lastBuddyListUpdate >= BUDDY_LIST_BROADCAST_INTERVAL) { | |
| 87 | - | log.debug('Broadcasting buddy list update...'); | |
| 88 | - | // Query the complete buddy list from the database. | |
| 89 | - | const buddyList = await db.select().from(users); | |
| 90 | - | // Use createSafeUser to properly sanitize user data | |
| 91 | - | const safeBuddyList = buddyList.map(user => createSafeUser(user)); | |
| 92 | - | ||
| 93 | - | // Only broadcast if data has changed | |
| 94 | - | if (JSON.stringify(safeBuddyList) !== JSON.stringify(lastBuddyListData)) { | |
| 95 | - | lastBuddyListData = safeBuddyList; | |
| 96 | - | sseEmitter.emit('sse', { | |
| 97 | - | type: 'buddyListUpdate', | |
| 98 | - | data: safeBuddyList | |
| 99 | - | }); | |
| 92 | + | // Only query database if cache is invalid or doesn't exist | |
| 93 | + | if (buddyListCache.needsRefresh()) { | |
| 94 | + | log.debug('Fetching buddy list from database (cache miss)...'); | |
| 95 | + | // Query the complete buddy list from the database. | |
| 96 | + | const buddyList = await db.select().from(users); | |
| 97 | + | // Use createSafeUser to properly sanitize user data | |
| 98 | + | const safeBuddyList = buddyList.map(user => createSafeUser(user)); | |
| 99 | + | ||
| 100 | + | // Update cache and check if data changed | |
| 101 | + | const hasChanged = buddyListCache.update(safeBuddyList); | |
| 102 | + | ||
| 103 | + | if (hasChanged) { | |
| 104 | + | log.debug('Broadcasting buddy list update (data changed)...'); | |
| 105 | + | sseEmitter.emit('sse', { | |
| 106 | + | type: 'buddyListUpdate', | |
| 107 | + | data: safeBuddyList | |
| 108 | + | }); | |
| 109 | + | } | |
| 110 | + | } else { | |
| 111 | + | // Use cached data | |
| 112 | + | const cachedData = buddyListCache.get(); | |
| 113 | + | if (cachedData && JSON.stringify(cachedData) !== JSON.stringify(lastBuddyListData)) { | |
| 114 | + | log.debug('Broadcasting buddy list update from cache...'); | |
| 115 | + | lastBuddyListData = cachedData; | |
| 116 | + | sseEmitter.emit('sse', { | |
| 117 | + | type: 'buddyListUpdate', | |
| 118 | + | data: cachedData | |
| 119 | + | }); | |
| 120 | + | } | |
| 100 | 121 | } | |
| 101 | 122 | lastBuddyListUpdate = now; | |
| 102 | 123 | } | |
| @@ -106,10 +127,38 @@ function startBuddyListUpdateInterval() { | |||
| 106 | 127 | }, 1000); // Check every second but only broadcast every 10 seconds | |
| 107 | 128 | } | |
| 108 | 129 | ||
| 130 | + | // Add periodic cleanup for stale connections | |
| 131 | + | let connectionCleanupInterval: ReturnType<typeof setInterval>; | |
| 132 | + | ||
| 133 | + | function startConnectionCleanup() { | |
| 134 | + | if (connectionCleanupInterval) { | |
| 135 | + | clearInterval(connectionCleanupInterval); | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | connectionCleanupInterval = setInterval(() => { | |
| 139 | + | const staleUserIds = sseConnectionTracker.cleanupStaleConnections(); | |
| 140 | + | if (staleUserIds.length > 0) { | |
| 141 | + | // Mark stale connections as offline | |
| 142 | + | staleUserIds.forEach(async (userId) => { | |
| 143 | + | try { | |
| 144 | + | await db.update(users) | |
| 145 | + | .set({ status: 'offline', lastSeen: Date.now() }) | |
| 146 | + | .where(eq(users.id, userId)); | |
| 147 | + | ||
| 148 | + | buddyListCache.invalidate(); | |
| 149 | + | } catch (error) { | |
| 150 | + | log.error('Error updating stale user status', { userId, error }); | |
| 151 | + | } | |
| 152 | + | }); | |
| 153 | + | } | |
| 154 | + | }, 60000); // Run every minute | |
| 155 | + | } | |
| 156 | + | ||
| 109 | 157 | // <<< NEW: Ensure intervals are started only once >>> | |
| 110 | 158 | if (!globalThis.__sseIntervalsStarted) { | |
| 111 | 159 | startTimeoutCheck(); | |
| 112 | 160 | startBuddyListUpdateInterval(); | |
| 161 | + | startConnectionCleanup(); | |
| 113 | 162 | globalThis.__sseIntervalsStarted = true; | |
| 114 | 163 | log.info('Started global intervals for timeout and buddy list updates'); | |
| 115 | 164 | } else { | |
| @@ -121,18 +170,21 @@ if (typeof process !== 'undefined') { | |||
| 121 | 170 | process.on('beforeExit', () => { | |
| 122 | 171 | if (timeoutInterval) clearInterval(timeoutInterval); | |
| 123 | 172 | if (buddyListInterval) clearInterval(buddyListInterval); | |
| 173 | + | if (connectionCleanupInterval) clearInterval(connectionCleanupInterval); | |
| 124 | 174 | }); | |
| 125 | 175 | ||
| 126 | 176 | // Also handle SIGTERM and SIGINT | |
| 127 | 177 | process.on('SIGTERM', () => { | |
| 128 | 178 | if (timeoutInterval) clearInterval(timeoutInterval); | |
| 129 | 179 | if (buddyListInterval) clearInterval(buddyListInterval); | |
| 180 | + | if (connectionCleanupInterval) clearInterval(connectionCleanupInterval); | |
| 130 | 181 | process.exit(0); | |
| 131 | 182 | }); | |
| 132 | 183 | ||
| 133 | 184 | process.on('SIGINT', () => { | |
| 134 | 185 | if (timeoutInterval) clearInterval(timeoutInterval); | |
| 135 | 186 | if (buddyListInterval) clearInterval(buddyListInterval); | |
| 187 | + | if (connectionCleanupInterval) clearInterval(connectionCleanupInterval); | |
| 136 | 188 | process.exit(0); | |
| 137 | 189 | }); | |
| 138 | 190 | } | |
| @@ -160,29 +212,42 @@ export const GET: RequestHandler = async ({ request, locals, }) => { | |||
| 160 | 212 | ||
| 161 | 213 | const now = Date.now(); | |
| 162 | 214 | ||
| 163 | - | try { | |
| 164 | - | await db.update(users) | |
| 165 | - | .set({ | |
| 166 | - | status: 'online', | |
| 167 | - | lastSeen: now | |
| 168 | - | }) | |
| 169 | - | .where(eq(users.id, userId)); | |
| 170 | - | ||
| 171 | - | log.info('User status updated to online', { | |
| 172 | - | userId: `${userId.slice(0, 4)}...${userId.slice(-4)}`, | |
| 173 | - | timestamp: new Date(now).toISOString() | |
| 174 | - | }); | |
| 215 | + | // Only update status if this is a new connection | |
| 216 | + | const isNewConnection = sseConnectionTracker.addConnection(userId); | |
| 217 | + | ||
| 218 | + | if (isNewConnection) { | |
| 219 | + | try { | |
| 220 | + | await db.update(users) | |
| 221 | + | .set({ | |
| 222 | + | status: 'online', | |
| 223 | + | lastSeen: now | |
| 224 | + | }) | |
| 225 | + | .where(eq(users.id, userId)); | |
| 175 | 226 | ||
| 176 | - | sseEmitter.emit('sse', { | |
| 177 | - | type: 'userStatusUpdate', | |
| 178 | - | data: { | |
| 179 | - | userId, | |
| 180 | - | status: 'online', | |
| 181 | - | lastSeen: now | |
| 182 | - | } | |
| 227 | + | log.info('User status updated to online', { | |
| 228 | + | userId: `${userId.slice(0, 4)}...${userId.slice(-4)}`, | |
| 229 | + | timestamp: new Date(now).toISOString() | |
| 230 | + | }); | |
| 231 | + | ||
| 232 | + | // Invalidate buddy list cache when user status changes | |
| 233 | + | buddyListCache.invalidate(); | |
| 234 | + | ||
| 235 | + | sseEmitter.emit('sse', { | |
| 236 | + | type: 'userStatusUpdate', | |
| 237 | + | data: { | |
| 238 | + | userId, | |
| 239 | + | status: 'online', | |
| 240 | + | lastSeen: now | |
| 241 | + | } | |
| 242 | + | }); | |
| 243 | + | } catch (error) { | |
| 244 | + | log.error('Error updating user status', { error }); | |
| 245 | + | sseConnectionTracker.removeConnection(userId); | |
| 246 | + | } | |
| 247 | + | } else { | |
| 248 | + | log.debug('User already has active SSE connection, skipping status update', { | |
| 249 | + | userId: `${userId.slice(0, 4)}...${userId.slice(-4)}` | |
| 183 | 250 | }); | |
| 184 | - | } catch (error) { | |
| 185 | - | log.error('Error updating user status', { error }); | |
| 186 | 251 | } | |
| 187 | 252 | ||
| 188 | 253 | const stream = new ReadableStream({ | |
| @@ -225,19 +290,28 @@ export const GET: RequestHandler = async ({ request, locals, }) => { | |||
| 225 | 290 | const maskedUserId = `${userId.slice(0, 4)}...${userId.slice(-4)}`; | |
| 226 | 291 | log.info('Connection closed', { userId: maskedUserId, reason }); | |
| 227 | 292 | ||
| 228 | - | const now = Date.now(); | |
| 229 | - | await db.update(users) | |
| 230 | - | .set({ status: 'offline', lastSeen: now }) | |
| 231 | - | .where(eq(users.id, userId)); | |
| 293 | + | // Remove from connection tracker | |
| 294 | + | const wasTracked = sseConnectionTracker.removeConnection(userId); | |
| 232 | 295 | ||
| 233 | - | sseEmitter.emit('sse', { | |
| 234 | - | type: 'userStatusUpdate', | |
| 235 | - | data: { | |
| 236 | - | userId, | |
| 237 | - | status: 'offline', | |
| 238 | - | lastSeen: now | |
| 239 | - | } | |
| 240 | - | }); | |
| 296 | + | // Only update database if this was the last connection | |
| 297 | + | if (wasTracked && !sseConnectionTracker.hasConnection(userId)) { | |
| 298 | + | const now = Date.now(); | |
| 299 | + | await db.update(users) | |
| 300 | + | .set({ status: 'offline', lastSeen: now }) | |
| 301 | + | .where(eq(users.id, userId)); | |
| 302 | + | ||
| 303 | + | // Invalidate buddy list cache when user goes offline | |
| 304 | + | buddyListCache.invalidate(); | |
| 305 | + | ||
| 306 | + | sseEmitter.emit('sse', { | |
| 307 | + | type: 'userStatusUpdate', | |
| 308 | + | data: { | |
| 309 | + | userId, | |
| 310 | + | status: 'offline', | |
| 311 | + | lastSeen: now | |
| 312 | + | } | |
| 313 | + | }); | |
| 314 | + | } | |
| 241 | 315 | } | |
| 242 | 316 | }); | |
| 243 | 317 | ||
| @@ -8,9 +8,11 @@ import type { RequestHandler } from './$types'; | |||||
| 8 | import { sseEmitter } from '$lib/sseEmitter'; | 8 | import { sseEmitter } from '$lib/sseEmitter'; | ||
| 9 | import db from '$lib/db/db.server'; | 9 | import db from '$lib/db/db.server'; | ||
| 10 | import { users } from '$lib/db/schema'; | 10 | import { users } from '$lib/db/schema'; | ||
| 11 | - | import { eq, and, lt } from 'drizzle-orm'; | 11 | + | import { eq, and, lt, ne } from 'drizzle-orm'; |
| 12 | import { createSafeUser } from '$lib/types/chat'; | 12 | import { createSafeUser } from '$lib/types/chat'; | ||
| 13 | import { createLogger } from '$lib/utils/logger.server'; | 13 | import { createLogger } from '$lib/utils/logger.server'; | ||
| 14 | + | import { sseConnectionTracker } from '$lib/sseConnectionTracker'; | |||
| 15 | + | import { buddyListCache } from '$lib/buddyListCache'; | |||
| 14 | 16 | ||||
| 15 | const log = createLogger('sse-server'); | 17 | const log = createLogger('sse-server'); | ||
| 16 | 18 | ||||
| @@ -52,7 +54,7 @@ function startTimeoutCheck() { | |||||
| 52 | }) | 54 | }) | ||
| 53 | .where( | 55 | .where( | ||
| 54 | and( | 56 | and( | ||
| 55 | - | eq(users.status, 'online'), // Only update online users | 57 | + | ne(users.status, 'offline'), // Update any non-offline users |
| 56 | lt(users.lastSeen, timeoutThreshold) // Who haven't been seen recently | 58 | lt(users.lastSeen, timeoutThreshold) // Who haven't been seen recently | ||
| 57 | ) | 59 | ) | ||
| 58 | ) | 60 | ) | ||
| @@ -67,6 +69,9 @@ function startTimeoutCheck() { | |||||
| 67 | usersUpdated: result.length, | 69 | usersUpdated: result.length, | ||
| 68 | timeoutThreshold: new Date(timeoutThreshold).toISOString() | 70 | timeoutThreshold: new Date(timeoutThreshold).toISOString() | ||
| 69 | }); | 71 | }); | ||
| 72 | + | ||||
| 73 | + | // Invalidate buddy list cache when users timeout | |||
| 74 | + | buddyListCache.invalidate(); | |||
| 70 | } | 75 | } | ||
| 71 | } catch (error) { | 76 | } catch (error) { | ||
| 72 | log.error('Error during timeout check', { error }); | 77 | log.error('Error during timeout check', { error }); | ||
| @@ -84,19 +89,35 @@ function startBuddyListUpdateInterval() { | |||||
| 84 | const now = Date.now(); | 89 | const now = Date.now(); | ||
| 85 | // Only fetch and broadcast if 10 seconds have passed since last update | 90 | // Only fetch and broadcast if 10 seconds have passed since last update | ||
| 86 | if (now - lastBuddyListUpdate >= BUDDY_LIST_BROADCAST_INTERVAL) { | 91 | if (now - lastBuddyListUpdate >= BUDDY_LIST_BROADCAST_INTERVAL) { | ||
| 87 | - | log.debug('Broadcasting buddy list update...'); | 92 | + | // Only query database if cache is invalid or doesn't exist |
| 88 | - | // Query the complete buddy list from the database. | 93 | + | if (buddyListCache.needsRefresh()) { |
| 89 | - | const buddyList = await db.select().from(users); | 94 | + | log.debug('Fetching buddy list from database (cache miss)...'); |
| 90 | - | // Use createSafeUser to properly sanitize user data | 95 | + | // Query the complete buddy list from the database. |
| 91 | - | const safeBuddyList = buddyList.map(user => createSafeUser(user)); | 96 | + | const buddyList = await db.select().from(users); |
| 92 | - | 97 | + | // Use createSafeUser to properly sanitize user data | |
| 93 | - | // Only broadcast if data has changed | 98 | + | const safeBuddyList = buddyList.map(user => createSafeUser(user)); |
| 94 | - | if (JSON.stringify(safeBuddyList) !== JSON.stringify(lastBuddyListData)) { | 99 | + | |
| 95 | - | lastBuddyListData = safeBuddyList; | 100 | + | // Update cache and check if data changed |
| 96 | - | sseEmitter.emit('sse', { | 101 | + | const hasChanged = buddyListCache.update(safeBuddyList); |
| 97 | - | type: 'buddyListUpdate', | 102 | + | |
| 98 | - | data: safeBuddyList | 103 | + | if (hasChanged) { |
| 99 | - | }); | 104 | + | log.debug('Broadcasting buddy list update (data changed)...'); |
| 105 | + | sseEmitter.emit('sse', { | |||
| 106 | + | type: 'buddyListUpdate', | |||
| 107 | + | data: safeBuddyList | |||
| 108 | + | }); | |||
| 109 | + | } | |||
| 110 | + | } else { | |||
| 111 | + | // Use cached data | |||
| 112 | + | const cachedData = buddyListCache.get(); | |||
| 113 | + | if (cachedData && JSON.stringify(cachedData) !== JSON.stringify(lastBuddyListData)) { | |||
| 114 | + | log.debug('Broadcasting buddy list update from cache...'); | |||
| 115 | + | lastBuddyListData = cachedData; | |||
| 116 | + | sseEmitter.emit('sse', { | |||
| 117 | + | type: 'buddyListUpdate', | |||
| 118 | + | data: cachedData | |||
| 119 | + | }); | |||
| 120 | + | } | |||
| 100 | } | 121 | } | ||
| 101 | lastBuddyListUpdate = now; | 122 | lastBuddyListUpdate = now; | ||
| 102 | } | 123 | } | ||
| @@ -106,10 +127,38 @@ function startBuddyListUpdateInterval() { | |||||
| 106 | }, 1000); // Check every second but only broadcast every 10 seconds | 127 | }, 1000); // Check every second but only broadcast every 10 seconds | ||
| 107 | } | 128 | } | ||
| 108 | 129 | ||||
| 130 | + | // Add periodic cleanup for stale connections | |||
| 131 | + | let connectionCleanupInterval: ReturnType<typeof setInterval>; | |||
| 132 | + | ||||
| 133 | + | function startConnectionCleanup() { | |||
| 134 | + | if (connectionCleanupInterval) { | |||
| 135 | + | clearInterval(connectionCleanupInterval); | |||
| 136 | + | } | |||
| 137 | + | ||||
| 138 | + | connectionCleanupInterval = setInterval(() => { | |||
| 139 | + | const staleUserIds = sseConnectionTracker.cleanupStaleConnections(); | |||
| 140 | + | if (staleUserIds.length > 0) { | |||
| 141 | + | // Mark stale connections as offline | |||
| 142 | + | staleUserIds.forEach(async (userId) => { | |||
| 143 | + | try { | |||
| 144 | + | await db.update(users) | |||
| 145 | + | .set({ status: 'offline', lastSeen: Date.now() }) | |||
| 146 | + | .where(eq(users.id, userId)); | |||
| 147 | + | ||||
| 148 | + | buddyListCache.invalidate(); | |||
| 149 | + | } catch (error) { | |||
| 150 | + | log.error('Error updating stale user status', { userId, error }); | |||
| 151 | + | } | |||
| 152 | + | }); | |||
| 153 | + | } | |||
| 154 | + | }, 60000); // Run every minute | |||
| 155 | + | } | |||
| 156 | + | ||||
| 109 | // <<< NEW: Ensure intervals are started only once >>> | 157 | // <<< NEW: Ensure intervals are started only once >>> | ||
| 110 | if (!globalThis.__sseIntervalsStarted) { | 158 | if (!globalThis.__sseIntervalsStarted) { | ||
| 111 | startTimeoutCheck(); | 159 | startTimeoutCheck(); | ||
| 112 | startBuddyListUpdateInterval(); | 160 | startBuddyListUpdateInterval(); | ||
| 161 | + | startConnectionCleanup(); | |||
| 113 | globalThis.__sseIntervalsStarted = true; | 162 | globalThis.__sseIntervalsStarted = true; | ||
| 114 | log.info('Started global intervals for timeout and buddy list updates'); | 163 | log.info('Started global intervals for timeout and buddy list updates'); | ||
| 115 | } else { | 164 | } else { | ||
| @@ -121,18 +170,21 @@ if (typeof process !== 'undefined') { | |||||
| 121 | process.on('beforeExit', () => { | 170 | process.on('beforeExit', () => { | ||
| 122 | if (timeoutInterval) clearInterval(timeoutInterval); | 171 | if (timeoutInterval) clearInterval(timeoutInterval); | ||
| 123 | if (buddyListInterval) clearInterval(buddyListInterval); | 172 | if (buddyListInterval) clearInterval(buddyListInterval); | ||
| 173 | + | if (connectionCleanupInterval) clearInterval(connectionCleanupInterval); | |||
| 124 | }); | 174 | }); | ||
| 125 | 175 | ||||
| 126 | // Also handle SIGTERM and SIGINT | 176 | // Also handle SIGTERM and SIGINT | ||
| 127 | process.on('SIGTERM', () => { | 177 | process.on('SIGTERM', () => { | ||
| 128 | if (timeoutInterval) clearInterval(timeoutInterval); | 178 | if (timeoutInterval) clearInterval(timeoutInterval); | ||
| 129 | if (buddyListInterval) clearInterval(buddyListInterval); | 179 | if (buddyListInterval) clearInterval(buddyListInterval); | ||
| 180 | + | if (connectionCleanupInterval) clearInterval(connectionCleanupInterval); | |||
| 130 | process.exit(0); | 181 | process.exit(0); | ||
| 131 | }); | 182 | }); | ||
| 132 | 183 | ||||
| 133 | process.on('SIGINT', () => { | 184 | process.on('SIGINT', () => { | ||
| 134 | if (timeoutInterval) clearInterval(timeoutInterval); | 185 | if (timeoutInterval) clearInterval(timeoutInterval); | ||
| 135 | if (buddyListInterval) clearInterval(buddyListInterval); | 186 | if (buddyListInterval) clearInterval(buddyListInterval); | ||
| 187 | + | if (connectionCleanupInterval) clearInterval(connectionCleanupInterval); | |||
| 136 | process.exit(0); | 188 | process.exit(0); | ||
| 137 | }); | 189 | }); | ||
| 138 | } | 190 | } | ||
| @@ -160,29 +212,42 @@ export const GET: RequestHandler = async ({ request, locals, }) => { | |||||
| 160 | 212 | ||||
| 161 | const now = Date.now(); | 213 | const now = Date.now(); | ||
| 162 | 214 | ||||
| 163 | - | try { | 215 | + | // Only update status if this is a new connection |
| 164 | - | await db.update(users) | 216 | + | const isNewConnection = sseConnectionTracker.addConnection(userId); |
| 165 | - | .set({ | 217 | + | |
| 166 | - | status: 'online', | 218 | + | if (isNewConnection) { |
| 167 | - | lastSeen: now | 219 | + | try { |
| 168 | - | }) | 220 | + | await db.update(users) |
| 169 | - | .where(eq(users.id, userId)); | 221 | + | .set({ |
| 170 | - | 222 | + | status: 'online', | |
| 171 | - | log.info('User status updated to online', { | 223 | + | lastSeen: now |
| 172 | - | userId: `${userId.slice(0, 4)}...${userId.slice(-4)}`, | 224 | + | }) |
| 173 | - | timestamp: new Date(now).toISOString() | 225 | + | .where(eq(users.id, userId)); |
| 174 | - | }); | |||
| 175 | 226 | ||||
| 176 | - | sseEmitter.emit('sse', { | 227 | + | log.info('User status updated to online', { |
| 177 | - | type: 'userStatusUpdate', | 228 | + | userId: `${userId.slice(0, 4)}...${userId.slice(-4)}`, |
| 178 | - | data: { | 229 | + | timestamp: new Date(now).toISOString() |
| 179 | - | userId, | 230 | + | }); |
| 180 | - | status: 'online', | 231 | + | |
| 181 | - | lastSeen: now | 232 | + | // Invalidate buddy list cache when user status changes |
| 182 | - | } | 233 | + | buddyListCache.invalidate(); |
| 234 | + | ||||
| 235 | + | sseEmitter.emit('sse', { | |||
| 236 | + | type: 'userStatusUpdate', | |||
| 237 | + | data: { | |||
| 238 | + | userId, | |||
| 239 | + | status: 'online', | |||
| 240 | + | lastSeen: now | |||
| 241 | + | } | |||
| 242 | + | }); | |||
| 243 | + | } catch (error) { | |||
| 244 | + | log.error('Error updating user status', { error }); | |||
| 245 | + | sseConnectionTracker.removeConnection(userId); | |||
| 246 | + | } | |||
| 247 | + | } else { | |||
| 248 | + | log.debug('User already has active SSE connection, skipping status update', { | |||
| 249 | + | userId: `${userId.slice(0, 4)}...${userId.slice(-4)}` | |||
| 183 | }); | 250 | }); | ||
| 184 | - | } catch (error) { | |||
| 185 | - | log.error('Error updating user status', { error }); | |||
| 186 | } | 251 | } | ||
| 187 | 252 | ||||
| 188 | const stream = new ReadableStream({ | 253 | const stream = new ReadableStream({ | ||
| @@ -225,19 +290,28 @@ export const GET: RequestHandler = async ({ request, locals, }) => { | |||||
| 225 | const maskedUserId = `${userId.slice(0, 4)}...${userId.slice(-4)}`; | 290 | const maskedUserId = `${userId.slice(0, 4)}...${userId.slice(-4)}`; | ||
| 226 | log.info('Connection closed', { userId: maskedUserId, reason }); | 291 | log.info('Connection closed', { userId: maskedUserId, reason }); | ||
| 227 | 292 | ||||
| 228 | - | const now = Date.now(); | 293 | + | // Remove from connection tracker |
| 229 | - | await db.update(users) | 294 | + | const wasTracked = sseConnectionTracker.removeConnection(userId); |
| 230 | - | .set({ status: 'offline', lastSeen: now }) | |||
| 231 | - | .where(eq(users.id, userId)); | |||
| 232 | 295 | ||||
| 233 | - | sseEmitter.emit('sse', { | 296 | + | // Only update database if this was the last connection |
| 234 | - | type: 'userStatusUpdate', | 297 | + | if (wasTracked && !sseConnectionTracker.hasConnection(userId)) { |
| 235 | - | data: { | 298 | + | const now = Date.now(); |
| 236 | - | userId, | 299 | + | await db.update(users) |
| 237 | - | status: 'offline', | 300 | + | .set({ status: 'offline', lastSeen: now }) |
| 238 | - | lastSeen: now | 301 | + | .where(eq(users.id, userId)); |
| 239 | - | } | 302 | + | |
| 240 | - | }); | 303 | + | // Invalidate buddy list cache when user goes offline |
| 304 | + | buddyListCache.invalidate(); | |||
| 305 | + | ||||
| 306 | + | sseEmitter.emit('sse', { | |||
| 307 | + | type: 'userStatusUpdate', | |||
| 308 | + | data: { | |||
| 309 | + | userId, | |||
| 310 | + | status: 'offline', | |||
| 311 | + | lastSeen: now | |||
| 312 | + | } | |||
| 313 | + | }); | |||
| 314 | + | } | |||
| 241 | } | 315 | } | ||
| 242 | }); | 316 | }); | ||
| 243 | 317 | ||||
M src/routes/api/status/+server.ts +4 -0
| @@ -5,6 +5,7 @@ import { validateSessionToken, generateSessionToken, createSession } from '$lib/ | |||
| 5 | 5 | import { setSessionTokenCookie } from '$lib/api/session.cookie'; | |
| 6 | 6 | import { createSafeUser } from '$lib/types/chat'; | |
| 7 | 7 | import { createLogger } from '$lib/utils/logger.server'; | |
| 8 | + | import { buddyListCache } from '$lib/buddyListCache'; | |
| 8 | 9 | import type { RequestHandler } from './$types'; | |
| 9 | 10 | ||
| 10 | 11 | const log = createLogger('status-server'); | |
| @@ -46,6 +47,9 @@ export const POST: RequestHandler = async ({ request, cookies, locals }) => { | |||
| 46 | 47 | .returning() | |
| 47 | 48 | .get(); | |
| 48 | 49 | log.debug('Database updated successfully', { userId: maskedUserId, status }); | |
| 50 | + | ||
| 51 | + | // Invalidate buddy list cache when status changes | |
| 52 | + | buddyListCache.invalidate(); | |
| 49 | 53 | ||
| 50 | 54 | // Renew session if status is online and session expires in less than 1 day | |
| 51 | 55 | if (status === 'online') { | |
| @@ -5,6 +5,7 @@ import { validateSessionToken, generateSessionToken, createSession } from '$lib/ | |||||
| 5 | import { setSessionTokenCookie } from '$lib/api/session.cookie'; | 5 | import { setSessionTokenCookie } from '$lib/api/session.cookie'; | ||
| 6 | import { createSafeUser } from '$lib/types/chat'; | 6 | import { createSafeUser } from '$lib/types/chat'; | ||
| 7 | import { createLogger } from '$lib/utils/logger.server'; | 7 | import { createLogger } from '$lib/utils/logger.server'; | ||
| 8 | + | import { buddyListCache } from '$lib/buddyListCache'; | |||
| 8 | import type { RequestHandler } from './$types'; | 9 | import type { RequestHandler } from './$types'; | ||
| 9 | 10 | ||||
| 10 | const log = createLogger('status-server'); | 11 | const log = createLogger('status-server'); | ||
| @@ -46,6 +47,9 @@ export const POST: RequestHandler = async ({ request, cookies, locals }) => { | |||||
| 46 | .returning() | 47 | .returning() | ||
| 47 | .get(); | 48 | .get(); | ||
| 48 | log.debug('Database updated successfully', { userId: maskedUserId, status }); | 49 | log.debug('Database updated successfully', { userId: maskedUserId, status }); | ||
| 50 | + | ||||
| 51 | + | // Invalidate buddy list cache when status changes | |||
| 52 | + | buddyListCache.invalidate(); | |||
| 49 | 53 | ||||
| 50 | // Renew session if status is online and session expires in less than 1 day | 54 | // Renew session if status is online and session expires in less than 1 day | ||
| 51 | if (status === 'online') { | 55 | if (status === 'online') { | ||