feat: Implement message caching for improved chat performance
- Add message cache initialization on server start - Fetch and cache messages from database with room-specific support - Enhance GET endpoint to use cached messages when available - Implement fallback to database query if cache initialization fails - Add logging for cache initialization and message retrieval - Support room-specific message fetching with default room handling
- commit
- cce43cf979b5b6675cca5ac82dadadffd388e127
- author
- Kit · · 2025-02-14 13:48 UTC
- parent
- 28660c8
- signature
- SSH Unverified
SHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6TkThe key is unknown
2 files · +244 -14
A src/lib/cache/message-cache.ts +175 -0
| @@ -0,0 +1,175 @@ | |||
| 1 | + | import type { Message } from '$lib/types/chat'; | |
| 2 | + | import { DEFAULT_CHAT_ROOM_ID } from '$lib/db/schema'; | |
| 3 | + | ||
| 4 | + | interface RoomCache { | |
| 5 | + | messages: Message[]; | |
| 6 | + | lastUpdated: number; | |
| 7 | + | } | |
| 8 | + | ||
| 9 | + | class MessageCache { | |
| 10 | + | private static instance: MessageCache; | |
| 11 | + | private rooms: Map<string, RoomCache>; | |
| 12 | + | private readonly maxMessagesPerRoom: number; | |
| 13 | + | private readonly maxRooms: number; | |
| 14 | + | private readonly cacheExpiryMs: number; | |
| 15 | + | ||
| 16 | + | private constructor() { | |
| 17 | + | this.rooms = new Map(); | |
| 18 | + | this.maxMessagesPerRoom = 100; // Keep last 100 messages per room | |
| 19 | + | this.maxRooms = 100; // Maximum number of rooms to cache | |
| 20 | + | this.cacheExpiryMs = 30 * 60 * 1000; // 30 minutes cache expiry | |
| 21 | + | console.debug('[MessageCache] Initialized with settings:', { | |
| 22 | + | maxMessagesPerRoom: this.maxMessagesPerRoom, | |
| 23 | + | maxRooms: this.maxRooms, | |
| 24 | + | cacheExpiryMs: this.cacheExpiryMs | |
| 25 | + | }); | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | public static getInstance(): MessageCache { | |
| 29 | + | if (!MessageCache.instance) { | |
| 30 | + | MessageCache.instance = new MessageCache(); | |
| 31 | + | } | |
| 32 | + | return MessageCache.instance; | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | private cleanExpiredRooms(): void { | |
| 36 | + | const now = Date.now(); | |
| 37 | + | let expiredCount = 0; | |
| 38 | + | ||
| 39 | + | for (const [roomId, cache] of this.rooms.entries()) { | |
| 40 | + | if (now - cache.lastUpdated > this.cacheExpiryMs) { | |
| 41 | + | this.rooms.delete(roomId); | |
| 42 | + | expiredCount++; | |
| 43 | + | } | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | if (expiredCount > 0) { | |
| 47 | + | console.debug('[MessageCache] Cleaned expired rooms:', { expiredCount }); | |
| 48 | + | } | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | private ensureRoomCapacity(): void { | |
| 52 | + | // If we're over capacity, remove the least recently used rooms | |
| 53 | + | if (this.rooms.size > this.maxRooms) { | |
| 54 | + | const sortedRooms = Array.from(this.rooms.entries()) | |
| 55 | + | .sort(([, a], [, b]) => a.lastUpdated - b.lastUpdated); | |
| 56 | + | ||
| 57 | + | const toRemove = sortedRooms.slice(0, sortedRooms.length - this.maxRooms); | |
| 58 | + | for (const [roomId] of toRemove) { | |
| 59 | + | this.rooms.delete(roomId); | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | console.debug('[MessageCache] Removed LRU rooms:', { | |
| 63 | + | removedCount: toRemove.length, | |
| 64 | + | remainingRooms: this.rooms.size | |
| 65 | + | }); | |
| 66 | + | } | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | public async initialize(messages: Message[]): Promise<void> { | |
| 70 | + | console.debug('[MessageCache] Initializing with messages:', messages.length); | |
| 71 | + | // Group messages by chat room | |
| 72 | + | const messagesByRoom = new Map<string, Message[]>(); | |
| 73 | + | ||
| 74 | + | for (const message of messages) { | |
| 75 | + | const roomMessages = messagesByRoom.get(message.chatRoomId) || []; | |
| 76 | + | roomMessages.push(message); | |
| 77 | + | messagesByRoom.set(message.chatRoomId, roomMessages); | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | const now = Date.now(); | |
| 81 | + | ||
| 82 | + | // Sort messages by timestamp and limit to maxMessagesPerRoom | |
| 83 | + | for (const [roomId, roomMessages] of messagesByRoom) { | |
| 84 | + | roomMessages.sort((a, b) => a.timestamp - b.timestamp); | |
| 85 | + | this.rooms.set(roomId, { | |
| 86 | + | messages: roomMessages.slice(-this.maxMessagesPerRoom), | |
| 87 | + | lastUpdated: now | |
| 88 | + | }); | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | this.ensureRoomCapacity(); | |
| 92 | + | console.debug('[MessageCache] Initialization complete:', { | |
| 93 | + | roomCount: this.rooms.size, | |
| 94 | + | totalMessages: messages.length | |
| 95 | + | }); | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | public getMessages(roomId: string = DEFAULT_CHAT_ROOM_ID, limit?: number): Message[] { | |
| 99 | + | const roomCache = this.rooms.get(roomId); | |
| 100 | + | if (!roomCache) { | |
| 101 | + | return []; | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | // Update last accessed time | |
| 105 | + | roomCache.lastUpdated = Date.now(); | |
| 106 | + | ||
| 107 | + | if (limit) { | |
| 108 | + | return roomCache.messages.slice(-limit); | |
| 109 | + | } | |
| 110 | + | return [...roomCache.messages]; // Return a copy to prevent external modifications | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | public addMessage(message: Message): void { | |
| 114 | + | // Clean expired rooms periodically | |
| 115 | + | this.cleanExpiredRooms(); | |
| 116 | + | ||
| 117 | + | const roomCache = this.rooms.get(message.chatRoomId) || { | |
| 118 | + | messages: [], | |
| 119 | + | lastUpdated: Date.now() | |
| 120 | + | }; | |
| 121 | + | ||
| 122 | + | roomCache.messages.push(message); | |
| 123 | + | roomCache.lastUpdated = Date.now(); | |
| 124 | + | ||
| 125 | + | // Keep only the last maxMessagesPerRoom messages | |
| 126 | + | if (roomCache.messages.length > this.maxMessagesPerRoom) { | |
| 127 | + | roomCache.messages.shift(); // Remove oldest message | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | this.rooms.set(message.chatRoomId, roomCache); | |
| 131 | + | this.ensureRoomCapacity(); | |
| 132 | + | ||
| 133 | + | console.debug('[MessageCache] Added message to cache:', { | |
| 134 | + | messageId: message.id, | |
| 135 | + | roomId: message.chatRoomId, | |
| 136 | + | roomMessageCount: roomCache.messages.length, | |
| 137 | + | totalRooms: this.rooms.size | |
| 138 | + | }); | |
| 139 | + | } | |
| 140 | + | ||
| 141 | + | public getRoomMessageCount(roomId: string): number { | |
| 142 | + | return this.rooms.get(roomId)?.messages.length || 0; | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | public clear(): void { | |
| 146 | + | this.rooms.clear(); | |
| 147 | + | console.debug('[MessageCache] Cache cleared'); | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | public getCacheStats(): { | |
| 151 | + | roomCount: number; | |
| 152 | + | totalMessages: number; | |
| 153 | + | roomStats: { roomId: string; messageCount: number; lastUpdated: number }[]; | |
| 154 | + | } { | |
| 155 | + | let totalMessages = 0; | |
| 156 | + | const roomStats = []; | |
| 157 | + | ||
| 158 | + | for (const [roomId, cache] of this.rooms.entries()) { | |
| 159 | + | totalMessages += cache.messages.length; | |
| 160 | + | roomStats.push({ | |
| 161 | + | roomId, | |
| 162 | + | messageCount: cache.messages.length, | |
| 163 | + | lastUpdated: cache.lastUpdated | |
| 164 | + | }); | |
| 165 | + | } | |
| 166 | + | ||
| 167 | + | return { | |
| 168 | + | roomCount: this.rooms.size, | |
| 169 | + | totalMessages, | |
| 170 | + | roomStats: roomStats.sort((a, b) => b.lastUpdated - a.lastUpdated) | |
| 171 | + | }; | |
| 172 | + | } | |
| 173 | + | } | |
| 174 | + | ||
| 175 | + | export const messageCache = MessageCache.getInstance(); ⏎̸ | |
| @@ -0,0 +1,175 @@ | |||||
| 1 | + | import type { Message } from '$lib/types/chat'; | |||
| 2 | + | import { DEFAULT_CHAT_ROOM_ID } from '$lib/db/schema'; | |||
| 3 | + | ||||
| 4 | + | interface RoomCache { | |||
| 5 | + | messages: Message[]; | |||
| 6 | + | lastUpdated: number; | |||
| 7 | + | } | |||
| 8 | + | ||||
| 9 | + | class MessageCache { | |||
| 10 | + | private static instance: MessageCache; | |||
| 11 | + | private rooms: Map<string, RoomCache>; | |||
| 12 | + | private readonly maxMessagesPerRoom: number; | |||
| 13 | + | private readonly maxRooms: number; | |||
| 14 | + | private readonly cacheExpiryMs: number; | |||
| 15 | + | ||||
| 16 | + | private constructor() { | |||
| 17 | + | this.rooms = new Map(); | |||
| 18 | + | this.maxMessagesPerRoom = 100; // Keep last 100 messages per room | |||
| 19 | + | this.maxRooms = 100; // Maximum number of rooms to cache | |||
| 20 | + | this.cacheExpiryMs = 30 * 60 * 1000; // 30 minutes cache expiry | |||
| 21 | + | console.debug('[MessageCache] Initialized with settings:', { | |||
| 22 | + | maxMessagesPerRoom: this.maxMessagesPerRoom, | |||
| 23 | + | maxRooms: this.maxRooms, | |||
| 24 | + | cacheExpiryMs: this.cacheExpiryMs | |||
| 25 | + | }); | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | public static getInstance(): MessageCache { | |||
| 29 | + | if (!MessageCache.instance) { | |||
| 30 | + | MessageCache.instance = new MessageCache(); | |||
| 31 | + | } | |||
| 32 | + | return MessageCache.instance; | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | private cleanExpiredRooms(): void { | |||
| 36 | + | const now = Date.now(); | |||
| 37 | + | let expiredCount = 0; | |||
| 38 | + | ||||
| 39 | + | for (const [roomId, cache] of this.rooms.entries()) { | |||
| 40 | + | if (now - cache.lastUpdated > this.cacheExpiryMs) { | |||
| 41 | + | this.rooms.delete(roomId); | |||
| 42 | + | expiredCount++; | |||
| 43 | + | } | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | if (expiredCount > 0) { | |||
| 47 | + | console.debug('[MessageCache] Cleaned expired rooms:', { expiredCount }); | |||
| 48 | + | } | |||
| 49 | + | } | |||
| 50 | + | ||||
| 51 | + | private ensureRoomCapacity(): void { | |||
| 52 | + | // If we're over capacity, remove the least recently used rooms | |||
| 53 | + | if (this.rooms.size > this.maxRooms) { | |||
| 54 | + | const sortedRooms = Array.from(this.rooms.entries()) | |||
| 55 | + | .sort(([, a], [, b]) => a.lastUpdated - b.lastUpdated); | |||
| 56 | + | ||||
| 57 | + | const toRemove = sortedRooms.slice(0, sortedRooms.length - this.maxRooms); | |||
| 58 | + | for (const [roomId] of toRemove) { | |||
| 59 | + | this.rooms.delete(roomId); | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | console.debug('[MessageCache] Removed LRU rooms:', { | |||
| 63 | + | removedCount: toRemove.length, | |||
| 64 | + | remainingRooms: this.rooms.size | |||
| 65 | + | }); | |||
| 66 | + | } | |||
| 67 | + | } | |||
| 68 | + | ||||
| 69 | + | public async initialize(messages: Message[]): Promise<void> { | |||
| 70 | + | console.debug('[MessageCache] Initializing with messages:', messages.length); | |||
| 71 | + | // Group messages by chat room | |||
| 72 | + | const messagesByRoom = new Map<string, Message[]>(); | |||
| 73 | + | ||||
| 74 | + | for (const message of messages) { | |||
| 75 | + | const roomMessages = messagesByRoom.get(message.chatRoomId) || []; | |||
| 76 | + | roomMessages.push(message); | |||
| 77 | + | messagesByRoom.set(message.chatRoomId, roomMessages); | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | const now = Date.now(); | |||
| 81 | + | ||||
| 82 | + | // Sort messages by timestamp and limit to maxMessagesPerRoom | |||
| 83 | + | for (const [roomId, roomMessages] of messagesByRoom) { | |||
| 84 | + | roomMessages.sort((a, b) => a.timestamp - b.timestamp); | |||
| 85 | + | this.rooms.set(roomId, { | |||
| 86 | + | messages: roomMessages.slice(-this.maxMessagesPerRoom), | |||
| 87 | + | lastUpdated: now | |||
| 88 | + | }); | |||
| 89 | + | } | |||
| 90 | + | ||||
| 91 | + | this.ensureRoomCapacity(); | |||
| 92 | + | console.debug('[MessageCache] Initialization complete:', { | |||
| 93 | + | roomCount: this.rooms.size, | |||
| 94 | + | totalMessages: messages.length | |||
| 95 | + | }); | |||
| 96 | + | } | |||
| 97 | + | ||||
| 98 | + | public getMessages(roomId: string = DEFAULT_CHAT_ROOM_ID, limit?: number): Message[] { | |||
| 99 | + | const roomCache = this.rooms.get(roomId); | |||
| 100 | + | if (!roomCache) { | |||
| 101 | + | return []; | |||
| 102 | + | } | |||
| 103 | + | ||||
| 104 | + | // Update last accessed time | |||
| 105 | + | roomCache.lastUpdated = Date.now(); | |||
| 106 | + | ||||
| 107 | + | if (limit) { | |||
| 108 | + | return roomCache.messages.slice(-limit); | |||
| 109 | + | } | |||
| 110 | + | return [...roomCache.messages]; // Return a copy to prevent external modifications | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | public addMessage(message: Message): void { | |||
| 114 | + | // Clean expired rooms periodically | |||
| 115 | + | this.cleanExpiredRooms(); | |||
| 116 | + | ||||
| 117 | + | const roomCache = this.rooms.get(message.chatRoomId) || { | |||
| 118 | + | messages: [], | |||
| 119 | + | lastUpdated: Date.now() | |||
| 120 | + | }; | |||
| 121 | + | ||||
| 122 | + | roomCache.messages.push(message); | |||
| 123 | + | roomCache.lastUpdated = Date.now(); | |||
| 124 | + | ||||
| 125 | + | // Keep only the last maxMessagesPerRoom messages | |||
| 126 | + | if (roomCache.messages.length > this.maxMessagesPerRoom) { | |||
| 127 | + | roomCache.messages.shift(); // Remove oldest message | |||
| 128 | + | } | |||
| 129 | + | ||||
| 130 | + | this.rooms.set(message.chatRoomId, roomCache); | |||
| 131 | + | this.ensureRoomCapacity(); | |||
| 132 | + | ||||
| 133 | + | console.debug('[MessageCache] Added message to cache:', { | |||
| 134 | + | messageId: message.id, | |||
| 135 | + | roomId: message.chatRoomId, | |||
| 136 | + | roomMessageCount: roomCache.messages.length, | |||
| 137 | + | totalRooms: this.rooms.size | |||
| 138 | + | }); | |||
| 139 | + | } | |||
| 140 | + | ||||
| 141 | + | public getRoomMessageCount(roomId: string): number { | |||
| 142 | + | return this.rooms.get(roomId)?.messages.length || 0; | |||
| 143 | + | } | |||
| 144 | + | ||||
| 145 | + | public clear(): void { | |||
| 146 | + | this.rooms.clear(); | |||
| 147 | + | console.debug('[MessageCache] Cache cleared'); | |||
| 148 | + | } | |||
| 149 | + | ||||
| 150 | + | public getCacheStats(): { | |||
| 151 | + | roomCount: number; | |||
| 152 | + | totalMessages: number; | |||
| 153 | + | roomStats: { roomId: string; messageCount: number; lastUpdated: number }[]; | |||
| 154 | + | } { | |||
| 155 | + | let totalMessages = 0; | |||
| 156 | + | const roomStats = []; | |||
| 157 | + | ||||
| 158 | + | for (const [roomId, cache] of this.rooms.entries()) { | |||
| 159 | + | totalMessages += cache.messages.length; | |||
| 160 | + | roomStats.push({ | |||
| 161 | + | roomId, | |||
| 162 | + | messageCount: cache.messages.length, | |||
| 163 | + | lastUpdated: cache.lastUpdated | |||
| 164 | + | }); | |||
| 165 | + | } | |||
| 166 | + | ||||
| 167 | + | return { | |||
| 168 | + | roomCount: this.rooms.size, | |||
| 169 | + | totalMessages, | |||
| 170 | + | roomStats: roomStats.sort((a, b) => b.lastUpdated - a.lastUpdated) | |||
| 171 | + | }; | |||
| 172 | + | } | |||
| 173 | + | } | |||
| 174 | + | ||||
| 175 | + | export const messageCache = MessageCache.getInstance(); ⏎̸ | |||
M src/routes/api/chat/messages/+server.ts +69 -14
| @@ -14,6 +14,7 @@ import { eq } from 'drizzle-orm'; | |||
| 14 | 14 | import { chatRooms, DEFAULT_CHAT_ROOM_ID } from '$lib/db/schema'; | |
| 15 | 15 | import { users } from '$lib/db/schema'; | |
| 16 | 16 | import { ensureDefaultChatRoom } from '$lib/db/schema'; | |
| 17 | + | import { messageCache } from '$lib/cache/message-cache'; | |
| 17 | 18 | ||
| 18 | 19 | // Rate limiting configuration | |
| 19 | 20 | const INITIAL_COOLDOWN = 1000; // 1 second | |
| @@ -47,9 +48,39 @@ function updateUserCooldown(userId: string): { canSend: boolean; retryAfter?: nu | |||
| 47 | 48 | return { canSend: true }; | |
| 48 | 49 | } | |
| 49 | 50 | ||
| 50 | - | // GET endpoint: fetch messages from the DB | |
| 51 | + | // Initialize message cache on server start | |
| 52 | + | let isCacheInitialized = false; | |
| 53 | + | async function initializeCache() { | |
| 54 | + | if (isCacheInitialized) return; | |
| 55 | + | ||
| 56 | + | try { | |
| 57 | + | console.log('[Chat] Initializing message cache'); | |
| 58 | + | // Fetch messages from all rooms, limited to last 100 per room | |
| 59 | + | const fetchedMessages = await db.select() | |
| 60 | + | .from(messages) | |
| 61 | + | .orderBy(desc(messages.timestamp)) | |
| 62 | + | .limit(500); // Increased limit to accommodate multiple rooms | |
| 63 | + | ||
| 64 | + | await messageCache.initialize(fetchedMessages.reverse()); | |
| 65 | + | isCacheInitialized = true; | |
| 66 | + | ||
| 67 | + | // Log cache stats after initialization | |
| 68 | + | const stats = messageCache.getCacheStats(); | |
| 69 | + | console.log('[Chat] Message cache initialized successfully:', { | |
| 70 | + | roomCount: stats.roomCount, | |
| 71 | + | totalMessages: stats.totalMessages | |
| 72 | + | }); | |
| 73 | + | } catch (error) { | |
| 74 | + | console.error('[Chat] Failed to initialize message cache:', error); | |
| 75 | + | // Don't set isCacheInitialized to true on error | |
| 76 | + | } | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | // GET endpoint: fetch messages from cache or DB | |
| 51 | 80 | export async function GET({ request, locals }) { | |
| 52 | 81 | const isPublic = new URL(request.url).searchParams.get('public') === 'true'; | |
| 82 | + | const roomId = new URL(request.url).searchParams.get('roomId') || DEFAULT_CHAT_ROOM_ID; | |
| 83 | + | ||
| 53 | 84 | if (!isPublic && !locals.user) { | |
| 54 | 85 | throw error(401, 'Authentication required'); | |
| 55 | 86 | } | |
| @@ -58,22 +89,40 @@ export async function GET({ request, locals }) { | |||
| 58 | 89 | // Ensure default chat room exists | |
| 59 | 90 | await ensureDefaultChatRoom(db); | |
| 60 | 91 | ||
| 61 | - | console.log('[Chat] Fetching messages', { isPublic }); | |
| 62 | - | const query = db.select() | |
| 63 | - | .from(messages) | |
| 64 | - | .orderBy(desc(messages.timestamp)); | |
| 65 | - | ||
| 66 | - | if (isPublic) { | |
| 67 | - | query.limit(10); | |
| 68 | - | } else { | |
| 69 | - | query.limit(50); | |
| 92 | + | // Initialize cache if needed | |
| 93 | + | if (!isCacheInitialized) { | |
| 94 | + | await initializeCache(); | |
| 70 | 95 | } | |
| 96 | + | ||
| 97 | + | console.log('[Chat] Fetching messages', { | |
| 98 | + | isPublic, | |
| 99 | + | fromCache: isCacheInitialized, | |
| 100 | + | roomId | |
| 101 | + | }); | |
| 102 | + | ||
| 103 | + | let fetchedMessages: Message[]; | |
| 104 | + | if (isCacheInitialized) { | |
| 105 | + | // Get from cache | |
| 106 | + | fetchedMessages = messageCache.getMessages(roomId, isPublic ? 10 : 50); | |
| 107 | + | } else { | |
| 108 | + | // Fallback to DB if cache initialization failed | |
| 109 | + | const query = db.select() | |
| 110 | + | .from(messages) | |
| 111 | + | .where(eq(messages.chatRoomId, roomId)) | |
| 112 | + | .orderBy(desc(messages.timestamp)); | |
| 71 | 113 | ||
| 72 | - | const fetchedMessages = await query; | |
| 114 | + | if (isPublic) { | |
| 115 | + | query.limit(10); | |
| 116 | + | } else { | |
| 117 | + | query.limit(50); | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | fetchedMessages = (await query).reverse(); | |
| 121 | + | } | |
| 73 | 122 | ||
| 74 | 123 | const response: GetMessagesResponse = { | |
| 75 | 124 | success: true, | |
| 76 | - | messages: fetchedMessages.reverse() | |
| 125 | + | messages: fetchedMessages | |
| 77 | 126 | }; | |
| 78 | 127 | ||
| 79 | 128 | return new Response( | |
| @@ -83,8 +132,8 @@ export async function GET({ request, locals }) { | |||
| 83 | 132 | headers: { 'Content-Type': 'application/json' } | |
| 84 | 133 | } | |
| 85 | 134 | ); | |
| 86 | - | } catch { | |
| 87 | - | console.log('[Chat] Error fetching messages'); | |
| 135 | + | } catch (error) { | |
| 136 | + | console.log('[Chat] Error fetching messages:', error); | |
| 88 | 137 | const errorResponse: GetMessagesResponse = { | |
| 89 | 138 | success: false, | |
| 90 | 139 | error: 'Failed to fetch messages' | |
| @@ -187,6 +236,7 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||
| 187 | 236 | timestamp: Date.now() | |
| 188 | 237 | }; | |
| 189 | 238 | ||
| 239 | + | // Save to DB | |
| 190 | 240 | await db.insert(messages).values(newMessage); | |
| 191 | 241 | console.log('[Chat] Message saved in DB', { | |
| 192 | 242 | messageId: newMessage.id, | |
| @@ -195,6 +245,11 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||
| 195 | 245 | timestamp: newMessage.timestamp | |
| 196 | 246 | }); | |
| 197 | 247 | ||
| 248 | + | // Add to cache if initialized | |
| 249 | + | if (isCacheInitialized) { | |
| 250 | + | messageCache.addMessage(newMessage); | |
| 251 | + | } | |
| 252 | + | ||
| 198 | 253 | // Broadcast the new message via the unified SSE emitter | |
| 199 | 254 | sseEmitter.emit('sse', { type: 'chatMessage', data: newMessage }); | |
| 200 | 255 | ||
| @@ -14,6 +14,7 @@ import { eq } from 'drizzle-orm'; | |||||
| 14 | import { chatRooms, DEFAULT_CHAT_ROOM_ID } from '$lib/db/schema'; | 14 | import { chatRooms, DEFAULT_CHAT_ROOM_ID } from '$lib/db/schema'; | ||
| 15 | import { users } from '$lib/db/schema'; | 15 | import { users } from '$lib/db/schema'; | ||
| 16 | import { ensureDefaultChatRoom } from '$lib/db/schema'; | 16 | import { ensureDefaultChatRoom } from '$lib/db/schema'; | ||
| 17 | + | import { messageCache } from '$lib/cache/message-cache'; | |||
| 17 | 18 | ||||
| 18 | // Rate limiting configuration | 19 | // Rate limiting configuration | ||
| 19 | const INITIAL_COOLDOWN = 1000; // 1 second | 20 | const INITIAL_COOLDOWN = 1000; // 1 second | ||
| @@ -47,9 +48,39 @@ function updateUserCooldown(userId: string): { canSend: boolean; retryAfter?: nu | |||||
| 47 | return { canSend: true }; | 48 | return { canSend: true }; | ||
| 48 | } | 49 | } | ||
| 49 | 50 | ||||
| 50 | - | // GET endpoint: fetch messages from the DB | 51 | + | // Initialize message cache on server start |
| 52 | + | let isCacheInitialized = false; | |||
| 53 | + | async function initializeCache() { | |||
| 54 | + | if (isCacheInitialized) return; | |||
| 55 | + | ||||
| 56 | + | try { | |||
| 57 | + | console.log('[Chat] Initializing message cache'); | |||
| 58 | + | // Fetch messages from all rooms, limited to last 100 per room | |||
| 59 | + | const fetchedMessages = await db.select() | |||
| 60 | + | .from(messages) | |||
| 61 | + | .orderBy(desc(messages.timestamp)) | |||
| 62 | + | .limit(500); // Increased limit to accommodate multiple rooms | |||
| 63 | + | ||||
| 64 | + | await messageCache.initialize(fetchedMessages.reverse()); | |||
| 65 | + | isCacheInitialized = true; | |||
| 66 | + | ||||
| 67 | + | // Log cache stats after initialization | |||
| 68 | + | const stats = messageCache.getCacheStats(); | |||
| 69 | + | console.log('[Chat] Message cache initialized successfully:', { | |||
| 70 | + | roomCount: stats.roomCount, | |||
| 71 | + | totalMessages: stats.totalMessages | |||
| 72 | + | }); | |||
| 73 | + | } catch (error) { | |||
| 74 | + | console.error('[Chat] Failed to initialize message cache:', error); | |||
| 75 | + | // Don't set isCacheInitialized to true on error | |||
| 76 | + | } | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | // GET endpoint: fetch messages from cache or DB | |||
| 51 | export async function GET({ request, locals }) { | 80 | export async function GET({ request, locals }) { | ||
| 52 | const isPublic = new URL(request.url).searchParams.get('public') === 'true'; | 81 | const isPublic = new URL(request.url).searchParams.get('public') === 'true'; | ||
| 82 | + | const roomId = new URL(request.url).searchParams.get('roomId') || DEFAULT_CHAT_ROOM_ID; | |||
| 83 | + | ||||
| 53 | if (!isPublic && !locals.user) { | 84 | if (!isPublic && !locals.user) { | ||
| 54 | throw error(401, 'Authentication required'); | 85 | throw error(401, 'Authentication required'); | ||
| 55 | } | 86 | } | ||
| @@ -58,22 +89,40 @@ export async function GET({ request, locals }) { | |||||
| 58 | // Ensure default chat room exists | 89 | // Ensure default chat room exists | ||
| 59 | await ensureDefaultChatRoom(db); | 90 | await ensureDefaultChatRoom(db); | ||
| 60 | 91 | ||||
| 61 | - | console.log('[Chat] Fetching messages', { isPublic }); | 92 | + | // Initialize cache if needed |
| 62 | - | const query = db.select() | 93 | + | if (!isCacheInitialized) { |
| 63 | - | .from(messages) | 94 | + | await initializeCache(); |
| 64 | - | .orderBy(desc(messages.timestamp)); | |||
| 65 | - | ||||
| 66 | - | if (isPublic) { | |||
| 67 | - | query.limit(10); | |||
| 68 | - | } else { | |||
| 69 | - | query.limit(50); | |||
| 70 | } | 95 | } | ||
| 96 | + | ||||
| 97 | + | console.log('[Chat] Fetching messages', { | |||
| 98 | + | isPublic, | |||
| 99 | + | fromCache: isCacheInitialized, | |||
| 100 | + | roomId | |||
| 101 | + | }); | |||
| 102 | + | ||||
| 103 | + | let fetchedMessages: Message[]; | |||
| 104 | + | if (isCacheInitialized) { | |||
| 105 | + | // Get from cache | |||
| 106 | + | fetchedMessages = messageCache.getMessages(roomId, isPublic ? 10 : 50); | |||
| 107 | + | } else { | |||
| 108 | + | // Fallback to DB if cache initialization failed | |||
| 109 | + | const query = db.select() | |||
| 110 | + | .from(messages) | |||
| 111 | + | .where(eq(messages.chatRoomId, roomId)) | |||
| 112 | + | .orderBy(desc(messages.timestamp)); | |||
| 71 | 113 | ||||
| 72 | - | const fetchedMessages = await query; | 114 | + | if (isPublic) { |
| 115 | + | query.limit(10); | |||
| 116 | + | } else { | |||
| 117 | + | query.limit(50); | |||
| 118 | + | } | |||
| 119 | + | ||||
| 120 | + | fetchedMessages = (await query).reverse(); | |||
| 121 | + | } | |||
| 73 | 122 | ||||
| 74 | const response: GetMessagesResponse = { | 123 | const response: GetMessagesResponse = { | ||
| 75 | success: true, | 124 | success: true, | ||
| 76 | - | messages: fetchedMessages.reverse() | 125 | + | messages: fetchedMessages |
| 77 | }; | 126 | }; | ||
| 78 | 127 | ||||
| 79 | return new Response( | 128 | return new Response( | ||
| @@ -83,8 +132,8 @@ export async function GET({ request, locals }) { | |||||
| 83 | headers: { 'Content-Type': 'application/json' } | 132 | headers: { 'Content-Type': 'application/json' } | ||
| 84 | } | 133 | } | ||
| 85 | ); | 134 | ); | ||
| 86 | - | } catch { | 135 | + | } catch (error) { |
| 87 | - | console.log('[Chat] Error fetching messages'); | 136 | + | console.log('[Chat] Error fetching messages:', error); |
| 88 | const errorResponse: GetMessagesResponse = { | 137 | const errorResponse: GetMessagesResponse = { | ||
| 89 | success: false, | 138 | success: false, | ||
| 90 | error: 'Failed to fetch messages' | 139 | error: 'Failed to fetch messages' | ||
| @@ -187,6 +236,7 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||||
| 187 | timestamp: Date.now() | 236 | timestamp: Date.now() | ||
| 188 | }; | 237 | }; | ||
| 189 | 238 | ||||
| 239 | + | // Save to DB | |||
| 190 | await db.insert(messages).values(newMessage); | 240 | await db.insert(messages).values(newMessage); | ||
| 191 | console.log('[Chat] Message saved in DB', { | 241 | console.log('[Chat] Message saved in DB', { | ||
| 192 | messageId: newMessage.id, | 242 | messageId: newMessage.id, | ||
| @@ -195,6 +245,11 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||||
| 195 | timestamp: newMessage.timestamp | 245 | timestamp: newMessage.timestamp | ||
| 196 | }); | 246 | }); | ||
| 197 | 247 | ||||
| 248 | + | // Add to cache if initialized | |||
| 249 | + | if (isCacheInitialized) { | |||
| 250 | + | messageCache.addMessage(newMessage); | |||
| 251 | + | } | |||
| 252 | + | ||||
| 198 | // Broadcast the new message via the unified SSE emitter | 253 | // Broadcast the new message via the unified SSE emitter | ||
| 199 | sseEmitter.emit('sse', { type: 'chatMessage', data: newMessage }); | 254 | sseEmitter.emit('sse', { type: 'chatMessage', data: newMessage }); | ||
| 200 | 255 | ||||