diff --git a/docs/en/references/data/preference-overview.md b/docs/en/references/data/preference-overview.md index 755571c659..dc6eacd9e0 100644 --- a/docs/en/references/data/preference-overview.md +++ b/docs/en/references/data/preference-overview.md @@ -5,6 +5,7 @@ The Preference system provides centralized management for user configuration and ## Purpose PreferenceService handles data that: + - Is a **user-modifiable setting that affects app behavior** - Has a **fixed key structure** with stable value types - Needs to **persist permanently** until explicitly changed @@ -13,16 +14,19 @@ PreferenceService handles data that: ## Key Characteristics ### Fixed Key Structure + - Predefined keys in the schema (users modify values, not keys) - Supports 158 configuration items - Nested key paths supported (e.g., `app.theme.mode`) ### Atomic Values + - Each preference item represents one logical setting - Values are typically: boolean, string, number, or simple array/object - Changes are independent (updating one doesn't affect others) ### Cross-Window Synchronization + - Changes automatically broadcast to all windows - Consistent state across main window, mini window, etc. - Conflict resolution handled by Main process @@ -30,19 +34,23 @@ PreferenceService handles data that: ## Update Strategies ### Optimistic Updates (Default) + ```typescript // UI updates immediately, then syncs to database -await preferenceService.set('app.theme.mode', 'dark') +await preferenceService.set("app.theme.mode", "dark"); ``` + - Best for: frequent, non-critical settings - Behavior: Local state updates first, then persists - Rollback: Automatic revert if persistence fails ### Pessimistic Updates + ```typescript // Waits for database confirmation before updating UI -await preferenceService.set('api.key', 'secret', { optimistic: false }) +await preferenceService.set("api.key", "secret", { optimistic: false }); ``` + - Best for: critical settings (API keys, security options) - Behavior: Persists first, then updates local state - No rollback needed: UI only updates on success @@ -86,6 +94,7 @@ await preferenceService.set('api.key', 'secret', { optimistic: false }) ## Main vs Renderer Responsibilities ### Main Process PreferenceService + - **Source of truth** for all preferences - Full memory cache for fast access - SQLite persistence via preference table @@ -93,12 +102,25 @@ await preferenceService.set('api.key', 'secret', { optimistic: false }) - Handles batch operations and transactions ### Renderer Process PreferenceService + - Local cache for read performance - Proxies write operations to Main - Manages React hook subscriptions - Handles optimistic update rollbacks - Listens for cross-window updates +### Statistics (Debug) + +Main process provides `getStats(details?)` for debugging subscription status: + +- Returns total keys, main process subscriptions, and window subscriptions +- Pass `details=true` for per-key breakdown +- **Warning**: Resource-intensive, recommended for development only + +```typescript +const stats = preferenceService.getStats(true); +``` + ## Database Schema Preferences are stored in the `preference` table: @@ -106,29 +128,33 @@ Preferences are stored in the `preference` table: ```typescript // Simplified schema { - scope: string // e.g., 'default', 'user' - key: string // e.g., 'app.theme.mode' - value: json // The preference value - createdAt: number - updatedAt: number + scope: string; // e.g., 'default', 'user' + key: string; // e.g., 'app.theme.mode' + value: json; // The preference value + createdAt: number; + updatedAt: number; } ``` ## Preference Categories ### Application Settings + - Theme mode, language, font sizes - Window behavior, startup options ### Feature Toggles + - Show/hide UI elements - Enable/disable features ### User Customization + - Keyboard shortcuts - Default values for operations ### Provider Configuration + - AI provider settings - API endpoints and tokens @@ -136,9 +162,9 @@ Preferences are stored in the `preference` table: For detailed code examples and API usage, see [Preference Usage Guide](./preference-usage.md). -| Operation | Hook | Service Method | -|-----------|------|----------------| -| Read single | `usePreference(key)` | `preferenceService.get(key)` | -| Write single | `setPreference(value)` | `preferenceService.set(key, value)` | -| Read multiple | `usePreferences([...keys])` | `preferenceService.getMultiple([...keys])` | -| Write multiple | - | `preferenceService.setMultiple({...})` | +| Operation | Hook | Service Method | +| -------------- | --------------------------- | ------------------------------------------ | +| Read single | `usePreference(key)` | `preferenceService.get(key)` | +| Write single | `setPreference(value)` | `preferenceService.set(key, value)` | +| Read multiple | `usePreferences([...keys])` | `preferenceService.getMultiple([...keys])` | +| Write multiple | - | `preferenceService.setMultiple({...})` | diff --git a/src/main/data/PreferenceService.ts b/src/main/data/PreferenceService.ts index 15f69b94fe..49046673ab 100644 --- a/src/main/data/PreferenceService.ts +++ b/src/main/data/PreferenceService.ts @@ -1,5 +1,6 @@ import { dbService } from '@data/db/DbService' import { loggerService } from '@logger' +import { isDev } from '@main/constant' import { DefaultPreferences } from '@shared/data/preference/preferenceSchemas' import type { PreferenceDefaultScopeType, @@ -14,6 +15,50 @@ import { preferenceTable } from './db/schemas/preference' const logger = loggerService.withContext('PreferenceService') +/** + * Preference statistics summary + */ +interface PreferenceStatsSummary { + /** Timestamp when statistics were collected */ + collectedAt: number + /** Total number of preference keys */ + totalKeys: number + /** Number of keys with main process subscriptions */ + mainProcessSubscribedKeys: number + /** Total main process subscription count */ + mainProcessTotalSubscriptions: number + /** Number of keys with window subscriptions */ + windowSubscribedKeys: number + /** Total window subscription count (one window subscribing to one key counts as one) */ + windowTotalSubscriptions: number + /** Number of active windows with subscriptions */ + activeWindowCount: number +} + +/** + * Statistics for a single preference key + */ +interface PreferenceKeyStats { + /** Preference key */ + key: string + /** Main process subscription count */ + mainProcessSubscriptions: number + /** Window subscription count */ + windowSubscriptions: number + /** List of window IDs subscribed to this key */ + subscribedWindowIds: number[] +} + +/** + * Complete statistics result + */ +interface PreferenceStats { + /** Summary statistics */ + summary: PreferenceStatsSummary + /** Detailed per-key statistics (only when details=true) */ + details?: PreferenceKeyStats[] +} + /** * Custom observer pattern implementation for preference change notifications * Replaces EventEmitter to avoid listener limits and improve performance @@ -486,6 +531,113 @@ export class PreferenceService { return this.notifier.getSubscriptionStats() } + /** + * Get preference statistics + * @param details Whether to include per-key detailed statistics + * @returns Statistics object with summary and optional details + */ + public getStats(details: boolean = false): PreferenceStats { + if (!isDev) { + logger.warn('getStats() is resource-intensive and should be used in development environment only') + } + + const summary = this.collectStatsSummary() + + if (!details) { + return { summary } + } + + return { + summary, + details: this.collectStatsDetails() + } + } + + /** + * Collect statistics summary + */ + private collectStatsSummary(): PreferenceStatsSummary { + const mainProcessStats = this.notifier.getSubscriptionStats() + const mainProcessSubscribedKeys = Object.keys(mainProcessStats).length + const mainProcessTotalSubscriptions = this.notifier.getTotalSubscriptionCount() + + const { windowSubscribedKeys, windowTotalSubscriptions, activeWindowCount } = this.collectWindowSubscriptionStats() + + return { + collectedAt: Date.now(), + totalKeys: Object.keys(this.cache).length, + mainProcessSubscribedKeys, + mainProcessTotalSubscriptions, + windowSubscribedKeys, + windowTotalSubscriptions, + activeWindowCount + } + } + + /** + * Collect window subscription statistics + */ + private collectWindowSubscriptionStats(): { + windowSubscribedKeys: number + windowTotalSubscriptions: number + activeWindowCount: number + } { + const keyToWindows = new Map>() + let totalSubscriptions = 0 + + for (const [windowId, keys] of this.subscriptions.entries()) { + for (const key of keys) { + if (!keyToWindows.has(key)) { + keyToWindows.set(key, new Set()) + } + keyToWindows.get(key)!.add(windowId) + totalSubscriptions++ + } + } + + return { + windowSubscribedKeys: keyToWindows.size, + windowTotalSubscriptions: totalSubscriptions, + activeWindowCount: this.subscriptions.size + } + } + + /** + * Collect per-key detailed statistics + */ + private collectStatsDetails(): PreferenceKeyStats[] { + const mainProcessStats = this.notifier.getSubscriptionStats() + + const keyToWindowIds = new Map() + for (const [windowId, keys] of this.subscriptions.entries()) { + for (const key of keys) { + if (!keyToWindowIds.has(key)) { + keyToWindowIds.set(key, []) + } + keyToWindowIds.get(key)!.push(windowId) + } + } + + const allSubscribedKeys = new Set([...Object.keys(mainProcessStats), ...keyToWindowIds.keys()]) + + const details: PreferenceKeyStats[] = [] + for (const key of allSubscribedKeys) { + details.push({ + key, + mainProcessSubscriptions: mainProcessStats[key] || 0, + windowSubscriptions: keyToWindowIds.get(key)?.length || 0, + subscribedWindowIds: keyToWindowIds.get(key) || [] + }) + } + + details.sort( + (a, b) => + b.mainProcessSubscriptions + b.windowSubscriptions - (a.mainProcessSubscriptions + a.windowSubscriptions) + ) + + return details + } + /** * Unified notification method for both main and renderer processes * Broadcasts preference changes to main process listeners and subscribed renderer windows