import { app, BrowserWindow, Menu, MenuItemConstructorOptions, ipcMain, protocol, net, shell, screen } from 'electron'; import * as path from 'path'; import * as fs from 'fs'; import { getDatabase } from './database'; import { registerIpcHandlers, registerChatHandlers, initializeChatHandlers, cleanupChatHandlers } from './ipc'; import { media } from './database/schema'; import { eq } from 'drizzle-orm'; import { getMediaEngine } from './engine/MediaEngine'; import { getPostEngine } from './engine/PostEngine'; import { getMetaEngine } from './engine/MetaEngine'; import { getTemplateEngine } from './engine/TemplateEngine'; import { getBlogmarkTransformService } from './engine/BlogmarkTransformService'; import { PreviewServer } from './engine/PreviewServer'; import { getMCPServer } from './engine/MCPServer'; import { APP_MENU_ACTION_EVENT_MAP, APP_MENU_GROUPS, APP_MENU_ITEM_IDS, type AppMenuAction, type AppMenuItemDefinition } from './shared/menuCommands'; import { resolveUiLanguageFromSystemLocale, translateMenu } from './shared/i18n'; import { buildBlogmarkMarkdownLink, extractBlogmarkPayloadFromDeepLink, normalizeBlogmarkCategory } from './shared/blogmark'; let mainWindow: BrowserWindow | null = null; let previewServer: PreviewServer | null = null; let activePreviewPostId: string | null = null; let appInitialized = false; let blogmarkQueue: string[] = []; let blogmarkQueueProcessing = false; let pendingBlogmarkCreatedEvents: unknown[] = []; let rendererReady = false; const PREVIEW_SERVER_PORT = 4123; const MCP_SERVER_PORT = 4124; const BLOG_PREVIEW_POST_MENU_ID = APP_MENU_ITEM_IDS.previewPost; const BLOGMARK_PROTOCOL = 'bds'; const BLOGMARK_NEW_POST_PREFIX = `${BLOGMARK_PROTOCOL}://new-post`; const WINDOW_MIN_WIDTH = 800; const WINDOW_MIN_HEIGHT = 600; const WINDOW_DEFAULT_WIDTH = 1400; const WINDOW_DEFAULT_HEIGHT = 900; const WINDOW_STATE_FILE_NAME = 'window-state.json'; interface PersistedWindowState { x: number; y: number; width: number; height: number; } interface Rectangle { x: number; y: number; width: number; height: number; } // Check if dev server is likely running (only in development) const isDev = process.env.NODE_ENV === 'development'; function toggleDetachedDevTools(targetWindow: BrowserWindow | null): void { const webContents = targetWindow?.webContents; if (!webContents) { return; } if (webContents.isDevToolsOpened()) { webContents.closeDevTools(); return; } webContents.openDevTools({ mode: 'detach' }); } function getWindowStatePath(): string | null { if (typeof app.getPath !== 'function') { return null; } return path.join(app.getPath('userData'), WINDOW_STATE_FILE_NAME); } function isFiniteNumber(value: unknown): value is number { return typeof value === 'number' && Number.isFinite(value); } function parsePersistedWindowState(raw: unknown): PersistedWindowState | null { if (typeof raw !== 'object' || raw === null) { return null; } const state = raw as Partial; if (!isFiniteNumber(state.x) || !isFiniteNumber(state.y) || !isFiniteNumber(state.width) || !isFiniteNumber(state.height)) { return null; } if (state.width <= 0 || state.height <= 0) { return null; } return { x: state.x, y: state.y, width: state.width, height: state.height, }; } function readPersistedWindowState(): PersistedWindowState | null { const windowStatePath = getWindowStatePath(); if (!windowStatePath || !fs.existsSync(windowStatePath)) { return null; } try { const content = fs.readFileSync(windowStatePath, 'utf8'); const parsed = JSON.parse(content); return parsePersistedWindowState(parsed); } catch { return null; } } function writePersistedWindowState(state: PersistedWindowState): void { const windowStatePath = getWindowStatePath(); if (!windowStatePath) { return; } try { fs.writeFileSync(windowStatePath, JSON.stringify(state), 'utf8'); } catch { // best effort persistence, ignore write errors } } function getWorkAreaForState(state: PersistedWindowState): Rectangle { if (screen && typeof screen.getDisplayMatching === 'function') { const matchingDisplay = screen.getDisplayMatching({ x: state.x, y: state.y, width: state.width, height: state.height, }); if (matchingDisplay?.workArea) { return matchingDisplay.workArea; } } return { x: 0, y: 0, width: WINDOW_DEFAULT_WIDTH, height: WINDOW_DEFAULT_HEIGHT, }; } function clampWindowStateToWorkArea(state: PersistedWindowState, workArea: Rectangle): PersistedWindowState { const width = Math.max(WINDOW_MIN_WIDTH, Math.min(state.width, workArea.width)); const height = Math.max(WINDOW_MIN_HEIGHT, Math.min(state.height, workArea.height)); const maxX = workArea.x + workArea.width - width; const maxY = workArea.y + workArea.height - height; return { x: Math.min(Math.max(state.x, workArea.x), maxX), y: Math.min(Math.max(state.y, workArea.y), maxY), width, height, }; } function resolveInitialWindowState(): PersistedWindowState { const persistedState = readPersistedWindowState(); if (!persistedState) { return { x: 0, y: 0, width: WINDOW_DEFAULT_WIDTH, height: WINDOW_DEFAULT_HEIGHT, }; } const workArea = getWorkAreaForState(persistedState); return clampWindowStateToWorkArea(persistedState, workArea); } function persistMainWindowState(windowToPersist: BrowserWindow): void { if (typeof windowToPersist.isFullScreen === 'function' && windowToPersist.isFullScreen()) { return; } let bounds = windowToPersist.getBounds(); if (typeof windowToPersist.isMaximized === 'function' && windowToPersist.isMaximized() && typeof windowToPersist.getNormalBounds === 'function') { bounds = windowToPersist.getNormalBounds(); } writePersistedWindowState({ x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height, }); } // Register custom protocol scheme as privileged (must be done before app is ready) protocol.registerSchemesAsPrivileged([ { scheme: 'bds-media', privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: true, }, }, { scheme: 'bds-thumb', privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: true, }, }, ]); function createWindow(): void { rendererReady = false; const isMac = process.platform === 'darwin'; const initialWindowState = resolveInitialWindowState(); mainWindow = new BrowserWindow({ x: initialWindowState.x, y: initialWindowState.y, width: initialWindowState.width, height: initialWindowState.height, minWidth: WINDOW_MIN_WIDTH, minHeight: WINDOW_MIN_HEIGHT, title: 'Blogging Desktop Server', backgroundColor: '#1e1e1e', // VS Code dark background titleBarStyle: isMac ? 'hiddenInset' : 'hidden', ...(isMac ? {} : { titleBarOverlay: { color: '#252526', symbolColor: '#cccccc', height: 34, }, autoHideMenuBar: false, }), webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, contextIsolation: true, sandbox: false, devTools: true, }, icon: path.join(__dirname, '../../assets/icon.png'), }); // Set up the application menu const menu = createApplicationMenu(); Menu.setApplicationMenu(menu); // Register keyboard shortcuts for DevTools via before-input-event (more reliable) mainWindow.webContents.on('before-input-event', (event, input) => { // F12 or Ctrl+Shift+I to toggle DevTools if (input.key === 'F12' || (input.control && input.shift && input.key.toLowerCase() === 'i')) { toggleDetachedDevTools(mainWindow); event.preventDefault(); } }); // Load the app - use built files unless explicitly in dev mode const rendererPath = path.join(__dirname, '../renderer/index.html'); if (isDev) { mainWindow.loadURL('http://localhost:5173'); mainWindow.webContents.openDevTools({ mode: 'detach' }); } else if (fs.existsSync(rendererPath)) { mainWindow.loadFile(rendererPath); } else { // Fallback to dev server if built files don't exist mainWindow.loadURL('http://localhost:5173'); mainWindow.webContents.openDevTools({ mode: 'detach' }); } // Forward events to renderer // Note: ipcMain.emit() (used by forwardEvent in handlers) is a raw EventEmitter emit, // so the first arg is NOT an IpcMainEvent — it's the event name string directly. ipcMain.on('forward-to-renderer', (eventNameOrEvent: any, ...args: unknown[]) => { // When called via ipcMain.emit(), first arg is the channel string directly const eventName: string = typeof eventNameOrEvent === 'string' ? eventNameOrEvent : args.shift() as string; if (mainWindow && !mainWindow.isDestroyed() && typeof eventName === 'string') { mainWindow.webContents.send(eventName, ...args); } }); mainWindow.on('closed', () => { mainWindow = null; }); mainWindow.on('close', () => { if (mainWindow) { persistMainWindowState(mainWindow); } }); } async function openPreviewInBrowser(): Promise { if (!previewServer) { previewServer = new PreviewServer(); } await previewServer.start(PREVIEW_SERVER_PORT); await shell.openExternal(`${previewServer.getBaseUrl()}/`); } function buildCanonicalPostPath(createdAt: Date, slug: string): string { const year = createdAt.getFullYear(); const month = String(createdAt.getMonth() + 1).padStart(2, '0'); const day = String(createdAt.getDate()).padStart(2, '0'); return `/${year}/${month}/${day}/${slug}`; } function setPreviewPostMenuEnabled(enabled: boolean): void { const appMenu = Menu.getApplicationMenu(); const previewPostMenuItem = appMenu?.getMenuItemById(BLOG_PREVIEW_POST_MENU_ID); if (previewPostMenuItem) { previewPostMenuItem.enabled = enabled; } } async function openActivePostPreviewInBrowser(): Promise { if (!activePreviewPostId) { return; } const postEngine = getPostEngine(); const post = await postEngine.getPost(activePreviewPostId); if (!post) { setPreviewPostMenuEnabled(false); return; } if (!previewServer) { previewServer = new PreviewServer(); } await previewServer.start(PREVIEW_SERVER_PORT); const canonicalPath = buildCanonicalPostPath(post.createdAt, post.slug); await shell.openExternal(`${previewServer.getBaseUrl()}${canonicalPath}`); } async function startPreviewServerOnAppStart(): Promise { if (!previewServer) { previewServer = new PreviewServer(); } await previewServer.start(PREVIEW_SERVER_PORT); } function extractBlogmarkDeepLinks(argv: string[]): string[] { return argv.filter((argument) => typeof argument === 'string' && argument.startsWith(BLOGMARK_NEW_POST_PREFIX)); } function enqueueBlogmarkDeepLink(rawDeepLink: string): void { if (rawDeepLink.startsWith(BLOGMARK_NEW_POST_PREFIX)) { blogmarkQueue.push(rawDeepLink); } } function focusMainWindow(): void { if (!mainWindow || mainWindow.isDestroyed()) { return; } if (typeof mainWindow.isMinimized === 'function' && mainWindow.isMinimized()) { mainWindow.restore(); } if (typeof mainWindow.focus === 'function') { mainWindow.focus(); } } async function processBlogmarkDeepLink(rawDeepLink: string): Promise { const payload = extractBlogmarkPayloadFromDeepLink(rawDeepLink); if (!payload) { return; } const metadata = await getMetaEngine().getProjectMetadata(); const preferredCategory = normalizeBlogmarkCategory((metadata as { blogmarkCategory?: unknown } | null)?.blogmarkCategory); const transformService = getBlogmarkTransformService(); const transformResult = await transformService.applyTransforms({ post: { title: payload.title, content: buildBlogmarkMarkdownLink(payload.title, payload.url), tags: [], categories: preferredCategory ? [preferredCategory] : [], }, context: { source: 'blogmark', url: payload.url, }, }); const createdPost = await getPostEngine().createPost({ title: transformResult.post.title, content: transformResult.post.content, tags: transformResult.post.tags, categories: transformResult.post.categories, }); const blogmarkCreatedPayload = { post: createdPost, transform: { appliedScriptIds: transformResult.appliedScriptIds, errors: transformResult.errors, toasts: transformResult.toasts, }, }; if (mainWindow && !mainWindow.isDestroyed() && rendererReady) { mainWindow.webContents.send('blogmark:created', blogmarkCreatedPayload); } else { pendingBlogmarkCreatedEvents.push(blogmarkCreatedPayload); } } function flushPendingBlogmarkCreatedEvents(): void { if (!rendererReady || !mainWindow || mainWindow.isDestroyed() || pendingBlogmarkCreatedEvents.length === 0) { return; } const queuedEvents = pendingBlogmarkCreatedEvents; pendingBlogmarkCreatedEvents = []; for (const payload of queuedEvents) { mainWindow.webContents.send('blogmark:created', payload); } } async function processBlogmarkQueue(): Promise { if (!appInitialized || blogmarkQueueProcessing || blogmarkQueue.length === 0) { return; } blogmarkQueueProcessing = true; try { while (blogmarkQueue.length > 0) { const rawDeepLink = blogmarkQueue.shift(); if (!rawDeepLink) { continue; } try { await processBlogmarkDeepLink(rawDeepLink); } catch (error) { console.error('Failed to process blogmark deep link:', error); } } } finally { blogmarkQueueProcessing = false; } } function registerBlogmarkProtocolClient(): void { if (typeof app.setAsDefaultProtocolClient === 'function') { app.setAsDefaultProtocolClient(BLOGMARK_PROTOCOL); } } async function initializeActiveProjectContext(): Promise { try { const { getProjectEngine } = await import('./engine/ProjectEngine'); const projectEngine = getProjectEngine(); const project = await projectEngine.getActiveProject(); if (!project) { return; } const dataDir = projectEngine.getDataDir(project.id, project.dataPath); const postEngine = getPostEngine() as { setProjectContext?: (projectId: string, dataDir?: string) => void; setSearchLanguage?: (language: string) => void; }; const mediaEngine = getMediaEngine() as { setProjectContext?: (projectId: string, dataDir?: string, internalDir?: string) => void; setSearchLanguage?: (language: string) => void; }; const metaEngine = getMetaEngine() as { setProjectContext?: (projectId: string, dataDir?: string) => void; syncOnStartup?: () => Promise; getProjectMetadata?: () => Promise<{ mainLanguage?: string } | null>; }; postEngine.setProjectContext?.(project.id, dataDir); mediaEngine.setProjectContext?.(project.id, dataDir, dataDir); metaEngine.setProjectContext?.(project.id, dataDir); const templateEngine = getTemplateEngine() as { setProjectContext?: (projectId: string, dataDir?: string) => void; }; templateEngine.setProjectContext?.(project.id, dataDir); await metaEngine.syncOnStartup?.(); const metadata = await metaEngine.getProjectMetadata?.(); if (metadata?.mainLanguage) { const { isoToStemmerLanguage } = await import('./engine/stemmer'); const stemmerLang = isoToStemmerLanguage(metadata.mainLanguage); postEngine.setSearchLanguage?.(stemmerLang); mediaEngine.setSearchLanguage?.(stemmerLang); } } catch (error) { console.error('Failed to initialize active project context:', error); } } function createApplicationMenu(): Menu { const systemLocale = typeof app.getLocale === 'function' ? app.getLocale() : 'en'; const uiLanguage = resolveUiLanguageFromSystemLocale(systemLocale); const commandDefinitions = APP_MENU_GROUPS .flatMap(group => group.items) .filter(item => !item.separator) .reduce>((acc, item) => { acc[item.action] = item; return acc; }, {}); const triggerMenuAction = async (action: AppMenuAction): Promise => { if (action === 'quit') { app.quit(); return; } if (action === 'openInBrowser') { await openPreviewInBrowser().catch((error) => { console.error('Failed to open preview in browser:', error); }); return; } if (action === 'openDataFolder') { const paths = getDatabase().getDataPaths(); void shell.openPath(path.dirname(paths.database)); return; } if (action === 'previewPost') { await openActivePostPreviewInBrowser().catch((error) => { console.error('Failed to preview active post in browser:', error); }); return; } if (action === 'reload') { mainWindow?.webContents.reload(); return; } if (action === 'forceReload') { mainWindow?.webContents.reloadIgnoringCache(); return; } if (action === 'resetZoom') { mainWindow?.webContents.setZoomLevel(0); return; } if (action === 'zoomIn') { if (mainWindow) { const zoomLevel = mainWindow.webContents.getZoomLevel(); mainWindow.webContents.setZoomLevel(zoomLevel + 0.5); } return; } if (action === 'zoomOut') { if (mainWindow) { const zoomLevel = mainWindow.webContents.getZoomLevel(); mainWindow.webContents.setZoomLevel(zoomLevel - 0.5); } return; } if (action === 'toggleFullScreen') { if (mainWindow) { mainWindow.setFullScreen(!mainWindow.isFullScreen()); } return; } if (action === 'toggleDevTools') { toggleDetachedDevTools(mainWindow); return; } if (action === 'viewOnGitHub') { void shell.openExternal('https://github.com/rfc1437/bDS'); return; } if (action === 'reportIssue') { void shell.openExternal('https://github.com/rfc1437/bDS/issues'); return; } const channel = APP_MENU_ACTION_EVENT_MAP[action]; if (channel) { mainWindow?.webContents.send(channel); } }; const getMenuItemLabel = (action: AppMenuAction, fallback: string): string => { return translateMenu(uiLanguage, `menu.item.${action}`) || fallback; }; const getMenuGroupLabel = (groupLabel: string): string => { return translateMenu(uiLanguage, `menu.group.${groupLabel.toLowerCase()}`) || groupLabel; }; const buildSharedMenuItem = (action: AppMenuAction): MenuItemConstructorOptions => { const definition = commandDefinitions[action]; if (!definition) { throw new Error(`Unknown shared menu action: ${action}`); } const translatedLabel = getMenuItemLabel(action, definition.label); if (definition.role) { return { label: translatedLabel, role: definition.role, accelerator: definition.accelerator, }; } return { label: translatedLabel, accelerator: definition.accelerator, id: definition.id, enabled: definition.enabled, click: async () => { await triggerMenuAction(action); }, }; }; const buildSharedGroupMenuItems = (groupLabel: string): MenuItemConstructorOptions[] => { const group = APP_MENU_GROUPS.find(item => item.label === groupLabel); if (!group) { return []; } const filteredItems = group.items.filter(item => isDev || item.action !== 'toggleDevTools'); return filteredItems.map((item) => { if (item.separator) { return { type: 'separator' }; } return buildSharedMenuItem(item.action as AppMenuAction); }); }; const template: MenuItemConstructorOptions[] = [ { label: getMenuGroupLabel('File'), submenu: buildSharedGroupMenuItems('File'), }, { label: getMenuGroupLabel('Edit'), submenu: buildSharedGroupMenuItems('Edit'), }, { label: getMenuGroupLabel('View'), submenu: buildSharedGroupMenuItems('View'), }, { label: getMenuGroupLabel('Blog'), submenu: buildSharedGroupMenuItems('Blog'), }, { label: getMenuGroupLabel('Help'), submenu: buildSharedGroupMenuItems('Help'), }, ]; // macOS specific menu adjustments if (process.platform === 'darwin') { template.unshift({ label: app.name, submenu: [ { role: 'about' }, { type: 'separator' }, { role: 'services' }, { type: 'separator' }, { role: 'hide' }, { role: 'hideOthers' }, { role: 'unhide' }, { type: 'separator' }, { role: 'quit' }, ], }); } return Menu.buildFromTemplate(template); } async function initialize(): Promise { // Initialize database const db = getDatabase(); await db.initializeLocal(); // Register custom protocol for serving media files // URLs like bds-media://media-id will be resolved to the actual file protocol.handle('bds-media', async (request) => { try { const url = new URL(request.url); const mediaIdentifier = url.hostname; // bds-media://media-id or bds-media://filename.jpg const database = getDatabase().getLocal(); // First, try to find by ID (most common case) let mediaItem = await database .select() .from(media) .where(eq(media.id, mediaIdentifier)) .get(); // If not found by ID, try by filename if (!mediaItem) { mediaItem = await database .select() .from(media) .where(eq(media.filename, mediaIdentifier)) .get(); } // If still not found, try by original name if (!mediaItem) { mediaItem = await database .select() .from(media) .where(eq(media.originalName, mediaIdentifier)) .get(); } if (mediaItem && mediaItem.filePath) { // Check if file exists before attempting to fetch if (!fs.existsSync(mediaItem.filePath)) { console.error(`[bds-media] File not found at path: ${mediaItem.filePath} (media id: ${mediaIdentifier})`); return new Response(`Media file not found at: ${mediaItem.filePath}`, { status: 404 }); } // Use net.fetch to get the file - this handles the file protocol properly return net.fetch(`file://${mediaItem.filePath}`); } // Return a 404 response if media not found console.error(`[bds-media] No media record found for identifier: ${mediaIdentifier}`); return new Response('Media not found in database', { status: 404 }); } catch (error) { console.error('[bds-media] Error serving media:', error); return new Response('Internal server error', { status: 500 }); } }); // Register custom protocol for serving thumbnail images // URLs like bds-thumb://media-id will serve the small thumbnail webp protocol.handle('bds-thumb', async (request) => { try { const url = new URL(request.url); const mediaId = url.hostname; const engine = getMediaEngine(); const thumbnails = await engine.getThumbnailPaths(mediaId); if (thumbnails.small) { // Check if thumbnail file exists if (!fs.existsSync(thumbnails.small)) { console.error(`[bds-thumb] Thumbnail not found at path: ${thumbnails.small} (media id: ${mediaId})`); // Fall through to try full image } else { return net.fetch(`file://${thumbnails.small}`); } } // Fallback to full image if thumbnail doesn't exist const database = getDatabase().getLocal(); const mediaItem = await database .select() .from(media) .where(eq(media.id, mediaId)) .get(); if (mediaItem && mediaItem.filePath) { // Check if file exists before attempting to fetch if (!fs.existsSync(mediaItem.filePath)) { console.error(`[bds-thumb] Media file not found at path: ${mediaItem.filePath} (media id: ${mediaId})`); return new Response(`Media file not found at: ${mediaItem.filePath}`, { status: 404 }); } return net.fetch(`file://${mediaItem.filePath}`); } console.error(`[bds-thumb] No media record found for id: ${mediaId}`); return new Response('Thumbnail not found', { status: 404 }); } catch (error) { console.error('[bds-thumb] Error serving thumbnail:', error); return new Response('Internal server error', { status: 500 }); } }); // Register IPC handlers registerIpcHandlers(); ipcMain.handle('app:setPreviewPostTarget', async (_, postId: string | null) => { activePreviewPostId = typeof postId === 'string' && postId.length > 0 ? postId : null; setPreviewPostMenuEnabled(Boolean(activePreviewPostId)); }); ipcMain.handle('app:rendererReady', async () => { rendererReady = true; flushPendingBlogmarkCreatedEvents(); return true; }); // Initialize and register chat handlers initializeChatHandlers(() => mainWindow); registerChatHandlers(); } const hasSingleInstanceLock = typeof app.requestSingleInstanceLock !== 'function' ? true : app.requestSingleInstanceLock(); if (!hasSingleInstanceLock) { app.quit(); } app.on('second-instance', (_event, argv) => { focusMainWindow(); const deepLinks = extractBlogmarkDeepLinks(argv); for (const deepLink of deepLinks) { enqueueBlogmarkDeepLink(deepLink); } void processBlogmarkQueue(); }); app.on('open-url', (event, deepLink) => { event.preventDefault(); enqueueBlogmarkDeepLink(deepLink); focusMainWindow(); void processBlogmarkQueue(); }); // App lifecycle app.whenReady().then(async () => { await initialize(); const activeProjectContextReady = initializeActiveProjectContext(); registerBlogmarkProtocolClient(); try { await startPreviewServerOnAppStart(); } catch (error) { console.error('Failed to start preview server on app startup:', error); } try { const mcpServer = getMCPServer(); await mcpServer.start(MCP_SERVER_PORT); } catch (error) { console.error('Failed to start MCP server on app startup:', error); } createWindow(); await activeProjectContextReady; appInitialized = true; const startupDeepLinks = extractBlogmarkDeepLinks(process.argv); for (const deepLink of startupDeepLinks) { enqueueBlogmarkDeepLink(deepLink); } await processBlogmarkQueue(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); app.on('before-quit', async () => { // Cleanup chat resources await cleanupChatHandlers(); if (previewServer) { await previewServer.stop(); previewServer = null; } try { const mcpServer = getMCPServer(); await mcpServer.cleanup(); } catch (error) { console.error('Failed to cleanup MCP server:', error); } const db = getDatabase(); await db.close(); }); // Handle any uncaught exceptions process.on('uncaughtException', (error) => { console.error('Uncaught exception:', error); }); process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled rejection at:', promise, 'reason:', reason); });