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())