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
Preference_Get = 'preference:get',
Preference_Set = 'preference:set',
Preference_GetMultiple = 'preference:get-multiple',
Preference_GetMultipleRaw = 'preference:get-multiple-raw',
Preference_SetMultiple = 'preference:set-multiple',
Preference_GetAll = 'preference:get-all',
Preference_Subscribe = 'preference:subscribe',

View File

@ -3,6 +3,13 @@ import type { PreferenceSchemas } from './preferenceSchemas'
export type PreferenceDefaultScopeType = PreferenceSchemas['default']
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 = {
optimistic: boolean
}

View File

@ -1,7 +1,11 @@
import { dbService } from '@data/db/DbService'
import { loggerService } from '@logger'
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 { and, eq } from 'drizzle-orm'
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'
/**
* PreferenceService manages preference data storage and synchronization across multiple windows
@ -153,6 +155,7 @@ export class PreferenceService {
/**
* Get the singleton instance of PreferenceService
* @returns The singleton PreferenceService instance
*/
public static getInstance(): PreferenceService {
if (!PreferenceService.instance) {
@ -164,6 +167,7 @@ export class PreferenceService {
/**
* Initialize preference cache from database
* Should be called once at application startup
* @returns Promise that resolves when initialization completes
*/
public async initialize(): Promise<void> {
if (this.initialized) {
@ -194,6 +198,8 @@ export class PreferenceService {
/**
* Get a single preference value from memory cache
* 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] {
if (!this.initialized) {
@ -208,6 +214,9 @@ export class PreferenceService {
* Set a single preference value
* Updates both database and memory cache, then broadcasts changes to all listeners
* 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> {
try {
@ -247,8 +256,10 @@ export class PreferenceService {
/**
* Get multiple preferences at once from memory cache
* 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) {
logger.warn('Preference cache not initialized, returning defaults for multiple keys')
const output: MultiPreferencesResultType<K> = {} as MultiPreferencesResultType<K>
@ -274,10 +285,38 @@ export class PreferenceService {
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
* Updates both database and memory cache in a transaction, then broadcasts changes
* 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> {
try {
@ -345,6 +384,8 @@ export class PreferenceService {
/**
* Subscribe a window to preference changes
* 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 {
if (!this.subscriptions.has(windowId)) {
@ -359,6 +400,7 @@ export class PreferenceService {
/**
* Unsubscribe a window from preference changes
* @param windowId The ID of the BrowserWindow to unsubscribe
*/
public unsubscribeForWindow(windowId: number): void {
this.subscriptions.delete(windowId)
@ -369,7 +411,9 @@ export class PreferenceService {
/**
* 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>(
key: K,
@ -386,7 +430,9 @@ export class PreferenceService {
/**
* 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(
keys: PreferenceKeyType[],
@ -417,6 +463,7 @@ export class PreferenceService {
/**
* Get main process listener count for debugging
* @returns Total number of change listeners
*/
public getChangeListenerCount(): number {
return this.notifier.getTotalSubscriptionCount()
@ -424,6 +471,8 @@ export class PreferenceService {
/**
* 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 {
return this.notifier.getKeySubscriptionCount(key)
@ -431,6 +480,7 @@ export class PreferenceService {
/**
* Get all subscribed preference keys
* @returns Array of preference keys that have active subscriptions
*/
public getSubscribedKeys(): string[] {
return this.notifier.getSubscribedKeys()
@ -438,6 +488,7 @@ export class PreferenceService {
/**
* Get detailed subscription statistics for debugging
* @returns Record mapping preference keys to their listener counts
*/
public getSubscriptionStats(): Record<string, number> {
return this.notifier.getSubscriptionStats()
@ -446,6 +497,10 @@ export class PreferenceService {
/**
* Unified notification method for both main and renderer processes
* 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> {
// 1. Notify main process listeners
@ -512,6 +567,7 @@ export class PreferenceService {
/**
* Get all preferences from memory cache
* Returns complete preference object for bulk operations
* @returns Complete preference object with all values
*/
public getAll(): PreferenceDefaultScopeType {
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>> {
return new Map(this.subscriptions)
@ -548,6 +605,9 @@ export class PreferenceService {
/**
* Deep equality check for preference values
* 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 {
// Handle strict equality (primitives, same reference)
@ -603,8 +663,8 @@ export class PreferenceService {
}
)
ipcMain.handle(IpcChannel.Preference_GetMultiple, (_, keys: PreferenceKeyType[]) => {
return instance.getMultiple(keys)
ipcMain.handle(IpcChannel.Preference_GetMultipleRaw, (_, keys: PreferenceKeyType[]) => {
return instance.getMultipleRaw(keys)
})
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
*/
getCurrentConfig(): ApiServerConfig {
const config = preferenceService.getMultiple([
const config = preferenceService.getMultipleRaw([
'feature.csaas.enabled',
'feature.csaas.host',
'feature.csaas.port',

View File

@ -101,7 +101,7 @@ class ClaudeCodeService implements AgentServiceInterface {
return aiStream
}
const apiConfig = preferenceService.getMultiple([
const apiConfig = preferenceService.getMultipleRaw([
'feature.csaas.host',
'feature.csaas.port',
'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 { CacheSyncMessage } from '@shared/data/cache/cacheTypes'
import type {
MultiPreferencesResultType,
PreferenceDefaultScopeType,
PreferenceKeyType,
SelectionActionItem
@ -553,8 +554,8 @@ const api = {
ipcRenderer.invoke(IpcChannel.Preference_Get, key),
set: <K extends PreferenceKeyType>(key: K, value: PreferenceDefaultScopeType[K]): Promise<void> =>
ipcRenderer.invoke(IpcChannel.Preference_Set, key, value),
getMultiple: (keys: PreferenceKeyType[]): Promise<Partial<PreferenceDefaultScopeType>> =>
ipcRenderer.invoke(IpcChannel.Preference_GetMultiple, keys),
getMultipleRaw: <K extends PreferenceKeyType>(keys: K[]): Promise<MultiPreferencesResultType<K>> =>
ipcRenderer.invoke(IpcChannel.Preference_GetMultipleRaw, keys),
setMultiple: (updates: Partial<PreferenceDefaultScopeType>) =>
ipcRenderer.invoke(IpcChannel.Preference_SetMultiple, updates),
getAll: (): Promise<PreferenceDefaultScopeType> => ipcRenderer.invoke(IpcChannel.Preference_GetAll),

View File

@ -1,6 +1,7 @@
import { loggerService } from '@logger'
import { DefaultPreferences } from '@shared/data/preference/preferenceSchemas'
import type {
MultiPreferencesResultType,
PreferenceDefaultScopeType,
PreferenceKeyType,
PreferenceUpdateOptions
@ -241,9 +242,9 @@ export class PreferenceService {
/**
* Get multiple preferences at once with caching and auto-subscription
* @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
const cachedResults: Partial<PreferenceDefaultScopeType> = {}
const uncachedKeys: PreferenceKeyType[] = []
@ -260,7 +261,7 @@ export class PreferenceService {
// Fetch uncached keys from main process
if (uncachedKeys.length > 0) {
try {
const uncachedResults = await window.api.preference.getMultiple(uncachedKeys)
const uncachedResults = await window.api.preference.getMultipleRaw(uncachedKeys)
// Update cache with new results
for (const [key, value] of Object.entries(uncachedResults)) {
@ -271,7 +272,7 @@ export class PreferenceService {
await this.subscribeToKeyInternal([key as PreferenceKeyType])
}
return { ...cachedResults, ...uncachedResults }
return { ...cachedResults, ...uncachedResults } as MultiPreferencesResultType<K>
} catch (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()}`
}
const regenerateApiKey = () => {
setApiServerConfig({ apiKey: generateApiKey() })
window.toast.success(t('apiServer.messages.apiKeyRegenerated'))