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.
This commit is contained in:
fullex 2025-09-04 12:47:22 +08:00
parent 7f114ade4d
commit c2a1178dff
3 changed files with 142 additions and 86 deletions

View File

@ -1,6 +1,6 @@
/** /**
* Auto-generated preferences configuration * 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 * This file is automatically generated from classification.json
* To update this file, modify classification.json and run: * To update this file, modify classification.json and run:
@ -10,24 +10,7 @@
*/ */
import { TRANSLATE_PROMPT } from '@shared/config/prompts' import { TRANSLATE_PROMPT } from '@shared/config/prompts'
import type { import * as PreferenceTypes from '@shared/data/preferenceTypes'
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'
/* eslint @typescript-eslint/member-ordering: ["error", { /* eslint @typescript-eslint/member-ordering: ["error", {
"interfaces": { "order": "alphabetically" }, "interfaces": { "order": "alphabetically" },
@ -43,11 +26,11 @@ export interface PreferencesType {
// redux/settings/autoCheckUpdate // redux/settings/autoCheckUpdate
'app.dist.auto_update.enabled': boolean 'app.dist.auto_update.enabled': boolean
// redux/settings/testChannel // redux/settings/testChannel
'app.dist.test_plan.channel': UpgradeChannel 'app.dist.test_plan.channel': PreferenceTypes.UpgradeChannel
// redux/settings/testPlan // redux/settings/testPlan
'app.dist.test_plan.enabled': boolean 'app.dist.test_plan.enabled': boolean
// redux/settings/language // redux/settings/language
'app.language': LanguageVarious | null 'app.language': PreferenceTypes.LanguageVarious | null
// redux/settings/launchOnBoot // redux/settings/launchOnBoot
'app.launch_on_boot': boolean 'app.launch_on_boot': boolean
// redux/settings/notification.assistant // redux/settings/notification.assistant
@ -61,7 +44,7 @@ export interface PreferencesType {
// redux/settings/proxyBypassRules // redux/settings/proxyBypassRules
'app.proxy.bypass_rules': string 'app.proxy.bypass_rules': string
// redux/settings/proxyMode // redux/settings/proxyMode
'app.proxy.mode': ProxyMode 'app.proxy.mode': PreferenceTypes.ProxyMode
// redux/settings/proxyUrl // redux/settings/proxyUrl
'app.proxy.url': string 'app.proxy.url': string
// redux/settings/enableSpellCheck // redux/settings/enableSpellCheck
@ -83,11 +66,11 @@ export interface PreferencesType {
// redux/settings/clickAssistantToShowTopic // redux/settings/clickAssistantToShowTopic
'assistant.click_to_show_topic': boolean 'assistant.click_to_show_topic': boolean
// redux/settings/assistantIconType // redux/settings/assistantIconType
'assistant.icon_type': AssistantIconType 'assistant.icon_type': PreferenceTypes.AssistantIconType
// redux/settings/showAssistants // redux/settings/showAssistants
'assistant.tab.show': boolean 'assistant.tab.show': boolean
// redux/settings/assistantsTabSortType // redux/settings/assistantsTabSortType
'assistant.tab.sort_type': AssistantTabSortType 'assistant.tab.sort_type': PreferenceTypes.AssistantTabSortType
// redux/settings/codeCollapsible // redux/settings/codeCollapsible
'chat.code.collapsible': boolean 'chat.code.collapsible': boolean
// redux/settings/codeEditor.autocompletion // redux/settings/codeEditor.autocompletion
@ -129,7 +112,7 @@ export interface PreferencesType {
// redux/settings/enableQuickPanelTriggers // redux/settings/enableQuickPanelTriggers
'chat.input.quick_panel.triggers_enabled': boolean 'chat.input.quick_panel.triggers_enabled': boolean
// redux/settings/sendMessageShortcut // redux/settings/sendMessageShortcut
'chat.input.send_message_shortcut': SendMessageShortcut 'chat.input.send_message_shortcut': PreferenceTypes.SendMessageShortcut
// redux/settings/showInputEstimatedTokens // redux/settings/showInputEstimatedTokens
'chat.input.show_estimated_tokens': boolean 'chat.input.show_estimated_tokens': boolean
// redux/settings/autoTranslateWithSpace // redux/settings/autoTranslateWithSpace
@ -149,15 +132,15 @@ export interface PreferencesType {
// redux/settings/mathEnableSingleDollar // redux/settings/mathEnableSingleDollar
'chat.message.math.single_dollar': boolean 'chat.message.math.single_dollar': boolean
// redux/settings/foldDisplayMode // redux/settings/foldDisplayMode
'chat.message.multi_model.fold_display_mode': MultiModelFoldDisplayMode 'chat.message.multi_model.fold_display_mode': PreferenceTypes.MultiModelFoldDisplayMode
// redux/settings/gridColumns // redux/settings/gridColumns
'chat.message.multi_model.grid_columns': number 'chat.message.multi_model.grid_columns': number
// redux/settings/gridPopoverTrigger // redux/settings/gridPopoverTrigger
'chat.message.multi_model.grid_popover_trigger': MultiModelGridPopoverTrigger 'chat.message.multi_model.grid_popover_trigger': PreferenceTypes.MultiModelGridPopoverTrigger
// redux/settings/multiModelMessageStyle // redux/settings/multiModelMessageStyle
'chat.message.multi_model.style': MultiModelMessageStyle 'chat.message.multi_model.style': PreferenceTypes.MultiModelMessageStyle
// redux/settings/messageNavigation // redux/settings/messageNavigation
'chat.message.navigation_mode': ChatMessageNavigationMode 'chat.message.navigation_mode': PreferenceTypes.ChatMessageNavigationMode
// redux/settings/renderInputMessageAsMarkdown // redux/settings/renderInputMessageAsMarkdown
'chat.message.render_as_markdown': boolean 'chat.message.render_as_markdown': boolean
// redux/settings/showMessageDivider // redux/settings/showMessageDivider
@ -167,7 +150,7 @@ export interface PreferencesType {
// redux/settings/showPrompt // redux/settings/showPrompt
'chat.message.show_prompt': boolean 'chat.message.show_prompt': boolean
// redux/settings/messageStyle // redux/settings/messageStyle
'chat.message.style': ChatMessageStyle 'chat.message.style': PreferenceTypes.ChatMessageStyle
// redux/settings/thoughtAutoCollapse // redux/settings/thoughtAutoCollapse
'chat.message.thought.auto_collapse': boolean 'chat.message.thought.auto_collapse': boolean
// redux/settings/narrowMode // redux/settings/narrowMode
@ -312,7 +295,17 @@ export interface PreferencesType {
'feature.minapp.open_link_external': boolean 'feature.minapp.open_link_external': boolean
// redux/settings/showOpenedMinappsInSidebar // redux/settings/showOpenedMinappsInSidebar
'feature.minapp.show_opened_in_sidebar': boolean '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 'feature.notes.show_workspace': boolean
// redux/settings/clickTrayToShowQuickAssistant // redux/settings/clickTrayToShowQuickAssistant
'feature.quick_assistant.click_tray_to_show': boolean 'feature.quick_assistant.click_tray_to_show': boolean
@ -321,7 +314,7 @@ export interface PreferencesType {
// redux/settings/readClipboardAtStartup // redux/settings/readClipboardAtStartup
'feature.quick_assistant.read_clipboard_at_startup': boolean 'feature.quick_assistant.read_clipboard_at_startup': boolean
// redux/selectionStore/actionItems // redux/selectionStore/actionItems
'feature.selection.action_items': SelectionActionItem[] 'feature.selection.action_items': PreferenceTypes.SelectionActionItem[]
// redux/selectionStore/actionWindowOpacity // redux/selectionStore/actionWindowOpacity
'feature.selection.action_window_opacity': number 'feature.selection.action_window_opacity': number
// redux/selectionStore/isAutoClose // redux/selectionStore/isAutoClose
@ -335,13 +328,13 @@ export interface PreferencesType {
// redux/selectionStore/filterList // redux/selectionStore/filterList
'feature.selection.filter_list': string[] 'feature.selection.filter_list': string[]
// redux/selectionStore/filterMode // redux/selectionStore/filterMode
'feature.selection.filter_mode': SelectionFilterMode 'feature.selection.filter_mode': PreferenceTypes.SelectionFilterMode
// redux/selectionStore/isFollowToolbar // redux/selectionStore/isFollowToolbar
'feature.selection.follow_toolbar': boolean 'feature.selection.follow_toolbar': boolean
// redux/selectionStore/isRemeberWinSize // redux/selectionStore/isRemeberWinSize
'feature.selection.remember_win_size': boolean 'feature.selection.remember_win_size': boolean
// redux/selectionStore/triggerMode // redux/selectionStore/triggerMode
'feature.selection.trigger_mode': SelectionTriggerMode 'feature.selection.trigger_mode': PreferenceTypes.SelectionTriggerMode
// redux/settings/translateModelPrompt // redux/settings/translateModelPrompt
'feature.translate.model_prompt': string 'feature.translate.model_prompt': string
// redux/settings/targetLanguage // redux/settings/targetLanguage
@ -395,15 +388,15 @@ export interface PreferencesType {
// redux/settings/navbarPosition // redux/settings/navbarPosition
'ui.navbar.position': 'left' | 'top' 'ui.navbar.position': 'left' | 'top'
// redux/settings/sidebarIcons.disabled // redux/settings/sidebarIcons.disabled
'ui.sidebar.icons.invisible': SidebarIcon[] 'ui.sidebar.icons.invisible': PreferenceTypes.SidebarIcon[]
// redux/settings/sidebarIcons.visible // redux/settings/sidebarIcons.visible
'ui.sidebar.icons.visible': SidebarIcon[] 'ui.sidebar.icons.visible': PreferenceTypes.SidebarIcon[]
// redux/settings/theme // redux/settings/theme
'ui.theme_mode': ThemeMode 'ui.theme_mode': PreferenceTypes.ThemeMode
// redux/settings/userTheme.colorPrimary // redux/settings/userTheme.colorPrimary
'ui.theme_user.color_primary': string 'ui.theme_user.color_primary': string
// redux/settings/windowStyle // 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.developer_mode.enabled': false,
'app.disable_hardware_acceleration': false, 'app.disable_hardware_acceleration': false,
'app.dist.auto_update.enabled': true, '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.dist.test_plan.enabled': false,
'app.language': null, 'app.language': null,
'app.launch_on_boot': false, 'app.launch_on_boot': false,
@ -548,6 +541,11 @@ export const DefaultPreferences: PreferencesType = {
'feature.minapp.max_keep_alive': 3, 'feature.minapp.max_keep_alive': 3,
'feature.minapp.open_link_external': false, 'feature.minapp.open_link_external': false,
'feature.minapp.show_opened_in_sidebar': true, '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.notes.show_workspace': true,
'feature.quick_assistant.click_tray_to_show': false, 'feature.quick_assistant.click_tray_to_show': false,
'feature.quick_assistant.enabled': false, 'feature.quick_assistant.enabled': false,
@ -648,7 +646,7 @@ export const DefaultPreferences: PreferencesType = {
'code_tools', 'code_tools',
'notes' 'notes'
], ],
'ui.theme_mode': ThemeMode.system, 'ui.theme_mode': PreferenceTypes.ThemeMode.system,
'ui.theme_user.color_primary': '#00b96b', 'ui.theme_user.color_primary': '#00b96b',
'ui.window_style': 'opaque' 'ui.window_style': 'opaque'
} }
@ -658,8 +656,8 @@ export const DefaultPreferences: PreferencesType = {
/** /**
* : * :
* - 总配置项: 184 * - 总配置项: 189
* - electronStore项: 1 * - electronStore项: 1
* - redux项: 183 * - redux项: 188
* - localStorage项: 0 * - localStorage项: 0
*/ */

View File

@ -7,9 +7,17 @@ import type {
} from '@shared/data/preferenceTypes' } from '@shared/data/preferenceTypes'
const logger = loggerService.withContext('PreferenceService') const logger = loggerService.withContext('PreferenceService')
/** /**
* Renderer-side PreferenceService providing cached access to preferences * Renderer-side PreferenceService providing cached access to preferences with real-time synchronization
* with real-time synchronization across windows using useSyncExternalStore *
* 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 { export class PreferenceService {
private static instance: PreferenceService private static instance: PreferenceService
@ -53,6 +61,7 @@ export class PreferenceService {
/** /**
* Get the singleton instance of PreferenceService * Get the singleton instance of PreferenceService
* @returns The singleton PreferenceService instance
*/ */
public static getInstance(): PreferenceService { public static getInstance(): PreferenceService {
if (!PreferenceService.instance) { if (!PreferenceService.instance) {
@ -63,6 +72,7 @@ export class PreferenceService {
/** /**
* Setup IPC change listener for preference updates from main process * Setup IPC change listener for preference updates from main process
* Establishes communication channel for real-time preference synchronization
*/ */
private setupChangeListeners() { private setupChangeListeners() {
if (!window.api?.preference?.onChanged) { if (!window.api?.preference?.onChanged) {
@ -83,6 +93,7 @@ export class PreferenceService {
/** /**
* Notify all relevant listeners about preference changes * Notify all relevant listeners about preference changes
* @param key The preference key that changed
*/ */
private notifyChangeListeners(key: string) { private notifyChangeListeners(key: string) {
// Notify global listeners // 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<K extends PreferenceKeyType>(key: K): Promise<PreferenceDefaultScopeType[K]> { public async get<K extends PreferenceKeyType>(key: K): Promise<PreferenceDefaultScopeType[K]> {
// Check cache first // Check cache first
@ -127,6 +140,10 @@ export class PreferenceService {
/** /**
* Set a single preference value with configurable update strategy * 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<K extends PreferenceKeyType>( public async set<K extends PreferenceKeyType>(
key: K, key: K,
@ -142,6 +159,10 @@ export class PreferenceService {
/** /**
* Optimistic update: Queue request to prevent race conditions * 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<K extends PreferenceKeyType>( private async setOptimistic<K extends PreferenceKeyType>(
key: K, key: K,
@ -153,6 +174,10 @@ export class PreferenceService {
/** /**
* Execute optimistic update with proper race condition handling * 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<void> { private async executeOptimisticUpdate(key: PreferenceKeyType, value: any, requestId: string): Promise<void> {
const existingState = this.optimisticValues.get(key) const existingState = this.optimisticValues.get(key)
@ -190,6 +215,10 @@ export class PreferenceService {
/** /**
* Pessimistic update: Wait for database confirmation before updating UI * 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<K extends PreferenceKeyType>( private async setPessimistic<K extends PreferenceKeyType>(
key: K, key: K,
@ -210,7 +239,9 @@ export class PreferenceService {
} }
/** /**
* Get multiple preferences at once, return is Partial<PreferenceDefaultScopeType> * 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<Partial<PreferenceDefaultScopeType>> { public async getMultipleRaw(keys: PreferenceKeyType[]): Promise<Partial<PreferenceDefaultScopeType>> {
// Check which keys are already cached // 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 * 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<T extends Record<string, PreferenceKeyType>>( public async getMultiple<T extends Record<string, PreferenceKeyType>>(
keys: T keys: T
@ -277,6 +310,9 @@ export class PreferenceService {
/** /**
* Set multiple preferences at once with configurable update strategy * 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( public async setMultiple(
updates: Partial<PreferenceDefaultScopeType>, updates: Partial<PreferenceDefaultScopeType>,
@ -291,6 +327,8 @@ export class PreferenceService {
/** /**
* Optimistic batch update: Update UI immediately, then sync to database * 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<PreferenceDefaultScopeType>): Promise<void> { private async setMultipleOptimistic(updates: Partial<PreferenceDefaultScopeType>): Promise<void> {
const batchRequestId = this.generateRequestId() const batchRequestId = this.generateRequestId()
@ -346,6 +384,8 @@ export class PreferenceService {
/** /**
* Pessimistic batch update: Wait for database confirmation before updating UI * 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<PreferenceDefaultScopeType>): Promise<void> { private async setMultiplePessimistic(updates: Partial<PreferenceDefaultScopeType>): Promise<void> {
try { 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<void> { private async subscribeToKeyInternal(keys: PreferenceKeyType[]): Promise<void> {
const keysToSubscribe = keys.filter((key) => !this.subscribedKeys.has(key)) const keysToSubscribe = keys.filter((key) => !this.subscribedKeys.has(key))
@ -382,6 +424,8 @@ export class PreferenceService {
/** /**
* Subscribe to global preference changes (for useSyncExternalStore) * Subscribe to global preference changes (for useSyncExternalStore)
* @param callback Function to call when any preference changes
* @returns Unsubscribe function
*/ */
public subscribeAllChanges = (callback: () => void): (() => void) => { public subscribeAllChanges = (callback: () => void): (() => void) => {
this.allChangesListeners.add(callback) this.allChangesListeners.add(callback)
@ -392,6 +436,8 @@ export class PreferenceService {
/** /**
* Subscribe to specific key changes (for useSyncExternalStore) * 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 = public subscribeChange =
(key: PreferenceKeyType) => (key: PreferenceKeyType) =>
@ -416,6 +462,8 @@ export class PreferenceService {
/** /**
* Get cached value without async fetch * 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<K extends PreferenceKeyType>(key: K): PreferenceDefaultScopeType[K] | undefined { public getCachedValue<K extends PreferenceKeyType>(key: K): PreferenceDefaultScopeType[K] | undefined {
return this.cache[key] return this.cache[key]
@ -423,14 +471,16 @@ export class PreferenceService {
/** /**
* Check if a preference is cached * 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 { public isCached(key: PreferenceKeyType): boolean {
return key in this.cache && this.cache[key] !== undefined return key in this.cache && this.cache[key] !== undefined
} }
/** /**
* Load all preferences from main process at once * Load all preferences from main process at once for optimal performance
* Provides optimal performance by loading complete preference set into memory * @returns Promise resolving to all preference values
*/ */
public async preloadAll(): Promise<PreferenceDefaultScopeType> { public async preloadAll(): Promise<PreferenceDefaultScopeType> {
try { try {
@ -458,6 +508,7 @@ export class PreferenceService {
/** /**
* Check if all preferences are loaded in cache * Check if all preferences are loaded in cache
* @returns True if full cache has been loaded, false otherwise
*/ */
public isFullyCached(): boolean { public isFullyCached(): boolean {
return this.fullCacheLoaded return this.fullCacheLoaded
@ -465,6 +516,8 @@ export class PreferenceService {
/** /**
* Preload specific preferences into cache * 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<void> { public async preload(keys: PreferenceKeyType[]): Promise<void> {
const uncachedKeys = keys.filter((key) => !this.isCached(key)) 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 { private confirmOptimistic(key: PreferenceKeyType, requestId: string): void {
const optimisticState = this.optimisticValues.get(key) 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 { private rollbackOptimistic(key: PreferenceKeyType, requestId: string): void {
const optimisticState = this.optimisticValues.get(key) 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<{ public getPendingOptimisticUpdates(): Array<{
key: string key: string
@ -545,13 +603,18 @@ export class PreferenceService {
/** /**
* Generate unique request ID for tracking concurrent requests * Generate unique request ID for tracking concurrent requests
* @returns Unique request identifier string
*/ */
private generateRequestId(): string { private generateRequestId(): string {
return `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}` 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<void> { private enqueueRequest(key: PreferenceKeyType, requestId: string, value: any): Promise<void> {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
@ -571,6 +634,8 @@ export class PreferenceService {
/** /**
* Process the next queued request for a key * 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<void> { private async processNextQueuedRequest(key: PreferenceKeyType): Promise<void> {
const queue = this.requestQueues.get(key) const queue = this.requestQueues.get(key)
@ -589,6 +654,7 @@ export class PreferenceService {
/** /**
* Complete current request and process next in queue * Complete current request and process next in queue
* @param key The preference key to complete processing for
*/ */
private completeQueuedRequest(key: PreferenceKeyType): void { private completeQueuedRequest(key: PreferenceKeyType): void {
const queue = this.requestQueues.get(key) 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 { public clearCache(): void {
this.cache = {} 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 { public cleanup(): void {
if (this.changeListenerCleanup) { if (this.changeListenerCleanup) {

View File

@ -17,9 +17,9 @@ const logger = loggerService.withContext('usePreference')
* *
* @param key - The preference key to manage (must be a valid PreferenceKeyType) * @param key - The preference key to manage (must be a valid PreferenceKeyType)
* @param options - Optional configuration for update behavior: * @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: * @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 * - setValue: Async function to update the preference value
* *
* @example * @example
@ -28,19 +28,14 @@ const logger = loggerService.withContext('usePreference')
* const [theme, setTheme] = usePreference('app.theme.mode') * const [theme, setTheme] = usePreference('app.theme.mode')
* *
* // Pessimistic updates for critical settings * // Pessimistic updates for critical settings
* const [apiKey, setApiKey] = usePreference('api.key', { strategy: 'pessimistic' }) * const [apiKey, setApiKey] = usePreference('api.key', { optimistic: false })
* *
* // Simple optimistic updates * // Simple optimistic updates
* const [fontSize, setFontSize] = usePreference('chat.message.font_size', { * const [fontSize, setFontSize] = usePreference('chat.message.font_size', {
* strategy: 'optimistic' * optimistic: true
* }) * })
* *
* // Conditional rendering based on preference value * // Value is never undefined - defaults are applied automatically
* if (theme === undefined) {
* return <LoadingSpinner />
* }
*
* // Updating preference value
* const handleThemeChange = async (newTheme: string) => { * const handleThemeChange = async (newTheme: string) => {
* try { * try {
* await setTheme(newTheme) // UI updates immediately with optimistic strategy * await setTheme(newTheme) // UI updates immediately with optimistic strategy
@ -62,7 +57,7 @@ const logger = loggerService.withContext('usePreference')
* ```typescript * ```typescript
* // Advanced usage with form handling for message font size * // Advanced usage with form handling for message font size
* const [fontSize, setFontSize] = usePreference('chat.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) => { * const handleFontSizeChange = useCallback(async (size: number) => {
@ -75,7 +70,7 @@ const logger = loggerService.withContext('usePreference')
* return ( * return (
* <input * <input
* type="number" * type="number"
* value={fontSize ?? 14} * value={fontSize}
* onChange={(e) => handleFontSizeChange(Number(e.target.value))} * onChange={(e) => handleFontSizeChange(Number(e.target.value))}
* min={8} * min={8}
* max={72} * max={72}
@ -131,9 +126,9 @@ export function usePreference<K extends PreferenceKeyType>(
* @param keys - Object mapping local names to preference keys. Keys are your custom names, * @param keys - Object mapping local names to preference keys. Keys are your custom names,
* values must be valid PreferenceKeyType identifiers * values must be valid PreferenceKeyType identifiers
* @param options - Optional configuration for update behavior: * @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: * @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 * - updateValues: Async function to batch update multiple preferences at once
* *
* @example * @example
@ -149,12 +144,12 @@ export function usePreference<K extends PreferenceKeyType>(
* const [apiSettings, setApiSettings] = useMultiplePreferences({ * const [apiSettings, setApiSettings] = useMultiplePreferences({
* apiKey: 'api.key', * apiKey: 'api.key',
* endpoint: 'api.endpoint' * endpoint: 'api.endpoint'
* }, { strategy: 'pessimistic' }) * }, { optimistic: false })
* *
* // Accessing individual values with type safety * // Accessing individual values with type safety (defaults applied automatically)
* const currentTheme = uiSettings.theme // string | undefined * const currentTheme = uiSettings.theme // string (never undefined)
* const currentFontSize = uiSettings.fontSize // number | undefined * const currentFontSize = uiSettings.fontSize // number (never undefined)
* const showLines = uiSettings.showLineNumbers // boolean | undefined * const showLines = uiSettings.showLineNumbers // boolean (never undefined)
* *
* // Batch updating multiple preferences * // Batch updating multiple preferences
* const resetToDefaults = async () => { * const resetToDefaults = async () => {
@ -198,11 +193,7 @@ export function usePreference<K extends PreferenceKeyType>(
* } * }
* } * }
* *
* // Conditional rendering based on loading state * // No need to check for undefined - defaults are applied automatically
* if (Object.values(settings).every(val => val === undefined)) {
* return <SettingsSkeletonLoader />
* }
*
* return ( * return (
* <form onSubmit={(e) => { * <form onSubmit={(e) => {
* e.preventDefault() * e.preventDefault()
@ -214,13 +205,13 @@ export function usePreference<K extends PreferenceKeyType>(
* <input * <input
* name="maxBackups" * name="maxBackups"
* type="number" * type="number"
* defaultValue={settings.maxBackups ?? 10} * defaultValue={settings.maxBackups}
* min="0" * min="0"
* /> * />
* <input * <input
* name="syncInterval" * name="syncInterval"
* type="number" * type="number"
* defaultValue={settings.syncInterval ?? 3600} * defaultValue={settings.syncInterval}
* min="60" * min="60"
* /> * />
* <button type="submit">Save Backup Settings</button> * <button type="submit">Save Backup Settings</button>
@ -241,12 +232,13 @@ export function usePreference<K extends PreferenceKeyType>(
* *
* // Single subscription handles all code preferences * // Single subscription handles all code preferences
* // More efficient than 5 separate usePreference calls * // More efficient than 5 separate usePreference calls
* // No need for null checks - defaults are already applied
* const codeConfig = useMemo(() => ({ * const codeConfig = useMemo(() => ({
* showLineNumbers: codePrefs.showLineNumbers ?? false, * showLineNumbers: codePrefs.showLineNumbers,
* wrappable: codePrefs.wrappable ?? false, * wrappable: codePrefs.wrappable,
* collapsible: codePrefs.collapsible ?? false, * collapsible: codePrefs.collapsible,
* autocompletion: codePrefs.autocompletion ?? true, * autocompletion: codePrefs.autocompletion,
* foldGutter: codePrefs.foldGutter ?? false * foldGutter: codePrefs.foldGutter
* }), [codePrefs]) * }), [codePrefs])
* *
* return <CodeBlock config={codeConfig} /> * return <CodeBlock config={codeConfig} />