mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-25 11:20:07 +08:00
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:
parent
8981d0a09d
commit
c0cca4ae44
@ -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": {
|
||||
|
||||
@ -38,6 +38,7 @@
|
||||
"!.github/**",
|
||||
"!.husky/**",
|
||||
"!.vscode/**",
|
||||
"!.claude/**",
|
||||
"!*.yaml",
|
||||
"!*.yml",
|
||||
"!*.mjs",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -44,7 +44,7 @@ const CustomTag: FC<CustomTagProps> = ({
|
||||
...(disabled && { cursor: 'not-allowed' }),
|
||||
...style
|
||||
}}>
|
||||
{icon && icon} {children}
|
||||
{icon} {children}
|
||||
{closable && (
|
||||
<CloseIcon
|
||||
$size={size}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,13 +17,6 @@
|
||||
"isolatedModules": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.*",
|
||||
"**/__tests__/**"
|
||||
]
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.*", "**/__tests__/**"]
|
||||
}
|
||||
|
||||
@ -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 ============
|
||||
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -218,7 +218,7 @@ const DataApiHookTests: React.FC = () => {
|
||||
}
|
||||
|
||||
const optimisticData = {
|
||||
...(singleItem || {}),
|
||||
...singleItem,
|
||||
title: `${(singleItem as any)?.title || 'Unknown'} (Optimistic Update)`,
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user