refactor(preferences): improve PreferenceService and related hooks with enhanced type safety and listener management

This commit refactors the PreferenceService to improve type safety for preference handling and updates the listener management system. It renames methods for clarity, ensuring that preference changes are properly notified to all relevant listeners. Additionally, the usePreference hook is updated to align with these changes, enhancing the overall consistency and reliability of preference management in the application.
This commit is contained in:
fullex 2025-08-12 21:23:02 +08:00
parent 2860935e5b
commit 85bdcdc206
3 changed files with 34 additions and 38 deletions

View File

@ -396,10 +396,12 @@ const api = {
ipcRenderer.invoke(IpcChannel.TRACE_ADD_STREAM_MESSAGE, spanId, modelName, context, message) ipcRenderer.invoke(IpcChannel.TRACE_ADD_STREAM_MESSAGE, spanId, modelName, context, message)
}, },
preference: { preference: {
get: <K extends PreferenceKeyType>(key: K) => ipcRenderer.invoke(IpcChannel.Preference_Get, key), get: <K extends PreferenceKeyType>(key: K): Promise<PreferenceDefaultScopeType[K]> =>
set: <K extends PreferenceKeyType>(key: K, value: PreferenceDefaultScopeType[K]) => ipcRenderer.invoke(IpcChannel.Preference_Get, key),
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[]) => ipcRenderer.invoke(IpcChannel.Preference_GetMultiple, keys), getMultiple: (keys: PreferenceKeyType[]): Promise<Partial<PreferenceDefaultScopeType>> =>
ipcRenderer.invoke(IpcChannel.Preference_GetMultiple, 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

@ -10,15 +10,19 @@ const logger = loggerService.withContext('PreferenceService')
*/ */
export class PreferenceService { export class PreferenceService {
private static instance: PreferenceService private static instance: PreferenceService
private cache: Record<string, any> = {} private cache: Record<string, any> = {}
private listeners = new Set<() => void>()
private keyListeners = new Map<string, Set<() => void>>() private allChangesListeners = new Set<() => void>()
private keyChangeListeners = new Map<string, Set<() => void>>()
private changeListenerCleanup: (() => void) | null = null private changeListenerCleanup: (() => void) | null = null
private subscribedKeys = new Set<string>() private subscribedKeys = new Set<string>()
private fullCacheLoaded = false private fullCacheLoaded = false
private constructor() { private constructor() {
this.setupChangeListener() this.setupChangeListeners()
} }
/** /**
@ -34,7 +38,7 @@ export class PreferenceService {
/** /**
* Setup IPC change listener for preference updates from main process * Setup IPC change listener for preference updates from main process
*/ */
private setupChangeListener() { private setupChangeListeners() {
if (!window.api?.preference?.onChanged) { if (!window.api?.preference?.onChanged) {
logger.error('Preference API not available in preload context') logger.error('Preference API not available in preload context')
return return
@ -45,7 +49,7 @@ export class PreferenceService {
if (oldValue !== value) { if (oldValue !== value) {
this.cache[key] = value this.cache[key] = value
this.notifyListeners(key) this.notifyChangeListeners(key)
logger.debug(`Preference ${key} updated to:`, { value }) logger.debug(`Preference ${key} updated to:`, { value })
} }
}) })
@ -54,12 +58,12 @@ export class PreferenceService {
/** /**
* Notify all relevant listeners about preference changes * Notify all relevant listeners about preference changes
*/ */
private notifyListeners(key: string) { private notifyChangeListeners(key: string) {
// Notify global listeners // Notify global listeners
this.listeners.forEach((listener) => listener()) this.allChangesListeners.forEach((listener) => listener())
// Notify specific key listeners // Notify specific key listeners
const keyListeners = this.keyListeners.get(key) const keyListeners = this.keyChangeListeners.get(key)
if (keyListeners) { if (keyListeners) {
keyListeners.forEach((listener) => listener()) keyListeners.forEach((listener) => listener())
} }
@ -101,7 +105,7 @@ export class PreferenceService {
// Update local cache immediately for responsive UI // Update local cache immediately for responsive UI
this.cache[key] = value this.cache[key] = value
this.notifyListeners(key) this.notifyChangeListeners(key)
logger.debug(`Preference ${key} set to:`, { value }) logger.debug(`Preference ${key} set to:`, { value })
} catch (error) { } catch (error) {
@ -113,7 +117,7 @@ export class PreferenceService {
/** /**
* Get multiple preferences at once * Get multiple preferences at once
*/ */
public async getMultiple(keys: PreferenceKeyType[]): Promise<Record<string, any>> { public async getMultiple(keys: PreferenceKeyType[]): Promise<Partial<PreferenceDefaultScopeType>> {
// 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[] = []
@ -148,10 +152,10 @@ export class PreferenceService {
logger.error('Failed to get multiple preferences:', error as Error) logger.error('Failed to get multiple preferences:', error as Error)
// Fill in default values for failed keys // Fill in default values for failed keys
const defaultResults: Record<string, any> = {} const defaultResults: Partial<PreferenceDefaultScopeType> = {}
for (const key of uncachedKeys) { for (const key of uncachedKeys) {
if (key in DefaultPreferences.default) { if (key in DefaultPreferences.default) {
defaultResults[key] = DefaultPreferences.default[key as PreferenceKeyType] ;(defaultResults as any)[key] = DefaultPreferences.default[key]
} }
} }
@ -161,7 +165,6 @@ export class PreferenceService {
return cachedResults return cachedResults
} }
/** /**
* Set multiple preferences at once * Set multiple preferences at once
*/ */
@ -172,7 +175,7 @@ export class PreferenceService {
// Update local cache for all updated values // Update local cache for all updated values
for (const [key, value] of Object.entries(updates)) { for (const [key, value] of Object.entries(updates)) {
this.cache[key as PreferenceKeyType] = value this.cache[key as PreferenceKeyType] = value
this.notifyListeners(key) this.notifyChangeListeners(key)
} }
logger.debug(`Updated ${Object.keys(updates).length} preferences`) logger.debug(`Updated ${Object.keys(updates).length} preferences`)
@ -200,24 +203,24 @@ export class PreferenceService {
/** /**
* Subscribe to global preference changes (for useSyncExternalStore) * Subscribe to global preference changes (for useSyncExternalStore)
*/ */
public subscribe = (callback: () => void): (() => void) => { public subscribeAllChanges = (callback: () => void): (() => void) => {
this.listeners.add(callback) this.allChangesListeners.add(callback)
return () => { return () => {
this.listeners.delete(callback) this.allChangesListeners.delete(callback)
} }
} }
/** /**
* Subscribe to specific key changes (for useSyncExternalStore) * Subscribe to specific key changes (for useSyncExternalStore)
*/ */
public subscribeToKey = public subscribeKeyChange =
(key: PreferenceKeyType) => (key: PreferenceKeyType) =>
(callback: () => void): (() => void) => { (callback: () => void): (() => void) => {
if (!this.keyListeners.has(key)) { if (!this.keyChangeListeners.has(key)) {
this.keyListeners.set(key, new Set()) this.keyChangeListeners.set(key, new Set())
} }
const keyListeners = this.keyListeners.get(key)! const keyListeners = this.keyChangeListeners.get(key)!
keyListeners.add(callback) keyListeners.add(callback)
// Auto-subscribe to this key for updates // Auto-subscribe to this key for updates
@ -226,20 +229,11 @@ export class PreferenceService {
return () => { return () => {
keyListeners.delete(callback) keyListeners.delete(callback)
if (keyListeners.size === 0) { if (keyListeners.size === 0) {
this.keyListeners.delete(key) this.keyChangeListeners.delete(key)
} }
} }
} }
/**
* Get snapshot for useSyncExternalStore
*/
public getSnapshot =
<K extends PreferenceKeyType>(key: K) =>
(): PreferenceDefaultScopeType[K] | undefined => {
return this.cache[key]
}
/** /**
* Get cached value without async fetch * Get cached value without async fetch
*/ */
@ -323,8 +317,8 @@ export class PreferenceService {
this.changeListenerCleanup = null this.changeListenerCleanup = null
} }
this.clearCache() this.clearCache()
this.listeners.clear() this.allChangesListeners.clear()
this.keyListeners.clear() this.keyChangeListeners.clear()
this.subscribedKeys.clear() this.subscribedKeys.clear()
} }
} }

View File

@ -70,7 +70,7 @@ export function usePreference<K extends PreferenceKeyType>(
): [PreferenceDefaultScopeType[K] | undefined, (value: PreferenceDefaultScopeType[K]) => Promise<void>] { ): [PreferenceDefaultScopeType[K] | undefined, (value: PreferenceDefaultScopeType[K]) => Promise<void>] {
// Subscribe to changes for this specific preference // Subscribe to changes for this specific preference
const value = useSyncExternalStore( const value = useSyncExternalStore(
useCallback((callback) => preferenceService.subscribeToKey(key)(callback), [key]), useCallback((callback) => preferenceService.subscribeKeyChange(key)(callback), [key]),
useCallback(() => preferenceService.getCachedValue(key), [key]), useCallback(() => preferenceService.getCachedValue(key), [key]),
() => undefined // SSR snapshot (not used in Electron context) () => undefined // SSR snapshot (not used in Electron context)
) )
@ -236,7 +236,7 @@ export function useMultiplePreferences<T extends Record<string, PreferenceKeyTyp
useCallback( useCallback(
(callback: () => void) => { (callback: () => void) => {
// Subscribe to all keys and aggregate the unsubscribe functions // Subscribe to all keys and aggregate the unsubscribe functions
const unsubscribeFunctions = keyList.map((key) => preferenceService.subscribeToKey(key)(callback)) const unsubscribeFunctions = keyList.map((key) => preferenceService.subscribeKeyChange(key)(callback))
return () => { return () => {
unsubscribeFunctions.forEach((unsubscribe) => unsubscribe()) unsubscribeFunctions.forEach((unsubscribe) => unsubscribe())