From 85bdcdc206cbd381fcdf5f4c5fa30163081cdfe9 Mon Sep 17 00:00:00 2001 From: fullex <0xfullex@gmail.com> Date: Tue, 12 Aug 2025 21:23:02 +0800 Subject: [PATCH] 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. --- src/preload/index.ts | 8 ++- src/renderer/src/data/PreferenceService.ts | 60 +++++++++----------- src/renderer/src/data/hooks/usePreference.ts | 4 +- 3 files changed, 34 insertions(+), 38 deletions(-) diff --git a/src/preload/index.ts b/src/preload/index.ts index 23a081028d..9a6b3075c6 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -396,10 +396,12 @@ const api = { ipcRenderer.invoke(IpcChannel.TRACE_ADD_STREAM_MESSAGE, spanId, modelName, context, message) }, preference: { - get: (key: K) => ipcRenderer.invoke(IpcChannel.Preference_Get, key), - set: (key: K, value: PreferenceDefaultScopeType[K]) => + get: (key: K): Promise => + ipcRenderer.invoke(IpcChannel.Preference_Get, key), + set: (key: K, value: PreferenceDefaultScopeType[K]): Promise => ipcRenderer.invoke(IpcChannel.Preference_Set, key, value), - getMultiple: (keys: PreferenceKeyType[]) => ipcRenderer.invoke(IpcChannel.Preference_GetMultiple, keys), + getMultiple: (keys: PreferenceKeyType[]): Promise> => + ipcRenderer.invoke(IpcChannel.Preference_GetMultiple, keys), setMultiple: (updates: Partial) => ipcRenderer.invoke(IpcChannel.Preference_SetMultiple, updates), getAll: (): Promise => ipcRenderer.invoke(IpcChannel.Preference_GetAll), diff --git a/src/renderer/src/data/PreferenceService.ts b/src/renderer/src/data/PreferenceService.ts index 77d15cbd1e..f2d2853321 100644 --- a/src/renderer/src/data/PreferenceService.ts +++ b/src/renderer/src/data/PreferenceService.ts @@ -10,15 +10,19 @@ const logger = loggerService.withContext('PreferenceService') */ export class PreferenceService { private static instance: PreferenceService + private cache: Record = {} - private listeners = new Set<() => void>() - private keyListeners = new Map void>>() + + private allChangesListeners = new Set<() => void>() + private keyChangeListeners = new Map void>>() private changeListenerCleanup: (() => void) | null = null + private subscribedKeys = new Set() + 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> { + public async getMultiple(keys: PreferenceKeyType[]): Promise> { // Check which keys are already cached const cachedResults: Partial = {} 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 = {} + const defaultResults: Partial = {} 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 = - (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() } } diff --git a/src/renderer/src/data/hooks/usePreference.ts b/src/renderer/src/data/hooks/usePreference.ts index 743663d68a..7facdf1cc7 100644 --- a/src/renderer/src/data/hooks/usePreference.ts +++ b/src/renderer/src/data/hooks/usePreference.ts @@ -70,7 +70,7 @@ export function usePreference( ): [PreferenceDefaultScopeType[K] | undefined, (value: PreferenceDefaultScopeType[K]) => Promise] { // 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 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())