diff --git a/packages/shared/data/types.d.ts b/packages/shared/data/types.d.ts new file mode 100644 index 0000000000..ca6481fef3 --- /dev/null +++ b/packages/shared/data/types.d.ts @@ -0,0 +1,4 @@ +import { PreferencesType } from './preferences' + +export type PreferenceDefaultScopeType = PreferencesType['default'] +export type PreferenceKeyType = keyof PreferenceDefaultScopeType diff --git a/src/main/data/PreferenceService.ts b/src/main/data/PreferenceService.ts index 2aec6cb94c..8e9655b15a 100644 --- a/src/main/data/PreferenceService.ts +++ b/src/main/data/PreferenceService.ts @@ -1,6 +1,6 @@ import { loggerService } from '@logger' -import type { PreferencesType } from '@shared/data/preferences' import { DefaultPreferences } from '@shared/data/preferences' +import type { PreferenceDefaultScopeType, PreferenceKeyType } from '@shared/data/types' import { IpcChannel } from '@shared/IpcChannel' import { and, eq } from 'drizzle-orm' import { BrowserWindow } from 'electron' @@ -10,8 +10,9 @@ import { preferenceTable } from './db/schemas/preference' const logger = loggerService.withContext('PreferenceService') -type PreferenceKey = keyof PreferencesType['default'] +type MultiPreferencesResultType = { [P in K]: PreferenceDefaultScopeType[P] | undefined } +const DefaultScope = 'default' /** * PreferenceService manages preference data storage and synchronization across multiple windows * @@ -26,7 +27,7 @@ type PreferenceKey = keyof PreferencesType['default'] export class PreferenceService { private static instance: PreferenceService private subscriptions = new Map>() // windowId -> Set - private cache: Record = { ...DefaultPreferences.default } + private cache: PreferenceDefaultScopeType = DefaultPreferences.default private initialized = false private constructor() { @@ -54,13 +55,13 @@ export class PreferenceService { try { const db = dbService.getDb() - const results = await db.select().from(preferenceTable).where(eq(preferenceTable.scope, 'default')) + const results = await db.select().from(preferenceTable).where(eq(preferenceTable.scope, DefaultScope)) // Update cache with database values, keeping defaults for missing keys for (const result of results) { - const key = result.key as PreferenceKey + const key = result.key if (key in this.cache) { - this.cache[key] = result.value as any + this.cache[key] = result.value } } @@ -69,7 +70,7 @@ export class PreferenceService { } catch (error) { logger.error('Failed to initialize preference cache:', error as Error) // Keep default values on initialization failure - this.initialized = true + this.initialized = false } } @@ -77,7 +78,7 @@ export class PreferenceService { * Get a single preference value from memory cache * Fast synchronous access - no database queries after initialization */ - get(key: K): PreferencesType['default'][K] { + get(key: K): PreferenceDefaultScopeType[K] { if (!this.initialized) { logger.warn(`Preference cache not initialized, returning default for ${key}`) return DefaultPreferences.default[key] @@ -90,34 +91,20 @@ export class PreferenceService { * Set a single preference value * Updates both database and memory cache, then broadcasts changes to subscribed windows */ - async set(key: K, value: PreferencesType['default'][K]): Promise { + async set(key: K, value: PreferenceDefaultScopeType[K]): Promise { try { + if (!(key in this.cache)) { + throw new Error(`Preference ${key} not found in cache`) + } + const db = dbService.getDb() - const scope = 'default' - // First try to update existing record - const existing = await db - .select() - .from(preferenceTable) - .where(and(eq(preferenceTable.scope, scope), eq(preferenceTable.key, key))) - .limit(1) - - if (existing.length > 0) { - // Update existing record - await db - .update(preferenceTable) - .set({ - value: value as any - }) - .where(and(eq(preferenceTable.scope, scope), eq(preferenceTable.key, key))) - } else { - // Insert new record - await db.insert(preferenceTable).values({ - scope, - key, + await db + .update(preferenceTable) + .set({ value: value as any }) - } + .where(and(eq(preferenceTable.scope, DefaultScope), eq(preferenceTable.key, key))) // Update memory cache immediately this.cache[key] = value @@ -136,24 +123,24 @@ export class PreferenceService { * Get multiple preferences at once from memory cache * Fast synchronous access - no database queries */ - getMultiple(keys: string[]): Record { + getMultiple(keys: K[]): MultiPreferencesResultType { if (!this.initialized) { logger.warn('Preference cache not initialized, returning defaults for multiple keys') - const output: Record = {} + const output: MultiPreferencesResultType = {} as MultiPreferencesResultType for (const key of keys) { if (key in DefaultPreferences.default) { - output[key] = DefaultPreferences.default[key as PreferenceKey] + output[key] = DefaultPreferences.default[key] } else { - output[key] = undefined + output[key] = undefined as MultiPreferencesResultType[K] } } return output } - const output: Record = {} + const output: MultiPreferencesResultType = {} as MultiPreferencesResultType for (const key of keys) { if (key in this.cache) { - output[key] = this.cache[key as PreferenceKey] + output[key] = this.cache[key] } else { output[key] = undefined } @@ -166,42 +153,30 @@ export class PreferenceService { * Set multiple preferences at once * Updates both database and memory cache in a transaction, then broadcasts changes */ - async setMultiple(updates: Record): Promise { + async setMultiple(updates: Partial): Promise { try { - const scope = 'default' + //check if all keys are in the cache + for (const [key, value] of Object.entries(updates)) { + if (!(key in this.cache) || value === undefined || value === null) { + throw new Error(`Preference ${key} not found in cache or value is undefined or null`) + } + } - await dbService.transaction(async (tx) => { + await dbService.getDb().transaction(async (tx) => { for (const [key, value] of Object.entries(updates)) { - // Check if record exists - const existing = await tx - .select() - .from(preferenceTable) - .where(and(eq(preferenceTable.scope, scope), eq(preferenceTable.key, key))) - .limit(1) - - if (existing.length > 0) { - // Update existing record - await tx - .update(preferenceTable) - .set({ - value - }) - .where(and(eq(preferenceTable.scope, scope), eq(preferenceTable.key, key))) - } else { - // Insert new record - await tx.insert(preferenceTable).values({ - scope, - key, + await tx + .update(preferenceTable) + .set({ value }) - } + .where(and(eq(preferenceTable.scope, DefaultScope), eq(preferenceTable.key, key))) } }) // Update memory cache for all changed keys for (const [key, value] of Object.entries(updates)) { if (key in this.cache) { - this.cache[key as PreferenceKey] = value + this.cache[key] = value } } @@ -260,7 +235,7 @@ export class PreferenceService { try { const window = BrowserWindow.fromId(windowId) if (window && !window.isDestroyed()) { - window.webContents.send(IpcChannel.Preference_Changed, key, value, 'default') + window.webContents.send(IpcChannel.Preference_Changed, key, value, DefaultScope) } else { // Clean up invalid window subscription this.subscriptions.delete(windowId) diff --git a/src/main/data/db/DbService.ts b/src/main/data/db/DbService.ts index e5aef202fb..00d87a6ba0 100644 --- a/src/main/data/db/DbService.ts +++ b/src/main/data/db/DbService.ts @@ -40,26 +40,6 @@ class DbService { return this.db } - /** - * Execute operations within a database transaction - * Automatically handles rollback on error and commit on success - */ - public async transaction(callback: (tx: any) => Promise): Promise { - logger.debug('Starting database transaction') - - try { - const result = await this.db.transaction(async (tx) => { - return await callback(tx) - }) - - logger.debug('Database transaction completed successfully') - return result - } catch (error) { - logger.error('Database transaction failed, rolling back', error as Error) - throw error - } - } - public async migrateSeed(seedName: keyof typeof Seeding): Promise { try { const Seed = Seeding[seedName] diff --git a/src/main/data/migrate/dataRefactor/migrators/PreferencesMigrator.ts b/src/main/data/migrate/dataRefactor/migrators/PreferencesMigrator.ts index f18a6f4b30..3fff882706 100644 --- a/src/main/data/migrate/dataRefactor/migrators/PreferencesMigrator.ts +++ b/src/main/data/migrate/dataRefactor/migrators/PreferencesMigrator.ts @@ -332,7 +332,7 @@ export class PreferencesMigrator { // Validate batch data before starting transaction this.validateBatchData(batchData) - await dbService.transaction(async (tx) => { + await this.db.transaction(async (tx) => { const scope = 'default' const timestamp = Date.now() let completedOperations = 0 diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 30641f653e..f9e2e07ca5 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -8,6 +8,7 @@ import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/pro import { handleZoomFactor } from '@main/utils/zoom' import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core' import { UpgradeChannel } from '@shared/config/constant' +import type { PreferenceDefaultScopeType, PreferenceKeyType } from '@shared/data/types' import { IpcChannel } from '@shared/IpcChannel' import { FileMetadata, Provider, Shortcut, ThemeMode } from '@types' import { BrowserWindow, dialog, ipcMain, ProxyConfig, session, shell, systemPreferences, webContents } from 'electron' @@ -699,19 +700,25 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ) // Preference handlers - ipcMain.handle(IpcChannel.Preference_Get, async (_, key: string) => { - return preferenceService.get(key as any) + + // TODO move to preferenceService + + ipcMain.handle(IpcChannel.Preference_Get, (_, key: PreferenceKeyType) => { + return preferenceService.get(key) }) - ipcMain.handle(IpcChannel.Preference_Set, async (_, key: string, value: any) => { - await preferenceService.set(key as any, value) - }) + ipcMain.handle( + IpcChannel.Preference_Set, + async (_, key: PreferenceKeyType, value: PreferenceDefaultScopeType[PreferenceKeyType]) => { + await preferenceService.set(key, value) + } + ) - ipcMain.handle(IpcChannel.Preference_GetMultiple, async (_, keys: string[]) => { + ipcMain.handle(IpcChannel.Preference_GetMultiple, (_, keys: PreferenceKeyType[]) => { return preferenceService.getMultiple(keys) }) - ipcMain.handle(IpcChannel.Preference_SetMultiple, async (_, updates: Record) => { + ipcMain.handle(IpcChannel.Preference_SetMultiple, async (_, updates: Partial) => { await preferenceService.setMultiple(updates) })