refactor: Centralize default chat room configuration and improve modularity
- Move default chat room logic to a dedicated utility module - Use environment variable for default chat room ID - Remove default chat room creation from schema module - Update imports and references across components and routes - Enhance logging and error handling for chat room initialization
- commit
- 22cc76c81db2b349f185cc7ea1778d307d82a59f
- author
- Kit · · 2025-02-15 09:29 UTC
- parent
- 2960188
- signature
- SSH Unverified
SHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6TkThe key is unknown
7 files · +82 -71
M src/hooks.server.ts +2 -0
| @@ -7,6 +7,7 @@ import { handleRateLimit } from "$lib/api/rate-limiter"; | |||
| 7 | 7 | import { users } from "$lib/db/schema"; | |
| 8 | 8 | import { initializeMessageCache } from "$lib/cache/initialize-cache"; | |
| 9 | 9 | import { createLogger } from "$lib/utils/logger.server"; | |
| 10 | + | import { ensureDefaultChatRoom } from "$lib/utils/chat.server"; | |
| 10 | 11 | ||
| 11 | 12 | const log = createLogger('hooks-server'); | |
| 12 | 13 | ||
| @@ -27,6 +28,7 @@ async function setAllUsersOffline() { | |||
| 27 | 28 | log.debug("Initializing database on server start..."); | |
| 28 | 29 | initializeDatabase() | |
| 29 | 30 | .then(async () => { | |
| 31 | + | await ensureDefaultChatRoom(db); | |
| 30 | 32 | await setAllUsersOffline(); | |
| 31 | 33 | await initializeMessageCache(); | |
| 32 | 34 | }) | |
| @@ -7,6 +7,7 @@ import { handleRateLimit } from "$lib/api/rate-limiter"; | |||||
| 7 | import { users } from "$lib/db/schema"; | 7 | import { users } from "$lib/db/schema"; | ||
| 8 | import { initializeMessageCache } from "$lib/cache/initialize-cache"; | 8 | import { initializeMessageCache } from "$lib/cache/initialize-cache"; | ||
| 9 | import { createLogger } from "$lib/utils/logger.server"; | 9 | import { createLogger } from "$lib/utils/logger.server"; | ||
| 10 | + | import { ensureDefaultChatRoom } from "$lib/utils/chat.server"; | |||
| 10 | 11 | ||||
| 11 | const log = createLogger('hooks-server'); | 12 | const log = createLogger('hooks-server'); | ||
| 12 | 13 | ||||
| @@ -27,6 +28,7 @@ async function setAllUsersOffline() { | |||||
| 27 | log.debug("Initializing database on server start..."); | 28 | log.debug("Initializing database on server start..."); | ||
| 28 | initializeDatabase() | 29 | initializeDatabase() | ||
| 29 | .then(async () => { | 30 | .then(async () => { | ||
| 31 | + | await ensureDefaultChatRoom(db); | |||
| 30 | await setAllUsersOffline(); | 32 | await setAllUsersOffline(); | ||
| 31 | await initializeMessageCache(); | 33 | await initializeMessageCache(); | ||
| 32 | }) | 34 | }) | ||
M src/lib/cache/message-cache.ts +11 -8
| @@ -1,5 +1,8 @@ | |||
| 1 | 1 | import type { Message } from '$lib/types/chat'; | |
| 2 | - | import { DEFAULT_CHAT_ROOM_ID } from '$lib/db/schema'; | |
| 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'); | |
| 3 | 6 | ||
| 4 | 7 | interface RoomCache { | |
| 5 | 8 | messages: Message[]; | |
| @@ -18,7 +21,7 @@ class MessageCache { | |||
| 18 | 21 | this.maxMessagesPerRoom = 100; // Keep last 100 messages per room | |
| 19 | 22 | this.maxRooms = 100; // Maximum number of rooms to cache | |
| 20 | 23 | this.cacheExpiryMs = 30 * 60 * 1000; // 30 minutes cache expiry | |
| 21 | - | console.debug('[MessageCache] Initialized with settings:', { | |
| 24 | + | log.debug('[MessageCache] Initialized with settings:', { | |
| 22 | 25 | maxMessagesPerRoom: this.maxMessagesPerRoom, | |
| 23 | 26 | maxRooms: this.maxRooms, | |
| 24 | 27 | cacheExpiryMs: this.cacheExpiryMs | |
| @@ -44,7 +47,7 @@ class MessageCache { | |||
| 44 | 47 | } | |
| 45 | 48 | ||
| 46 | 49 | if (expiredCount > 0) { | |
| 47 | - | console.debug('[MessageCache] Cleaned expired rooms:', { expiredCount }); | |
| 50 | + | log.debug('[MessageCache] Cleaned expired rooms:', { expiredCount }); | |
| 48 | 51 | } | |
| 49 | 52 | } | |
| 50 | 53 | ||
| @@ -59,7 +62,7 @@ class MessageCache { | |||
| 59 | 62 | this.rooms.delete(roomId); | |
| 60 | 63 | } | |
| 61 | 64 | ||
| 62 | - | console.debug('[MessageCache] Removed LRU rooms:', { | |
| 65 | + | log.debug('[MessageCache] Removed LRU rooms:', { | |
| 63 | 66 | removedCount: toRemove.length, | |
| 64 | 67 | remainingRooms: this.rooms.size | |
| 65 | 68 | }); | |
| @@ -67,7 +70,7 @@ class MessageCache { | |||
| 67 | 70 | } | |
| 68 | 71 | ||
| 69 | 72 | public async initialize(messages: Message[]): Promise<void> { | |
| 70 | - | console.debug('[MessageCache] Initializing with messages:', messages.length); | |
| 73 | + | log.debug('[MessageCache] Initializing with messages:', { messageCount: messages.length }); | |
| 71 | 74 | // Group messages by chat room | |
| 72 | 75 | const messagesByRoom = new Map<string, Message[]>(); | |
| 73 | 76 | ||
| @@ -89,7 +92,7 @@ class MessageCache { | |||
| 89 | 92 | } | |
| 90 | 93 | ||
| 91 | 94 | this.ensureRoomCapacity(); | |
| 92 | - | console.debug('[MessageCache] Initialization complete:', { | |
| 95 | + | log.debug('[MessageCache] Initialization complete:', { | |
| 93 | 96 | roomCount: this.rooms.size, | |
| 94 | 97 | totalMessages: messages.length | |
| 95 | 98 | }); | |
| @@ -130,7 +133,7 @@ class MessageCache { | |||
| 130 | 133 | this.rooms.set(message.chatRoomId, roomCache); | |
| 131 | 134 | this.ensureRoomCapacity(); | |
| 132 | 135 | ||
| 133 | - | console.debug('[MessageCache] Added message to cache:', { | |
| 136 | + | log.debug('[MessageCache] Added message to cache:', { | |
| 134 | 137 | messageId: message.id, | |
| 135 | 138 | roomId: message.chatRoomId, | |
| 136 | 139 | roomMessageCount: roomCache.messages.length, | |
| @@ -144,7 +147,7 @@ class MessageCache { | |||
| 144 | 147 | ||
| 145 | 148 | public clear(): void { | |
| 146 | 149 | this.rooms.clear(); | |
| 147 | - | console.debug('[MessageCache] Cache cleared'); | |
| 150 | + | log.debug('[MessageCache] Cache cleared'); | |
| 148 | 151 | } | |
| 149 | 152 | ||
| 150 | 153 | public getCacheStats(): { | |
| @@ -1,5 +1,8 @@ | |||||
| 1 | import type { Message } from '$lib/types/chat'; | 1 | import type { Message } from '$lib/types/chat'; | ||
| 2 | - | import { DEFAULT_CHAT_ROOM_ID } from '$lib/db/schema'; | 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'); | |||
| 3 | 6 | ||||
| 4 | interface RoomCache { | 7 | interface RoomCache { | ||
| 5 | messages: Message[]; | 8 | messages: Message[]; | ||
| @@ -18,7 +21,7 @@ class MessageCache { | |||||
| 18 | this.maxMessagesPerRoom = 100; // Keep last 100 messages per room | 21 | this.maxMessagesPerRoom = 100; // Keep last 100 messages per room | ||
| 19 | this.maxRooms = 100; // Maximum number of rooms to cache | 22 | this.maxRooms = 100; // Maximum number of rooms to cache | ||
| 20 | this.cacheExpiryMs = 30 * 60 * 1000; // 30 minutes cache expiry | 23 | this.cacheExpiryMs = 30 * 60 * 1000; // 30 minutes cache expiry | ||
| 21 | - | console.debug('[MessageCache] Initialized with settings:', { | 24 | + | log.debug('[MessageCache] Initialized with settings:', { |
| 22 | maxMessagesPerRoom: this.maxMessagesPerRoom, | 25 | maxMessagesPerRoom: this.maxMessagesPerRoom, | ||
| 23 | maxRooms: this.maxRooms, | 26 | maxRooms: this.maxRooms, | ||
| 24 | cacheExpiryMs: this.cacheExpiryMs | 27 | cacheExpiryMs: this.cacheExpiryMs | ||
| @@ -44,7 +47,7 @@ class MessageCache { | |||||
| 44 | } | 47 | } | ||
| 45 | 48 | ||||
| 46 | if (expiredCount > 0) { | 49 | if (expiredCount > 0) { | ||
| 47 | - | console.debug('[MessageCache] Cleaned expired rooms:', { expiredCount }); | 50 | + | log.debug('[MessageCache] Cleaned expired rooms:', { expiredCount }); |
| 48 | } | 51 | } | ||
| 49 | } | 52 | } | ||
| 50 | 53 | ||||
| @@ -59,7 +62,7 @@ class MessageCache { | |||||
| 59 | this.rooms.delete(roomId); | 62 | this.rooms.delete(roomId); | ||
| 60 | } | 63 | } | ||
| 61 | 64 | ||||
| 62 | - | console.debug('[MessageCache] Removed LRU rooms:', { | 65 | + | log.debug('[MessageCache] Removed LRU rooms:', { |
| 63 | removedCount: toRemove.length, | 66 | removedCount: toRemove.length, | ||
| 64 | remainingRooms: this.rooms.size | 67 | remainingRooms: this.rooms.size | ||
| 65 | }); | 68 | }); | ||
| @@ -67,7 +70,7 @@ class MessageCache { | |||||
| 67 | } | 70 | } | ||
| 68 | 71 | ||||
| 69 | public async initialize(messages: Message[]): Promise<void> { | 72 | public async initialize(messages: Message[]): Promise<void> { | ||
| 70 | - | console.debug('[MessageCache] Initializing with messages:', messages.length); | 73 | + | log.debug('[MessageCache] Initializing with messages:', { messageCount: messages.length }); |
| 71 | // Group messages by chat room | 74 | // Group messages by chat room | ||
| 72 | const messagesByRoom = new Map<string, Message[]>(); | 75 | const messagesByRoom = new Map<string, Message[]>(); | ||
| 73 | 76 | ||||
| @@ -89,7 +92,7 @@ class MessageCache { | |||||
| 89 | } | 92 | } | ||
| 90 | 93 | ||||
| 91 | this.ensureRoomCapacity(); | 94 | this.ensureRoomCapacity(); | ||
| 92 | - | console.debug('[MessageCache] Initialization complete:', { | 95 | + | log.debug('[MessageCache] Initialization complete:', { |
| 93 | roomCount: this.rooms.size, | 96 | roomCount: this.rooms.size, | ||
| 94 | totalMessages: messages.length | 97 | totalMessages: messages.length | ||
| 95 | }); | 98 | }); | ||
| @@ -130,7 +133,7 @@ class MessageCache { | |||||
| 130 | this.rooms.set(message.chatRoomId, roomCache); | 133 | this.rooms.set(message.chatRoomId, roomCache); | ||
| 131 | this.ensureRoomCapacity(); | 134 | this.ensureRoomCapacity(); | ||
| 132 | 135 | ||||
| 133 | - | console.debug('[MessageCache] Added message to cache:', { | 136 | + | log.debug('[MessageCache] Added message to cache:', { |
| 134 | messageId: message.id, | 137 | messageId: message.id, | ||
| 135 | roomId: message.chatRoomId, | 138 | roomId: message.chatRoomId, | ||
| 136 | roomMessageCount: roomCache.messages.length, | 139 | roomMessageCount: roomCache.messages.length, | ||
| @@ -144,7 +147,7 @@ class MessageCache { | |||||
| 144 | 147 | ||||
| 145 | public clear(): void { | 148 | public clear(): void { | ||
| 146 | this.rooms.clear(); | 149 | this.rooms.clear(); | ||
| 147 | - | console.debug('[MessageCache] Cache cleared'); | 150 | + | log.debug('[MessageCache] Cache cleared'); |
| 148 | } | 151 | } | ||
| 149 | 152 | ||||
| 150 | public getCacheStats(): { | 153 | public getCacheStats(): { | ||
M src/lib/components/chat-room.svelte +5 -3
| @@ -8,7 +8,6 @@ import { resizable } from '$lib/actions/resizable'; | |||
| 8 | 8 | import { maximizable } from '$lib/actions/maximizable'; | |
| 9 | 9 | import LoadingButton from './ui/button-loading.svelte'; | |
| 10 | 10 | import Tooltip from './ui/tooltip.svelte'; | |
| 11 | - | import { DEFAULT_CHAT_ROOM_ID } from '$lib/db/schema'; | |
| 12 | 11 | import { formatFrenchDateTime, formatFrenchRelativeTimeSafe } from '$lib/utils/date-format'; | |
| 13 | 12 | ||
| 14 | 13 | // Initialize with default values for SSR | |
| @@ -65,7 +64,7 @@ let currentUser = $state<User | null>(null); | |||
| 65 | 64 | let isLoadingMore = $state(false); | |
| 66 | 65 | let hasMoreMessages = $state(true); | |
| 67 | 66 | let oldestMessageTimestamp = $state<number | null>(null); | |
| 68 | - | let currentRoomId = $state(DEFAULT_CHAT_ROOM_ID); | |
| 67 | + | let currentRoomId = $state(''); | |
| 69 | 68 | ||
| 70 | 69 | // Compute visible messages based on login status | |
| 71 | 70 | let visibleMessages = $derived((() => { | |
| @@ -131,9 +130,12 @@ $effect(() => { | |||
| 131 | 130 | fetchedPublicRoom = true; | |
| 132 | 131 | fetchedPublicMessages = true; | |
| 133 | 132 | ||
| 133 | + | // Get the default chat room id | |
| 134 | + | const defaultRoomId = chatState.getDefaultChatRoomId(); | |
| 135 | + | ||
| 134 | 136 | // Fetch both in parallel | |
| 135 | 137 | Promise.all([ | |
| 136 | - | fetch(`/api/rooms/${DEFAULT_CHAT_ROOM_ID}?public=true`).then(r => r.json()), | |
| 138 | + | fetch(`/api/rooms/${defaultRoomId}?public=true`).then(r => r.json()), | |
| 137 | 139 | fetch('/api/chat/messages?public=true').then(r => r.json()) | |
| 138 | 140 | ]) | |
| 139 | 141 | .then(([roomData, messagesData]) => { | |
| @@ -8,7 +8,6 @@ import { resizable } from '$lib/actions/resizable'; | |||||
| 8 | import { maximizable } from '$lib/actions/maximizable'; | 8 | import { maximizable } from '$lib/actions/maximizable'; | ||
| 9 | import LoadingButton from './ui/button-loading.svelte'; | 9 | import LoadingButton from './ui/button-loading.svelte'; | ||
| 10 | import Tooltip from './ui/tooltip.svelte'; | 10 | import Tooltip from './ui/tooltip.svelte'; | ||
| 11 | - | import { DEFAULT_CHAT_ROOM_ID } from '$lib/db/schema'; | |||
| 12 | import { formatFrenchDateTime, formatFrenchRelativeTimeSafe } from '$lib/utils/date-format'; | 11 | import { formatFrenchDateTime, formatFrenchRelativeTimeSafe } from '$lib/utils/date-format'; | ||
| 13 | 12 | ||||
| 14 | // Initialize with default values for SSR | 13 | // Initialize with default values for SSR | ||
| @@ -65,7 +64,7 @@ let currentUser = $state<User | null>(null); | |||||
| 65 | let isLoadingMore = $state(false); | 64 | let isLoadingMore = $state(false); | ||
| 66 | let hasMoreMessages = $state(true); | 65 | let hasMoreMessages = $state(true); | ||
| 67 | let oldestMessageTimestamp = $state<number | null>(null); | 66 | let oldestMessageTimestamp = $state<number | null>(null); | ||
| 68 | - | let currentRoomId = $state(DEFAULT_CHAT_ROOM_ID); | 67 | + | let currentRoomId = $state(''); |
| 69 | 68 | ||||
| 70 | // Compute visible messages based on login status | 69 | // Compute visible messages based on login status | ||
| 71 | let visibleMessages = $derived((() => { | 70 | let visibleMessages = $derived((() => { | ||
| @@ -131,9 +130,12 @@ $effect(() => { | |||||
| 131 | fetchedPublicRoom = true; | 130 | fetchedPublicRoom = true; | ||
| 132 | fetchedPublicMessages = true; | 131 | fetchedPublicMessages = true; | ||
| 133 | 132 | ||||
| 133 | + | // Get the default chat room id | |||
| 134 | + | const defaultRoomId = chatState.getDefaultChatRoomId(); | |||
| 135 | + | ||||
| 134 | // Fetch both in parallel | 136 | // Fetch both in parallel | ||
| 135 | Promise.all([ | 137 | Promise.all([ | ||
| 136 | - | fetch(`/api/rooms/${DEFAULT_CHAT_ROOM_ID}?public=true`).then(r => r.json()), | 138 | + | fetch(`/api/rooms/${defaultRoomId}?public=true`).then(r => r.json()), |
| 137 | fetch('/api/chat/messages?public=true').then(r => r.json()) | 139 | fetch('/api/chat/messages?public=true').then(r => r.json()) | ||
| 138 | ]) | 140 | ]) | ||
| 139 | .then(([roomData, messagesData]) => { | 141 | .then(([roomData, messagesData]) => { | ||
M src/lib/db/schema.ts +0 -50
| @@ -3,23 +3,7 @@ import * as t from "drizzle-orm/sqlite-core"; | |||
| 3 | 3 | import type { User, ChatRoom, Message, UserStatus, MessageType, Session } from "../types/chat"; | |
| 4 | 4 | import { v4 as uuidv4 } from "uuid"; | |
| 5 | 5 | import { eq } from "drizzle-orm"; | |
| 6 | - | import type { LibSQLDatabase } from "drizzle-orm/libsql"; | |
| 7 | - | import { createLogger } from "$lib/utils/logger.server"; | |
| 8 | 6 | ||
| 9 | - | const log = createLogger('schema'); | |
| 10 | - | ||
| 11 | - | // Default chat room ID - using a UUID to avoid collisions | |
| 12 | - | export const DEFAULT_CHAT_ROOM_ID = '00000000-0000-0000-0000-000000000001'; | |
| 13 | - | ||
| 14 | - | // Type guard for LibSQL errors | |
| 15 | - | interface LibSQLError { | |
| 16 | - | code: string; | |
| 17 | - | message: string; | |
| 18 | - | } | |
| 19 | - | ||
| 20 | - | function isLibSQLError(error: unknown): error is LibSQLError { | |
| 21 | - | return typeof error === 'object' && error !== null && 'code' in error; | |
| 22 | - | } | |
| 23 | 7 | ||
| 24 | 8 | export const users = table( | |
| 25 | 9 | "users", | |
| @@ -121,40 +105,6 @@ const schema = { | |||
| 121 | 105 | messageView | |
| 122 | 106 | }; | |
| 123 | 107 | ||
| 124 | - | // Function to ensure default chat room exists | |
| 125 | - | export async function ensureDefaultChatRoom(db: LibSQLDatabase<typeof schema>) { | |
| 126 | - | log.debug('Ensuring default chat room exists...'); | |
| 127 | - | try { | |
| 128 | - | // Use a transaction to handle concurrent creation attempts | |
| 129 | - | await db.transaction(async (tx) => { | |
| 130 | - | const defaultRoom = await tx.select() | |
| 131 | - | .from(chatRooms) | |
| 132 | - | .where(eq(chatRooms.id, DEFAULT_CHAT_ROOM_ID)) | |
| 133 | - | .get(); | |
| 134 | - | ||
| 135 | - | if (!defaultRoom) { | |
| 136 | - | log.debug('Creating default chat room...'); | |
| 137 | - | await tx.insert(chatRooms).values({ | |
| 138 | - | id: DEFAULT_CHAT_ROOM_ID, | |
| 139 | - | name: 'General', | |
| 140 | - | type: 'group', | |
| 141 | - | createdAt: Date.now() | |
| 142 | - | }); | |
| 143 | - | log.debug('Default chat room created successfully'); | |
| 144 | - | } else { | |
| 145 | - | log.debug('Default chat room already exists'); | |
| 146 | - | } | |
| 147 | - | }); | |
| 148 | - | } catch (error: unknown) { | |
| 149 | - | // If the error is a unique constraint violation, another process probably created the room | |
| 150 | - | if (isLibSQLError(error) && error.code === 'SQLITE_CONSTRAINT') { | |
| 151 | - | log.debug('Default chat room was created by another process'); | |
| 152 | - | return; | |
| 153 | - | } | |
| 154 | - | log.error('Error ensuring default chat room:', { error: error instanceof Error ? error.message : 'Unknown error' }); | |
| 155 | - | throw error; | |
| 156 | - | } | |
| 157 | - | } | |
| 158 | 108 | ||
| 159 | 109 | export default schema; | |
| 160 | 110 | ||
| @@ -3,23 +3,7 @@ import * as t from "drizzle-orm/sqlite-core"; | |||||
| 3 | import type { User, ChatRoom, Message, UserStatus, MessageType, Session } from "../types/chat"; | 3 | import type { User, ChatRoom, Message, UserStatus, MessageType, Session } from "../types/chat"; | ||
| 4 | import { v4 as uuidv4 } from "uuid"; | 4 | import { v4 as uuidv4 } from "uuid"; | ||
| 5 | import { eq } from "drizzle-orm"; | 5 | import { eq } from "drizzle-orm"; | ||
| 6 | - | import type { LibSQLDatabase } from "drizzle-orm/libsql"; | |||
| 7 | - | import { createLogger } from "$lib/utils/logger.server"; | |||
| 8 | 6 | ||||
| 9 | - | const log = createLogger('schema'); | |||
| 10 | - | ||||
| 11 | - | // Default chat room ID - using a UUID to avoid collisions | |||
| 12 | - | export const DEFAULT_CHAT_ROOM_ID = '00000000-0000-0000-0000-000000000001'; | |||
| 13 | - | ||||
| 14 | - | // Type guard for LibSQL errors | |||
| 15 | - | interface LibSQLError { | |||
| 16 | - | code: string; | |||
| 17 | - | message: string; | |||
| 18 | - | } | |||
| 19 | - | ||||
| 20 | - | function isLibSQLError(error: unknown): error is LibSQLError { | |||
| 21 | - | return typeof error === 'object' && error !== null && 'code' in error; | |||
| 22 | - | } | |||
| 23 | 7 | ||||
| 24 | export const users = table( | 8 | export const users = table( | ||
| 25 | "users", | 9 | "users", | ||
| @@ -121,40 +105,6 @@ const schema = { | |||||
| 121 | messageView | 105 | messageView | ||
| 122 | }; | 106 | }; | ||
| 123 | 107 | ||||
| 124 | - | // Function to ensure default chat room exists | |||
| 125 | - | export async function ensureDefaultChatRoom(db: LibSQLDatabase<typeof schema>) { | |||
| 126 | - | log.debug('Ensuring default chat room exists...'); | |||
| 127 | - | try { | |||
| 128 | - | // Use a transaction to handle concurrent creation attempts | |||
| 129 | - | await db.transaction(async (tx) => { | |||
| 130 | - | const defaultRoom = await tx.select() | |||
| 131 | - | .from(chatRooms) | |||
| 132 | - | .where(eq(chatRooms.id, DEFAULT_CHAT_ROOM_ID)) | |||
| 133 | - | .get(); | |||
| 134 | - | ||||
| 135 | - | if (!defaultRoom) { | |||
| 136 | - | log.debug('Creating default chat room...'); | |||
| 137 | - | await tx.insert(chatRooms).values({ | |||
| 138 | - | id: DEFAULT_CHAT_ROOM_ID, | |||
| 139 | - | name: 'General', | |||
| 140 | - | type: 'group', | |||
| 141 | - | createdAt: Date.now() | |||
| 142 | - | }); | |||
| 143 | - | log.debug('Default chat room created successfully'); | |||
| 144 | - | } else { | |||
| 145 | - | log.debug('Default chat room already exists'); | |||
| 146 | - | } | |||
| 147 | - | }); | |||
| 148 | - | } catch (error: unknown) { | |||
| 149 | - | // If the error is a unique constraint violation, another process probably created the room | |||
| 150 | - | if (isLibSQLError(error) && error.code === 'SQLITE_CONSTRAINT') { | |||
| 151 | - | log.debug('Default chat room was created by another process'); | |||
| 152 | - | return; | |||
| 153 | - | } | |||
| 154 | - | log.error('Error ensuring default chat room:', { error: error instanceof Error ? error.message : 'Unknown error' }); | |||
| 155 | - | throw error; | |||
| 156 | - | } | |||
| 157 | - | } | |||
| 158 | 108 | ||||
| 159 | export default schema; | 109 | export default schema; | ||
| 160 | 110 | ||||
M src/lib/states/chat.svelte.ts +7 -2
| @@ -5,8 +5,9 @@ import type { | |||
| 5 | 5 | GetMessagesResponse | |
| 6 | 6 | } from '../types/payloads'; | |
| 7 | 7 | import { invalidate } from '$app/navigation'; | |
| 8 | - | import { DEFAULT_CHAT_ROOM_ID } from '$lib/db/schema'; | |
| 8 | + | import { env } from '$env/dynamic/public'; | |
| 9 | 9 | ||
| 10 | + | const DEFAULT_CHAT_ROOM_ID = env.PUBLIC_DEFAULT_CHAT_ROOM_ID; | |
| 10 | 11 | ||
| 11 | 12 | class ChatState { | |
| 12 | 13 | private users = $state<User[]>([]); | |
| @@ -17,7 +18,7 @@ class ChatState { | |||
| 17 | 18 | private maxReconnectAttempts = 5; | |
| 18 | 19 | private reconnectDelay = 1000; // Start with 1 second | |
| 19 | 20 | private isInitializing = $state(false); | |
| 20 | - | private currentRoomId = $state<string>(DEFAULT_CHAT_ROOM_ID); | |
| 21 | + | private currentRoomId = $state<string>(); | |
| 21 | 22 | private userCache = $state<Record<string, User>>({}); | |
| 22 | 23 | private sseError = $state<string | null>(null); | |
| 23 | 24 | private sseRetryAfter = $state<number | null>(null); | |
| @@ -668,6 +669,10 @@ class ChatState { | |||
| 668 | 669 | } | |
| 669 | 670 | })); | |
| 670 | 671 | } | |
| 672 | + | ||
| 673 | + | getDefaultChatRoomId() { | |
| 674 | + | return DEFAULT_CHAT_ROOM_ID; | |
| 675 | + | } | |
| 671 | 676 | } | |
| 672 | 677 | ||
| 673 | 678 | export const chatState = new ChatState();⏎̸ | |
| @@ -5,8 +5,9 @@ import type { | |||||
| 5 | GetMessagesResponse | 5 | GetMessagesResponse | ||
| 6 | } from '../types/payloads'; | 6 | } from '../types/payloads'; | ||
| 7 | import { invalidate } from '$app/navigation'; | 7 | import { invalidate } from '$app/navigation'; | ||
| 8 | - | import { DEFAULT_CHAT_ROOM_ID } from '$lib/db/schema'; | 8 | + | import { env } from '$env/dynamic/public'; |
| 9 | 9 | ||||
| 10 | + | const DEFAULT_CHAT_ROOM_ID = env.PUBLIC_DEFAULT_CHAT_ROOM_ID; | |||
| 10 | 11 | ||||
| 11 | class ChatState { | 12 | class ChatState { | ||
| 12 | private users = $state<User[]>([]); | 13 | private users = $state<User[]>([]); | ||
| @@ -17,7 +18,7 @@ class ChatState { | |||||
| 17 | private maxReconnectAttempts = 5; | 18 | private maxReconnectAttempts = 5; | ||
| 18 | private reconnectDelay = 1000; // Start with 1 second | 19 | private reconnectDelay = 1000; // Start with 1 second | ||
| 19 | private isInitializing = $state(false); | 20 | private isInitializing = $state(false); | ||
| 20 | - | private currentRoomId = $state<string>(DEFAULT_CHAT_ROOM_ID); | 21 | + | private currentRoomId = $state<string>(); |
| 21 | private userCache = $state<Record<string, User>>({}); | 22 | private userCache = $state<Record<string, User>>({}); | ||
| 22 | private sseError = $state<string | null>(null); | 23 | private sseError = $state<string | null>(null); | ||
| 23 | private sseRetryAfter = $state<number | null>(null); | 24 | private sseRetryAfter = $state<number | null>(null); | ||
| @@ -668,6 +669,10 @@ class ChatState { | |||||
| 668 | } | 669 | } | ||
| 669 | })); | 670 | })); | ||
| 670 | } | 671 | } | ||
| 672 | + | ||||
| 673 | + | getDefaultChatRoomId() { | |||
| 674 | + | return DEFAULT_CHAT_ROOM_ID; | |||
| 675 | + | } | |||
| 671 | } | 676 | } | ||
| 672 | 677 | ||||
| 673 | export const chatState = new ChatState();⏎̸ | 678 | export const chatState = new ChatState();⏎̸ | ||
A src/lib/utils/chat.server.ts +55 -0
| @@ -0,0 +1,55 @@ | |||
| 1 | + | import type { LibSQLDatabase } from "drizzle-orm/libsql"; | |
| 2 | + | import { createLogger } from "$lib/utils/logger.server"; | |
| 3 | + | import schema, { chatRooms } from "$lib/db/schema"; | |
| 4 | + | import { eq } from "drizzle-orm"; | |
| 5 | + | import { env } from "$env/dynamic/public"; | |
| 6 | + | ||
| 7 | + | const log = createLogger('chat-utils'); | |
| 8 | + | ||
| 9 | + | // Default chat room ID from environment variable | |
| 10 | + | export const DEFAULT_CHAT_ROOM_ID = env.PUBLIC_DEFAULT_CHAT_ROOM_ID || '00000000-0000-0000-0000-000000000001'; | |
| 11 | + | ||
| 12 | + | // Type guard for LibSQL errors | |
| 13 | + | interface LibSQLError { | |
| 14 | + | code: string; | |
| 15 | + | message: string; | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | function isLibSQLError(error: unknown): error is LibSQLError { | |
| 19 | + | return typeof error === 'object' && error !== null && 'code' in error; | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | // Function to ensure default chat room exists | |
| 23 | + | export async function ensureDefaultChatRoom(db: LibSQLDatabase<typeof schema>) { | |
| 24 | + | log.debug('Ensuring default chat room exists...'); | |
| 25 | + | try { | |
| 26 | + | // Use a transaction to handle concurrent creation attempts | |
| 27 | + | await db.transaction(async (tx) => { | |
| 28 | + | const defaultRoom = await tx.select() | |
| 29 | + | .from(chatRooms) | |
| 30 | + | .where(eq(chatRooms.id, DEFAULT_CHAT_ROOM_ID)) | |
| 31 | + | .get(); | |
| 32 | + | ||
| 33 | + | if (!defaultRoom) { | |
| 34 | + | log.debug('Creating default chat room...'); | |
| 35 | + | await tx.insert(chatRooms).values({ | |
| 36 | + | id: DEFAULT_CHAT_ROOM_ID, | |
| 37 | + | name: 'General', | |
| 38 | + | type: 'group', | |
| 39 | + | createdAt: Date.now() | |
| 40 | + | }); | |
| 41 | + | log.debug('Default chat room created successfully'); | |
| 42 | + | } else { | |
| 43 | + | log.debug('Default chat room already exists'); | |
| 44 | + | } | |
| 45 | + | }); | |
| 46 | + | } catch (error: unknown) { | |
| 47 | + | // If the error is a unique constraint violation, another process probably created the room | |
| 48 | + | if (isLibSQLError(error) && error.code === 'SQLITE_CONSTRAINT') { | |
| 49 | + | log.debug('Default chat room was created by another process'); | |
| 50 | + | return; | |
| 51 | + | } | |
| 52 | + | log.error('Error ensuring default chat room:', { error: error instanceof Error ? error.message : 'Unknown error' }); | |
| 53 | + | throw error; | |
| 54 | + | } | |
| 55 | + | } ⏎̸ | |
| @@ -0,0 +1,55 @@ | |||||
| 1 | + | import type { LibSQLDatabase } from "drizzle-orm/libsql"; | |||
| 2 | + | import { createLogger } from "$lib/utils/logger.server"; | |||
| 3 | + | import schema, { chatRooms } from "$lib/db/schema"; | |||
| 4 | + | import { eq } from "drizzle-orm"; | |||
| 5 | + | import { env } from "$env/dynamic/public"; | |||
| 6 | + | ||||
| 7 | + | const log = createLogger('chat-utils'); | |||
| 8 | + | ||||
| 9 | + | // Default chat room ID from environment variable | |||
| 10 | + | export const DEFAULT_CHAT_ROOM_ID = env.PUBLIC_DEFAULT_CHAT_ROOM_ID || '00000000-0000-0000-0000-000000000001'; | |||
| 11 | + | ||||
| 12 | + | // Type guard for LibSQL errors | |||
| 13 | + | interface LibSQLError { | |||
| 14 | + | code: string; | |||
| 15 | + | message: string; | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | function isLibSQLError(error: unknown): error is LibSQLError { | |||
| 19 | + | return typeof error === 'object' && error !== null && 'code' in error; | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | // Function to ensure default chat room exists | |||
| 23 | + | export async function ensureDefaultChatRoom(db: LibSQLDatabase<typeof schema>) { | |||
| 24 | + | log.debug('Ensuring default chat room exists...'); | |||
| 25 | + | try { | |||
| 26 | + | // Use a transaction to handle concurrent creation attempts | |||
| 27 | + | await db.transaction(async (tx) => { | |||
| 28 | + | const defaultRoom = await tx.select() | |||
| 29 | + | .from(chatRooms) | |||
| 30 | + | .where(eq(chatRooms.id, DEFAULT_CHAT_ROOM_ID)) | |||
| 31 | + | .get(); | |||
| 32 | + | ||||
| 33 | + | if (!defaultRoom) { | |||
| 34 | + | log.debug('Creating default chat room...'); | |||
| 35 | + | await tx.insert(chatRooms).values({ | |||
| 36 | + | id: DEFAULT_CHAT_ROOM_ID, | |||
| 37 | + | name: 'General', | |||
| 38 | + | type: 'group', | |||
| 39 | + | createdAt: Date.now() | |||
| 40 | + | }); | |||
| 41 | + | log.debug('Default chat room created successfully'); | |||
| 42 | + | } else { | |||
| 43 | + | log.debug('Default chat room already exists'); | |||
| 44 | + | } | |||
| 45 | + | }); | |||
| 46 | + | } catch (error: unknown) { | |||
| 47 | + | // If the error is a unique constraint violation, another process probably created the room | |||
| 48 | + | if (isLibSQLError(error) && error.code === 'SQLITE_CONSTRAINT') { | |||
| 49 | + | log.debug('Default chat room was created by another process'); | |||
| 50 | + | return; | |||
| 51 | + | } | |||
| 52 | + | log.error('Error ensuring default chat room:', { error: error instanceof Error ? error.message : 'Unknown error' }); | |||
| 53 | + | throw error; | |||
| 54 | + | } | |||
| 55 | + | } ⏎̸ | |||
M src/routes/api/chat/messages/+server.ts +2 -8
| @@ -11,9 +11,9 @@ import { desc } from 'drizzle-orm'; | |||
| 11 | 11 | import { error } from '@sveltejs/kit'; | |
| 12 | 12 | import { sseEmitter } from '$lib/sseEmitter'; | |
| 13 | 13 | import { eq, lt, and } from 'drizzle-orm'; | |
| 14 | - | import { chatRooms, DEFAULT_CHAT_ROOM_ID } from '$lib/db/schema'; | |
| 14 | + | import { chatRooms } from '$lib/db/schema'; | |
| 15 | 15 | import { users } from '$lib/db/schema'; | |
| 16 | - | import { ensureDefaultChatRoom } from '$lib/db/schema'; | |
| 16 | + | import { DEFAULT_CHAT_ROOM_ID } from '$lib/utils/chat.server'; | |
| 17 | 17 | import { messageCache } from '$lib/cache/message-cache'; | |
| 18 | 18 | import { createLogger } from '$lib/utils/logger.server'; | |
| 19 | 19 | ||
| @@ -87,9 +87,6 @@ export async function GET({ request, locals }) { | |||
| 87 | 87 | let fetchedMessages: Message[] = []; | |
| 88 | 88 | ||
| 89 | 89 | try { | |
| 90 | - | // Ensure default chat room exists | |
| 91 | - | await ensureDefaultChatRoom(db); | |
| 92 | - | ||
| 93 | 90 | // Initialize cache if needed | |
| 94 | 91 | if (!isCacheInitialized) { | |
| 95 | 92 | await initializeCache(); | |
| @@ -203,9 +200,6 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||
| 203 | 200 | }); | |
| 204 | 201 | } | |
| 205 | 202 | ||
| 206 | - | // Ensure default chat room exists | |
| 207 | - | await ensureDefaultChatRoom(db); | |
| 208 | - | ||
| 209 | 203 | const data = await request.json() as SendMessageRequest; | |
| 210 | 204 | if (!data.content || !data.userId) { | |
| 211 | 205 | log.warn('Invalid payload received', { | |
| @@ -11,9 +11,9 @@ import { desc } from 'drizzle-orm'; | |||||
| 11 | import { error } from '@sveltejs/kit'; | 11 | import { error } from '@sveltejs/kit'; | ||
| 12 | import { sseEmitter } from '$lib/sseEmitter'; | 12 | import { sseEmitter } from '$lib/sseEmitter'; | ||
| 13 | import { eq, lt, and } from 'drizzle-orm'; | 13 | import { eq, lt, and } from 'drizzle-orm'; | ||
| 14 | - | import { chatRooms, DEFAULT_CHAT_ROOM_ID } from '$lib/db/schema'; | 14 | + | import { chatRooms } 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 { DEFAULT_CHAT_ROOM_ID } from '$lib/utils/chat.server'; |
| 17 | import { messageCache } from '$lib/cache/message-cache'; | 17 | import { messageCache } from '$lib/cache/message-cache'; | ||
| 18 | import { createLogger } from '$lib/utils/logger.server'; | 18 | import { createLogger } from '$lib/utils/logger.server'; | ||
| 19 | 19 | ||||
| @@ -87,9 +87,6 @@ export async function GET({ request, locals }) { | |||||
| 87 | let fetchedMessages: Message[] = []; | 87 | let fetchedMessages: Message[] = []; | ||
| 88 | 88 | ||||
| 89 | try { | 89 | try { | ||
| 90 | - | // Ensure default chat room exists | |||
| 91 | - | await ensureDefaultChatRoom(db); | |||
| 92 | - | ||||
| 93 | // Initialize cache if needed | 90 | // Initialize cache if needed | ||
| 94 | if (!isCacheInitialized) { | 91 | if (!isCacheInitialized) { | ||
| 95 | await initializeCache(); | 92 | await initializeCache(); | ||
| @@ -203,9 +200,6 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||||
| 203 | }); | 200 | }); | ||
| 204 | } | 201 | } | ||
| 205 | 202 | ||||
| 206 | - | // Ensure default chat room exists | |||
| 207 | - | await ensureDefaultChatRoom(db); | |||
| 208 | - | ||||
| 209 | const data = await request.json() as SendMessageRequest; | 203 | const data = await request.json() as SendMessageRequest; | ||
| 210 | if (!data.content || !data.userId) { | 204 | if (!data.content || !data.userId) { | ||
| 211 | log.warn('Invalid payload received', { | 205 | log.warn('Invalid payload received', { | ||