refactor(preferences): rename and enhance multiple preferences retrieval

- Renamed `Preference_GetMultiple` to `Preference_GetMultipleRaw` in IpcChannel for clarity.
- Introduced `MultiPreferencesResultType` to better map requested keys to their values or undefined in preferenceTypes.
- Updated `PreferenceService` to implement `getMultipleRaw` for synchronous access to multiple preferences.
- Adjusted related components and services to utilize the new method for fetching multiple preferences.
- Cleaned up imports and ensured consistent usage across the application.
This commit is contained in:
fullex 2025-12-02 14:03:35 +08:00
parent 819f6de64d
commit 8ea550d566
8 changed files with 89 additions and 21 deletions

View File

@ -319,7 +319,7 @@ export enum IpcChannel {
// Data: Preference // Data: Preference
Preference_Get = 'preference:get', Preference_Get = 'preference:get',
Preference_Set = 'preference:set', Preference_Set = 'preference:set',
Preference_GetMultiple = 'preference:get-multiple', Preference_GetMultipleRaw = 'preference:get-multiple-raw',
Preference_SetMultiple = 'preference:set-multiple', Preference_SetMultiple = 'preference:set-multiple',
Preference_GetAll = 'preference:get-all', Preference_GetAll = 'preference:get-all',
Preference_Subscribe = 'preference:subscribe', Preference_Subscribe = 'preference:subscribe',

View File

@ -3,6 +3,13 @@ import type { PreferenceSchemas } from './preferenceSchemas'
export type PreferenceDefaultScopeType = PreferenceSchemas['default'] export type PreferenceDefaultScopeType = PreferenceSchemas['default']
export type PreferenceKeyType = keyof PreferenceDefaultScopeType export type PreferenceKeyType = keyof PreferenceDefaultScopeType
/**
* Result type for getMultipleRaw - maps requested keys to their values or undefined
*/
export type MultiPreferencesResultType<K extends PreferenceKeyType> = {
[P in K]: PreferenceDefaultScopeType[P] | undefined
}
export type PreferenceUpdateOptions = { export type PreferenceUpdateOptions = {
optimistic: boolean optimistic: boolean
} }

View File

@ -1,7 +1,11 @@
import { dbService } from '@data/db/DbService' import { dbService } from '@data/db/DbService'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { DefaultPreferences } from '@shared/data/preference/preferenceSchemas' import { DefaultPreferences } from '@shared/data/preference/preferenceSchemas'
import type { PreferenceDefaultScopeType, PreferenceKeyType } from '@shared/data/preference/preferenceTypes' import type {
MultiPreferencesResultType,
PreferenceDefaultScopeType,
PreferenceKeyType
} from '@shared/data/preference/preferenceTypes'
import { IpcChannel } from '@shared/IpcChannel' import { IpcChannel } from '@shared/IpcChannel'
import { and, eq } from 'drizzle-orm' import { and, eq } from 'drizzle-orm'
import { BrowserWindow, ipcMain } from 'electron' import { BrowserWindow, ipcMain } from 'electron'
@ -118,8 +122,6 @@ class PreferenceNotifier {
} }
} }
type MultiPreferencesResultType<K extends PreferenceKeyType> = { [P in K]: PreferenceDefaultScopeType[P] | undefined }
const DefaultScope = 'default' const DefaultScope = 'default'
/** /**
* PreferenceService manages preference data storage and synchronization across multiple windows * PreferenceService manages preference data storage and synchronization across multiple windows
@ -153,6 +155,7 @@ export class PreferenceService {
/** /**
* Get the singleton instance of PreferenceService * Get the singleton instance of PreferenceService
* @returns The singleton PreferenceService instance
*/ */
public static getInstance(): PreferenceService { public static getInstance(): PreferenceService {
if (!PreferenceService.instance) { if (!PreferenceService.instance) {
@ -164,6 +167,7 @@ export class PreferenceService {
/** /**
* Initialize preference cache from database * Initialize preference cache from database
* Should be called once at application startup * Should be called once at application startup
* @returns Promise that resolves when initialization completes
*/ */
public async initialize(): Promise<void> { public async initialize(): Promise<void> {
if (this.initialized) { if (this.initialized) {
@ -194,6 +198,8 @@ export class PreferenceService {
/** /**
* Get a single preference value from memory cache * Get a single preference value from memory cache
* Fast synchronous access - no database queries after initialization * Fast synchronous access - no database queries after initialization
* @param key The preference key to retrieve
* @returns The preference value with defaults applied
*/ */
public get<K extends PreferenceKeyType>(key: K): PreferenceDefaultScopeType[K] { public get<K extends PreferenceKeyType>(key: K): PreferenceDefaultScopeType[K] {
if (!this.initialized) { if (!this.initialized) {
@ -208,6 +214,9 @@ export class PreferenceService {
* Set a single preference value * Set a single preference value
* Updates both database and memory cache, then broadcasts changes to all listeners * Updates both database and memory cache, then broadcasts changes to all listeners
* Optimized to skip database writes and notifications when value hasn't changed * Optimized to skip database writes and notifications when value hasn't changed
* @param key The preference key to update
* @param value The new value to set
* @returns Promise that resolves when update completes
*/ */
public async set<K extends PreferenceKeyType>(key: K, value: PreferenceDefaultScopeType[K]): Promise<void> { public async set<K extends PreferenceKeyType>(key: K, value: PreferenceDefaultScopeType[K]): Promise<void> {
try { try {
@ -247,8 +256,10 @@ export class PreferenceService {
/** /**
* Get multiple preferences at once from memory cache * Get multiple preferences at once from memory cache
* Fast synchronous access - no database queries * Fast synchronous access - no database queries
* @param keys Array of preference keys to retrieve
* @returns Object with preference values for requested keys
*/ */
public getMultiple<K extends PreferenceKeyType>(keys: K[]): MultiPreferencesResultType<K> { public getMultipleRaw<K extends PreferenceKeyType>(keys: K[]): MultiPreferencesResultType<K> {
if (!this.initialized) { if (!this.initialized) {
logger.warn('Preference cache not initialized, returning defaults for multiple keys') logger.warn('Preference cache not initialized, returning defaults for multiple keys')
const output: MultiPreferencesResultType<K> = {} as MultiPreferencesResultType<K> const output: MultiPreferencesResultType<K> = {} as MultiPreferencesResultType<K>
@ -274,10 +285,38 @@ export class PreferenceService {
return output return output
} }
/**
* Get multiple preferences with custom key mapping
* @param keys Object mapping local names to preference keys
* @returns Object with mapped preference values
* @example
* ```typescript
* const { host, port } = preferenceService.getMultiple({
* host: 'feature.csaas.host',
* port: 'feature.csaas.port'
* })
* ```
*/
public getMultiple<T extends Record<string, PreferenceKeyType>>(
keys: T
): { [P in keyof T]: PreferenceDefaultScopeType[T[P]] } {
const preferenceKeys = Object.values(keys) as PreferenceKeyType[]
const values = this.getMultipleRaw(preferenceKeys)
const result = {} as { [P in keyof T]: PreferenceDefaultScopeType[T[P]] }
for (const key in keys) {
result[key] = values[keys[key]]!
}
return result
}
/** /**
* Set multiple preferences at once * Set multiple preferences at once
* Updates both database and memory cache in a transaction, then broadcasts changes * Updates both database and memory cache in a transaction, then broadcasts changes
* Optimized to skip unchanged values and reduce database operations * Optimized to skip unchanged values and reduce database operations
* @param updates Object containing preference key-value pairs to update
* @returns Promise that resolves when all updates complete
*/ */
public async setMultiple(updates: Partial<PreferenceDefaultScopeType>): Promise<void> { public async setMultiple(updates: Partial<PreferenceDefaultScopeType>): Promise<void> {
try { try {
@ -345,6 +384,8 @@ export class PreferenceService {
/** /**
* Subscribe a window to preference changes * Subscribe a window to preference changes
* Window will receive notifications for specified keys * Window will receive notifications for specified keys
* @param windowId The ID of the BrowserWindow to subscribe
* @param keys Array of preference keys to subscribe to
*/ */
public subscribeForWindow(windowId: number, keys: string[]): void { public subscribeForWindow(windowId: number, keys: string[]): void {
if (!this.subscriptions.has(windowId)) { if (!this.subscriptions.has(windowId)) {
@ -359,6 +400,7 @@ export class PreferenceService {
/** /**
* Unsubscribe a window from preference changes * Unsubscribe a window from preference changes
* @param windowId The ID of the BrowserWindow to unsubscribe
*/ */
public unsubscribeForWindow(windowId: number): void { public unsubscribeForWindow(windowId: number): void {
this.subscriptions.delete(windowId) this.subscriptions.delete(windowId)
@ -369,7 +411,9 @@ export class PreferenceService {
/** /**
* Subscribe to preference changes in main process * Subscribe to preference changes in main process
* Returns unsubscribe function for cleanup * @param key The preference key to watch for changes
* @param callback Function to call when the preference changes
* @returns Unsubscribe function for cleanup
*/ */
public subscribeChange<K extends PreferenceKeyType>( public subscribeChange<K extends PreferenceKeyType>(
key: K, key: K,
@ -386,7 +430,9 @@ export class PreferenceService {
/** /**
* Subscribe to multiple preference changes in main process * Subscribe to multiple preference changes in main process
* Returns unsubscribe function for cleanup * @param keys Array of preference keys to watch for changes
* @param callback Function to call when any of the preferences change
* @returns Unsubscribe function for cleanup
*/ */
public subscribeMultipleChanges( public subscribeMultipleChanges(
keys: PreferenceKeyType[], keys: PreferenceKeyType[],
@ -417,6 +463,7 @@ export class PreferenceService {
/** /**
* Get main process listener count for debugging * Get main process listener count for debugging
* @returns Total number of change listeners
*/ */
public getChangeListenerCount(): number { public getChangeListenerCount(): number {
return this.notifier.getTotalSubscriptionCount() return this.notifier.getTotalSubscriptionCount()
@ -424,6 +471,8 @@ export class PreferenceService {
/** /**
* Get subscription count for a specific preference key * Get subscription count for a specific preference key
* @param key The preference key to check
* @returns Number of listeners subscribed to this key
*/ */
public getKeyListenerCount(key: PreferenceKeyType): number { public getKeyListenerCount(key: PreferenceKeyType): number {
return this.notifier.getKeySubscriptionCount(key) return this.notifier.getKeySubscriptionCount(key)
@ -431,6 +480,7 @@ export class PreferenceService {
/** /**
* Get all subscribed preference keys * Get all subscribed preference keys
* @returns Array of preference keys that have active subscriptions
*/ */
public getSubscribedKeys(): string[] { public getSubscribedKeys(): string[] {
return this.notifier.getSubscribedKeys() return this.notifier.getSubscribedKeys()
@ -438,6 +488,7 @@ export class PreferenceService {
/** /**
* Get detailed subscription statistics for debugging * Get detailed subscription statistics for debugging
* @returns Record mapping preference keys to their listener counts
*/ */
public getSubscriptionStats(): Record<string, number> { public getSubscriptionStats(): Record<string, number> {
return this.notifier.getSubscriptionStats() return this.notifier.getSubscriptionStats()
@ -446,6 +497,10 @@ export class PreferenceService {
/** /**
* Unified notification method for both main and renderer processes * Unified notification method for both main and renderer processes
* Broadcasts preference changes to main process listeners and subscribed renderer windows * Broadcasts preference changes to main process listeners and subscribed renderer windows
* @param key The preference key that changed
* @param value The new value
* @param oldValue The previous value
* @returns Promise that resolves when all notifications are sent
*/ */
private async notifyChange(key: string, value: any, oldValue?: any): Promise<void> { private async notifyChange(key: string, value: any, oldValue?: any): Promise<void> {
// 1. Notify main process listeners // 1. Notify main process listeners
@ -512,6 +567,7 @@ export class PreferenceService {
/** /**
* Get all preferences from memory cache * Get all preferences from memory cache
* Returns complete preference object for bulk operations * Returns complete preference object for bulk operations
* @returns Complete preference object with all values
*/ */
public getAll(): PreferenceDefaultScopeType { public getAll(): PreferenceDefaultScopeType {
if (!this.initialized) { if (!this.initialized) {
@ -523,7 +579,8 @@ export class PreferenceService {
} }
/** /**
* Get all current subscriptions (for debugging) * Get all current window subscriptions (for debugging)
* @returns Map of window IDs to their subscribed preference keys
*/ */
public getSubscriptions(): Map<number, Set<string>> { public getSubscriptions(): Map<number, Set<string>> {
return new Map(this.subscriptions) return new Map(this.subscriptions)
@ -548,6 +605,9 @@ export class PreferenceService {
/** /**
* Deep equality check for preference values * Deep equality check for preference values
* Handles primitives, arrays, and plain objects * Handles primitives, arrays, and plain objects
* @param a First value to compare
* @param b Second value to compare
* @returns True if values are deeply equal, false otherwise
*/ */
private isEqual(a: any, b: any): boolean { private isEqual(a: any, b: any): boolean {
// Handle strict equality (primitives, same reference) // Handle strict equality (primitives, same reference)
@ -603,8 +663,8 @@ export class PreferenceService {
} }
) )
ipcMain.handle(IpcChannel.Preference_GetMultiple, (_, keys: PreferenceKeyType[]) => { ipcMain.handle(IpcChannel.Preference_GetMultipleRaw, (_, keys: PreferenceKeyType[]) => {
return instance.getMultiple(keys) return instance.getMultipleRaw(keys)
}) })
ipcMain.handle(IpcChannel.Preference_SetMultiple, async (_, updates: Partial<PreferenceDefaultScopeType>) => { ipcMain.handle(IpcChannel.Preference_SetMultiple, async (_, updates: Partial<PreferenceDefaultScopeType>) => {

View File

@ -61,7 +61,7 @@ export class ApiServerService {
* Get current API server configuration from preference service * Get current API server configuration from preference service
*/ */
getCurrentConfig(): ApiServerConfig { getCurrentConfig(): ApiServerConfig {
const config = preferenceService.getMultiple([ const config = preferenceService.getMultipleRaw([
'feature.csaas.enabled', 'feature.csaas.enabled',
'feature.csaas.host', 'feature.csaas.host',
'feature.csaas.port', 'feature.csaas.port',

View File

@ -101,7 +101,7 @@ class ClaudeCodeService implements AgentServiceInterface {
return aiStream return aiStream
} }
const apiConfig = preferenceService.getMultiple([ const apiConfig = preferenceService.getMultipleRaw([
'feature.csaas.host', 'feature.csaas.host',
'feature.csaas.port', 'feature.csaas.port',
'feature.csaas.api_key' 'feature.csaas.api_key'

View File

@ -7,6 +7,7 @@ import type { LogLevel, LogSourceWithContext } from '@shared/config/logger'
import type { FileChangeEvent, WebviewKeyEvent } from '@shared/config/types' import type { FileChangeEvent, WebviewKeyEvent } from '@shared/config/types'
import type { CacheSyncMessage } from '@shared/data/cache/cacheTypes' import type { CacheSyncMessage } from '@shared/data/cache/cacheTypes'
import type { import type {
MultiPreferencesResultType,
PreferenceDefaultScopeType, PreferenceDefaultScopeType,
PreferenceKeyType, PreferenceKeyType,
SelectionActionItem SelectionActionItem
@ -553,8 +554,8 @@ const api = {
ipcRenderer.invoke(IpcChannel.Preference_Get, key), ipcRenderer.invoke(IpcChannel.Preference_Get, key),
set: <K extends PreferenceKeyType>(key: K, value: PreferenceDefaultScopeType[K]): Promise<void> => set: <K extends PreferenceKeyType>(key: K, value: PreferenceDefaultScopeType[K]): Promise<void> =>
ipcRenderer.invoke(IpcChannel.Preference_Set, key, value), ipcRenderer.invoke(IpcChannel.Preference_Set, key, value),
getMultiple: (keys: PreferenceKeyType[]): Promise<Partial<PreferenceDefaultScopeType>> => getMultipleRaw: <K extends PreferenceKeyType>(keys: K[]): Promise<MultiPreferencesResultType<K>> =>
ipcRenderer.invoke(IpcChannel.Preference_GetMultiple, keys), ipcRenderer.invoke(IpcChannel.Preference_GetMultipleRaw, keys),
setMultiple: (updates: Partial<PreferenceDefaultScopeType>) => setMultiple: (updates: Partial<PreferenceDefaultScopeType>) =>
ipcRenderer.invoke(IpcChannel.Preference_SetMultiple, updates), ipcRenderer.invoke(IpcChannel.Preference_SetMultiple, updates),
getAll: (): Promise<PreferenceDefaultScopeType> => ipcRenderer.invoke(IpcChannel.Preference_GetAll), getAll: (): Promise<PreferenceDefaultScopeType> => ipcRenderer.invoke(IpcChannel.Preference_GetAll),

View File

@ -1,6 +1,7 @@
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { DefaultPreferences } from '@shared/data/preference/preferenceSchemas' import { DefaultPreferences } from '@shared/data/preference/preferenceSchemas'
import type { import type {
MultiPreferencesResultType,
PreferenceDefaultScopeType, PreferenceDefaultScopeType,
PreferenceKeyType, PreferenceKeyType,
PreferenceUpdateOptions PreferenceUpdateOptions
@ -241,9 +242,9 @@ export class PreferenceService {
/** /**
* Get multiple preferences at once with caching and auto-subscription * Get multiple preferences at once with caching and auto-subscription
* @param keys Array of preference keys to retrieve * @param keys Array of preference keys to retrieve
* @returns Promise resolving to partial object with preference values * @returns Promise resolving to object with preference values for requested keys
*/ */
public async getMultipleRaw(keys: PreferenceKeyType[]): Promise<Partial<PreferenceDefaultScopeType>> { public async getMultipleRaw<K extends PreferenceKeyType>(keys: K[]): Promise<MultiPreferencesResultType<K>> {
// Check which keys are already cached // Check which keys are already cached
const cachedResults: Partial<PreferenceDefaultScopeType> = {} const cachedResults: Partial<PreferenceDefaultScopeType> = {}
const uncachedKeys: PreferenceKeyType[] = [] const uncachedKeys: PreferenceKeyType[] = []
@ -260,7 +261,7 @@ export class PreferenceService {
// Fetch uncached keys from main process // Fetch uncached keys from main process
if (uncachedKeys.length > 0) { if (uncachedKeys.length > 0) {
try { try {
const uncachedResults = await window.api.preference.getMultiple(uncachedKeys) const uncachedResults = await window.api.preference.getMultipleRaw(uncachedKeys)
// Update cache with new results // Update cache with new results
for (const [key, value] of Object.entries(uncachedResults)) { for (const [key, value] of Object.entries(uncachedResults)) {
@ -271,7 +272,7 @@ export class PreferenceService {
await this.subscribeToKeyInternal([key as PreferenceKeyType]) await this.subscribeToKeyInternal([key as PreferenceKeyType])
} }
return { ...cachedResults, ...uncachedResults } return { ...cachedResults, ...uncachedResults } as MultiPreferencesResultType<K>
} catch (error) { } catch (error) {
logger.error('Failed to get multiple preferences:', error as Error) logger.error('Failed to get multiple preferences:', error as Error)
@ -283,11 +284,11 @@ export class PreferenceService {
} }
} }
return { ...cachedResults, ...defaultResults } return { ...cachedResults, ...defaultResults } as MultiPreferencesResultType<K>
} }
} }
return cachedResults return cachedResults as MultiPreferencesResultType<K>
} }
/** /**

View File

@ -57,7 +57,6 @@ const ApiServerSettings: FC = () => {
return `cs-sk-${uuidv4()}` return `cs-sk-${uuidv4()}`
} }
const regenerateApiKey = () => { const regenerateApiKey = () => {
setApiServerConfig({ apiKey: generateApiKey() }) setApiServerConfig({ apiKey: generateApiKey() })
window.toast.success(t('apiServer.messages.apiKeyRegenerated')) window.toast.success(t('apiServer.messages.apiKeyRegenerated'))