detached 840793d

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 UnverifiedSHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6Tk The key is unknown
4 files · +44 -398
M src/hooks.server.ts +27 -110
@@ -1,140 +1,57 @@
11 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";
52 import db from "$lib/db/db.server";
63 import { handleRateLimit } from "$lib/api/rate-limiter";
74 import { users } from "$lib/db/schema";
8-import { initializeMessageCache } from "$lib/cache/initialize-cache";
95 import { createLogger } from "$lib/utils/logger.server";
106 import { ensureDefaultChatRoom } from "$lib/utils/chat.server";
117
128 const log = createLogger('hooks-server');
139
10+// Set all users to offline on server start
1411 async function setAllUsersOffline() {
1512 try {
16- log.info("Setting all users to offline status...");
13+ const now = Date.now();
1714 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 });
2419 }
2520 }
2621
27-// Initialize database when the server starts
28-log.debug("Initializing database on server start...");
29-initializeDatabase()
22+// Initialize server
23+Promise.resolve()
3024 .then(async () => {
25+ log.info('Initializing server...');
3126 await ensureDefaultChatRoom(db);
3227 await setAllUsersOffline();
33- await initializeMessageCache();
28+ log.info('Server initialized successfully');
3429 })
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);
3733 });
3834
3935 export const handle: Handle = async ({ event, resolve }) => {
4036 log.debug("Handling request", { path: event.url.pathname });
4137
4238 // 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+ });
9552 }
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 }));
12853 }
12954
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-
13855 const response = await resolve(event);
139- return addDebugHeaders(response);
56+ return response;
14057 }; ⏎̸
@@ -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 starts22+// 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 requests38 // 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';
1313 import { chatRooms } from '$lib/db/schema';
1414 import { users } from '$lib/db/schema';
1515 import { DEFAULT_CHAT_ROOM_ID } from '$lib/utils/chat.server';
16-import { messageCache } from '$lib/cache/message-cache';
1716 import { createLogger } from '$lib/utils/logger.server';
1817 import { SQL } from 'drizzle-orm';
1918
@@ -51,82 +50,30 @@ function updateUserCooldown(userId: string): { canSend: boolean; retryAfter?: nu
5150 return { canSend: true };
5251 }
5352
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
8354 export async function GET({ request, locals }) {
8455 const url = new URL(request.url);
8556 const beforeTimestamp = url.searchParams.get('before');
8657 const roomId = url.searchParams.get('roomId') || DEFAULT_CHAT_ROOM_ID;
87- let fetchedMessages: Message[] = [];
8858
8959 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>;
10168 }
10269
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);
13077
13178 const response: GetMessagesResponse = {
13279 success: true,
@@ -237,9 +184,7 @@ export async function POST({ request, locals }: { request: Request, locals: App.
237184 await db.insert(messages).values(newMessage);
238185 log.debug('Message saved in DB', { messageId: newMessage.id, chatRoomId: newMessage.chatRoomId, type: newMessage.type, timestamp: newMessage.timestamp });
239186
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
243188 sseEmitter.emit('sse', { type: 'chatMessage', data: newMessage });
244189
245190 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 start53+// 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 requested60+ 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 DB70+ // 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 updates187+ // 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 });

Keyboard shortcuts

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

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

Branches 3

Tags 8

↑↓ moveenter openesc close