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)
},
preference: {
get: <K extends PreferenceKeyType>(key: K) => ipcRenderer.invoke(IpcChannel.Preference_Get, key),
set: <K extends PreferenceKeyType>(key: K, value: PreferenceDefaultScopeType[K]) =>
get: <K extends PreferenceKeyType>(key: K): Promise<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),
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>) =>
ipcRenderer.invoke(IpcChannel.Preference_SetMultiple, updates),
getAll: (): Promise<PreferenceDefaultScopeType> => ipcRenderer.invoke(IpcChannel.Preference_GetAll),

View File

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

View File

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