From c2a1178dff4e66c8915b3d5d19cbf36d1f7948a9 Mon Sep 17 00:00:00 2001 From: fullex <0xfullex@gmail.com> Date: Thu, 4 Sep 2025 12:47:22 +0800 Subject: [PATCH] refactor: update preferences management and enhance PreferenceService documentation - Updated preferences types in preferences.ts to use PreferenceTypes for better type safety. - Added new preference keys related to notes features in preferences.ts. - Enhanced documentation in PreferenceService.ts and usePreference.ts to clarify usage and update strategies. - Improved caching and subscription mechanisms in PreferenceService for better performance and reliability. --- packages/shared/data/preferences.ts | 82 +++++++++-------- src/renderer/src/data/PreferenceService.ts | 92 +++++++++++++++++--- src/renderer/src/data/hooks/usePreference.ts | 54 +++++------- 3 files changed, 142 insertions(+), 86 deletions(-) diff --git a/packages/shared/data/preferences.ts b/packages/shared/data/preferences.ts index 50c15f4967..43732d49d2 100644 --- a/packages/shared/data/preferences.ts +++ b/packages/shared/data/preferences.ts @@ -1,6 +1,6 @@ /** * Auto-generated preferences configuration - * Generated at: 2025-09-03T04:46:03.708Z + * Generated at: 2025-09-03T13:39:01.110Z * * This file is automatically generated from classification.json * To update this file, modify classification.json and run: @@ -10,24 +10,7 @@ */ import { TRANSLATE_PROMPT } from '@shared/config/prompts' -import type { - AssistantIconType, - AssistantTabSortType, - ChatMessageNavigationMode, - ChatMessageStyle, - LanguageVarious, - MultiModelFoldDisplayMode, - MultiModelGridPopoverTrigger, - MultiModelMessageStyle, - ProxyMode, - SelectionActionItem, - SelectionFilterMode, - SelectionTriggerMode, - SendMessageShortcut, - SidebarIcon, - WindowStyle -} from '@shared/data/preferenceTypes' -import { ThemeMode, UpgradeChannel } from '@shared/data/preferenceTypes' +import * as PreferenceTypes from '@shared/data/preferenceTypes' /* eslint @typescript-eslint/member-ordering: ["error", { "interfaces": { "order": "alphabetically" }, @@ -43,11 +26,11 @@ export interface PreferencesType { // redux/settings/autoCheckUpdate 'app.dist.auto_update.enabled': boolean // redux/settings/testChannel - 'app.dist.test_plan.channel': UpgradeChannel + 'app.dist.test_plan.channel': PreferenceTypes.UpgradeChannel // redux/settings/testPlan 'app.dist.test_plan.enabled': boolean // redux/settings/language - 'app.language': LanguageVarious | null + 'app.language': PreferenceTypes.LanguageVarious | null // redux/settings/launchOnBoot 'app.launch_on_boot': boolean // redux/settings/notification.assistant @@ -61,7 +44,7 @@ export interface PreferencesType { // redux/settings/proxyBypassRules 'app.proxy.bypass_rules': string // redux/settings/proxyMode - 'app.proxy.mode': ProxyMode + 'app.proxy.mode': PreferenceTypes.ProxyMode // redux/settings/proxyUrl 'app.proxy.url': string // redux/settings/enableSpellCheck @@ -83,11 +66,11 @@ export interface PreferencesType { // redux/settings/clickAssistantToShowTopic 'assistant.click_to_show_topic': boolean // redux/settings/assistantIconType - 'assistant.icon_type': AssistantIconType + 'assistant.icon_type': PreferenceTypes.AssistantIconType // redux/settings/showAssistants 'assistant.tab.show': boolean // redux/settings/assistantsTabSortType - 'assistant.tab.sort_type': AssistantTabSortType + 'assistant.tab.sort_type': PreferenceTypes.AssistantTabSortType // redux/settings/codeCollapsible 'chat.code.collapsible': boolean // redux/settings/codeEditor.autocompletion @@ -129,7 +112,7 @@ export interface PreferencesType { // redux/settings/enableQuickPanelTriggers 'chat.input.quick_panel.triggers_enabled': boolean // redux/settings/sendMessageShortcut - 'chat.input.send_message_shortcut': SendMessageShortcut + 'chat.input.send_message_shortcut': PreferenceTypes.SendMessageShortcut // redux/settings/showInputEstimatedTokens 'chat.input.show_estimated_tokens': boolean // redux/settings/autoTranslateWithSpace @@ -149,15 +132,15 @@ export interface PreferencesType { // redux/settings/mathEnableSingleDollar 'chat.message.math.single_dollar': boolean // redux/settings/foldDisplayMode - 'chat.message.multi_model.fold_display_mode': MultiModelFoldDisplayMode + 'chat.message.multi_model.fold_display_mode': PreferenceTypes.MultiModelFoldDisplayMode // redux/settings/gridColumns 'chat.message.multi_model.grid_columns': number // redux/settings/gridPopoverTrigger - 'chat.message.multi_model.grid_popover_trigger': MultiModelGridPopoverTrigger + 'chat.message.multi_model.grid_popover_trigger': PreferenceTypes.MultiModelGridPopoverTrigger // redux/settings/multiModelMessageStyle - 'chat.message.multi_model.style': MultiModelMessageStyle + 'chat.message.multi_model.style': PreferenceTypes.MultiModelMessageStyle // redux/settings/messageNavigation - 'chat.message.navigation_mode': ChatMessageNavigationMode + 'chat.message.navigation_mode': PreferenceTypes.ChatMessageNavigationMode // redux/settings/renderInputMessageAsMarkdown 'chat.message.render_as_markdown': boolean // redux/settings/showMessageDivider @@ -167,7 +150,7 @@ export interface PreferencesType { // redux/settings/showPrompt 'chat.message.show_prompt': boolean // redux/settings/messageStyle - 'chat.message.style': ChatMessageStyle + 'chat.message.style': PreferenceTypes.ChatMessageStyle // redux/settings/thoughtAutoCollapse 'chat.message.thought.auto_collapse': boolean // redux/settings/narrowMode @@ -312,7 +295,17 @@ export interface PreferencesType { 'feature.minapp.open_link_external': boolean // redux/settings/showOpenedMinappsInSidebar 'feature.minapp.show_opened_in_sidebar': boolean - // redux/settings/showWorkspace + // redux/note/settings.defaultEditMode + 'feature.notes.default_edit_mode': string + // redux/note/settings.fontFamily + 'feature.notes.default_font_family': string + // redux/note/settings.defaultViewMode + 'feature.notes.default_view_mode': string + // redux/note/settings.isFullWidth + 'feature.notes.full_width': boolean + // redux/note/settings.showTabStatus + 'feature.notes.show_tab_status': boolean + // redux/note/settings.showWorkspace 'feature.notes.show_workspace': boolean // redux/settings/clickTrayToShowQuickAssistant 'feature.quick_assistant.click_tray_to_show': boolean @@ -321,7 +314,7 @@ export interface PreferencesType { // redux/settings/readClipboardAtStartup 'feature.quick_assistant.read_clipboard_at_startup': boolean // redux/selectionStore/actionItems - 'feature.selection.action_items': SelectionActionItem[] + 'feature.selection.action_items': PreferenceTypes.SelectionActionItem[] // redux/selectionStore/actionWindowOpacity 'feature.selection.action_window_opacity': number // redux/selectionStore/isAutoClose @@ -335,13 +328,13 @@ export interface PreferencesType { // redux/selectionStore/filterList 'feature.selection.filter_list': string[] // redux/selectionStore/filterMode - 'feature.selection.filter_mode': SelectionFilterMode + 'feature.selection.filter_mode': PreferenceTypes.SelectionFilterMode // redux/selectionStore/isFollowToolbar 'feature.selection.follow_toolbar': boolean // redux/selectionStore/isRemeberWinSize 'feature.selection.remember_win_size': boolean // redux/selectionStore/triggerMode - 'feature.selection.trigger_mode': SelectionTriggerMode + 'feature.selection.trigger_mode': PreferenceTypes.SelectionTriggerMode // redux/settings/translateModelPrompt 'feature.translate.model_prompt': string // redux/settings/targetLanguage @@ -395,15 +388,15 @@ export interface PreferencesType { // redux/settings/navbarPosition 'ui.navbar.position': 'left' | 'top' // redux/settings/sidebarIcons.disabled - 'ui.sidebar.icons.invisible': SidebarIcon[] + 'ui.sidebar.icons.invisible': PreferenceTypes.SidebarIcon[] // redux/settings/sidebarIcons.visible - 'ui.sidebar.icons.visible': SidebarIcon[] + 'ui.sidebar.icons.visible': PreferenceTypes.SidebarIcon[] // redux/settings/theme - 'ui.theme_mode': ThemeMode + 'ui.theme_mode': PreferenceTypes.ThemeMode // redux/settings/userTheme.colorPrimary 'ui.theme_user.color_primary': string // redux/settings/windowStyle - 'ui.window_style': WindowStyle + 'ui.window_style': PreferenceTypes.WindowStyle } } @@ -413,7 +406,7 @@ export const DefaultPreferences: PreferencesType = { 'app.developer_mode.enabled': false, 'app.disable_hardware_acceleration': false, 'app.dist.auto_update.enabled': true, - 'app.dist.test_plan.channel': UpgradeChannel.LATEST, + 'app.dist.test_plan.channel': PreferenceTypes.UpgradeChannel.LATEST, 'app.dist.test_plan.enabled': false, 'app.language': null, 'app.launch_on_boot': false, @@ -548,6 +541,11 @@ export const DefaultPreferences: PreferencesType = { 'feature.minapp.max_keep_alive': 3, 'feature.minapp.open_link_external': false, 'feature.minapp.show_opened_in_sidebar': true, + 'feature.notes.default_edit_mode': 'preview', + 'feature.notes.default_font_family': 'default', + 'feature.notes.default_view_mode': 'edit', + 'feature.notes.full_width': true, + 'feature.notes.show_tab_status': true, 'feature.notes.show_workspace': true, 'feature.quick_assistant.click_tray_to_show': false, 'feature.quick_assistant.enabled': false, @@ -648,7 +646,7 @@ export const DefaultPreferences: PreferencesType = { 'code_tools', 'notes' ], - 'ui.theme_mode': ThemeMode.system, + 'ui.theme_mode': PreferenceTypes.ThemeMode.system, 'ui.theme_user.color_primary': '#00b96b', 'ui.window_style': 'opaque' } @@ -658,8 +656,8 @@ export const DefaultPreferences: PreferencesType = { /** * 生成统计: - * - 总配置项: 184 + * - 总配置项: 189 * - electronStore项: 1 - * - redux项: 183 + * - redux项: 188 * - localStorage项: 0 */ diff --git a/src/renderer/src/data/PreferenceService.ts b/src/renderer/src/data/PreferenceService.ts index abe37fa445..204f09035a 100644 --- a/src/renderer/src/data/PreferenceService.ts +++ b/src/renderer/src/data/PreferenceService.ts @@ -7,9 +7,17 @@ import type { } from '@shared/data/preferenceTypes' const logger = loggerService.withContext('PreferenceService') + /** - * Renderer-side PreferenceService providing cached access to preferences - * with real-time synchronization across windows using useSyncExternalStore + * Renderer-side PreferenceService providing cached access to preferences with real-time synchronization + * + * Features: + * - Caching system for fast access to frequently used preferences + * - Optimistic and pessimistic update strategies + * - Real-time synchronization across windows via IPC + * - Race condition handling for concurrent updates + * - Batch operations for multiple preferences + * - Integration with React's useSyncExternalStore */ export class PreferenceService { private static instance: PreferenceService @@ -53,6 +61,7 @@ export class PreferenceService { /** * Get the singleton instance of PreferenceService + * @returns The singleton PreferenceService instance */ public static getInstance(): PreferenceService { if (!PreferenceService.instance) { @@ -63,6 +72,7 @@ export class PreferenceService { /** * Setup IPC change listener for preference updates from main process + * Establishes communication channel for real-time preference synchronization */ private setupChangeListeners() { if (!window.api?.preference?.onChanged) { @@ -83,6 +93,7 @@ export class PreferenceService { /** * Notify all relevant listeners about preference changes + * @param key The preference key that changed */ private notifyChangeListeners(key: string) { // Notify global listeners @@ -96,7 +107,9 @@ export class PreferenceService { } /** - * Get a single preference value with caching + * Get a single preference value with caching and auto-subscription + * @param key The preference key to retrieve + * @returns Promise resolving to the preference value with defaults applied */ public async get(key: K): Promise { // Check cache first @@ -127,6 +140,10 @@ export class PreferenceService { /** * Set a single preference value with configurable update strategy + * @param key The preference key to update + * @param value The new value to set + * @param options Update strategy options (optimistic by default) + * @returns Promise that resolves when update completes */ public async set( key: K, @@ -142,6 +159,10 @@ export class PreferenceService { /** * Optimistic update: Queue request to prevent race conditions + * Updates UI immediately, then syncs to database with rollback on failure + * @param key The preference key to update + * @param value The new value to set + * @returns Promise that resolves when update completes */ private async setOptimistic( key: K, @@ -153,6 +174,10 @@ export class PreferenceService { /** * Execute optimistic update with proper race condition handling + * @param key The preference key to update + * @param value The new value to set + * @param requestId Unique identifier for this update request + * @returns Promise that resolves when update completes */ private async executeOptimisticUpdate(key: PreferenceKeyType, value: any, requestId: string): Promise { const existingState = this.optimisticValues.get(key) @@ -190,6 +215,10 @@ export class PreferenceService { /** * Pessimistic update: Wait for database confirmation before updating UI + * Updates database first, then UI on success + * @param key The preference key to update + * @param value The new value to set + * @returns Promise that resolves when update completes */ private async setPessimistic( key: K, @@ -210,7 +239,9 @@ export class PreferenceService { } /** - * Get multiple preferences at once, return is Partial + * 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 */ public async getMultipleRaw(keys: PreferenceKeyType[]): Promise> { // Check which keys are already cached @@ -261,6 +292,8 @@ export class PreferenceService { /** * Get multiple preferences at once and return them as a record of key-value pairs + * @param keys Object mapping local names to preference keys + * @returns Promise resolving to object with mapped preference values */ public async getMultiple>( keys: T @@ -277,6 +310,9 @@ export class PreferenceService { /** * Set multiple preferences at once with configurable update strategy + * @param updates Object containing preference key-value pairs to update + * @param options Update strategy options (optimistic by default) + * @returns Promise that resolves when all updates complete */ public async setMultiple( updates: Partial, @@ -291,6 +327,8 @@ export class PreferenceService { /** * Optimistic batch update: Update UI immediately, then sync to database + * @param updates Object containing preference key-value pairs to update + * @returns Promise that resolves when batch update completes */ private async setMultipleOptimistic(updates: Partial): Promise { const batchRequestId = this.generateRequestId() @@ -346,6 +384,8 @@ export class PreferenceService { /** * Pessimistic batch update: Wait for database confirmation before updating UI + * @param updates Object containing preference key-value pairs to update + * @returns Promise that resolves when batch update completes */ private async setMultiplePessimistic(updates: Partial): Promise { try { @@ -365,7 +405,9 @@ export class PreferenceService { } /** - * Subscribe to a specific key for change notifications + * Subscribe to specific keys for change notifications from main process + * @param keys Array of preference keys to subscribe to + * @returns Promise that resolves when subscription is established */ private async subscribeToKeyInternal(keys: PreferenceKeyType[]): Promise { const keysToSubscribe = keys.filter((key) => !this.subscribedKeys.has(key)) @@ -382,6 +424,8 @@ export class PreferenceService { /** * Subscribe to global preference changes (for useSyncExternalStore) + * @param callback Function to call when any preference changes + * @returns Unsubscribe function */ public subscribeAllChanges = (callback: () => void): (() => void) => { this.allChangesListeners.add(callback) @@ -392,6 +436,8 @@ export class PreferenceService { /** * Subscribe to specific key changes (for useSyncExternalStore) + * @param key The preference key to watch for changes + * @returns Function that takes a callback and returns an unsubscribe function */ public subscribeChange = (key: PreferenceKeyType) => @@ -416,6 +462,8 @@ export class PreferenceService { /** * Get cached value without async fetch + * @param key The preference key to retrieve from cache + * @returns The cached value or undefined if not cached */ public getCachedValue(key: K): PreferenceDefaultScopeType[K] | undefined { return this.cache[key] @@ -423,14 +471,16 @@ export class PreferenceService { /** * Check if a preference is cached + * @param key The preference key to check + * @returns True if the key is cached, false otherwise */ public isCached(key: PreferenceKeyType): boolean { return key in this.cache && this.cache[key] !== undefined } /** - * Load all preferences from main process at once - * Provides optimal performance by loading complete preference set into memory + * Load all preferences from main process at once for optimal performance + * @returns Promise resolving to all preference values */ public async preloadAll(): Promise { try { @@ -458,6 +508,7 @@ export class PreferenceService { /** * Check if all preferences are loaded in cache + * @returns True if full cache has been loaded, false otherwise */ public isFullyCached(): boolean { return this.fullCacheLoaded @@ -465,6 +516,8 @@ export class PreferenceService { /** * Preload specific preferences into cache + * @param keys Array of preference keys to preload + * @returns Promise that resolves when preloading completes */ public async preload(keys: PreferenceKeyType[]): Promise { const uncachedKeys = keys.filter((key) => !this.isCached(key)) @@ -480,7 +533,9 @@ export class PreferenceService { } /** - * Confirm an optimistic update (called when main process confirms the update) + * Confirm an optimistic update when main process confirms the update + * @param key The preference key that was updated + * @param requestId The unique identifier for the update request */ private confirmOptimistic(key: PreferenceKeyType, requestId: string): void { const optimisticState = this.optimisticValues.get(key) @@ -498,7 +553,9 @@ export class PreferenceService { } /** - * Rollback an optimistic update (called on failure) + * Rollback an optimistic update when main process update fails + * @param key The preference key to rollback + * @param requestId The unique identifier for the failed update request */ private rollbackOptimistic(key: PreferenceKeyType, requestId: string): void { const optimisticState = this.optimisticValues.get(key) @@ -523,7 +580,8 @@ export class PreferenceService { } /** - * Get all pending optimistic updates (for debugging) + * Get all pending optimistic updates for debugging purposes + * @returns Array of pending optimistic update information */ public getPendingOptimisticUpdates(): Array<{ key: string @@ -545,13 +603,18 @@ export class PreferenceService { /** * Generate unique request ID for tracking concurrent requests + * @returns Unique request identifier string */ private generateRequestId(): string { return `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}` } /** - * Add request to queue for a specific key + * Add request to queue for a specific key to prevent race conditions + * @param key The preference key to update + * @param requestId Unique identifier for this request + * @param value The value to set + * @returns Promise that resolves when the request is processed */ private enqueueRequest(key: PreferenceKeyType, requestId: string, value: any): Promise { return new Promise((resolve, reject) => { @@ -571,6 +634,8 @@ export class PreferenceService { /** * Process the next queued request for a key + * @param key The preference key to process requests for + * @returns Promise that resolves when processing completes */ private async processNextQueuedRequest(key: PreferenceKeyType): Promise { const queue = this.requestQueues.get(key) @@ -589,6 +654,7 @@ export class PreferenceService { /** * Complete current request and process next in queue + * @param key The preference key to complete processing for */ private completeQueuedRequest(key: PreferenceKeyType): void { const queue = this.requestQueues.get(key) @@ -606,7 +672,7 @@ export class PreferenceService { } /** - * Clear all cached preferences (for testing/debugging) + * Clear all cached preferences for testing/debugging */ public clearCache(): void { this.cache = {} @@ -615,7 +681,7 @@ export class PreferenceService { } /** - * Cleanup service (call when shutting down) + * Cleanup service resources - call when shutting down */ public cleanup(): void { if (this.changeListenerCleanup) { diff --git a/src/renderer/src/data/hooks/usePreference.ts b/src/renderer/src/data/hooks/usePreference.ts index e0675fd285..e6992eb823 100644 --- a/src/renderer/src/data/hooks/usePreference.ts +++ b/src/renderer/src/data/hooks/usePreference.ts @@ -17,9 +17,9 @@ const logger = loggerService.withContext('usePreference') * * @param key - The preference key to manage (must be a valid PreferenceKeyType) * @param options - Optional configuration for update behavior: - * - strategy: 'optimistic' (default) for immediate UI updates, 'pessimistic' for database-first updates + * - optimistic: true (default) for immediate UI updates, false for database-first updates * @returns A tuple [value, setValue] where: - * - value: Current preference value or undefined if not loaded/cached + * - value: Current preference value with defaults applied (never undefined) * - setValue: Async function to update the preference value * * @example @@ -28,19 +28,14 @@ const logger = loggerService.withContext('usePreference') * const [theme, setTheme] = usePreference('app.theme.mode') * * // Pessimistic updates for critical settings - * const [apiKey, setApiKey] = usePreference('api.key', { strategy: 'pessimistic' }) + * const [apiKey, setApiKey] = usePreference('api.key', { optimistic: false }) * * // Simple optimistic updates * const [fontSize, setFontSize] = usePreference('chat.message.font_size', { - * strategy: 'optimistic' + * optimistic: true * }) * - * // Conditional rendering based on preference value - * if (theme === undefined) { - * return - * } - * - * // Updating preference value + * // Value is never undefined - defaults are applied automatically * const handleThemeChange = async (newTheme: string) => { * try { * await setTheme(newTheme) // UI updates immediately with optimistic strategy @@ -62,7 +57,7 @@ const logger = loggerService.withContext('usePreference') * ```typescript * // Advanced usage with form handling for message font size * const [fontSize, setFontSize] = usePreference('chat.message.font_size', { - * strategy: 'optimistic' // Immediate feedback for UI preferences + * optimistic: true // Immediate feedback for UI preferences * }) * * const handleFontSizeChange = useCallback(async (size: number) => { @@ -75,7 +70,7 @@ const logger = loggerService.withContext('usePreference') * return ( * handleFontSizeChange(Number(e.target.value))} * min={8} * max={72} @@ -131,9 +126,9 @@ export function usePreference( * @param keys - Object mapping local names to preference keys. Keys are your custom names, * values must be valid PreferenceKeyType identifiers * @param options - Optional configuration for update behavior: - * - strategy: 'optimistic' (default) for immediate UI updates, 'pessimistic' for database-first updates + * - optimistic: true (default) for immediate UI updates, false for database-first updates * @returns A tuple [values, updateValues] where: - * - values: Object with your local keys mapped to current preference values (undefined if not loaded) + * - values: Object with your local keys mapped to current preference values with defaults applied * - updateValues: Async function to batch update multiple preferences at once * * @example @@ -149,12 +144,12 @@ export function usePreference( * const [apiSettings, setApiSettings] = useMultiplePreferences({ * apiKey: 'api.key', * endpoint: 'api.endpoint' - * }, { strategy: 'pessimistic' }) + * }, { optimistic: false }) * - * // Accessing individual values with type safety - * const currentTheme = uiSettings.theme // string | undefined - * const currentFontSize = uiSettings.fontSize // number | undefined - * const showLines = uiSettings.showLineNumbers // boolean | undefined + * // Accessing individual values with type safety (defaults applied automatically) + * const currentTheme = uiSettings.theme // string (never undefined) + * const currentFontSize = uiSettings.fontSize // number (never undefined) + * const showLines = uiSettings.showLineNumbers // boolean (never undefined) * * // Batch updating multiple preferences * const resetToDefaults = async () => { @@ -198,11 +193,7 @@ export function usePreference( * } * } * - * // Conditional rendering based on loading state - * if (Object.values(settings).every(val => val === undefined)) { - * return - * } - * + * // No need to check for undefined - defaults are applied automatically * return ( *
{ * e.preventDefault() @@ -214,13 +205,13 @@ export function usePreference( * * * @@ -241,12 +232,13 @@ export function usePreference( * * // Single subscription handles all code preferences * // More efficient than 5 separate usePreference calls + * // No need for null checks - defaults are already applied * const codeConfig = useMemo(() => ({ - * showLineNumbers: codePrefs.showLineNumbers ?? false, - * wrappable: codePrefs.wrappable ?? false, - * collapsible: codePrefs.collapsible ?? false, - * autocompletion: codePrefs.autocompletion ?? true, - * foldGutter: codePrefs.foldGutter ?? false + * showLineNumbers: codePrefs.showLineNumbers, + * wrappable: codePrefs.wrappable, + * collapsible: codePrefs.collapsible, + * autocompletion: codePrefs.autocompletion, + * foldGutter: codePrefs.foldGutter * }), [codePrefs]) * * return