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.
This commit is contained in:
fullex 2025-09-16 11:27:18 +08:00
parent 8981d0a09d
commit c0cca4ae44
21 changed files with 289 additions and 210 deletions

View File

@ -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": {

View File

@ -38,6 +38,7 @@
"!.github/**",
"!.husky/**",
"!.vscode/**",
"!.claude/**",
"!*.yaml",
"!*.yml",
"!*.mjs",

View File

@ -35,29 +35,26 @@ export type MatchApiPath<Path extends string> = {
/**
* Extract query parameters type for a given concrete path
*/
export type QueryParamsForPath<Path extends string> =
MatchApiPath<Path> extends keyof ApiSchemas
? ApiSchemas[MatchApiPath<Path>] extends { GET: { query?: infer Q } }
? Q
: Record<string, any>
export type QueryParamsForPath<Path extends string> = MatchApiPath<Path> extends keyof ApiSchemas
? ApiSchemas[MatchApiPath<Path>] extends { GET: { query?: infer Q } }
? Q
: Record<string, any>
: Record<string, any>
/**
* Extract request body type for a given concrete path and HTTP method
*/
export type BodyForPath<Path extends string, Method extends string> =
MatchApiPath<Path> extends keyof ApiSchemas
? ApiSchemas[MatchApiPath<Path>] extends { [M in Method]: { body: infer B } }
? B
: any
export type BodyForPath<Path extends string, Method extends string> = MatchApiPath<Path> extends keyof ApiSchemas
? ApiSchemas[MatchApiPath<Path>] extends { [M in Method]: { body: infer B } }
? B
: any
: any
/**
* Extract response type for a given concrete path and HTTP method
*/
export type ResponseForPath<Path extends string, Method extends string> =
MatchApiPath<Path> extends keyof ApiSchemas
? ApiSchemas[MatchApiPath<Path>] extends { [M in Method]: { response: infer R } }
? R
: any
export type ResponseForPath<Path extends string, Method extends string> = MatchApiPath<Path> extends keyof ApiSchemas
? ApiSchemas[MatchApiPath<Path>] extends { [M in Method]: { response: infer R } }
? R
: any
: any

View File

@ -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
}
}

View File

@ -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
*/

View File

@ -44,7 +44,7 @@ const CustomTag: FC<CustomTagProps> = ({
...(disabled && { cursor: 'not-allowed' }),
...style
}}>
{icon && icon} {children}
{icon} {children}
{closable && (
<CloseIcon
$size={size}

View File

@ -95,7 +95,7 @@ export async function getNormalizedExtension(language: string) {
export function getCmThemeNames(): string[] {
return ['auto', 'light', 'dark']
.concat(Object.keys(cmThemes))
.filter((item) => 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))
}

View File

@ -192,14 +192,15 @@ export const LongArticle: Story = {
<p className="mb-4">
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.
</p>
<h2 className="mb-3 text-xl font-semibold">History of Scrolling</h2>
<p className="mb-4">
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.
</p>
<h2 className="mb-3 text-xl font-semibold">Types of Scrolling</h2>
@ -211,9 +212,7 @@ export const LongArticle: Story = {
</ul>
<h2 className="mb-3 text-xl font-semibold">Best Practices</h2>
<p className="mb-4">
When implementing scrolling in your applications, consider the following best practices:
</p>
<p className="mb-4">When implementing scrolling in your applications, consider the following best practices:</p>
<ol className="mb-4 ml-6 list-decimal">
<li className="mb-2">Always provide visual feedback for scrollable areas</li>
@ -243,8 +242,8 @@ export const LongArticle: Story = {
</ul>
<p className="mb-4">
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.
</p>
</article>
)
@ -256,4 +255,4 @@ export const LongArticle: Story = {
</div>
)
]
}
}

View File

@ -17,13 +17,6 @@
"isolatedModules": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist",
"**/*.test.*",
"**/__tests__/**"
]
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.*", "**/__tests__/**"]
}

View File

@ -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<T>(_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<T>(_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 ============

View File

@ -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 () => {

View File

@ -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

View File

@ -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 {

View File

@ -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<K extends UseCacheKey>(key: K): UseCacheSchema[K]
get<T>(key: Exclude<string, UseCacheKey>): 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<K extends UseCacheKey>(key: K, value: UseCacheSchema[K]): void
set<T>(key: Exclude<string, UseCacheKey>, 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<K extends UseCacheKey>(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<K extends UseCacheKey>(key: K): boolean
delete(key: Exclude<string, UseCacheKey>): 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<K extends UseCacheKey>(key: K): boolean
hasTTL(key: Exclude<string, UseCacheKey>): 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<K extends UseSharedCacheKey>(key: K): boolean
hasSharedTTL(key: Exclude<string, UseSharedCacheKey>): 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<K extends UseSharedCacheKey>(key: K): UseSharedCacheSchema[K]
getShared<T>(key: Exclude<string, UseSharedCacheKey>): 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<K extends UseSharedCacheKey>(key: K, value: UseSharedCacheSchema[K]): void
setShared<T>(key: Exclude<string, UseSharedCacheKey>, 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<K extends UseSharedCacheKey>(key: K): boolean
hasShared(key: Exclude<string, UseSharedCacheKey>): 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<K extends UseSharedCacheKey>(key: K): boolean
deleteShared(key: Exclude<string, UseSharedCacheKey>): 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<K extends RendererPersistCacheKey>(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<K extends RendererPersistCacheKey>(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

View File

@ -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<K extends UseCacheKey>(
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<UseCacheSchema[K]>(key), [key]),
useCallback(() => cacheService.get<UseCacheSchema[K]>(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<K extends UseCacheKey>(
}
}, [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<K extends UseCacheKey>(
}
}, [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<K extends UseCacheKey>(
/**
* 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<K extends UseSharedCacheKey>(
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<UseSharedCacheSchema[K]>(key), [key]),
useCallback(() => cacheService.getShared<UseSharedCacheSchema[K]>(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<K extends UseSharedCacheKey>(
}
}, [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<K extends UseSharedCacheKey>(
}
}, [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<K extends UseSharedCacheKey>(
/**
* 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<K extends RendererPersistCacheKey>(
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)

View File

@ -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
)

View File

@ -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'

View File

@ -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({

View File

@ -54,54 +54,58 @@ const selectionSlice = createSlice({
name: 'selectionStore',
initialState,
reducers: {
setSelectionEnabled: (state, action: PayloadAction<boolean>) => {
state.selectionEnabled = action.payload
},
setTriggerMode: (state, action: PayloadAction<SelectionTriggerMode>) => {
state.triggerMode = action.payload
},
setIsCompact: (state, action: PayloadAction<boolean>) => {
state.isCompact = action.payload
},
setIsAutoClose: (state, action: PayloadAction<boolean>) => {
state.isAutoClose = action.payload
},
setIsAutoPin: (state, action: PayloadAction<boolean>) => {
state.isAutoPin = action.payload
},
setIsFollowToolbar: (state, action: PayloadAction<boolean>) => {
state.isFollowToolbar = action.payload
},
setIsRemeberWinSize: (state, action: PayloadAction<boolean>) => {
state.isRemeberWinSize = action.payload
},
setFilterMode: (state, action: PayloadAction<SelectionFilterMode>) => {
state.filterMode = action.payload
},
setFilterList: (state, action: PayloadAction<string[]>) => {
state.filterList = action.payload
},
setActionWindowOpacity: (state, action: PayloadAction<number>) => {
state.actionWindowOpacity = action.payload
},
setActionItems: (state, action: PayloadAction<SelectionActionItem[]>) => {
state.actionItems = action.payload
// setSelectionEnabled: (state, action: PayloadAction<boolean>) => {
// state.selectionEnabled = action.payload
// },
// setTriggerMode: (state, action: PayloadAction<SelectionTriggerMode>) => {
// state.triggerMode = action.payload
// },
// setIsCompact: (state, action: PayloadAction<boolean>) => {
// state.isCompact = action.payload
// },
// setIsAutoClose: (state, action: PayloadAction<boolean>) => {
// state.isAutoClose = action.payload
// },
// setIsAutoPin: (state, action: PayloadAction<boolean>) => {
// state.isAutoPin = action.payload
// },
// setIsFollowToolbar: (state, action: PayloadAction<boolean>) => {
// state.isFollowToolbar = action.payload
// },
// setIsRemeberWinSize: (state, action: PayloadAction<boolean>) => {
// state.isRemeberWinSize = action.payload
// },
// setFilterMode: (state, action: PayloadAction<SelectionFilterMode>) => {
// state.filterMode = action.payload
// },
// setFilterList: (state, action: PayloadAction<string[]>) => {
// state.filterList = action.payload
// },
// setActionWindowOpacity: (state, action: PayloadAction<number>) => {
// state.actionWindowOpacity = action.payload
// },
// setActionItems: (state, action: PayloadAction<SelectionActionItem[]>) => {
// state.actionItems = action.payload
// },
setPlaceholder: (state, action: PayloadAction<Partial<SelectionState>>) => {
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

View File

@ -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<boolean>) => {
// state.codeImageTools = action.payload
// },
setCodeFancyBlock: (state, action: PayloadAction<boolean>) => {
state.codeFancyBlock = action.payload
},
// setCodeFancyBlock: (state, action: PayloadAction<boolean>) => {
// state.codeFancyBlock = action.payload
// },
// setMathEngine: (state, action: PayloadAction<MathEngine>) => {
// state.mathEngine = action.payload
// },
@ -822,18 +825,18 @@ const settingsSlice = createSlice({
setDefaultPaintingProvider: (state, action: PayloadAction<PaintingProvider>) => {
state.defaultPaintingProvider = action.payload
},
setS3: (state, action: PayloadAction<S3Config>) => {
state.s3 = action.payload
},
setS3Partial: (state, action: PayloadAction<Partial<S3Config>>) => {
state.s3 = { ...state.s3, ...action.payload }
},
setEnableDeveloperMode: (state, action: PayloadAction<boolean>) => {
state.enableDeveloperMode = action.payload
},
setNavbarPosition: (state, action: PayloadAction<'left' | 'top'>) => {
state.navbarPosition = action.payload
},
// setS3: (state, action: PayloadAction<S3Config>) => {
// state.s3 = action.payload
// },
// setS3Partial: (state, action: PayloadAction<Partial<S3Config>>) => {
// state.s3 = { ...state.s3, ...action.payload }
// },
// setEnableDeveloperMode: (state, action: PayloadAction<boolean>) => {
// state.enableDeveloperMode = action.payload
// },
// setNavbarPosition: (state, action: PayloadAction<'left' | 'top'>) => {
// state.navbarPosition = action.payload
// },
// API Server actions
setApiServerEnabled: (state, action: PayloadAction<boolean>) => {
state.apiServer = {
@ -913,7 +916,7 @@ export const {
// setCodeCollapsible,
// setCodeWrappable,
// setCodeImageTools,
setCodeFancyBlock,
// setCodeFancyBlock,
// setMathEngine,
// setMathEnableSingleDollar,
// setFoldDisplayMode,

View File

@ -218,7 +218,7 @@ const DataApiHookTests: React.FC = () => {
}
const optimisticData = {
...(singleItem || {}),
...singleItem,
title: `${(singleItem as any)?.title || 'Unknown'} (Optimistic Update)`,
updatedAt: new Date().toISOString()
}