From c0cca4ae4434702a6df60d65e9e8e0e43f81ef04 Mon Sep 17 00:00:00 2001 From: fullex <0xfullex@gmail.com> Date: Tue, 16 Sep 2025 11:27:18 +0800 Subject: [PATCH] chore: update configuration and improve cache management - Added "packages/ui/scripts/**" to .oxlintrc.json for linting. - Excluded ".claude/**" from biome.jsonc. - Refactored API path types in apiPaths.ts for better clarity. - Updated error handling in errorCodes.ts to ensure stack trace is always available. - Modified preferenceSchemas.ts to include new features and updated generated timestamp. - Cleaned up tsconfig.json for better organization. - Adjusted CustomTag component to improve rendering logic. - Enhanced CodeEditor utility functions for better type safety. - Improved Scrollbar story for better readability. - Refactored CacheService to streamline comments and improve documentation. - Updated useCache and useSharedCache hooks for better clarity and functionality. - Cleaned up selectionStore and settings.ts by commenting out deprecated actions. - Updated DataApiHookTests for better optimistic update handling. --- .oxlintrc.json | 2 +- biome.jsonc | 1 + packages/shared/data/api/apiPaths.ts | 27 ++-- packages/shared/data/api/errorCodes.ts | 2 +- .../data/preference/preferenceSchemas.ts | 12 +- .../src/components/base/CustomTag/index.tsx | 2 +- .../interactive/CodeEditor/utils.ts | 2 +- .../components/layout/Scrollbar.stories.tsx | 17 +-- packages/ui/tsconfig.json | 11 +- src/main/data/CacheService.ts | 28 +--- src/main/data/PreferenceService.ts | 13 +- src/main/data/api/services/TestService.ts | 8 +- src/preload/simplest.ts | 1 - src/renderer/src/data/CacheService.ts | 89 ++++++++--- src/renderer/src/data/hooks/useCache.ts | 140 +++++++++++++----- src/renderer/src/store/index.ts | 6 +- src/renderer/src/store/migrate.ts | 3 + src/renderer/src/store/runtime.ts | 8 +- src/renderer/src/store/selectionStore.ts | 90 +++++------ src/renderer/src/store/settings.ts | 35 +++-- .../components/DataApiHookTests.tsx | 2 +- 21 files changed, 289 insertions(+), 210 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 8cf8f056d0..1bb3ec37cc 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -27,7 +27,7 @@ "env": { "node": true }, - "files": ["src/main/**", "resources/scripts/**", "scripts/**", "playwright.config.ts", "electron.vite.config.ts"] + "files": ["src/main/**", "resources/scripts/**", "scripts/**", "playwright.config.ts", "electron.vite.config.ts", "packages/ui/scripts/**"] }, { "env": { diff --git a/biome.jsonc b/biome.jsonc index b86350d70d..03e93ed0a3 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -38,6 +38,7 @@ "!.github/**", "!.husky/**", "!.vscode/**", + "!.claude/**", "!*.yaml", "!*.yml", "!*.mjs", diff --git a/packages/shared/data/api/apiPaths.ts b/packages/shared/data/api/apiPaths.ts index 4dc52d53e6..a947157869 100644 --- a/packages/shared/data/api/apiPaths.ts +++ b/packages/shared/data/api/apiPaths.ts @@ -35,29 +35,26 @@ export type MatchApiPath = { /** * Extract query parameters type for a given concrete path */ -export type QueryParamsForPath = - MatchApiPath extends keyof ApiSchemas - ? ApiSchemas[MatchApiPath] extends { GET: { query?: infer Q } } - ? Q - : Record +export type QueryParamsForPath = MatchApiPath extends keyof ApiSchemas + ? ApiSchemas[MatchApiPath] extends { GET: { query?: infer Q } } + ? Q : Record + : Record /** * Extract request body type for a given concrete path and HTTP method */ -export type BodyForPath = - MatchApiPath extends keyof ApiSchemas - ? ApiSchemas[MatchApiPath] extends { [M in Method]: { body: infer B } } - ? B - : any +export type BodyForPath = MatchApiPath extends keyof ApiSchemas + ? ApiSchemas[MatchApiPath] extends { [M in Method]: { body: infer B } } + ? B : any + : any /** * Extract response type for a given concrete path and HTTP method */ -export type ResponseForPath = - MatchApiPath extends keyof ApiSchemas - ? ApiSchemas[MatchApiPath] extends { [M in Method]: { response: infer R } } - ? R - : any +export type ResponseForPath = MatchApiPath extends keyof ApiSchemas + ? ApiSchemas[MatchApiPath] extends { [M in Method]: { response: infer R } } + ? R : any + : any diff --git a/packages/shared/data/api/errorCodes.ts b/packages/shared/data/api/errorCodes.ts index 6c6a9dbd82..46fdf255f0 100644 --- a/packages/shared/data/api/errorCodes.ts +++ b/packages/shared/data/api/errorCodes.ts @@ -68,7 +68,7 @@ export class DataApiErrorFactory { message: customMessage || ERROR_MESSAGES[code], status: ERROR_STATUS_MAP[code], details, - stack: process.env.NODE_ENV === 'development' ? stack : undefined + stack: stack || undefined } } diff --git a/packages/shared/data/preference/preferenceSchemas.ts b/packages/shared/data/preference/preferenceSchemas.ts index 92483c7652..eea59b180d 100644 --- a/packages/shared/data/preference/preferenceSchemas.ts +++ b/packages/shared/data/preference/preferenceSchemas.ts @@ -1,6 +1,6 @@ /** * Auto-generated preferences configuration - * Generated at: 2025-09-14T09:02:01.333Z + * Generated at: 2025-09-16T03:17:03.354Z * * This file is automatically generated from classification.json * To update this file, modify classification.json and run: @@ -309,12 +309,16 @@ export interface PreferenceSchemas { 'feature.notes.font_size': number // redux/note/settings.isFullWidth 'feature.notes.full_width': boolean + // redux/note/notesPath + 'feature.notes.path': string // redux/note/settings.showTabStatus 'feature.notes.show_tab_status': boolean // redux/note/settings.showTableOfContents 'feature.notes.show_table_of_contents': boolean // redux/note/settings.showWorkspace 'feature.notes.show_workspace': boolean + // redux/note/sortType + 'feature.notes.sort_type': string // redux/settings/clickTrayToShowQuickAssistant 'feature.quick_assistant.click_tray_to_show': boolean // redux/settings/enableQuickAssistant @@ -560,9 +564,11 @@ export const DefaultPreferences: PreferenceSchemas = { 'feature.notes.font_family': 'default', 'feature.notes.font_size': 16, 'feature.notes.full_width': true, + 'feature.notes.path': '', 'feature.notes.show_tab_status': true, 'feature.notes.show_table_of_contents': true, 'feature.notes.show_workspace': true, + 'feature.notes.sort_type': 'sort_a2z', 'feature.quick_assistant.click_tray_to_show': false, 'feature.quick_assistant.enabled': false, 'feature.quick_assistant.read_clipboard_at_startup': true, @@ -674,8 +680,8 @@ export const DefaultPreferences: PreferenceSchemas = { /** * 生成统计: - * - 总配置项: 195 + * - 总配置项: 197 * - electronStore项: 1 - * - redux项: 194 + * - redux项: 196 * - localStorage项: 0 */ diff --git a/packages/ui/src/components/base/CustomTag/index.tsx b/packages/ui/src/components/base/CustomTag/index.tsx index 8276782236..a692db0f09 100644 --- a/packages/ui/src/components/base/CustomTag/index.tsx +++ b/packages/ui/src/components/base/CustomTag/index.tsx @@ -44,7 +44,7 @@ const CustomTag: FC = ({ ...(disabled && { cursor: 'not-allowed' }), ...style }}> - {icon && icon} {children} + {icon} {children} {closable && ( typeof cmThemes[item as keyof typeof cmThemes] !== 'function') + .filter((item) => typeof (cmThemes as any)[item] !== 'function') .filter((item) => !/^(defaultSettings)/.test(item as string) && !/(Style)$/.test(item as string)) } diff --git a/packages/ui/stories/components/layout/Scrollbar.stories.tsx b/packages/ui/stories/components/layout/Scrollbar.stories.tsx index d8dad28a83..8c8cd0cb90 100644 --- a/packages/ui/stories/components/layout/Scrollbar.stories.tsx +++ b/packages/ui/stories/components/layout/Scrollbar.stories.tsx @@ -192,14 +192,15 @@ export const LongArticle: Story = {

Scrolling is a fundamental interaction pattern in user interfaces. It allows users to navigate through content - that exceeds the visible viewport, making it possible to present large amounts of information in a limited space. + that exceeds the visible viewport, making it possible to present large amounts of information in a limited + space.

History of Scrolling

The concept of scrolling dates back to the early days of computing, when terminal displays could only show a - limited number of lines. As content grew beyond what could fit on a single screen, the need for scrolling became - apparent. + limited number of lines. As content grew beyond what could fit on a single screen, the need for scrolling + became apparent.

Types of Scrolling

@@ -211,9 +212,7 @@ export const LongArticle: Story = {

Best Practices

-

- When implementing scrolling in your applications, consider the following best practices: -

+

When implementing scrolling in your applications, consider the following best practices:

  1. Always provide visual feedback for scrollable areas
  2. @@ -243,8 +242,8 @@ export const LongArticle: Story = {

    - To optimize scrolling performance, consider using techniques like virtual scrolling for large lists, debouncing - scroll event handlers, and leveraging CSS transforms for animations. + To optimize scrolling performance, consider using techniques like virtual scrolling for large lists, + debouncing scroll event handlers, and leveraging CSS transforms for animations.

    ) @@ -256,4 +255,4 @@ export const LongArticle: Story = { ) ] -} \ No newline at end of file +} diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index 67c334760a..3dc3bee863 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -17,13 +17,6 @@ "isolatedModules": true, "noFallthroughCasesInSwitch": true }, - "include": [ - "src/**/*" - ], - "exclude": [ - "node_modules", - "dist", - "**/*.test.*", - "**/__tests__/**" - ] + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.*", "**/__tests__/**"] } diff --git a/src/main/data/CacheService.ts b/src/main/data/CacheService.ts index 524df97b43..db05692d5a 100644 --- a/src/main/data/CacheService.ts +++ b/src/main/data/CacheService.ts @@ -105,33 +105,7 @@ export class CacheService { // ============ Persist Cache Interface (Reserved) ============ - /** - * Get persist cache value (interface reserved for future) - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - getPersist(_key: string): T | undefined { - // TODO: Implement persist cache in future - logger.warn('getPersist not implemented yet') - return undefined - } - - /** - * Set persist cache value (interface reserved for future) - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - setPersist(_key: string, _value: T): void { - // TODO: Implement persist cache in future - logger.warn('setPersist not implemented yet') - } - - /** - * Check persist cache key (interface reserved for future) - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - hasPersist(_key: string): boolean { - // TODO: Implement persist cache in future - return false - } + // TODO: Implement persist cache in future // ============ IPC Handlers for Cache Synchronization ============ diff --git a/src/main/data/PreferenceService.ts b/src/main/data/PreferenceService.ts index 67662db5af..ca3d8af9a8 100644 --- a/src/main/data/PreferenceService.ts +++ b/src/main/data/PreferenceService.ts @@ -26,12 +26,7 @@ class PreferenceNotifier { * @param metadata - Optional metadata for debugging (unused but kept for API compatibility) * @returns Unsubscribe function */ - subscribe = ( - key: string, - callback: (key: string, newValue: any, oldValue?: any) => void, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _metadata?: string - ): (() => void) => { + subscribe = (key: string, callback: (key: string, newValue: any, oldValue?: any) => void): (() => void) => { if (!this.subscriptions.has(key)) { this.subscriptions.set(key, new Set()) } @@ -383,7 +378,7 @@ export class PreferenceService { } } - return this.notifier.subscribe(key, listener, `subscribeChange-${key}`) + return this.notifier.subscribe(key, listener) } /** @@ -401,9 +396,7 @@ export class PreferenceService { } // Subscribe to all keys and collect unsubscribe functions - const unsubscribeFunctions = keys.map((key) => - this.notifier.subscribe(key, listener, `subscribeMultipleChanges-${key}`) - ) + const unsubscribeFunctions = keys.map((key) => this.notifier.subscribe(key, listener)) // Return a function that unsubscribes from all keys return () => { diff --git a/src/main/data/api/services/TestService.ts b/src/main/data/api/services/TestService.ts index d678ba252d..1af016cf44 100644 --- a/src/main/data/api/services/TestService.ts +++ b/src/main/data/api/services/TestService.ts @@ -76,13 +76,7 @@ export class TestService { * Get paginated list of test items */ async getItems( - params: { - page?: number - limit?: number - type?: string - status?: string - search?: string - } = {} + params: { page?: number; limit?: number; type?: string; status?: string; search?: string } = {} ): Promise<{ items: any[] total: number diff --git a/src/preload/simplest.ts b/src/preload/simplest.ts index 785bdbca8f..43043aab85 100644 --- a/src/preload/simplest.ts +++ b/src/preload/simplest.ts @@ -8,7 +8,6 @@ if (process.contextIsolated) { try { contextBridge.exposeInMainWorld('electron', electronAPI) } catch (error) { - // eslint-disable-next-line no-restricted-syntax console.error('[Preload]Failed to expose APIs:', error as Error) } } else { diff --git a/src/renderer/src/data/CacheService.ts b/src/renderer/src/data/CacheService.ts index a69f4e4699..191111f6c8 100644 --- a/src/renderer/src/data/CacheService.ts +++ b/src/renderer/src/data/CacheService.ts @@ -61,6 +61,9 @@ export class CacheService { return CacheService.instance } + /** + * Initialize the cache service with persist cache loading and IPC listeners + */ public initialize(): void { this.loadPersistCache() this.setupIpcListeners() @@ -71,7 +74,9 @@ export class CacheService { // ============ Memory Cache (Cross-component) ============ /** - * Get value from memory cache + * Get value from memory cache with TTL validation + * @param key - Cache key to retrieve + * @returns Cached value or undefined if not found or expired */ get(key: K): UseCacheSchema[K] get(key: Exclude): T | undefined @@ -91,7 +96,10 @@ export class CacheService { } /** - * Set value in memory cache + * Set value in memory cache with optional TTL + * @param key - Cache key to store + * @param value - Value to cache + * @param ttl - Time to live in milliseconds (optional) */ set(key: K, value: UseCacheSchema[K]): void set(key: Exclude, value: T, ttl?: number): void @@ -122,7 +130,9 @@ export class CacheService { } /** - * Check if key exists in memory cache + * Check if key exists in memory cache and is not expired + * @param key - Cache key to check + * @returns True if key exists and is valid, false otherwise */ has(key: K): boolean @@ -144,7 +154,9 @@ export class CacheService { } /** - * Delete from memory cache + * Delete from memory cache with hook protection + * @param key - Cache key to delete + * @returns True if deletion succeeded, false if key is protected by active hooks */ delete(key: K): boolean delete(key: Exclude): boolean @@ -168,7 +180,9 @@ export class CacheService { } /** - * Check if a key has TTL set (for warning purposes) + * Check if a key has TTL set in memory cache + * @param key - Cache key to check + * @returns True if key has TTL configured */ hasTTL(key: K): boolean hasTTL(key: Exclude): boolean @@ -178,7 +192,9 @@ export class CacheService { } /** - * Check if a shared cache key has TTL set (for warning purposes) + * Check if a shared cache key has TTL set + * @param key - Shared cache key to check + * @returns True if key has TTL configured */ hasSharedTTL(key: K): boolean hasSharedTTL(key: Exclude): boolean @@ -190,7 +206,9 @@ export class CacheService { // ============ Shared Cache (Cross-window) ============ /** - * Get value from shared cache + * Get value from shared cache with TTL validation + * @param key - Shared cache key to retrieve + * @returns Cached value or undefined if not found or expired */ getShared(key: K): UseSharedCacheSchema[K] getShared(key: Exclude): T | undefined @@ -209,7 +227,10 @@ export class CacheService { } /** - * Set value in shared cache + * Set value in shared cache with cross-window synchronization + * @param key - Shared cache key to store + * @param value - Value to cache + * @param ttl - Time to live in milliseconds (optional) */ setShared(key: K, value: UseSharedCacheSchema[K]): void setShared(key: Exclude, value: T, ttl?: number): void @@ -256,7 +277,9 @@ export class CacheService { } /** - * Check if key exists in shared cache + * Check if key exists in shared cache and is not expired + * @param key - Shared cache key to check + * @returns True if key exists and is valid, false otherwise */ hasShared(key: K): boolean hasShared(key: Exclude): boolean @@ -275,7 +298,9 @@ export class CacheService { } /** - * Delete from shared cache + * Delete from shared cache with cross-window synchronization and hook protection + * @param key - Shared cache key to delete + * @returns True if deletion succeeded, false if key is protected by active hooks */ deleteShared(key: K): boolean deleteShared(key: Exclude): boolean @@ -308,7 +333,9 @@ export class CacheService { // ============ Persist Cache (Cross-window + localStorage) ============ /** - * Get value from persist cache + * Get value from persist cache with automatic default value fallback + * @param key - Persist cache key to retrieve + * @returns Cached value or default value if not found */ getPersist(key: K): RendererPersistCacheSchema[K] { const value = this.persistCache.get(key) @@ -325,7 +352,9 @@ export class CacheService { } /** - * Set value in persist cache + * Set value in persist cache with cross-window sync and localStorage persistence + * @param key - Persist cache key to store + * @param value - Value to cache (must match schema type) */ setPersist(key: K, value: RendererPersistCacheSchema[K]): void { const existingValue = this.persistCache.get(key) @@ -353,6 +382,8 @@ export class CacheService { /** * Check if key exists in persist cache + * @param key - Persist cache key to check + * @returns True if key exists in cache */ hasPersist(key: RendererPersistCacheKey): boolean { return this.persistCache.has(key) @@ -363,14 +394,16 @@ export class CacheService { // ============ Hook Reference Management ============ /** - * Register a hook as using a specific key + * Register a hook as using a specific cache key to prevent deletion + * @param key - Cache key being used by the hook */ registerHook(key: string): void { this.activeHooks.add(key) } /** - * Unregister a hook from using a specific key + * Unregister a hook from using a specific cache key + * @param key - Cache key no longer being used by the hook */ unregisterHook(key: string): void { this.activeHooks.delete(key) @@ -379,7 +412,10 @@ export class CacheService { // ============ Subscription Management ============ /** - * Subscribe to cache changes for specific key + * Subscribe to cache changes for a specific key + * @param key - Cache key to watch for changes + * @param callback - Function to call when key changes + * @returns Unsubscribe function */ subscribe(key: string, callback: CacheSubscriber): () => void { if (!this.subscribers.has(key)) { @@ -398,7 +434,8 @@ export class CacheService { } /** - * Notify subscribers for specific key + * Notify all subscribers when a cache key changes + * @param key - Cache key that changed */ notifySubscribers(key: string): void { const keySubscribers = this.subscribers.get(key) @@ -416,7 +453,10 @@ export class CacheService { // ============ Private Methods ============ /** - * Deep equality comparison for cache values + * Perform deep equality comparison for cache values + * @param a - First value to compare + * @param b - Second value to compare + * @returns True if values are deeply equal */ private deepEqual(a: any, b: any): boolean { // Use Object.is for primitive values and same reference @@ -451,7 +491,7 @@ export class CacheService { } /** - * Load persist cache from localStorage + * Load persist cache from localStorage with default value initialization */ private loadPersistCache(): void { // First, initialize with default values @@ -490,7 +530,7 @@ export class CacheService { } /** - * Save persist cache to localStorage + * Save persist cache to localStorage with size validation */ private savePersistCache(): void { try { @@ -517,7 +557,7 @@ export class CacheService { } /** - * Schedule persist cache save with debounce + * Schedule persist cache save with 200ms debounce to avoid excessive writes */ private schedulePersistSave(): void { this.persistDirty = true @@ -533,7 +573,8 @@ export class CacheService { } /** - * Broadcast cache sync message to other windows + * Broadcast cache sync message to other windows via IPC + * @param message - Cache sync message to broadcast */ private broadcastSync(message: CacheSyncMessage): void { if (window.api?.cache?.broadcastSync) { @@ -542,7 +583,7 @@ export class CacheService { } /** - * Setup IPC listeners for cache synchronization + * Setup IPC listeners for receiving cache sync messages from other windows */ private setupIpcListeners(): void { if (!window.api?.cache?.onSync) { @@ -574,7 +615,7 @@ export class CacheService { } /** - * Setup window unload handler to force save persist cache + * Setup window unload handler to ensure persist cache is saved before exit */ private setupWindowUnloadHandler(): void { window.addEventListener('beforeunload', () => { @@ -585,7 +626,7 @@ export class CacheService { } /** - * Cleanup service resources + * Cleanup service resources including timers, caches, and event listeners */ public cleanup(): void { // Force save persist cache if dirty diff --git a/src/renderer/src/data/hooks/useCache.ts b/src/renderer/src/data/hooks/useCache.ts index 96a6dce958..61e7328188 100644 --- a/src/renderer/src/data/hooks/useCache.ts +++ b/src/renderer/src/data/hooks/useCache.ts @@ -13,30 +13,45 @@ import { useCallback, useEffect, useSyncExternalStore } from 'react' const logger = loggerService.withContext('useCache') /** - * React hook for cross-component memory cache + * React hook for component-level memory cache * - * Features: - * - Synchronous API with useSyncExternalStore - * - Automatic default value setting - * - Hook lifecycle management - * - TTL support with warning when used + * Use this for data that needs to be shared between components in the same window. + * Data is lost when the app restarts. * - * @param key - Cache key - * @param initValue - Default value (set automatically if not exists) - * @returns [value, setValue] + * @param key - Cache key from the predefined schema + * @param initValue - Initial value (optional, uses schema default if not provided) + * @returns [value, setValue] - Similar to useState but shared across components + * + * @example + * ```typescript + * // Basic usage + * const [theme, setTheme] = useCache('ui.theme') + * + * // With custom initial value + * const [count, setCount] = useCache('counter', 0) + * + * // Update the value + * setTheme('dark') + * ``` */ export function useCache( key: K, initValue?: UseCacheSchema[K] ): [UseCacheSchema[K], (value: UseCacheSchema[K]) => void] { - // Subscribe to cache changes + /** + * Subscribe to cache changes using React's useSyncExternalStore + * This ensures the component re-renders when the cache value changes + */ const value = useSyncExternalStore( useCallback((callback) => cacheService.subscribe(key, callback), [key]), useCallback(() => cacheService.get(key), [key]), useCallback(() => cacheService.get(key), [key]) // SSR snapshot ) - // Set default value if not exists + /** + * Initialize cache with default value if it doesn't exist + * Priority: existing cache value > custom initValue > schema default + */ useEffect(() => { if (cacheService.has(key)) { return @@ -49,13 +64,19 @@ export function useCache( } }, [key, initValue]) - // Register hook lifecycle + /** + * Register this hook as actively using the cache key + * This prevents the cache service from deleting the key while the hook is active + */ useEffect(() => { cacheService.registerHook(key) return () => cacheService.unregisterHook(key) }, [key]) - // Check for TTL warning + /** + * Warn developers when using TTL with hooks + * TTL can cause values to expire between renders, leading to unstable behavior + */ useEffect(() => { if (cacheService.hasTTL(key)) { logger.warn( @@ -64,6 +85,10 @@ export function useCache( } }, [key]) + /** + * Memoized setter function for updating the cache value + * @param newValue - New value to store in cache + */ const setValue = useCallback( (newValue: UseCacheSchema[K]) => { cacheService.set(key, newValue) @@ -77,28 +102,43 @@ export function useCache( /** * React hook for cross-window shared cache * - * Features: - * - Synchronous API (uses local copy) - * - Cross-window synchronization via IPC - * - Automatic default value setting - * - Hook lifecycle management + * Use this for data that needs to be shared between all app windows. + * Data is lost when the app restarts. * - * @param key - Cache key - * @param initValue - Default value (set automatically if not exists) - * @returns [value, setValue] + * @param key - Cache key from the predefined schema + * @param initValue - Initial value (optional, uses schema default if not provided) + * @returns [value, setValue] - Similar to useState but shared across all windows + * + * @example + * ```typescript + * // Shared across all windows + * const [windowCount, setWindowCount] = useSharedCache('app.windowCount') + * + * // With custom initial value + * const [sharedState, setSharedState] = useSharedCache('app.state', { loaded: false }) + * + * // Changes automatically sync to all open windows + * setWindowCount(3) + * ``` */ export function useSharedCache( key: K, initValue?: UseSharedCacheSchema[K] ): [UseSharedCacheSchema[K], (value: UseSharedCacheSchema[K]) => void] { - // Subscribe to cache changes + /** + * Subscribe to shared cache changes using React's useSyncExternalStore + * This ensures the component re-renders when the shared cache value changes + */ const value = useSyncExternalStore( useCallback((callback) => cacheService.subscribe(key, callback), [key]), useCallback(() => cacheService.getShared(key), [key]), useCallback(() => cacheService.getShared(key), [key]) // SSR snapshot ) - // Set default value if not exists + /** + * Initialize shared cache with default value if it doesn't exist + * Priority: existing shared cache value > custom initValue > schema default + */ useEffect(() => { if (cacheService.hasShared(key)) { return @@ -111,13 +151,19 @@ export function useSharedCache( } }, [key, initValue]) - // Register hook lifecycle + /** + * Register this hook as actively using the shared cache key + * This prevents the cache service from deleting the key while the hook is active + */ useEffect(() => { cacheService.registerHook(key) return () => cacheService.unregisterHook(key) }, [key]) - // Check for TTL warning + /** + * Warn developers when using TTL with shared cache hooks + * TTL can cause values to expire between renders, leading to unstable behavior + */ useEffect(() => { if (cacheService.hasSharedTTL(key)) { logger.warn( @@ -126,6 +172,11 @@ export function useSharedCache( } }, [key]) + /** + * Memoized setter function for updating the shared cache value + * Changes will be synchronized across all renderer windows + * @param newValue - New value to store in shared cache + */ const setValue = useCallback( (newValue: UseSharedCacheSchema[K]) => { cacheService.setShared(key, newValue) @@ -139,31 +190,52 @@ export function useSharedCache( /** * React hook for persistent cache with localStorage * - * Features: - * - Type-safe with predefined schema - * - Cross-window synchronization - * - Automatic default value setting - * - No TTL support (as discussed) + * Use this for data that needs to persist across app restarts and be shared between all windows. + * Data is automatically saved to localStorage. * - * @param key - Predefined persist cache key - * @returns [value, setValue] + * @param key - Cache key from the predefined schema + * @returns [value, setValue] - Similar to useState but persisted and shared across all windows + * + * @example + * ```typescript + * // Persisted across app restarts + * const [userPrefs, setUserPrefs] = usePersistCache('user.preferences') + * + * // Automatically saved and synced across all windows + * const [appSettings, setAppSettings] = usePersistCache('app.settings') + * + * // Changes are automatically saved + * setUserPrefs({ theme: 'dark', language: 'en' }) + * ``` */ export function usePersistCache( key: K ): [RendererPersistCacheSchema[K], (value: RendererPersistCacheSchema[K]) => void] { - // Subscribe to cache changes + /** + * Subscribe to persist cache changes using React's useSyncExternalStore + * This ensures the component re-renders when the persist cache value changes + */ const value = useSyncExternalStore( useCallback((callback) => cacheService.subscribe(key, callback), [key]), useCallback(() => cacheService.getPersist(key), [key]), useCallback(() => cacheService.getPersist(key), [key]) // SSR snapshot ) - // Register hook lifecycle (using string key for tracking) + /** + * Register this hook as actively using the persist cache key + * This prevents the cache service from deleting the key while the hook is active + * Note: Persist cache keys are predefined and generally not deleted + */ useEffect(() => { cacheService.registerHook(key) return () => cacheService.unregisterHook(key) }, [key]) + /** + * Memoized setter function for updating the persist cache value + * Changes will be synchronized across all windows and persisted to localStorage + * @param newValue - New value to store in persist cache (must match schema type) + */ const setValue = useCallback( (newValue: RendererPersistCacheSchema[K]) => { cacheService.setPersist(key, newValue) diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index 4b5f61b4fa..b57bd9e9bd 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -20,7 +20,7 @@ import llm from './llm' import mcp from './mcp' import memory from './memory' import messageBlocksReducer from './messageBlock' -// import migrate from './migrate' +import migrate from './migrate' import minapps from './minapps' import newMessagesReducer from './newMessage' import { setNotesPath } from './note' @@ -72,8 +72,8 @@ const persistedReducer = persistReducer( key: 'cherry-studio', storage, version: 155, - blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'] - // migrate + blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'], + migrate }, rootReducer ) diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index 4bfc4231f1..5a90ea1060 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -1,3 +1,6 @@ +/** + * @deprecated this file will be removed after data refactor + */ import { loggerService } from '@logger' import { nanoid } from '@reduxjs/toolkit' import { DEFAULT_CONTEXTCOUNT, DEFAULT_TEMPERATURE, isMac } from '@renderer/config/constant' diff --git a/src/renderer/src/store/runtime.ts b/src/renderer/src/store/runtime.ts index a915c675ee..39bcf41672 100644 --- a/src/renderer/src/store/runtime.ts +++ b/src/renderer/src/store/runtime.ts @@ -49,7 +49,7 @@ export interface RuntimeState { // update: UpdateState // export: ExportState // chat: ChatState - websearch: WebSearchState + // websearch: WebSearchState } export interface ExportState { @@ -85,9 +85,9 @@ const initialState: RuntimeState = { // renamingTopics: [], // newlyRenamedTopics: [] // }, - websearch: { - activeSearches: {} - } + // websearch: { + // activeSearches: {} + // } } const runtimeSlice = createSlice({ diff --git a/src/renderer/src/store/selectionStore.ts b/src/renderer/src/store/selectionStore.ts index 2ae842e126..8c66481521 100644 --- a/src/renderer/src/store/selectionStore.ts +++ b/src/renderer/src/store/selectionStore.ts @@ -54,54 +54,58 @@ const selectionSlice = createSlice({ name: 'selectionStore', initialState, reducers: { - setSelectionEnabled: (state, action: PayloadAction) => { - state.selectionEnabled = action.payload - }, - setTriggerMode: (state, action: PayloadAction) => { - state.triggerMode = action.payload - }, - setIsCompact: (state, action: PayloadAction) => { - state.isCompact = action.payload - }, - setIsAutoClose: (state, action: PayloadAction) => { - state.isAutoClose = action.payload - }, - setIsAutoPin: (state, action: PayloadAction) => { - state.isAutoPin = action.payload - }, - setIsFollowToolbar: (state, action: PayloadAction) => { - state.isFollowToolbar = action.payload - }, - setIsRemeberWinSize: (state, action: PayloadAction) => { - state.isRemeberWinSize = action.payload - }, - setFilterMode: (state, action: PayloadAction) => { - state.filterMode = action.payload - }, - setFilterList: (state, action: PayloadAction) => { - state.filterList = action.payload - }, - setActionWindowOpacity: (state, action: PayloadAction) => { - state.actionWindowOpacity = action.payload - }, - setActionItems: (state, action: PayloadAction) => { - state.actionItems = action.payload + // setSelectionEnabled: (state, action: PayloadAction) => { + // state.selectionEnabled = action.payload + // }, + // setTriggerMode: (state, action: PayloadAction) => { + // state.triggerMode = action.payload + // }, + // setIsCompact: (state, action: PayloadAction) => { + // state.isCompact = action.payload + // }, + // setIsAutoClose: (state, action: PayloadAction) => { + // state.isAutoClose = action.payload + // }, + // setIsAutoPin: (state, action: PayloadAction) => { + // state.isAutoPin = action.payload + // }, + // setIsFollowToolbar: (state, action: PayloadAction) => { + // state.isFollowToolbar = action.payload + // }, + // setIsRemeberWinSize: (state, action: PayloadAction) => { + // state.isRemeberWinSize = action.payload + // }, + // setFilterMode: (state, action: PayloadAction) => { + // state.filterMode = action.payload + // }, + // setFilterList: (state, action: PayloadAction) => { + // state.filterList = action.payload + // }, + // setActionWindowOpacity: (state, action: PayloadAction) => { + // state.actionWindowOpacity = action.payload + // }, + // setActionItems: (state, action: PayloadAction) => { + // state.actionItems = action.payload + // }, + setPlaceholder: (state, action: PayloadAction>) => { + state = { ...state, ...action.payload } } } }) export const { - setSelectionEnabled, - setTriggerMode, - setIsCompact, - setIsAutoClose, - setIsAutoPin, - setIsFollowToolbar, - setIsRemeberWinSize, - setFilterMode, - setFilterList, - setActionWindowOpacity, - setActionItems + // setSelectionEnabled, + // setTriggerMode, + // setIsCompact, + // setIsAutoClose, + // setIsAutoPin, + // setIsFollowToolbar, + // setIsRemeberWinSize, + // setFilterMode, + // setFilterList, + // setActionWindowOpacity, + // setActionItems, + setPlaceholder } = selectionSlice.actions export default selectionSlice.reducer diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index 04c5a738a8..eea0c3fa2e 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -1,3 +1,6 @@ +/** + * //TODO @deprecated this file will be removed after data refactor + */ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { isMac } from '@renderer/config/constant' import { @@ -621,9 +624,9 @@ const settingsSlice = createSlice({ // setCodeImageTools: (state, action: PayloadAction) => { // state.codeImageTools = action.payload // }, - setCodeFancyBlock: (state, action: PayloadAction) => { - state.codeFancyBlock = action.payload - }, + // setCodeFancyBlock: (state, action: PayloadAction) => { + // state.codeFancyBlock = action.payload + // }, // setMathEngine: (state, action: PayloadAction) => { // state.mathEngine = action.payload // }, @@ -822,18 +825,18 @@ const settingsSlice = createSlice({ setDefaultPaintingProvider: (state, action: PayloadAction) => { state.defaultPaintingProvider = action.payload }, - setS3: (state, action: PayloadAction) => { - state.s3 = action.payload - }, - setS3Partial: (state, action: PayloadAction>) => { - state.s3 = { ...state.s3, ...action.payload } - }, - setEnableDeveloperMode: (state, action: PayloadAction) => { - state.enableDeveloperMode = action.payload - }, - setNavbarPosition: (state, action: PayloadAction<'left' | 'top'>) => { - state.navbarPosition = action.payload - }, + // setS3: (state, action: PayloadAction) => { + // state.s3 = action.payload + // }, + // setS3Partial: (state, action: PayloadAction>) => { + // state.s3 = { ...state.s3, ...action.payload } + // }, + // setEnableDeveloperMode: (state, action: PayloadAction) => { + // state.enableDeveloperMode = action.payload + // }, + // setNavbarPosition: (state, action: PayloadAction<'left' | 'top'>) => { + // state.navbarPosition = action.payload + // }, // API Server actions setApiServerEnabled: (state, action: PayloadAction) => { state.apiServer = { @@ -913,7 +916,7 @@ export const { // setCodeCollapsible, // setCodeWrappable, // setCodeImageTools, - setCodeFancyBlock, + // setCodeFancyBlock, // setMathEngine, // setMathEnableSingleDollar, // setFoldDisplayMode, diff --git a/src/renderer/src/windows/dataRefactorTest/components/DataApiHookTests.tsx b/src/renderer/src/windows/dataRefactorTest/components/DataApiHookTests.tsx index 1b6b70b778..0787fb183c 100644 --- a/src/renderer/src/windows/dataRefactorTest/components/DataApiHookTests.tsx +++ b/src/renderer/src/windows/dataRefactorTest/components/DataApiHookTests.tsx @@ -218,7 +218,7 @@ const DataApiHookTests: React.FC = () => { } const optimisticData = { - ...(singleItem || {}), + ...singleItem, title: `${(singleItem as any)?.title || 'Unknown'} (Optimistic Update)`, updatedAt: new Date().toISOString() }