mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-10 23:59:45 +08:00
- Add type utilities for template key matching (IsTemplateKey, ExpandTemplateKey, ProcessKey)
- Add InferUseCacheValue<K> for automatic value type inference from template patterns
- Update useCache hook to support template keys with default value fallback
- Extend ESLint rule to validate template key syntax (e.g., 'scroll.position:${id}')
- Update CacheService.get() docs: clarify | undefined return is intentional
(developers need to know when value doesn't exist after deletion/TTL expiry)
- Update cache documentation with template key usage examples
BREAKING CHANGE: CacheService.get() now explicitly returns T | undefined
(was implicit before). Callers should use ?? defaultValue for fallback.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
421 lines
12 KiB
Markdown
421 lines
12 KiB
Markdown
# Cache Usage Guide
|
|
|
|
This guide covers how to use the Cache system in React components and services.
|
|
|
|
## React Hooks
|
|
|
|
### useCache (Memory Cache)
|
|
|
|
Memory cache is lost on app restart. Best for temporary computed results.
|
|
|
|
```typescript
|
|
import { useCache } from '@data/hooks/useCache'
|
|
|
|
// Basic usage with default value
|
|
const [counter, setCounter] = useCache('ui.counter', 0)
|
|
|
|
// Update the value
|
|
setCounter(counter + 1)
|
|
|
|
// With TTL (30 seconds)
|
|
const [searchResults, setSearchResults] = useCache('search.results', [], { ttl: 30000 })
|
|
```
|
|
|
|
### useSharedCache (Cross-Window Cache)
|
|
|
|
Shared cache syncs across all windows, lost on app restart.
|
|
|
|
```typescript
|
|
import { useSharedCache } from '@data/hooks/useCache'
|
|
|
|
// Cross-window state
|
|
const [layout, setLayout] = useSharedCache('window.layout', defaultLayout)
|
|
|
|
// Sidebar state shared between windows
|
|
const [sidebarCollapsed, setSidebarCollapsed] = useSharedCache('ui.sidebar.collapsed', false)
|
|
```
|
|
|
|
### usePersistCache (Persistent Cache)
|
|
|
|
Persist cache survives app restarts via localStorage.
|
|
|
|
```typescript
|
|
import { usePersistCache } from '@data/hooks/useCache'
|
|
|
|
// Recent files list (survives restart)
|
|
const [recentFiles, setRecentFiles] = usePersistCache('app.recent_files', [])
|
|
|
|
// Search history
|
|
const [searchHistory, setSearchHistory] = usePersistCache('search.history', [])
|
|
```
|
|
|
|
## CacheService Direct Usage
|
|
|
|
For non-React code or more control, use CacheService directly.
|
|
|
|
### Memory Cache
|
|
|
|
```typescript
|
|
import { cacheService } from '@data/CacheService'
|
|
|
|
// Type-safe (schema key)
|
|
cacheService.set('temp.calculation', result)
|
|
const result = cacheService.get('temp.calculation')
|
|
|
|
// With TTL (30 seconds)
|
|
cacheService.set('temp.calculation', result, 30000)
|
|
|
|
// Casual (dynamic key, manual type)
|
|
cacheService.setCasual<TopicCache>(`topic:${id}`, topicData)
|
|
const topic = cacheService.getCasual<TopicCache>(`topic:${id}`)
|
|
|
|
// Check existence
|
|
if (cacheService.has('temp.calculation')) {
|
|
// ...
|
|
}
|
|
|
|
// Delete
|
|
cacheService.delete('temp.calculation')
|
|
cacheService.deleteCasual(`topic:${id}`)
|
|
```
|
|
|
|
### Shared Cache
|
|
|
|
```typescript
|
|
// Type-safe (schema key)
|
|
cacheService.setShared('window.layout', layoutConfig)
|
|
const layout = cacheService.getShared('window.layout')
|
|
|
|
// Casual (dynamic key)
|
|
cacheService.setSharedCasual<WindowState>(`window:${windowId}`, state)
|
|
const state = cacheService.getSharedCasual<WindowState>(`window:${windowId}`)
|
|
|
|
// Delete
|
|
cacheService.deleteShared('window.layout')
|
|
cacheService.deleteSharedCasual(`window:${windowId}`)
|
|
```
|
|
|
|
### Persist Cache
|
|
|
|
```typescript
|
|
// Schema keys only (no Casual methods for persist)
|
|
cacheService.setPersist('app.recent_files', recentFiles)
|
|
const files = cacheService.getPersist('app.recent_files')
|
|
|
|
// Delete
|
|
cacheService.deletePersist('app.recent_files')
|
|
```
|
|
|
|
## Main Process Usage
|
|
|
|
Main process CacheService provides SharedCache for cross-window state management.
|
|
|
|
### SharedCache in Main Process
|
|
|
|
```typescript
|
|
import { cacheService } from '@main/data/CacheService'
|
|
|
|
// Type-safe (schema key) - matches Renderer's type system
|
|
cacheService.setShared('window.layout', layoutConfig)
|
|
const layout = cacheService.getShared('window.layout')
|
|
|
|
// With TTL (30 seconds)
|
|
cacheService.setShared('temp.state', state, 30000)
|
|
|
|
// Check existence
|
|
if (cacheService.hasShared('window.layout')) {
|
|
// ...
|
|
}
|
|
|
|
// Delete
|
|
cacheService.deleteShared('window.layout')
|
|
```
|
|
|
|
**Note**: Main CacheService does NOT support Casual methods (`getSharedCasual`, etc.). Only schema-based type-safe access is available in Main process.
|
|
|
|
### Sync Strategy
|
|
|
|
- **Renderer → Main**: When Renderer calls `setShared()`, it broadcasts to Main via IPC. Main updates its SharedCache and relays to other windows.
|
|
- **Main → Renderer**: When Main calls `setShared()`, it broadcasts to all Renderer windows.
|
|
- **New Window Initialization**: New windows fetch complete SharedCache state from Main via `getAllShared()`. Uses Main-priority override strategy for conflicts.
|
|
|
|
## Type-Safe vs Casual Methods
|
|
|
|
### Type-Safe Methods
|
|
- Use predefined keys from cache schema
|
|
- Full auto-completion and type inference
|
|
- Compile-time key validation
|
|
|
|
```typescript
|
|
// Key 'ui.counter' must exist in schema
|
|
const [counter, setCounter] = useCache('ui.counter', 0)
|
|
```
|
|
|
|
### Casual Methods
|
|
- Use dynamically constructed keys
|
|
- Require manual type specification via generics
|
|
- No compile-time key validation
|
|
- **Cannot use keys that match schema patterns** (including template keys)
|
|
|
|
```typescript
|
|
// Dynamic key, must specify type
|
|
const topic = cacheService.getCasual<TopicCache>(`my.custom.key`)
|
|
|
|
// Compile error: cannot use schema keys with Casual methods
|
|
cacheService.getCasual('app.user.avatar') // Error: matches fixed key
|
|
cacheService.getCasual('scroll.position:topic-123') // Error: matches template key
|
|
```
|
|
|
|
### Template Keys
|
|
|
|
Template keys provide type-safe caching for dynamic key patterns. Define a template in the schema using `${variable}` syntax, and TypeScript will automatically match and infer types for concrete keys.
|
|
|
|
#### Defining Template Keys
|
|
|
|
```typescript
|
|
// packages/shared/data/cache/cacheSchemas.ts
|
|
export type UseCacheSchema = {
|
|
// Fixed key
|
|
'app.user.avatar': string
|
|
|
|
// Template keys - use ${variable} for dynamic segments
|
|
'scroll.position:${topicId}': number
|
|
'entity.cache:${type}:${id}': EntityData
|
|
}
|
|
|
|
// Default values for templates (shared by all instances)
|
|
export const DefaultUseCache: UseCacheSchema = {
|
|
'app.user.avatar': '',
|
|
'scroll.position:${topicId}': 0,
|
|
'entity.cache:${type}:${id}': { loaded: false }
|
|
}
|
|
```
|
|
|
|
#### Using Template Keys
|
|
|
|
```typescript
|
|
// TypeScript infers the value type from schema
|
|
const [scrollPos, setScrollPos] = useCache('scroll.position:topic-123')
|
|
// scrollPos is inferred as `number`
|
|
|
|
const [entity, setEntity] = useCache('entity.cache:user:456')
|
|
// entity is inferred as `EntityData`
|
|
|
|
// Direct CacheService usage
|
|
cacheService.set('scroll.position:my-topic', 150) // OK: value must be number
|
|
cacheService.set('scroll.position:my-topic', 'hi') // Error: type mismatch
|
|
```
|
|
|
|
#### Template Key Benefits
|
|
|
|
| Feature | Fixed Keys | Template Keys | Casual Methods |
|
|
|---------|-----------|---------------|----------------|
|
|
| Type inference | ✅ Automatic | ✅ Automatic | ❌ Manual |
|
|
| Auto-completion | ✅ Full | ✅ Partial (prefix) | ❌ None |
|
|
| Compile-time validation | ✅ Yes | ✅ Yes | ❌ No |
|
|
| Dynamic IDs | ❌ No | ✅ Yes | ✅ Yes |
|
|
| Default values | ✅ Yes | ✅ Shared per template | ❌ No |
|
|
|
|
### When to Use Which
|
|
|
|
| Scenario | Method | Example |
|
|
|----------|--------|---------|
|
|
| Fixed cache keys | Type-safe | `useCache('ui.counter')` |
|
|
| Dynamic keys with known pattern | Template key | `useCache('scroll.position:topic-123')` |
|
|
| Entity caching by ID | Template key | `get('entity.cache:user:456')` |
|
|
| Completely dynamic keys | Casual | `getCasual<T>(\`unknown.pattern:${x}\`)` |
|
|
| UI state | Type-safe | `useSharedCache('window.layout')` |
|
|
|
|
## Common Patterns
|
|
|
|
### Caching Expensive Computations
|
|
|
|
```typescript
|
|
function useExpensiveData(input: string) {
|
|
const [cached, setCached] = useCache(`computed:${input}`, null)
|
|
|
|
useEffect(() => {
|
|
if (cached === null) {
|
|
const result = expensiveComputation(input)
|
|
setCached(result)
|
|
}
|
|
}, [input, cached, setCached])
|
|
|
|
return cached
|
|
}
|
|
```
|
|
|
|
### Cross-Window Coordination
|
|
|
|
```typescript
|
|
// Window A: Update shared state
|
|
const [activeFile, setActiveFile] = useSharedCache('editor.activeFile', null)
|
|
setActiveFile(selectedFile)
|
|
|
|
// Window B: Reacts to change automatically
|
|
const [activeFile] = useSharedCache('editor.activeFile', null)
|
|
// activeFile updates when Window A changes it
|
|
```
|
|
|
|
### Recent Items with Limit
|
|
|
|
```typescript
|
|
const [recentItems, setRecentItems] = usePersistCache('app.recentItems', [])
|
|
|
|
const addRecentItem = (item: Item) => {
|
|
setRecentItems(prev => {
|
|
const filtered = prev.filter(i => i.id !== item.id)
|
|
return [item, ...filtered].slice(0, 10) // Keep last 10
|
|
})
|
|
}
|
|
```
|
|
|
|
### Cache with Expiration Check
|
|
|
|
```typescript
|
|
interface CachedData<T> {
|
|
data: T
|
|
timestamp: number
|
|
}
|
|
|
|
function useCachedWithExpiry<T>(key: string, fetcher: () => Promise<T>, maxAge: number) {
|
|
const [cached, setCached] = useCache<CachedData<T> | null>(key, null)
|
|
const [data, setData] = useState<T | null>(cached?.data ?? null)
|
|
|
|
useEffect(() => {
|
|
const isExpired = !cached || Date.now() - cached.timestamp > maxAge
|
|
|
|
if (isExpired) {
|
|
fetcher().then(result => {
|
|
setCached({ data: result, timestamp: Date.now() })
|
|
setData(result)
|
|
})
|
|
}
|
|
}, [key, maxAge])
|
|
|
|
return data
|
|
}
|
|
```
|
|
|
|
## Adding New Cache Keys
|
|
|
|
### Adding Fixed Keys
|
|
|
|
#### 1. Add to Cache Schema
|
|
|
|
```typescript
|
|
// packages/shared/data/cache/cacheSchemas.ts
|
|
export type UseCacheSchema = {
|
|
// Existing keys...
|
|
'myFeature.data': MyDataType
|
|
}
|
|
|
|
export const DefaultUseCache: UseCacheSchema = {
|
|
// Existing defaults...
|
|
'myFeature.data': { items: [], lastUpdated: 0 }
|
|
}
|
|
```
|
|
|
|
#### 2. Define Value Type (if complex)
|
|
|
|
```typescript
|
|
// packages/shared/data/cache/cacheValueTypes.ts
|
|
export interface MyDataType {
|
|
items: string[]
|
|
lastUpdated: number
|
|
}
|
|
```
|
|
|
|
#### 3. Use in Code
|
|
|
|
```typescript
|
|
// Now type-safe
|
|
const [data, setData] = useCache('myFeature.data')
|
|
```
|
|
|
|
### Adding Template Keys
|
|
|
|
#### 1. Add Template to Schema
|
|
|
|
```typescript
|
|
// packages/shared/data/cache/cacheSchemas.ts
|
|
export type UseCacheSchema = {
|
|
// Existing keys...
|
|
// Template key with dynamic segment
|
|
'scroll.position:${topicId}': number
|
|
}
|
|
|
|
export const DefaultUseCache: UseCacheSchema = {
|
|
// Existing defaults...
|
|
// Default shared by all instances of this template
|
|
'scroll.position:${topicId}': 0
|
|
}
|
|
```
|
|
|
|
#### 2. Use in Code
|
|
|
|
```typescript
|
|
// TypeScript infers number from template pattern
|
|
const [scrollPos, setScrollPos] = useCache(`scroll.position:${topicId}`)
|
|
|
|
// Works with any string in the dynamic segment
|
|
const [pos1, setPos1] = useCache('scroll.position:topic-123')
|
|
const [pos2, setPos2] = useCache('scroll.position:conversation-abc')
|
|
```
|
|
|
|
### Key Naming Convention
|
|
|
|
All keys (fixed and template) must follow the naming convention:
|
|
|
|
- **Format**: `namespace.sub.key_name` or `namespace.key:${variable}`
|
|
- **Rules**:
|
|
- Start with lowercase letter
|
|
- Use lowercase letters, numbers, and underscores
|
|
- Separate segments with dots (`.`)
|
|
- Use colons (`:`) before template placeholders
|
|
- **Examples**:
|
|
- ✅ `app.user.avatar`
|
|
- ✅ `scroll.position:${id}`
|
|
- ✅ `cache.entity:${type}:${id}`
|
|
- ❌ `UserAvatar` (no dots)
|
|
- ❌ `App.User` (uppercase)
|
|
|
|
## Shared Cache Ready State
|
|
|
|
Renderer CacheService provides ready state tracking for SharedCache initialization sync.
|
|
|
|
```typescript
|
|
import { cacheService } from '@data/CacheService'
|
|
|
|
// Check if shared cache is ready
|
|
if (cacheService.isSharedCacheReady()) {
|
|
// SharedCache has been synced from Main
|
|
}
|
|
|
|
// Register callback when ready
|
|
const unsubscribe = cacheService.onSharedCacheReady(() => {
|
|
// Called immediately if already ready, or when sync completes
|
|
console.log('SharedCache ready!')
|
|
})
|
|
|
|
// Cleanup
|
|
unsubscribe()
|
|
```
|
|
|
|
**Behavior notes**:
|
|
- `getShared()` returns `undefined` before ready (expected behavior)
|
|
- `setShared()` works immediately and broadcasts to Main (Main updates its cache)
|
|
- Hooks like `useSharedCache` work normally - they set initial values and update when sync completes
|
|
- Main-priority override: when sync completes, Main's values override local values
|
|
|
|
## Best Practices
|
|
|
|
1. **Choose the right tier**: Memory for temp, Shared for cross-window, Persist for survival
|
|
2. **Use TTL for stale data**: Prevent serving outdated cached values
|
|
3. **Prefer type-safe keys**: Add to schema when possible
|
|
4. **Use template keys for patterns**: When you have a recurring pattern (e.g., caching by ID), define a template key instead of using casual methods
|
|
5. **Reserve casual for truly dynamic keys**: Only use casual methods when the key pattern is completely unknown at development time
|
|
6. **Clean up dynamic keys**: Remove casual cache entries when no longer needed
|
|
7. **Consider data size**: Persist cache uses localStorage (limited to ~5MB)
|
|
8. **Use absolute timestamps for sync**: CacheSyncMessage uses `expireAt` (absolute Unix timestamp) for precise cross-window TTL sync
|