mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-12 08:59:02 +08:00
feat(PreferenceService): add preference statistics functionality
- Introduced methods to retrieve comprehensive statistics for preferences, including summary and detailed per-key information. - Implemented a new `getStats` method to provide insights into subscription counts and active windows. - Enhanced the PreferenceService with additional interfaces for better type safety and clarity in statistics handling. - Updated documentation to reflect the new statistics features and their usage.
This commit is contained in:
parent
ad37f0991a
commit
ee4d1a227c
@ -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({...})` |
|
||||
|
||||
@ -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<string, Set<number>>()
|
||||
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<string, number[]>()
|
||||
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<string>([...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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user