refactor: Simplify server hooks and remove message caching infrastructure
- Remove message cache and initialization logic - Streamline server hooks with simplified request handling - Reduce complexity of rate limiting and authentication checks - Remove unnecessary cache-related code and dependencies - Improve server startup initialization process
- commit
- 840793da2a22b1599e2e963c74f84403dc2e1378
- author
- Kit · · 2025-02-15 17:24 UTC
- parent
- 65825d7
- signature
- SSH Unverified
SHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6TkThe key is unknown
4 files · +44 -398
M src/hooks.server.ts +27 -110
| @@ -1,140 +1,57 @@ | |||
| 1 | 1 | import type { Handle } from "@sveltejs/kit"; | |
| 2 | - | import { validateSessionToken } from "$lib/api/session.server"; | |
| 3 | - | import { setSessionTokenCookie, deleteSessionTokenCookie } from "$lib/api/session.cookie"; | |
| 4 | - | import { initializeDatabase } from "$lib/db/db.server"; | |
| 5 | 2 | import db from "$lib/db/db.server"; | |
| 6 | 3 | import { handleRateLimit } from "$lib/api/rate-limiter"; | |
| 7 | 4 | import { users } from "$lib/db/schema"; | |
| 8 | - | import { initializeMessageCache } from "$lib/cache/initialize-cache"; | |
| 9 | 5 | import { createLogger } from "$lib/utils/logger.server"; | |
| 10 | 6 | import { ensureDefaultChatRoom } from "$lib/utils/chat.server"; | |
| 11 | 7 | ||
| 12 | 8 | const log = createLogger('hooks-server'); | |
| 13 | 9 | ||
| 10 | + | // Set all users to offline on server start | |
| 14 | 11 | async function setAllUsersOffline() { | |
| 15 | 12 | try { | |
| 16 | - | log.info("Setting all users to offline status..."); | |
| 13 | + | const now = Date.now(); | |
| 17 | 14 | await db.update(users) | |
| 18 | - | .set({ | |
| 19 | - | status: "offline" | |
| 20 | - | }); | |
| 21 | - | log.info("Successfully set all users to offline status"); | |
| 22 | - | } catch (error) { | |
| 23 | - | log.error("Failed to set users offline", { error }); | |
| 15 | + | .set({ status: 'offline', lastSeen: now }); | |
| 16 | + | log.info('All users set to offline on server start'); | |
| 17 | + | } catch (error: unknown) { | |
| 18 | + | log.error('Failed to set users offline on server start:', { error }); | |
| 24 | 19 | } | |
| 25 | 20 | } | |
| 26 | 21 | ||
| 27 | - | // Initialize database when the server starts | |
| 28 | - | log.debug("Initializing database on server start..."); | |
| 29 | - | initializeDatabase() | |
| 22 | + | // Initialize server | |
| 23 | + | Promise.resolve() | |
| 30 | 24 | .then(async () => { | |
| 25 | + | log.info('Initializing server...'); | |
| 31 | 26 | await ensureDefaultChatRoom(db); | |
| 32 | 27 | await setAllUsersOffline(); | |
| 33 | - | await initializeMessageCache(); | |
| 28 | + | log.info('Server initialized successfully'); | |
| 34 | 29 | }) | |
| 35 | - | .catch(error => { | |
| 36 | - | log.error('Failed to initialize database', { error }); | |
| 30 | + | .catch((error: unknown) => { | |
| 31 | + | log.error('Failed to initialize server:', { error }); | |
| 32 | + | process.exit(1); | |
| 37 | 33 | }); | |
| 38 | 34 | ||
| 39 | 35 | export const handle: Handle = async ({ event, resolve }) => { | |
| 40 | 36 | log.debug("Handling request", { path: event.url.pathname }); | |
| 41 | 37 | ||
| 42 | 38 | // Identify public chat and room requests | |
| 43 | - | const isPublicChatRequest = | |
| 44 | - | event.url.pathname === '/api/chat/messages' && | |
| 45 | - | event.url.searchParams.get('public') === 'true' && | |
| 46 | - | event.request.method === 'GET'; | |
| 47 | - | const isPublicRoomRequest = | |
| 48 | - | event.url.pathname.startsWith('/api/rooms/') && | |
| 49 | - | event.url.searchParams.get('public') === 'true' && | |
| 50 | - | event.request.method === 'GET'; | |
| 51 | - | ||
| 52 | - | // Public routes that don't require authentication (removed '/api/sse' here) | |
| 53 | - | const publicRoutes = [ | |
| 54 | - | "/", | |
| 55 | - | "/api/session/login", | |
| 56 | - | "/api/session/validate", | |
| 57 | - | "/api/register", | |
| 58 | - | "/login", | |
| 59 | - | "/register" | |
| 60 | - | ]; | |
| 61 | - | ||
| 62 | - | // Get session token from cookies | |
| 63 | - | const token = event.cookies.get("session") ?? null; | |
| 64 | - | ||
| 65 | - | // Function to add debug headers for SSE requests | |
| 66 | - | const addDebugHeaders = (response: Response) => { | |
| 67 | - | if (event.url.pathname.startsWith('/api/sse')) { | |
| 68 | - | response.headers.set('X-Debug-Has-Token', Boolean(token).toString()); | |
| 69 | - | response.headers.set('X-Debug-Token-Length', token ? token.length.toString() : '0'); | |
| 70 | - | response.headers.set('X-Debug-Cookie-Count', event.cookies.getAll().length.toString()); | |
| 71 | - | } | |
| 72 | - | return response; | |
| 73 | - | }; | |
| 74 | - | ||
| 75 | - | // Handle public chat/room requests | |
| 76 | - | if (isPublicChatRequest || isPublicRoomRequest) { | |
| 77 | - | log.debug("Public request accessed", { path: event.url.pathname }); | |
| 78 | - | event.locals.user = null; | |
| 79 | - | event.locals.session = null; // Public access doesn't require a session | |
| 80 | - | const response = await resolve(event); | |
| 81 | - | return addDebugHeaders(response); | |
| 82 | - | } | |
| 83 | - | ||
| 84 | - | // Handle public routes: if token exists, validate and propagate | |
| 85 | - | if (publicRoutes.includes(event.url.pathname)) { | |
| 86 | - | log.debug("Public route accessed", { path: event.url.pathname }); | |
| 87 | - | if (token) { | |
| 88 | - | const { session, user } = await validateSessionToken(token); | |
| 89 | - | if (session !== null) { | |
| 90 | - | log.debug("Valid session found on public route", { user: user?.nickname }); | |
| 91 | - | setSessionTokenCookie(event, token, session.expiresAt); | |
| 92 | - | event.locals.session = session; | |
| 93 | - | event.locals.user = user; | |
| 94 | - | } | |
| 39 | + | const isPublicChatRequest = event.url.pathname.startsWith('/api/chat/messages') && event.request.method === 'GET'; | |
| 40 | + | const isPublicRoomRequest = event.url.pathname.startsWith('/api/rooms') && event.request.method === 'GET'; | |
| 41 | + | ||
| 42 | + | // Skip rate limiting for public endpoints | |
| 43 | + | if (!isPublicChatRequest && !isPublicRoomRequest) { | |
| 44 | + | const rateLimitResult = await handleRateLimit(event); | |
| 45 | + | if (rateLimitResult?.status === 429) { | |
| 46 | + | return new Response('Too Many Requests', { | |
| 47 | + | status: 429, | |
| 48 | + | headers: { | |
| 49 | + | 'Retry-After': '60' | |
| 50 | + | } | |
| 51 | + | }); | |
| 95 | 52 | } | |
| 96 | - | const response = await resolve(event); | |
| 97 | - | return addDebugHeaders(response); | |
| 98 | - | } | |
| 99 | - | ||
| 100 | - | // Protected routes: ensure authentication | |
| 101 | - | if (token === null) { | |
| 102 | - | log.debug("No session token found"); | |
| 103 | - | event.locals.user = null; | |
| 104 | - | event.locals.session = null; | |
| 105 | - | const response = new Response("Unauthorized", { status: 401 }); | |
| 106 | - | return addDebugHeaders(response); | |
| 107 | - | } | |
| 108 | - | ||
| 109 | - | const { session, user } = await validateSessionToken(token); | |
| 110 | - | if (session !== null) { | |
| 111 | - | log.debug("Valid session found", { user: user?.nickname }); | |
| 112 | - | setSessionTokenCookie(event, token, session.expiresAt); | |
| 113 | - | event.locals.session = session; | |
| 114 | - | event.locals.user = user; | |
| 115 | - | } else { | |
| 116 | - | log.debug("Invalid session token"); | |
| 117 | - | deleteSessionTokenCookie(event); | |
| 118 | - | event.locals.session = null; | |
| 119 | - | event.locals.user = null; | |
| 120 | - | const response = new Response("Unauthorized", { status: 401 }); | |
| 121 | - | return addDebugHeaders(response); | |
| 122 | - | } | |
| 123 | - | ||
| 124 | - | // After authentication, ensure SSE endpoint does not trigger rate limiting | |
| 125 | - | if (event.url.pathname.startsWith('/api/sse') && !event.locals.user) { | |
| 126 | - | log.debug("SSE endpoint accessed without valid session, returning 401 without rate limiting"); | |
| 127 | - | return addDebugHeaders(new Response("Unauthorized", { status: 401 })); | |
| 128 | 53 | } | |
| 129 | 54 | ||
| 130 | - | // For any request without an authenticated user, apply rate limiting | |
| 131 | - | if (!event.locals.user) { | |
| 132 | - | const rateLimitResponse = await handleRateLimit(event); | |
| 133 | - | if (rateLimitResponse) { | |
| 134 | - | return rateLimitResponse; | |
| 135 | - | } | |
| 136 | - | } | |
| 137 | - | ||
| 138 | 55 | const response = await resolve(event); | |
| 139 | - | return addDebugHeaders(response); | |
| 56 | + | return response; | |
| 140 | 57 | }; ⏎̸ | |
| @@ -1,140 +1,57 @@ | |||||
| 1 | import type { Handle } from "@sveltejs/kit"; | 1 | import type { Handle } from "@sveltejs/kit"; | ||
| 2 | - | import { validateSessionToken } from "$lib/api/session.server"; | |||
| 3 | - | import { setSessionTokenCookie, deleteSessionTokenCookie } from "$lib/api/session.cookie"; | |||
| 4 | - | import { initializeDatabase } from "$lib/db/db.server"; | |||
| 5 | import db from "$lib/db/db.server"; | 2 | import db from "$lib/db/db.server"; | ||
| 6 | import { handleRateLimit } from "$lib/api/rate-limiter"; | 3 | import { handleRateLimit } from "$lib/api/rate-limiter"; | ||
| 7 | import { users } from "$lib/db/schema"; | 4 | import { users } from "$lib/db/schema"; | ||
| 8 | - | import { initializeMessageCache } from "$lib/cache/initialize-cache"; | |||
| 9 | import { createLogger } from "$lib/utils/logger.server"; | 5 | import { createLogger } from "$lib/utils/logger.server"; | ||
| 10 | import { ensureDefaultChatRoom } from "$lib/utils/chat.server"; | 6 | import { ensureDefaultChatRoom } from "$lib/utils/chat.server"; | ||
| 11 | 7 | ||||
| 12 | const log = createLogger('hooks-server'); | 8 | const log = createLogger('hooks-server'); | ||
| 13 | 9 | ||||
| 10 | + | // Set all users to offline on server start | |||
| 14 | async function setAllUsersOffline() { | 11 | async function setAllUsersOffline() { | ||
| 15 | try { | 12 | try { | ||
| 16 | - | log.info("Setting all users to offline status..."); | 13 | + | const now = Date.now(); |
| 17 | await db.update(users) | 14 | await db.update(users) | ||
| 18 | - | .set({ | 15 | + | .set({ status: 'offline', lastSeen: now }); |
| 19 | - | status: "offline" | 16 | + | log.info('All users set to offline on server start'); |
| 20 | - | }); | 17 | + | } catch (error: unknown) { |
| 21 | - | log.info("Successfully set all users to offline status"); | 18 | + | log.error('Failed to set users offline on server start:', { error }); |
| 22 | - | } catch (error) { | |||
| 23 | - | log.error("Failed to set users offline", { error }); | |||
| 24 | } | 19 | } | ||
| 25 | } | 20 | } | ||
| 26 | 21 | ||||
| 27 | - | // Initialize database when the server starts | 22 | + | // Initialize server |
| 28 | - | log.debug("Initializing database on server start..."); | 23 | + | Promise.resolve() |
| 29 | - | initializeDatabase() | |||
| 30 | .then(async () => { | 24 | .then(async () => { | ||
| 25 | + | log.info('Initializing server...'); | |||
| 31 | await ensureDefaultChatRoom(db); | 26 | await ensureDefaultChatRoom(db); | ||
| 32 | await setAllUsersOffline(); | 27 | await setAllUsersOffline(); | ||
| 33 | - | await initializeMessageCache(); | 28 | + | log.info('Server initialized successfully'); |
| 34 | }) | 29 | }) | ||
| 35 | - | .catch(error => { | 30 | + | .catch((error: unknown) => { |
| 36 | - | log.error('Failed to initialize database', { error }); | 31 | + | log.error('Failed to initialize server:', { error }); |
| 32 | + | process.exit(1); | |||
| 37 | }); | 33 | }); | ||
| 38 | 34 | ||||
| 39 | export const handle: Handle = async ({ event, resolve }) => { | 35 | export const handle: Handle = async ({ event, resolve }) => { | ||
| 40 | log.debug("Handling request", { path: event.url.pathname }); | 36 | log.debug("Handling request", { path: event.url.pathname }); | ||
| 41 | 37 | ||||
| 42 | // Identify public chat and room requests | 38 | // Identify public chat and room requests | ||
| 43 | - | const isPublicChatRequest = | 39 | + | const isPublicChatRequest = event.url.pathname.startsWith('/api/chat/messages') && event.request.method === 'GET'; |
| 44 | - | event.url.pathname === '/api/chat/messages' && | 40 | + | const isPublicRoomRequest = event.url.pathname.startsWith('/api/rooms') && event.request.method === 'GET'; |
| 45 | - | event.url.searchParams.get('public') === 'true' && | 41 | + | |
| 46 | - | event.request.method === 'GET'; | 42 | + | // Skip rate limiting for public endpoints |
| 47 | - | const isPublicRoomRequest = | 43 | + | if (!isPublicChatRequest && !isPublicRoomRequest) { |
| 48 | - | event.url.pathname.startsWith('/api/rooms/') && | 44 | + | const rateLimitResult = await handleRateLimit(event); |
| 49 | - | event.url.searchParams.get('public') === 'true' && | 45 | + | if (rateLimitResult?.status === 429) { |
| 50 | - | event.request.method === 'GET'; | 46 | + | return new Response('Too Many Requests', { |
| 51 | - | 47 | + | status: 429, | |
| 52 | - | // Public routes that don't require authentication (removed '/api/sse' here) | 48 | + | headers: { |
| 53 | - | const publicRoutes = [ | 49 | + | 'Retry-After': '60' |
| 54 | - | "/", | 50 | + | } |
| 55 | - | "/api/session/login", | 51 | + | }); |
| 56 | - | "/api/session/validate", | |||
| 57 | - | "/api/register", | |||
| 58 | - | "/login", | |||
| 59 | - | "/register" | |||
| 60 | - | ]; | |||
| 61 | - | ||||
| 62 | - | // Get session token from cookies | |||
| 63 | - | const token = event.cookies.get("session") ?? null; | |||
| 64 | - | ||||
| 65 | - | // Function to add debug headers for SSE requests | |||
| 66 | - | const addDebugHeaders = (response: Response) => { | |||
| 67 | - | if (event.url.pathname.startsWith('/api/sse')) { | |||
| 68 | - | response.headers.set('X-Debug-Has-Token', Boolean(token).toString()); | |||
| 69 | - | response.headers.set('X-Debug-Token-Length', token ? token.length.toString() : '0'); | |||
| 70 | - | response.headers.set('X-Debug-Cookie-Count', event.cookies.getAll().length.toString()); | |||
| 71 | - | } | |||
| 72 | - | return response; | |||
| 73 | - | }; | |||
| 74 | - | ||||
| 75 | - | // Handle public chat/room requests | |||
| 76 | - | if (isPublicChatRequest || isPublicRoomRequest) { | |||
| 77 | - | log.debug("Public request accessed", { path: event.url.pathname }); | |||
| 78 | - | event.locals.user = null; | |||
| 79 | - | event.locals.session = null; // Public access doesn't require a session | |||
| 80 | - | const response = await resolve(event); | |||
| 81 | - | return addDebugHeaders(response); | |||
| 82 | - | } | |||
| 83 | - | ||||
| 84 | - | // Handle public routes: if token exists, validate and propagate | |||
| 85 | - | if (publicRoutes.includes(event.url.pathname)) { | |||
| 86 | - | log.debug("Public route accessed", { path: event.url.pathname }); | |||
| 87 | - | if (token) { | |||
| 88 | - | const { session, user } = await validateSessionToken(token); | |||
| 89 | - | if (session !== null) { | |||
| 90 | - | log.debug("Valid session found on public route", { user: user?.nickname }); | |||
| 91 | - | setSessionTokenCookie(event, token, session.expiresAt); | |||
| 92 | - | event.locals.session = session; | |||
| 93 | - | event.locals.user = user; | |||
| 94 | - | } | |||
| 95 | } | 52 | } | ||
| 96 | - | const response = await resolve(event); | |||
| 97 | - | return addDebugHeaders(response); | |||
| 98 | - | } | |||
| 99 | - | ||||
| 100 | - | // Protected routes: ensure authentication | |||
| 101 | - | if (token === null) { | |||
| 102 | - | log.debug("No session token found"); | |||
| 103 | - | event.locals.user = null; | |||
| 104 | - | event.locals.session = null; | |||
| 105 | - | const response = new Response("Unauthorized", { status: 401 }); | |||
| 106 | - | return addDebugHeaders(response); | |||
| 107 | - | } | |||
| 108 | - | ||||
| 109 | - | const { session, user } = await validateSessionToken(token); | |||
| 110 | - | if (session !== null) { | |||
| 111 | - | log.debug("Valid session found", { user: user?.nickname }); | |||
| 112 | - | setSessionTokenCookie(event, token, session.expiresAt); | |||
| 113 | - | event.locals.session = session; | |||
| 114 | - | event.locals.user = user; | |||
| 115 | - | } else { | |||
| 116 | - | log.debug("Invalid session token"); | |||
| 117 | - | deleteSessionTokenCookie(event); | |||
| 118 | - | event.locals.session = null; | |||
| 119 | - | event.locals.user = null; | |||
| 120 | - | const response = new Response("Unauthorized", { status: 401 }); | |||
| 121 | - | return addDebugHeaders(response); | |||
| 122 | - | } | |||
| 123 | - | ||||
| 124 | - | // After authentication, ensure SSE endpoint does not trigger rate limiting | |||
| 125 | - | if (event.url.pathname.startsWith('/api/sse') && !event.locals.user) { | |||
| 126 | - | log.debug("SSE endpoint accessed without valid session, returning 401 without rate limiting"); | |||
| 127 | - | return addDebugHeaders(new Response("Unauthorized", { status: 401 })); | |||
| 128 | } | 53 | } | ||
| 129 | 54 | ||||
| 130 | - | // For any request without an authenticated user, apply rate limiting | |||
| 131 | - | if (!event.locals.user) { | |||
| 132 | - | const rateLimitResponse = await handleRateLimit(event); | |||
| 133 | - | if (rateLimitResponse) { | |||
| 134 | - | return rateLimitResponse; | |||
| 135 | - | } | |||
| 136 | - | } | |||
| 137 | - | ||||
| 138 | const response = await resolve(event); | 55 | const response = await resolve(event); | ||
| 139 | - | return addDebugHeaders(response); | 56 | + | return response; |
| 140 | }; ⏎̸ | 57 | }; ⏎̸ | ||
D src/lib/cache/initialize-cache.ts +0 -33
| @@ -1,33 +0,0 @@ | |||
| 1 | - | import { messageCache } from '$lib/cache/message-cache'; | |
| 2 | - | import db from '$lib/db/db.server'; | |
| 3 | - | import { messages } from '$lib/db/schema'; | |
| 4 | - | import { desc } from 'drizzle-orm'; | |
| 5 | - | import { createLogger } from '$lib/utils/logger.server'; | |
| 6 | - | ||
| 7 | - | const log = createLogger('cache-initialize'); | |
| 8 | - | ||
| 9 | - | let isCacheInitialized = false; | |
| 10 | - | ||
| 11 | - | export async function initializeMessageCache(): Promise<void> { | |
| 12 | - | if (isCacheInitialized) { | |
| 13 | - | log.debug('[Cache] Message cache already initialized.'); | |
| 14 | - | return; | |
| 15 | - | } | |
| 16 | - | try { | |
| 17 | - | log.debug('[Cache] Initializing message cache with pre-population for public view.'); | |
| 18 | - | const fetchedMessages = await db.select() | |
| 19 | - | .from(messages) | |
| 20 | - | .orderBy(desc(messages.timestamp)) | |
| 21 | - | .limit(500); | |
| 22 | - | // Reverse to get messages in chronological order | |
| 23 | - | await messageCache.initialize(fetchedMessages.reverse()); | |
| 24 | - | isCacheInitialized = true; | |
| 25 | - | const stats = messageCache.getCacheStats(); | |
| 26 | - | log.debug('[Cache] Message cache initialized successfully for public view:', { | |
| 27 | - | roomCount: stats.roomCount, | |
| 28 | - | totalMessages: stats.totalMessages | |
| 29 | - | }); | |
| 30 | - | } catch (error) { | |
| 31 | - | log.error('[Cache] Failed to initialize message cache:', { error: error instanceof Error ? error.message : 'Unknown error' }); | |
| 32 | - | } | |
| 33 | - | } ⏎̸ | |
| @@ -1,33 +0,0 @@ | |||||
| 1 | - | import { messageCache } from '$lib/cache/message-cache'; | |||
| 2 | - | import db from '$lib/db/db.server'; | |||
| 3 | - | import { messages } from '$lib/db/schema'; | |||
| 4 | - | import { desc } from 'drizzle-orm'; | |||
| 5 | - | import { createLogger } from '$lib/utils/logger.server'; | |||
| 6 | - | ||||
| 7 | - | const log = createLogger('cache-initialize'); | |||
| 8 | - | ||||
| 9 | - | let isCacheInitialized = false; | |||
| 10 | - | ||||
| 11 | - | export async function initializeMessageCache(): Promise<void> { | |||
| 12 | - | if (isCacheInitialized) { | |||
| 13 | - | log.debug('[Cache] Message cache already initialized.'); | |||
| 14 | - | return; | |||
| 15 | - | } | |||
| 16 | - | try { | |||
| 17 | - | log.debug('[Cache] Initializing message cache with pre-population for public view.'); | |||
| 18 | - | const fetchedMessages = await db.select() | |||
| 19 | - | .from(messages) | |||
| 20 | - | .orderBy(desc(messages.timestamp)) | |||
| 21 | - | .limit(500); | |||
| 22 | - | // Reverse to get messages in chronological order | |||
| 23 | - | await messageCache.initialize(fetchedMessages.reverse()); | |||
| 24 | - | isCacheInitialized = true; | |||
| 25 | - | const stats = messageCache.getCacheStats(); | |||
| 26 | - | log.debug('[Cache] Message cache initialized successfully for public view:', { | |||
| 27 | - | roomCount: stats.roomCount, | |||
| 28 | - | totalMessages: stats.totalMessages | |||
| 29 | - | }); | |||
| 30 | - | } catch (error) { | |||
| 31 | - | log.error('[Cache] Failed to initialize message cache:', { error: error instanceof Error ? error.message : 'Unknown error' }); | |||
| 32 | - | } | |||
| 33 | - | } ⏎̸ | |||
D src/lib/cache/message-cache.ts +0 -183
| @@ -1,183 +0,0 @@ | |||
| 1 | - | import type { Message } from '$lib/types/chat'; | |
| 2 | - | import { DEFAULT_CHAT_ROOM_ID } from '$lib/utils/chat.server'; | |
| 3 | - | import { createLogger } from '$lib/utils/logger.server'; | |
| 4 | - | ||
| 5 | - | const log = createLogger('message-cache'); | |
| 6 | - | ||
| 7 | - | interface RoomCache { | |
| 8 | - | messages: Message[]; | |
| 9 | - | lastUpdated: number; | |
| 10 | - | } | |
| 11 | - | ||
| 12 | - | class MessageCache { | |
| 13 | - | private static instance: MessageCache; | |
| 14 | - | private rooms: Map<string, RoomCache>; | |
| 15 | - | private readonly maxMessagesPerRoom: number; | |
| 16 | - | private readonly maxRooms: number; | |
| 17 | - | private readonly cacheExpiryMs: number; | |
| 18 | - | private readonly publicMessageLimit: number; | |
| 19 | - | ||
| 20 | - | private constructor() { | |
| 21 | - | this.rooms = new Map(); | |
| 22 | - | this.maxMessagesPerRoom = 100; // Keep last 100 messages per room | |
| 23 | - | this.maxRooms = 100; // Maximum number of rooms to cache | |
| 24 | - | this.cacheExpiryMs = 30 * 60 * 1000; // 30 minutes cache expiry | |
| 25 | - | this.publicMessageLimit = 10; // Strict limit for public/non-auth users | |
| 26 | - | log.debug('[MessageCache] Initialized with settings:', { | |
| 27 | - | maxMessagesPerRoom: this.maxMessagesPerRoom, | |
| 28 | - | maxRooms: this.maxRooms, | |
| 29 | - | cacheExpiryMs: this.cacheExpiryMs, | |
| 30 | - | publicMessageLimit: this.publicMessageLimit | |
| 31 | - | }); | |
| 32 | - | } | |
| 33 | - | ||
| 34 | - | public static getInstance(): MessageCache { | |
| 35 | - | if (!MessageCache.instance) { | |
| 36 | - | MessageCache.instance = new MessageCache(); | |
| 37 | - | } | |
| 38 | - | return MessageCache.instance; | |
| 39 | - | } | |
| 40 | - | ||
| 41 | - | private cleanExpiredRooms(): void { | |
| 42 | - | const now = Date.now(); | |
| 43 | - | let expiredCount = 0; | |
| 44 | - | ||
| 45 | - | for (const [roomId, cache] of this.rooms.entries()) { | |
| 46 | - | if (now - cache.lastUpdated > this.cacheExpiryMs) { | |
| 47 | - | this.rooms.delete(roomId); | |
| 48 | - | expiredCount++; | |
| 49 | - | } | |
| 50 | - | } | |
| 51 | - | ||
| 52 | - | if (expiredCount > 0) { | |
| 53 | - | log.debug('[MessageCache] Cleaned expired rooms:', { expiredCount }); | |
| 54 | - | } | |
| 55 | - | } | |
| 56 | - | ||
| 57 | - | private ensureRoomCapacity(): void { | |
| 58 | - | // If we're over capacity, remove the least recently used rooms | |
| 59 | - | if (this.rooms.size > this.maxRooms) { | |
| 60 | - | const sortedRooms = Array.from(this.rooms.entries()) | |
| 61 | - | .sort(([, a], [, b]) => a.lastUpdated - b.lastUpdated); | |
| 62 | - | ||
| 63 | - | const toRemove = sortedRooms.slice(0, sortedRooms.length - this.maxRooms); | |
| 64 | - | for (const [roomId] of toRemove) { | |
| 65 | - | this.rooms.delete(roomId); | |
| 66 | - | } | |
| 67 | - | ||
| 68 | - | log.debug('[MessageCache] Removed LRU rooms:', { | |
| 69 | - | removedCount: toRemove.length, | |
| 70 | - | remainingRooms: this.rooms.size | |
| 71 | - | }); | |
| 72 | - | } | |
| 73 | - | } | |
| 74 | - | ||
| 75 | - | public async initialize(messages: Message[]): Promise<void> { | |
| 76 | - | log.debug('[MessageCache] Initializing with messages:', { messageCount: messages.length }); | |
| 77 | - | // Group messages by chat room | |
| 78 | - | const messagesByRoom = new Map<string, Message[]>(); | |
| 79 | - | ||
| 80 | - | for (const message of messages) { | |
| 81 | - | const roomMessages = messagesByRoom.get(message.chatRoomId) || []; | |
| 82 | - | roomMessages.push(message); | |
| 83 | - | messagesByRoom.set(message.chatRoomId, roomMessages); | |
| 84 | - | } | |
| 85 | - | ||
| 86 | - | const now = Date.now(); | |
| 87 | - | ||
| 88 | - | // Sort messages by timestamp and limit to maxMessagesPerRoom | |
| 89 | - | for (const [roomId, roomMessages] of messagesByRoom) { | |
| 90 | - | roomMessages.sort((a, b) => a.timestamp - b.timestamp); | |
| 91 | - | this.rooms.set(roomId, { | |
| 92 | - | messages: roomMessages.slice(-this.maxMessagesPerRoom), | |
| 93 | - | lastUpdated: now | |
| 94 | - | }); | |
| 95 | - | } | |
| 96 | - | ||
| 97 | - | this.ensureRoomCapacity(); | |
| 98 | - | log.debug('[MessageCache] Initialization complete:', { | |
| 99 | - | roomCount: this.rooms.size, | |
| 100 | - | totalMessages: messages.length | |
| 101 | - | }); | |
| 102 | - | } | |
| 103 | - | ||
| 104 | - | /** | |
| 105 | - | * Get messages for public/non-authenticated users with strict limit | |
| 106 | - | */ | |
| 107 | - | public getPublicMessages(roomId: string = DEFAULT_CHAT_ROOM_ID): Message[] { | |
| 108 | - | this.cleanExpiredRooms(); | |
| 109 | - | const cache = this.rooms.get(roomId); | |
| 110 | - | ||
| 111 | - | if (!cache) { | |
| 112 | - | log.debug('[MessageCache] No public messages found for room:', { roomId }); | |
| 113 | - | return []; | |
| 114 | - | } | |
| 115 | - | ||
| 116 | - | cache.lastUpdated = Date.now(); | |
| 117 | - | const messages = [...cache.messages]; | |
| 118 | - | return messages.slice(-this.publicMessageLimit); | |
| 119 | - | } | |
| 120 | - | ||
| 121 | - | public addMessage(message: Message): void { | |
| 122 | - | // Clean expired rooms periodically | |
| 123 | - | this.cleanExpiredRooms(); | |
| 124 | - | ||
| 125 | - | const roomCache = this.rooms.get(message.chatRoomId) || { | |
| 126 | - | messages: [], | |
| 127 | - | lastUpdated: Date.now() | |
| 128 | - | }; | |
| 129 | - | ||
| 130 | - | roomCache.messages.push(message); | |
| 131 | - | roomCache.lastUpdated = Date.now(); | |
| 132 | - | ||
| 133 | - | // Keep only the last maxMessagesPerRoom messages | |
| 134 | - | if (roomCache.messages.length > this.maxMessagesPerRoom) { | |
| 135 | - | roomCache.messages.shift(); // Remove oldest message | |
| 136 | - | } | |
| 137 | - | ||
| 138 | - | this.rooms.set(message.chatRoomId, roomCache); | |
| 139 | - | this.ensureRoomCapacity(); | |
| 140 | - | ||
| 141 | - | log.debug('[MessageCache] Added message to cache:', { | |
| 142 | - | messageId: message.id, | |
| 143 | - | roomId: message.chatRoomId, | |
| 144 | - | roomMessageCount: roomCache.messages.length, | |
| 145 | - | totalRooms: this.rooms.size | |
| 146 | - | }); | |
| 147 | - | } | |
| 148 | - | ||
| 149 | - | public getRoomMessageCount(roomId: string): number { | |
| 150 | - | return this.rooms.get(roomId)?.messages.length || 0; | |
| 151 | - | } | |
| 152 | - | ||
| 153 | - | public clear(): void { | |
| 154 | - | this.rooms.clear(); | |
| 155 | - | log.debug('[MessageCache] Cache cleared'); | |
| 156 | - | } | |
| 157 | - | ||
| 158 | - | public getCacheStats(): { | |
| 159 | - | roomCount: number; | |
| 160 | - | totalMessages: number; | |
| 161 | - | roomStats: { roomId: string; messageCount: number; lastUpdated: number }[]; | |
| 162 | - | } { | |
| 163 | - | let totalMessages = 0; | |
| 164 | - | const roomStats = []; | |
| 165 | - | ||
| 166 | - | for (const [roomId, cache] of this.rooms.entries()) { | |
| 167 | - | totalMessages += cache.messages.length; | |
| 168 | - | roomStats.push({ | |
| 169 | - | roomId, | |
| 170 | - | messageCount: cache.messages.length, | |
| 171 | - | lastUpdated: cache.lastUpdated | |
| 172 | - | }); | |
| 173 | - | } | |
| 174 | - | ||
| 175 | - | return { | |
| 176 | - | roomCount: this.rooms.size, | |
| 177 | - | totalMessages, | |
| 178 | - | roomStats: roomStats.sort((a, b) => b.lastUpdated - a.lastUpdated) | |
| 179 | - | }; | |
| 180 | - | } | |
| 181 | - | } | |
| 182 | - | ||
| 183 | - | export const messageCache = MessageCache.getInstance(); ⏎̸ | |
| @@ -1,183 +0,0 @@ | |||||
| 1 | - | import type { Message } from '$lib/types/chat'; | |||
| 2 | - | import { DEFAULT_CHAT_ROOM_ID } from '$lib/utils/chat.server'; | |||
| 3 | - | import { createLogger } from '$lib/utils/logger.server'; | |||
| 4 | - | ||||
| 5 | - | const log = createLogger('message-cache'); | |||
| 6 | - | ||||
| 7 | - | interface RoomCache { | |||
| 8 | - | messages: Message[]; | |||
| 9 | - | lastUpdated: number; | |||
| 10 | - | } | |||
| 11 | - | ||||
| 12 | - | class MessageCache { | |||
| 13 | - | private static instance: MessageCache; | |||
| 14 | - | private rooms: Map<string, RoomCache>; | |||
| 15 | - | private readonly maxMessagesPerRoom: number; | |||
| 16 | - | private readonly maxRooms: number; | |||
| 17 | - | private readonly cacheExpiryMs: number; | |||
| 18 | - | private readonly publicMessageLimit: number; | |||
| 19 | - | ||||
| 20 | - | private constructor() { | |||
| 21 | - | this.rooms = new Map(); | |||
| 22 | - | this.maxMessagesPerRoom = 100; // Keep last 100 messages per room | |||
| 23 | - | this.maxRooms = 100; // Maximum number of rooms to cache | |||
| 24 | - | this.cacheExpiryMs = 30 * 60 * 1000; // 30 minutes cache expiry | |||
| 25 | - | this.publicMessageLimit = 10; // Strict limit for public/non-auth users | |||
| 26 | - | log.debug('[MessageCache] Initialized with settings:', { | |||
| 27 | - | maxMessagesPerRoom: this.maxMessagesPerRoom, | |||
| 28 | - | maxRooms: this.maxRooms, | |||
| 29 | - | cacheExpiryMs: this.cacheExpiryMs, | |||
| 30 | - | publicMessageLimit: this.publicMessageLimit | |||
| 31 | - | }); | |||
| 32 | - | } | |||
| 33 | - | ||||
| 34 | - | public static getInstance(): MessageCache { | |||
| 35 | - | if (!MessageCache.instance) { | |||
| 36 | - | MessageCache.instance = new MessageCache(); | |||
| 37 | - | } | |||
| 38 | - | return MessageCache.instance; | |||
| 39 | - | } | |||
| 40 | - | ||||
| 41 | - | private cleanExpiredRooms(): void { | |||
| 42 | - | const now = Date.now(); | |||
| 43 | - | let expiredCount = 0; | |||
| 44 | - | ||||
| 45 | - | for (const [roomId, cache] of this.rooms.entries()) { | |||
| 46 | - | if (now - cache.lastUpdated > this.cacheExpiryMs) { | |||
| 47 | - | this.rooms.delete(roomId); | |||
| 48 | - | expiredCount++; | |||
| 49 | - | } | |||
| 50 | - | } | |||
| 51 | - | ||||
| 52 | - | if (expiredCount > 0) { | |||
| 53 | - | log.debug('[MessageCache] Cleaned expired rooms:', { expiredCount }); | |||
| 54 | - | } | |||
| 55 | - | } | |||
| 56 | - | ||||
| 57 | - | private ensureRoomCapacity(): void { | |||
| 58 | - | // If we're over capacity, remove the least recently used rooms | |||
| 59 | - | if (this.rooms.size > this.maxRooms) { | |||
| 60 | - | const sortedRooms = Array.from(this.rooms.entries()) | |||
| 61 | - | .sort(([, a], [, b]) => a.lastUpdated - b.lastUpdated); | |||
| 62 | - | ||||
| 63 | - | const toRemove = sortedRooms.slice(0, sortedRooms.length - this.maxRooms); | |||
| 64 | - | for (const [roomId] of toRemove) { | |||
| 65 | - | this.rooms.delete(roomId); | |||
| 66 | - | } | |||
| 67 | - | ||||
| 68 | - | log.debug('[MessageCache] Removed LRU rooms:', { | |||
| 69 | - | removedCount: toRemove.length, | |||
| 70 | - | remainingRooms: this.rooms.size | |||
| 71 | - | }); | |||
| 72 | - | } | |||
| 73 | - | } | |||
| 74 | - | ||||
| 75 | - | public async initialize(messages: Message[]): Promise<void> { | |||
| 76 | - | log.debug('[MessageCache] Initializing with messages:', { messageCount: messages.length }); | |||
| 77 | - | // Group messages by chat room | |||
| 78 | - | const messagesByRoom = new Map<string, Message[]>(); | |||
| 79 | - | ||||
| 80 | - | for (const message of messages) { | |||
| 81 | - | const roomMessages = messagesByRoom.get(message.chatRoomId) || []; | |||
| 82 | - | roomMessages.push(message); | |||
| 83 | - | messagesByRoom.set(message.chatRoomId, roomMessages); | |||
| 84 | - | } | |||
| 85 | - | ||||
| 86 | - | const now = Date.now(); | |||
| 87 | - | ||||
| 88 | - | // Sort messages by timestamp and limit to maxMessagesPerRoom | |||
| 89 | - | for (const [roomId, roomMessages] of messagesByRoom) { | |||
| 90 | - | roomMessages.sort((a, b) => a.timestamp - b.timestamp); | |||
| 91 | - | this.rooms.set(roomId, { | |||
| 92 | - | messages: roomMessages.slice(-this.maxMessagesPerRoom), | |||
| 93 | - | lastUpdated: now | |||
| 94 | - | }); | |||
| 95 | - | } | |||
| 96 | - | ||||
| 97 | - | this.ensureRoomCapacity(); | |||
| 98 | - | log.debug('[MessageCache] Initialization complete:', { | |||
| 99 | - | roomCount: this.rooms.size, | |||
| 100 | - | totalMessages: messages.length | |||
| 101 | - | }); | |||
| 102 | - | } | |||
| 103 | - | ||||
| 104 | - | /** | |||
| 105 | - | * Get messages for public/non-authenticated users with strict limit | |||
| 106 | - | */ | |||
| 107 | - | public getPublicMessages(roomId: string = DEFAULT_CHAT_ROOM_ID): Message[] { | |||
| 108 | - | this.cleanExpiredRooms(); | |||
| 109 | - | const cache = this.rooms.get(roomId); | |||
| 110 | - | ||||
| 111 | - | if (!cache) { | |||
| 112 | - | log.debug('[MessageCache] No public messages found for room:', { roomId }); | |||
| 113 | - | return []; | |||
| 114 | - | } | |||
| 115 | - | ||||
| 116 | - | cache.lastUpdated = Date.now(); | |||
| 117 | - | const messages = [...cache.messages]; | |||
| 118 | - | return messages.slice(-this.publicMessageLimit); | |||
| 119 | - | } | |||
| 120 | - | ||||
| 121 | - | public addMessage(message: Message): void { | |||
| 122 | - | // Clean expired rooms periodically | |||
| 123 | - | this.cleanExpiredRooms(); | |||
| 124 | - | ||||
| 125 | - | const roomCache = this.rooms.get(message.chatRoomId) || { | |||
| 126 | - | messages: [], | |||
| 127 | - | lastUpdated: Date.now() | |||
| 128 | - | }; | |||
| 129 | - | ||||
| 130 | - | roomCache.messages.push(message); | |||
| 131 | - | roomCache.lastUpdated = Date.now(); | |||
| 132 | - | ||||
| 133 | - | // Keep only the last maxMessagesPerRoom messages | |||
| 134 | - | if (roomCache.messages.length > this.maxMessagesPerRoom) { | |||
| 135 | - | roomCache.messages.shift(); // Remove oldest message | |||
| 136 | - | } | |||
| 137 | - | ||||
| 138 | - | this.rooms.set(message.chatRoomId, roomCache); | |||
| 139 | - | this.ensureRoomCapacity(); | |||
| 140 | - | ||||
| 141 | - | log.debug('[MessageCache] Added message to cache:', { | |||
| 142 | - | messageId: message.id, | |||
| 143 | - | roomId: message.chatRoomId, | |||
| 144 | - | roomMessageCount: roomCache.messages.length, | |||
| 145 | - | totalRooms: this.rooms.size | |||
| 146 | - | }); | |||
| 147 | - | } | |||
| 148 | - | ||||
| 149 | - | public getRoomMessageCount(roomId: string): number { | |||
| 150 | - | return this.rooms.get(roomId)?.messages.length || 0; | |||
| 151 | - | } | |||
| 152 | - | ||||
| 153 | - | public clear(): void { | |||
| 154 | - | this.rooms.clear(); | |||
| 155 | - | log.debug('[MessageCache] Cache cleared'); | |||
| 156 | - | } | |||
| 157 | - | ||||
| 158 | - | public getCacheStats(): { | |||
| 159 | - | roomCount: number; | |||
| 160 | - | totalMessages: number; | |||
| 161 | - | roomStats: { roomId: string; messageCount: number; lastUpdated: number }[]; | |||
| 162 | - | } { | |||
| 163 | - | let totalMessages = 0; | |||
| 164 | - | const roomStats = []; | |||
| 165 | - | ||||
| 166 | - | for (const [roomId, cache] of this.rooms.entries()) { | |||
| 167 | - | totalMessages += cache.messages.length; | |||
| 168 | - | roomStats.push({ | |||
| 169 | - | roomId, | |||
| 170 | - | messageCount: cache.messages.length, | |||
| 171 | - | lastUpdated: cache.lastUpdated | |||
| 172 | - | }); | |||
| 173 | - | } | |||
| 174 | - | ||||
| 175 | - | return { | |||
| 176 | - | roomCount: this.rooms.size, | |||
| 177 | - | totalMessages, | |||
| 178 | - | roomStats: roomStats.sort((a, b) => b.lastUpdated - a.lastUpdated) | |||
| 179 | - | }; | |||
| 180 | - | } | |||
| 181 | - | } | |||
| 182 | - | ||||
| 183 | - | export const messageCache = MessageCache.getInstance(); ⏎̸ | |||
M src/routes/api/chat/messages/+server.ts +17 -72
| @@ -13,7 +13,6 @@ import { sseEmitter } from '$lib/sseEmitter'; | |||
| 13 | 13 | import { chatRooms } from '$lib/db/schema'; | |
| 14 | 14 | import { users } from '$lib/db/schema'; | |
| 15 | 15 | import { DEFAULT_CHAT_ROOM_ID } from '$lib/utils/chat.server'; | |
| 16 | - | import { messageCache } from '$lib/cache/message-cache'; | |
| 17 | 16 | import { createLogger } from '$lib/utils/logger.server'; | |
| 18 | 17 | import { SQL } from 'drizzle-orm'; | |
| 19 | 18 | ||
| @@ -51,82 +50,30 @@ function updateUserCooldown(userId: string): { canSend: boolean; retryAfter?: nu | |||
| 51 | 50 | return { canSend: true }; | |
| 52 | 51 | } | |
| 53 | 52 | ||
| 54 | - | // Initialize message cache on server start | |
| 55 | - | let isCacheInitialized = false; | |
| 56 | - | async function initializeCache() { | |
| 57 | - | if (isCacheInitialized) return; | |
| 58 | - | ||
| 59 | - | try { | |
| 60 | - | log.info('Initializing message cache'); | |
| 61 | - | // Fetch messages from all rooms, limited to last 100 per room | |
| 62 | - | const fetchedMessages = await db.select() | |
| 63 | - | .from(messages) | |
| 64 | - | .orderBy(desc(messages.timestamp)) | |
| 65 | - | .limit(500); // Increased limit to accommodate multiple rooms | |
| 66 | - | ||
| 67 | - | await messageCache.initialize(fetchedMessages.reverse()); | |
| 68 | - | isCacheInitialized = true; | |
| 69 | - | ||
| 70 | - | // Log cache stats after initialization | |
| 71 | - | const stats = messageCache.getCacheStats(); | |
| 72 | - | log.info('Message cache initialized successfully', { | |
| 73 | - | roomCount: stats.roomCount, | |
| 74 | - | totalMessages: stats.totalMessages | |
| 75 | - | }); | |
| 76 | - | } catch (error) { | |
| 77 | - | log.error('Failed to initialize message cache', { error }); | |
| 78 | - | // Don't set isCacheInitialized to true on error | |
| 79 | - | } | |
| 80 | - | } | |
| 81 | - | ||
| 82 | - | // GET endpoint: fetch messages from cache or DB | |
| 53 | + | // GET endpoint: fetch messages directly from DB | |
| 83 | 54 | export async function GET({ request, locals }) { | |
| 84 | 55 | const url = new URL(request.url); | |
| 85 | 56 | const beforeTimestamp = url.searchParams.get('before'); | |
| 86 | 57 | const roomId = url.searchParams.get('roomId') || DEFAULT_CHAT_ROOM_ID; | |
| 87 | - | let fetchedMessages: Message[] = []; | |
| 88 | 58 | ||
| 89 | 59 | try { | |
| 90 | - | // For public users (non-authenticated), attempt to fetch messages from cache if no pagination is requested | |
| 91 | - | if (!locals.session && !beforeTimestamp) { | |
| 92 | - | if (!isCacheInitialized) { | |
| 93 | - | await initializeCache(); | |
| 94 | - | } | |
| 95 | - | log.debug('Attempting to fetch messages from cache for public view', { roomId }); | |
| 96 | - | const cachedMessages = messageCache.getPublicMessages(roomId); | |
| 97 | - | if (cachedMessages && cachedMessages.length > 0) { | |
| 98 | - | log.debug('Messages found in cache for public view', { count: cachedMessages.length, roomId }); | |
| 99 | - | fetchedMessages = cachedMessages; | |
| 100 | - | } | |
| 60 | + | log.debug('Fetching messages from database', { beforeTimestamp, roomId, isAuthenticated: !!locals.session }); | |
| 61 | + | let conditions = eq(messages.chatRoomId, roomId) as SQL<unknown>; | |
| 62 | + | ||
| 63 | + | if (beforeTimestamp) { | |
| 64 | + | conditions = and( | |
| 65 | + | conditions, | |
| 66 | + | lt(messages.timestamp, parseInt(beforeTimestamp)) | |
| 67 | + | ) as SQL<unknown>; | |
| 101 | 68 | } | |
| 102 | 69 | ||
| 103 | - | // If no cached messages or pagination requested, fetch from DB | |
| 104 | - | if (fetchedMessages.length === 0 || beforeTimestamp) { | |
| 105 | - | log.debug('Fetching messages from database', { beforeTimestamp, roomId, isAuthenticated: !!locals.session }); | |
| 106 | - | let conditions = eq(messages.chatRoomId, roomId) as SQL<unknown>; | |
| 107 | - | ||
| 108 | - | if (beforeTimestamp) { | |
| 109 | - | conditions = and( | |
| 110 | - | conditions, | |
| 111 | - | lt(messages.timestamp, parseInt(beforeTimestamp)) | |
| 112 | - | ) as SQL<unknown>; | |
| 113 | - | } | |
| 114 | - | ||
| 115 | - | // Build and execute query with conditions | |
| 116 | - | const fetchLimit = locals.session ? 100 : 10; | |
| 117 | - | fetchedMessages = await db.select() | |
| 118 | - | .from(messages) | |
| 119 | - | .where(conditions) | |
| 120 | - | .orderBy(desc(messages.timestamp)) | |
| 121 | - | .limit(fetchLimit); | |
| 122 | - | ||
| 123 | - | // For public users, update cache with new messages if not paginating | |
| 124 | - | if (!locals.session && !beforeTimestamp && fetchedMessages.length > 0) { | |
| 125 | - | for (const msg of fetchedMessages) { | |
| 126 | - | messageCache.addMessage(msg); | |
| 127 | - | } | |
| 128 | - | } | |
| 129 | - | } | |
| 70 | + | // Fetch messages with a limit based on authentication status | |
| 71 | + | const fetchLimit = locals.session ? 100 : 10; | |
| 72 | + | const fetchedMessages = await db.select() | |
| 73 | + | .from(messages) | |
| 74 | + | .where(conditions) | |
| 75 | + | .orderBy(desc(messages.timestamp)) | |
| 76 | + | .limit(fetchLimit); | |
| 130 | 77 | ||
| 131 | 78 | const response: GetMessagesResponse = { | |
| 132 | 79 | success: true, | |
| @@ -237,9 +184,7 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||
| 237 | 184 | await db.insert(messages).values(newMessage); | |
| 238 | 185 | log.debug('Message saved in DB', { messageId: newMessage.id, chatRoomId: newMessage.chatRoomId, type: newMessage.type, timestamp: newMessage.timestamp }); | |
| 239 | 186 | ||
| 240 | - | // Removed updating message cache for authenticated users; relying on SSE for real-time updates | |
| 241 | - | ||
| 242 | - | // Broadcast the new message via the unified SSE emitter | |
| 187 | + | // Broadcast the new message via SSE | |
| 243 | 188 | sseEmitter.emit('sse', { type: 'chatMessage', data: newMessage }); | |
| 244 | 189 | ||
| 245 | 190 | log.debug('Message processed successfully', { messageId: newMessage.id, userId: `${newMessage.senderId.slice(0, 4)}...${newMessage.senderId.slice(-4)}`, roomId: newMessage.chatRoomId }); | |
| @@ -13,7 +13,6 @@ import { sseEmitter } from '$lib/sseEmitter'; | |||||
| 13 | import { chatRooms } from '$lib/db/schema'; | 13 | import { chatRooms } from '$lib/db/schema'; | ||
| 14 | import { users } from '$lib/db/schema'; | 14 | import { users } from '$lib/db/schema'; | ||
| 15 | import { DEFAULT_CHAT_ROOM_ID } from '$lib/utils/chat.server'; | 15 | import { DEFAULT_CHAT_ROOM_ID } from '$lib/utils/chat.server'; | ||
| 16 | - | import { messageCache } from '$lib/cache/message-cache'; | |||
| 17 | import { createLogger } from '$lib/utils/logger.server'; | 16 | import { createLogger } from '$lib/utils/logger.server'; | ||
| 18 | import { SQL } from 'drizzle-orm'; | 17 | import { SQL } from 'drizzle-orm'; | ||
| 19 | 18 | ||||
| @@ -51,82 +50,30 @@ function updateUserCooldown(userId: string): { canSend: boolean; retryAfter?: nu | |||||
| 51 | return { canSend: true }; | 50 | return { canSend: true }; | ||
| 52 | } | 51 | } | ||
| 53 | 52 | ||||
| 54 | - | // Initialize message cache on server start | 53 | + | // GET endpoint: fetch messages directly from DB |
| 55 | - | let isCacheInitialized = false; | |||
| 56 | - | async function initializeCache() { | |||
| 57 | - | if (isCacheInitialized) return; | |||
| 58 | - | ||||
| 59 | - | try { | |||
| 60 | - | log.info('Initializing message cache'); | |||
| 61 | - | // Fetch messages from all rooms, limited to last 100 per room | |||
| 62 | - | const fetchedMessages = await db.select() | |||
| 63 | - | .from(messages) | |||
| 64 | - | .orderBy(desc(messages.timestamp)) | |||
| 65 | - | .limit(500); // Increased limit to accommodate multiple rooms | |||
| 66 | - | ||||
| 67 | - | await messageCache.initialize(fetchedMessages.reverse()); | |||
| 68 | - | isCacheInitialized = true; | |||
| 69 | - | ||||
| 70 | - | // Log cache stats after initialization | |||
| 71 | - | const stats = messageCache.getCacheStats(); | |||
| 72 | - | log.info('Message cache initialized successfully', { | |||
| 73 | - | roomCount: stats.roomCount, | |||
| 74 | - | totalMessages: stats.totalMessages | |||
| 75 | - | }); | |||
| 76 | - | } catch (error) { | |||
| 77 | - | log.error('Failed to initialize message cache', { error }); | |||
| 78 | - | // Don't set isCacheInitialized to true on error | |||
| 79 | - | } | |||
| 80 | - | } | |||
| 81 | - | ||||
| 82 | - | // GET endpoint: fetch messages from cache or DB | |||
| 83 | export async function GET({ request, locals }) { | 54 | export async function GET({ request, locals }) { | ||
| 84 | const url = new URL(request.url); | 55 | const url = new URL(request.url); | ||
| 85 | const beforeTimestamp = url.searchParams.get('before'); | 56 | const beforeTimestamp = url.searchParams.get('before'); | ||
| 86 | const roomId = url.searchParams.get('roomId') || DEFAULT_CHAT_ROOM_ID; | 57 | const roomId = url.searchParams.get('roomId') || DEFAULT_CHAT_ROOM_ID; | ||
| 87 | - | let fetchedMessages: Message[] = []; | |||
| 88 | 58 | ||||
| 89 | try { | 59 | try { | ||
| 90 | - | // For public users (non-authenticated), attempt to fetch messages from cache if no pagination is requested | 60 | + | log.debug('Fetching messages from database', { beforeTimestamp, roomId, isAuthenticated: !!locals.session }); |
| 91 | - | if (!locals.session && !beforeTimestamp) { | 61 | + | let conditions = eq(messages.chatRoomId, roomId) as SQL<unknown>; |
| 92 | - | if (!isCacheInitialized) { | 62 | + | |
| 93 | - | await initializeCache(); | 63 | + | if (beforeTimestamp) { |
| 94 | - | } | 64 | + | conditions = and( |
| 95 | - | log.debug('Attempting to fetch messages from cache for public view', { roomId }); | 65 | + | conditions, |
| 96 | - | const cachedMessages = messageCache.getPublicMessages(roomId); | 66 | + | lt(messages.timestamp, parseInt(beforeTimestamp)) |
| 97 | - | if (cachedMessages && cachedMessages.length > 0) { | 67 | + | ) as SQL<unknown>; |
| 98 | - | log.debug('Messages found in cache for public view', { count: cachedMessages.length, roomId }); | |||
| 99 | - | fetchedMessages = cachedMessages; | |||
| 100 | - | } | |||
| 101 | } | 68 | } | ||
| 102 | 69 | ||||
| 103 | - | // If no cached messages or pagination requested, fetch from DB | 70 | + | // Fetch messages with a limit based on authentication status |
| 104 | - | if (fetchedMessages.length === 0 || beforeTimestamp) { | 71 | + | const fetchLimit = locals.session ? 100 : 10; |
| 105 | - | log.debug('Fetching messages from database', { beforeTimestamp, roomId, isAuthenticated: !!locals.session }); | 72 | + | const fetchedMessages = await db.select() |
| 106 | - | let conditions = eq(messages.chatRoomId, roomId) as SQL<unknown>; | 73 | + | .from(messages) |
| 107 | - | 74 | + | .where(conditions) | |
| 108 | - | if (beforeTimestamp) { | 75 | + | .orderBy(desc(messages.timestamp)) |
| 109 | - | conditions = and( | 76 | + | .limit(fetchLimit); |
| 110 | - | conditions, | |||
| 111 | - | lt(messages.timestamp, parseInt(beforeTimestamp)) | |||
| 112 | - | ) as SQL<unknown>; | |||
| 113 | - | } | |||
| 114 | - | ||||
| 115 | - | // Build and execute query with conditions | |||
| 116 | - | const fetchLimit = locals.session ? 100 : 10; | |||
| 117 | - | fetchedMessages = await db.select() | |||
| 118 | - | .from(messages) | |||
| 119 | - | .where(conditions) | |||
| 120 | - | .orderBy(desc(messages.timestamp)) | |||
| 121 | - | .limit(fetchLimit); | |||
| 122 | - | ||||
| 123 | - | // For public users, update cache with new messages if not paginating | |||
| 124 | - | if (!locals.session && !beforeTimestamp && fetchedMessages.length > 0) { | |||
| 125 | - | for (const msg of fetchedMessages) { | |||
| 126 | - | messageCache.addMessage(msg); | |||
| 127 | - | } | |||
| 128 | - | } | |||
| 129 | - | } | |||
| 130 | 77 | ||||
| 131 | const response: GetMessagesResponse = { | 78 | const response: GetMessagesResponse = { | ||
| 132 | success: true, | 79 | success: true, | ||
| @@ -237,9 +184,7 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||||
| 237 | await db.insert(messages).values(newMessage); | 184 | await db.insert(messages).values(newMessage); | ||
| 238 | log.debug('Message saved in DB', { messageId: newMessage.id, chatRoomId: newMessage.chatRoomId, type: newMessage.type, timestamp: newMessage.timestamp }); | 185 | log.debug('Message saved in DB', { messageId: newMessage.id, chatRoomId: newMessage.chatRoomId, type: newMessage.type, timestamp: newMessage.timestamp }); | ||
| 239 | 186 | ||||
| 240 | - | // Removed updating message cache for authenticated users; relying on SSE for real-time updates | 187 | + | // Broadcast the new message via SSE |
| 241 | - | ||||
| 242 | - | // Broadcast the new message via the unified SSE emitter | |||
| 243 | sseEmitter.emit('sse', { type: 'chatMessage', data: newMessage }); | 188 | sseEmitter.emit('sse', { type: 'chatMessage', data: newMessage }); | ||
| 244 | 189 | ||||
| 245 | log.debug('Message processed successfully', { messageId: newMessage.id, userId: `${newMessage.senderId.slice(0, 4)}...${newMessage.senderId.slice(-4)}`, roomId: newMessage.chatRoomId }); | 190 | log.debug('Message processed successfully', { messageId: newMessage.id, userId: `${newMessage.senderId.slice(0, 4)}...${newMessage.senderId.slice(-4)}`, roomId: newMessage.chatRoomId }); | ||