mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-04 13:11:56 +08:00
refactor(cache-shared): support template keys and drop sharedCasual API
Align SharedCache type system with Memory (UseCache) template support and
remove the sharedCasual escape hatch that existed only because SharedCache
could not type-check dynamic keys:
- Introduce InferSharedCacheValue and expand SharedCacheKey through
ProcessKey so template schema entries like
'web_search.provider.last_used_key.${providerId}' match concrete keys
with precise value types on both Main and Renderer.
- Extend useSharedCache hook with findMatchingSharedCacheSchemaKey /
getSharedCacheDefaultValue, mirroring useCache's template-aware default
resolution.
- Remove getSharedCasual / setSharedCasual / hasSharedCasual /
deleteSharedCasual / hasSharedTTLCasual from the Renderer CacheService
and all test mocks; Main CacheService never had them.
- Migrate BaseWebSearchProvider and OcrBaseApiClient rotation from
sharedCasual to type-safe getShared/setShared. Rename keys to conform
to schema naming rules (ESLint data-schema-key/valid-key):
web-search-provider:${id}:last_used_key
-> web_search.provider.last_used_key.${providerId}
ocr_provider:${id}:last_used_key
-> ocr.provider.last_used_key.${providerId}
Old values under legacy key names become orphans after rollout; this
is acceptable because rotation state is transient and consumers
reinitialize from keys[0] on a miss.
- Add type-level assertions for SharedCacheKey and InferSharedCacheValue
in useCache.types.test.ts to lock the contract in CI.
- Update cache-overview.md, cache-usage.md, and tests/__mocks__/README.md
to describe SharedCache's template support and reflect that casual
methods now exist only on the Memory tier.
This commit is contained in:
@@ -53,7 +53,7 @@ cacheService.set('temp.calculation', result, 30000)
|
||||
### Type Safety
|
||||
- **Fixed keys**: Schema-based keys for compile-time checking (e.g., `'app.user.avatar'`)
|
||||
- **Template keys**: Dynamic patterns with automatic type inference (e.g., `'scroll.position.${id}'` matches `'scroll.position.topic123'`)
|
||||
- **Casual methods**: For completely dynamic keys with manual typing (blocked from using schema-defined keys)
|
||||
- **Casual methods**: For completely dynamic keys with manual typing (Memory tier only; blocked from using schema-defined keys)
|
||||
|
||||
Note: Template keys follow the same dot-separated naming pattern as fixed keys. When `${xxx}` is treated as a literal string, the key must match the format: `xxx.yyy.zzz_www`
|
||||
|
||||
@@ -138,7 +138,7 @@ For detailed code examples and API usage, see [Cache Usage Guide](./cache-usage.
|
||||
| ------------ | --------------------------------- | --------------------------------- | -------------- |
|
||||
| Fixed key | `'app.user.avatar': string` | `get('app.user.avatar')` | Automatic |
|
||||
| Template key | `'scroll.position.${id}': number` | `get('scroll.position.topic123')` | Automatic |
|
||||
| Casual key | N/A | `getCasual<T>('my.custom.key')` | Manual |
|
||||
| Casual key | N/A (Memory only) | `getCasual<T>('my.custom.key')` | Manual |
|
||||
|
||||
### API Reference
|
||||
|
||||
@@ -146,6 +146,5 @@ For detailed code examples and API usage, see [Cache Usage Guide](./cache-usage.
|
||||
| ----------------------------------------------- | ------- | --------------------------------------- |
|
||||
| `useCache` / `get` / `set` | Memory | Fixed + Template keys |
|
||||
| `getCasual` / `setCasual` | Memory | Dynamic keys only (schema keys blocked) |
|
||||
| `useSharedCache` / `getShared` / `setShared` | Shared | Fixed keys only |
|
||||
| `getSharedCasual` / `setSharedCasual` | Shared | Dynamic keys only (schema keys blocked) |
|
||||
| `useSharedCache` / `getShared` / `setShared` | Shared | Fixed + Template keys |
|
||||
| `usePersistCache` / `getPersist` / `setPersist` | Persist | Fixed keys only |
|
||||
|
||||
@@ -87,17 +87,17 @@ cacheService.deleteCasual(`topic:${id}`);
|
||||
### Shared Cache
|
||||
|
||||
```typescript
|
||||
// Type-safe (schema key)
|
||||
// Fixed key (schema-defined)
|
||||
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}`);
|
||||
// Template key (schema: 'window.state.${windowId}')
|
||||
// Type is inferred automatically from the matching schema entry
|
||||
cacheService.setShared(`window.state.${windowId}` as const, state);
|
||||
const state = cacheService.getShared(`window.state.${windowId}` as const);
|
||||
|
||||
// Delete
|
||||
cacheService.deleteShared("window.layout");
|
||||
cacheService.deleteSharedCasual(`window:${windowId}`);
|
||||
```
|
||||
|
||||
### Persist Cache
|
||||
@@ -138,7 +138,7 @@ if (cacheService.hasShared("window.layout")) {
|
||||
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.
|
||||
**Note**: SharedCache supports both fixed and template keys on Main and Renderer (aligned with Memory cache). Casual (schema-bypassing) access is not supported on SharedCache — if you need a dynamic key that isn't worth schematising, use Memory `getCasual` / `setCasual` instead.
|
||||
|
||||
### Sync Strategy
|
||||
|
||||
@@ -159,8 +159,9 @@ cacheService.deleteShared("window.layout");
|
||||
const [counter, setCounter] = useCache("ui.counter", 0);
|
||||
```
|
||||
|
||||
### Casual Methods
|
||||
### Casual Methods (Memory tier only)
|
||||
|
||||
- Available only on the **Memory** tier (`getCasual` / `setCasual` / `hasCasual` / `deleteCasual` / `hasTTLCasual`). Shared and Persist tiers do not expose casual variants — all Shared access goes through the schema (fixed or template keys).
|
||||
- Use dynamically constructed keys
|
||||
- Require manual type specification via generics
|
||||
- No compile-time key validation
|
||||
@@ -175,6 +176,8 @@ cacheService.getCasual("app.user.avatar"); // Error: matches fixed key
|
||||
cacheService.getCasual("scroll.position.topic123"); // Error: matches template key
|
||||
```
|
||||
|
||||
If you need a cross-window dynamic key, define a template key in `SharedCacheSchema` and use the type-safe `getShared` / `setShared` — there is no `getSharedCasual`.
|
||||
|
||||
### 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.
|
||||
@@ -220,13 +223,14 @@ cacheService.set("scroll.position.mytopic", "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 |
|
||||
| Feature | Fixed Keys | Template Keys | Casual Methods (Memory only) |
|
||||
| ----------------------- | ------------ | ---------------------- | ---------------------------- |
|
||||
| Type inference | ✅ Automatic | ✅ Automatic | ❌ Manual |
|
||||
| Auto-completion | ✅ Full | ✅ Partial (prefix) | ❌ None |
|
||||
| Compile-time validation | ✅ Yes | ✅ Yes | ❌ No |
|
||||
| Dynamic IDs | ❌ No | ✅ Yes | ✅ Yes |
|
||||
| Cross-window (Shared) | ✅ Yes | ✅ Yes | ❌ No |
|
||||
| Default values | ✅ Yes | ✅ Shared per template | ❌ No |
|
||||
|
||||
### When to Use Which
|
||||
|
||||
|
||||
27
packages/shared/data/cache/cacheSchemas.ts
vendored
27
packages/shared/data/cache/cacheSchemas.ts
vendored
@@ -236,10 +236,15 @@ export const DefaultUseCache: UseCacheSchema = {
|
||||
*/
|
||||
export type SharedCacheSchema = {
|
||||
'chat.web_search.active_searches': CacheValueTypes.CacheActiveSearches
|
||||
// API key rotation state (cross-window, tracks last used key per provider)
|
||||
'web_search.provider.last_used_key.${providerId}': string
|
||||
'ocr.provider.last_used_key.${providerId}': string
|
||||
}
|
||||
|
||||
export const DefaultSharedCache: SharedCacheSchema = {
|
||||
'chat.web_search.active_searches': {}
|
||||
'chat.web_search.active_searches': {},
|
||||
'web_search.provider.last_used_key.${providerId}': '',
|
||||
'ocr.provider.last_used_key.${providerId}': ''
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -276,9 +281,25 @@ export const DefaultRendererPersistCache: RendererPersistCacheSchema = {
|
||||
export type RendererPersistCacheKey = keyof RendererPersistCacheSchema
|
||||
|
||||
/**
|
||||
* Key type for shared cache (fixed keys only)
|
||||
* Key type for shared cache (supports both fixed and template keys).
|
||||
*
|
||||
* Mirrors UseCacheKey: expands each schema key through ProcessKey so that
|
||||
* template keys like 'web_search.provider.last_used_key.${providerId}' match
|
||||
* any concrete instance (e.g. 'web_search.provider.last_used_key.google').
|
||||
*/
|
||||
export type SharedCacheKey = keyof SharedCacheSchema
|
||||
export type SharedCacheKey = {
|
||||
[K in keyof SharedCacheSchema]: ProcessKey<K & string>
|
||||
}[keyof SharedCacheSchema]
|
||||
|
||||
/**
|
||||
* Infers the value type for a given shared cache key from SharedCacheSchema.
|
||||
*
|
||||
* Mirrors InferUseCacheValue: resolves template instances back to the schema
|
||||
* entry that defines them, so concrete keys still get precise value types.
|
||||
*/
|
||||
export type InferSharedCacheValue<K extends string> = {
|
||||
[S in keyof SharedCacheSchema]: K extends ProcessKey<S & string> ? SharedCacheSchema[S] : never
|
||||
}[keyof SharedCacheSchema]
|
||||
|
||||
/**
|
||||
* Key type for memory cache (supports both fixed and template keys).
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { BaseService, Injectable, ServicePhase } from '@main/core/lifecycle'
|
||||
import { Phase } from '@main/core/lifecycle'
|
||||
import type { SharedCacheKey, SharedCacheSchema } from '@shared/data/cache/cacheSchemas'
|
||||
import type { InferSharedCacheValue, SharedCacheKey } from '@shared/data/cache/cacheSchemas'
|
||||
import type { CacheEntry, CacheSyncMessage } from '@shared/data/cache/cacheTypes'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { BrowserWindow } from 'electron'
|
||||
@@ -173,7 +173,7 @@ export class CacheService extends BaseService {
|
||||
* @param key - Schema-defined shared cache key
|
||||
* @returns Cached value or undefined if not found or expired
|
||||
*/
|
||||
getShared<K extends SharedCacheKey>(key: K): SharedCacheSchema[K] | undefined {
|
||||
getShared<K extends SharedCacheKey>(key: K): InferSharedCacheValue<K> | undefined {
|
||||
const entry = this.sharedCache.get(key)
|
||||
if (!entry) return undefined
|
||||
|
||||
@@ -183,7 +183,7 @@ export class CacheService extends BaseService {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return entry.value as SharedCacheSchema[K]
|
||||
return entry.value as InferSharedCacheValue<K>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -192,7 +192,7 @@ export class CacheService extends BaseService {
|
||||
* @param value - Value to cache (type inferred from schema)
|
||||
* @param ttl - Time to live in milliseconds (optional)
|
||||
*/
|
||||
setShared<K extends SharedCacheKey>(key: K, value: SharedCacheSchema[K], ttl?: number): void {
|
||||
setShared<K extends SharedCacheKey>(key: K, value: InferSharedCacheValue<K>, ttl?: number): void {
|
||||
const expireAt = ttl ? Date.now() + ttl : undefined
|
||||
const entry: CacheEntry = { value, expireAt }
|
||||
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import type {
|
||||
InferSharedCacheValue,
|
||||
InferUseCacheValue,
|
||||
RendererPersistCacheKey,
|
||||
RendererPersistCacheSchema,
|
||||
SharedCacheKey,
|
||||
SharedCacheSchema,
|
||||
UseCacheKey
|
||||
} from '@shared/data/cache/cacheSchemas'
|
||||
import { DefaultRendererPersistCache } from '@shared/data/cache/cacheSchemas'
|
||||
@@ -399,16 +399,6 @@ export class CacheService {
|
||||
return entry?.expireAt !== undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a shared cache key has TTL set (casual, dynamic key)
|
||||
* @param key - Dynamic shared cache key
|
||||
* @returns True if key has TTL configured
|
||||
*/
|
||||
hasSharedTTLCasual(key: Exclude<string, SharedCacheKey>): boolean {
|
||||
const entry = this.sharedCache.get(key)
|
||||
return entry?.expireAt !== undefined
|
||||
}
|
||||
|
||||
// ============ Shared Cache (Cross-window) ============
|
||||
|
||||
/**
|
||||
@@ -416,16 +406,7 @@ export class CacheService {
|
||||
* @param key - Schema-defined shared cache key
|
||||
* @returns Cached value or undefined if not found or expired
|
||||
*/
|
||||
getShared<K extends SharedCacheKey>(key: K): SharedCacheSchema[K] | undefined {
|
||||
return this.getSharedInternal(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from shared cache with TTL validation (casual, dynamic key)
|
||||
* @param key - Dynamic shared cache key (e.g., `window:${id}`)
|
||||
* @returns Cached value or undefined if not found or expired
|
||||
*/
|
||||
getSharedCasual<T>(key: Exclude<string, SharedCacheKey>): T | undefined {
|
||||
getShared<K extends SharedCacheKey>(key: K): InferSharedCacheValue<K> | undefined {
|
||||
return this.getSharedInternal(key)
|
||||
}
|
||||
|
||||
@@ -452,17 +433,7 @@ export class CacheService {
|
||||
* @param value - Value to cache (type inferred from schema)
|
||||
* @param ttl - Time to live in milliseconds (optional)
|
||||
*/
|
||||
setShared<K extends SharedCacheKey>(key: K, value: SharedCacheSchema[K], ttl?: number): void {
|
||||
this.setSharedInternal(key, value, ttl)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set value in shared cache with cross-window synchronization (casual, dynamic key)
|
||||
* @param key - Dynamic shared cache key (e.g., `window:${id}`)
|
||||
* @param value - Value to cache
|
||||
* @param ttl - Time to live in milliseconds (optional)
|
||||
*/
|
||||
setSharedCasual<T>(key: Exclude<string, SharedCacheKey>, value: T, ttl?: number): void {
|
||||
setShared<K extends SharedCacheKey>(key: K, value: InferSharedCacheValue<K>, ttl?: number): void {
|
||||
this.setSharedInternal(key, value, ttl)
|
||||
}
|
||||
|
||||
@@ -520,15 +491,6 @@ export class CacheService {
|
||||
return this.hasSharedInternal(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if key exists in shared cache and is not expired (casual, dynamic key)
|
||||
* @param key - Dynamic shared cache key
|
||||
* @returns True if key exists and is valid, false otherwise
|
||||
*/
|
||||
hasSharedCasual(key: Exclude<string, SharedCacheKey>): boolean {
|
||||
return this.hasSharedInternal(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal implementation for shared cache has
|
||||
*/
|
||||
@@ -555,15 +517,6 @@ export class CacheService {
|
||||
return this.deleteSharedInternal(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete from shared cache with cross-window synchronization and hook protection (casual, dynamic key)
|
||||
* @param key - Dynamic shared cache key
|
||||
* @returns True if deletion succeeded, false if key is protected by active hooks
|
||||
*/
|
||||
deleteSharedCasual(key: Exclude<string, SharedCacheKey>): boolean {
|
||||
return this.deleteSharedInternal(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal implementation for shared cache delete
|
||||
*/
|
||||
|
||||
@@ -9,9 +9,11 @@
|
||||
|
||||
import type {
|
||||
ExpandTemplateKey,
|
||||
InferSharedCacheValue,
|
||||
InferUseCacheValue,
|
||||
IsTemplateKey,
|
||||
ProcessKey,
|
||||
SharedCacheKey,
|
||||
UseCacheCasualKey,
|
||||
UseCacheKey
|
||||
} from '@shared/data/cache/cacheSchemas'
|
||||
@@ -146,4 +148,37 @@ describe('Template Key Type Utilities', () => {
|
||||
expect(isTemplate('chat.generating')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('SharedCacheKey', () => {
|
||||
it('should include fixed keys', () => {
|
||||
const key: SharedCacheKey = 'chat.web_search.active_searches'
|
||||
expect(key).toBe('chat.web_search.active_searches')
|
||||
})
|
||||
|
||||
it('should match template patterns', () => {
|
||||
const key1: SharedCacheKey = 'web_search.provider.last_used_key.google'
|
||||
const key2: SharedCacheKey = 'ocr.provider.last_used_key.tesseract'
|
||||
expect(key1).toBe('web_search.provider.last_used_key.google')
|
||||
expect(key2).toBe('ocr.provider.last_used_key.tesseract')
|
||||
})
|
||||
})
|
||||
|
||||
describe('InferSharedCacheValue', () => {
|
||||
it('should infer value type for fixed keys', () => {
|
||||
// 'chat.web_search.active_searches' -> CacheActiveSearches
|
||||
expectTypeOf<InferSharedCacheValue<'chat.web_search.active_searches'>>().toMatchTypeOf<Record<string, unknown>>()
|
||||
})
|
||||
|
||||
it('should infer value type for template key instances', () => {
|
||||
const webSearchLastKey: InferSharedCacheValue<'web_search.provider.last_used_key.google'> = 'key-1'
|
||||
const ocrLastKey: InferSharedCacheValue<'ocr.provider.last_used_key.tesseract'> = 'key-2'
|
||||
expectTypeOf(webSearchLastKey).toBeString()
|
||||
expectTypeOf(ocrLastKey).toBeString()
|
||||
})
|
||||
|
||||
it('should return never for unknown keys', () => {
|
||||
type UnknownValue = InferSharedCacheValue<'unknown.shared.key'>
|
||||
expectTypeOf<UnknownValue>().toBeNever()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { cacheService } from '@data/CacheService'
|
||||
import { loggerService } from '@logger'
|
||||
import type {
|
||||
InferSharedCacheValue,
|
||||
InferUseCacheValue,
|
||||
RendererPersistCacheKey,
|
||||
RendererPersistCacheSchema,
|
||||
@@ -139,6 +140,49 @@ function getUseCacheDefaultValue<K extends UseCacheKey>(key: K): InferUseCacheVa
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the shared schema key that matches a given concrete key.
|
||||
*
|
||||
* Mirrors findMatchingUseCacheSchemaKey but operates over SharedCacheSchema.
|
||||
* Returns the exact schema key (fixed or template pattern), not the concrete
|
||||
* instance — callers use it to look up the template's default value.
|
||||
*/
|
||||
function findMatchingSharedCacheSchemaKey(key: string): keyof SharedCacheSchema | undefined {
|
||||
if (key in DefaultSharedCache) {
|
||||
return key as keyof SharedCacheSchema
|
||||
}
|
||||
|
||||
const schemaKeys = Object.keys(DefaultSharedCache) as Array<keyof SharedCacheSchema>
|
||||
for (const schemaKey of schemaKeys) {
|
||||
if (isTemplateKey(schemaKey as string)) {
|
||||
const regex = templateToRegex(schemaKey as string)
|
||||
if (regex.test(key)) {
|
||||
return schemaKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default value for a shared cache key from the schema.
|
||||
*
|
||||
* Works with both fixed keys (direct lookup) and concrete keys that
|
||||
* match template patterns (finds template, returns its default).
|
||||
*
|
||||
* Note: template default values are shared across all instances — e.g., all
|
||||
* `web_search.provider.last_used_key.*` keys fall back to the single default
|
||||
* `''`. This mirrors getUseCacheDefaultValue semantics.
|
||||
*/
|
||||
function getSharedCacheDefaultValue<K extends SharedCacheKey>(key: K): InferSharedCacheValue<K> | undefined {
|
||||
const schemaKey = findMatchingSharedCacheSchemaKey(key)
|
||||
if (schemaKey) {
|
||||
return DefaultSharedCache[schemaKey] as InferSharedCacheValue<K>
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook for component-level memory cache
|
||||
*
|
||||
@@ -247,26 +291,34 @@ export function useCache<K extends UseCacheKey>(
|
||||
* Use this for data that needs to be shared between all app windows.
|
||||
* Data is lost when the app restarts.
|
||||
*
|
||||
* @param key - Cache key from the predefined schema
|
||||
* Supports both fixed keys and template keys (aligned with useCache):
|
||||
* - Fixed keys: `useSharedCache('chat.web_search.active_searches')`
|
||||
* - Template keys: `useSharedCache('web_search.provider.last_used_key.google')`
|
||||
* matches schema entry `'web_search.provider.last_used_key.${providerId}'`
|
||||
*
|
||||
* Template-instance defaults are shared across all matching instances (inherited
|
||||
* from useCache semantics) — the schema default is written to cache on hook mount.
|
||||
*
|
||||
* @param key - Cache key from the predefined schema (fixed or matching template)
|
||||
* @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')
|
||||
* // Fixed key
|
||||
* const [active, setActive] = useSharedCache('chat.web_search.active_searches')
|
||||
*
|
||||
* // With custom initial value
|
||||
* const [sharedState, setSharedState] = useSharedCache('app.state', { loaded: false })
|
||||
* // Template key (schema: 'web_search.provider.last_used_key.${providerId}')
|
||||
* const [lastKey, setLastKey] = useSharedCache('web_search.provider.last_used_key.google')
|
||||
*
|
||||
* // Changes automatically sync to all open windows
|
||||
* setWindowCount(3)
|
||||
* setLastKey('api-key-1')
|
||||
* ```
|
||||
*/
|
||||
export function useSharedCache<K extends SharedCacheKey>(
|
||||
key: K,
|
||||
initValue?: SharedCacheSchema[K]
|
||||
): [SharedCacheSchema[K], (value: SharedCacheSchema[K]) => void] {
|
||||
initValue?: InferSharedCacheValue<K>
|
||||
): [InferSharedCacheValue<K>, (value: InferSharedCacheValue<K>) => void] {
|
||||
/**
|
||||
* Subscribe to shared cache changes using React's useSyncExternalStore
|
||||
* This ensures the component re-renders when the shared cache value changes
|
||||
@@ -278,8 +330,13 @@ export function useSharedCache<K extends SharedCacheKey>(
|
||||
)
|
||||
|
||||
/**
|
||||
* Initialize shared cache with default value if it doesn't exist
|
||||
* Priority: existing shared cache value > custom initValue > schema default
|
||||
* Initialize shared cache with default value if it doesn't exist.
|
||||
* Priority: existing shared cache value > custom initValue > schema default.
|
||||
*
|
||||
* Template-instance defaults fall through getSharedCacheDefaultValue, which
|
||||
* resolves the concrete key back to its schema template and returns the
|
||||
* shared default (e.g. all 'web_search.provider.last_used_key.*' instances
|
||||
* share the single default '').
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (cacheService.hasShared(key)) {
|
||||
@@ -287,7 +344,10 @@ export function useSharedCache<K extends SharedCacheKey>(
|
||||
}
|
||||
|
||||
if (initValue === undefined) {
|
||||
cacheService.setShared(key, DefaultSharedCache[key])
|
||||
const defaultValue = getSharedCacheDefaultValue(key)
|
||||
if (defaultValue !== undefined) {
|
||||
cacheService.setShared(key, defaultValue)
|
||||
}
|
||||
} else {
|
||||
cacheService.setShared(key, initValue)
|
||||
}
|
||||
@@ -320,13 +380,13 @@ export function useSharedCache<K extends SharedCacheKey>(
|
||||
* @param newValue - New value to store in shared cache
|
||||
*/
|
||||
const setValue = useCallback(
|
||||
(newValue: SharedCacheSchema[K]) => {
|
||||
(newValue: InferSharedCacheValue<K>) => {
|
||||
cacheService.setShared(key, newValue)
|
||||
},
|
||||
[key]
|
||||
)
|
||||
|
||||
return [value ?? initValue ?? DefaultSharedCache[key], setValue]
|
||||
return [value ?? initValue ?? (getSharedCacheDefaultValue(key) as InferSharedCacheValue<K>), setValue]
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,22 +33,22 @@ export default abstract class BaseWebSearchProvider {
|
||||
|
||||
public getApiKey() {
|
||||
const keys = this.provider.apiKey?.split(',').map((key) => key.trim()) || []
|
||||
const keyName = `web-search-provider:${this.provider.id}:last_used_key`
|
||||
const keyName = `web_search.provider.last_used_key.${this.provider.id}` as const
|
||||
|
||||
if (keys.length === 1) {
|
||||
return keys[0]
|
||||
}
|
||||
|
||||
const lastUsedKey = cacheService.getSharedCasual<string>(keyName)
|
||||
const lastUsedKey = cacheService.getShared(keyName)
|
||||
if (lastUsedKey === undefined) {
|
||||
cacheService.setSharedCasual(keyName, keys[0])
|
||||
cacheService.setShared(keyName, keys[0])
|
||||
return keys[0]
|
||||
}
|
||||
|
||||
const currentIndex = keys.indexOf(lastUsedKey)
|
||||
const nextIndex = (currentIndex + 1) % keys.length
|
||||
const nextKey = keys[nextIndex]
|
||||
cacheService.setSharedCasual(keyName, nextKey)
|
||||
cacheService.setShared(keyName, nextKey)
|
||||
|
||||
return nextKey
|
||||
}
|
||||
|
||||
@@ -21,22 +21,22 @@ export abstract class OcrBaseApiClient {
|
||||
// copy from BaseApiClient
|
||||
public getApiKey() {
|
||||
const keys = this.provider.config.api.apiKey.split(',').map((key) => key.trim())
|
||||
const keyName = `ocr_provider:${this.provider.id}:last_used_key`
|
||||
const keyName = `ocr.provider.last_used_key.${this.provider.id}` as const
|
||||
|
||||
if (keys.length === 1) {
|
||||
return keys[0]
|
||||
}
|
||||
|
||||
const lastUsedKey = cacheService.getSharedCasual<string>(keyName)
|
||||
const lastUsedKey = cacheService.getShared(keyName)
|
||||
if (lastUsedKey === undefined) {
|
||||
cacheService.setSharedCasual(keyName, keys[0])
|
||||
cacheService.setShared(keyName, keys[0])
|
||||
return keys[0]
|
||||
}
|
||||
|
||||
const currentIndex = keys.indexOf(lastUsedKey)
|
||||
const nextIndex = (currentIndex + 1) % keys.length
|
||||
const nextKey = keys[nextIndex]
|
||||
cacheService.setSharedCasual(keyName, nextKey)
|
||||
cacheService.setShared(keyName, nextKey)
|
||||
|
||||
return nextKey
|
||||
}
|
||||
|
||||
@@ -62,13 +62,13 @@ import { MockMainCacheServiceUtils } from '@test-mocks/main/CacheService'
|
||||
|
||||
### CacheService
|
||||
|
||||
Three-tier cache system with type-safe and casual (dynamic key) methods.
|
||||
Three-tier cache system with type-safe methods (and casual/dynamic key methods on the Memory tier only).
|
||||
|
||||
#### Methods
|
||||
|
||||
| Category | Method | Signature |
|
||||
|----------|--------|-----------|
|
||||
| Memory (typed) | `get` | `<K>(key: K) => UseCacheSchema[K]` |
|
||||
| Memory (typed) | `get` | `<K>(key: K) => InferUseCacheValue<K>` |
|
||||
| Memory (typed) | `set` | `<K>(key: K, value, ttl?) => void` |
|
||||
| Memory (typed) | `has` | `<K>(key: K) => boolean` |
|
||||
| Memory (typed) | `delete` | `<K>(key: K) => boolean` |
|
||||
@@ -78,16 +78,11 @@ Three-tier cache system with type-safe and casual (dynamic key) methods.
|
||||
| Memory (casual) | `hasCasual` | `(key: string) => boolean` |
|
||||
| Memory (casual) | `deleteCasual` | `(key: string) => boolean` |
|
||||
| Memory (casual) | `hasTTLCasual` | `(key: string) => boolean` |
|
||||
| Shared (typed) | `getShared` | `<K>(key: K) => SharedCacheSchema[K]` |
|
||||
| Shared (typed) | `getShared` | `<K>(key: K) => InferSharedCacheValue<K>` |
|
||||
| Shared (typed) | `setShared` | `<K>(key: K, value, ttl?) => void` |
|
||||
| Shared (typed) | `hasShared` | `<K>(key: K) => boolean` |
|
||||
| Shared (typed) | `deleteShared` | `<K>(key: K) => boolean` |
|
||||
| Shared (typed) | `hasSharedTTL` | `<K>(key: K) => boolean` |
|
||||
| Shared (casual) | `getSharedCasual` | `<T>(key: string) => T \| undefined` |
|
||||
| Shared (casual) | `setSharedCasual` | `<T>(key, value, ttl?) => void` |
|
||||
| Shared (casual) | `hasSharedCasual` | `(key: string) => boolean` |
|
||||
| Shared (casual) | `deleteSharedCasual` | `(key: string) => boolean` |
|
||||
| Shared (casual) | `hasSharedTTLCasual` | `(key: string) => boolean` |
|
||||
| Persist | `getPersist` | `<K>(key: K) => RendererPersistCacheSchema[K]` |
|
||||
| Persist | `setPersist` | `<K>(key: K, value) => void` |
|
||||
| Persist | `hasPersist` | `(key) => boolean` |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SharedCacheKey, SharedCacheSchema } from '@shared/data/cache/cacheSchemas'
|
||||
import type { InferSharedCacheValue, SharedCacheKey } from '@shared/data/cache/cacheSchemas'
|
||||
import type { CacheEntry, CacheSyncMessage } from '@shared/data/cache/cacheTypes'
|
||||
import { vi } from 'vitest'
|
||||
|
||||
@@ -78,7 +78,7 @@ export class MockMainCacheService {
|
||||
|
||||
// ============ Shared Cache Methods ============
|
||||
|
||||
public getShared = vi.fn(<K extends SharedCacheKey>(key: K): SharedCacheSchema[K] | undefined => {
|
||||
public getShared = vi.fn(<K extends SharedCacheKey>(key: K): InferSharedCacheValue<K> | undefined => {
|
||||
const entry = mockSharedCache.get(key)
|
||||
if (!entry) return undefined
|
||||
|
||||
@@ -88,10 +88,10 @@ export class MockMainCacheService {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return entry.value as SharedCacheSchema[K]
|
||||
return entry.value as InferSharedCacheValue<K>
|
||||
})
|
||||
|
||||
public setShared = vi.fn(<K extends SharedCacheKey>(key: K, value: SharedCacheSchema[K], ttl?: number): void => {
|
||||
public setShared = vi.fn(<K extends SharedCacheKey>(key: K, value: InferSharedCacheValue<K>, ttl?: number): void => {
|
||||
const entry: CacheEntry = {
|
||||
value,
|
||||
expireAt: ttl ? Date.now() + ttl : undefined
|
||||
@@ -240,7 +240,7 @@ export const MockMainCacheServiceUtils = {
|
||||
/**
|
||||
* Set shared cache value for testing
|
||||
*/
|
||||
setSharedCacheValue: <K extends SharedCacheKey>(key: K, value: SharedCacheSchema[K], ttl?: number) => {
|
||||
setSharedCacheValue: <K extends SharedCacheKey>(key: K, value: InferSharedCacheValue<K>, ttl?: number) => {
|
||||
const entry: CacheEntry = {
|
||||
value,
|
||||
expireAt: ttl ? Date.now() + ttl : undefined
|
||||
@@ -251,7 +251,7 @@ export const MockMainCacheServiceUtils = {
|
||||
/**
|
||||
* Get shared cache value for testing
|
||||
*/
|
||||
getSharedCacheValue: <K extends SharedCacheKey>(key: K): SharedCacheSchema[K] | undefined => {
|
||||
getSharedCacheValue: <K extends SharedCacheKey>(key: K): InferSharedCacheValue<K> | undefined => {
|
||||
const entry = mockSharedCache.get(key)
|
||||
if (!entry) return undefined
|
||||
|
||||
@@ -261,7 +261,7 @@ export const MockMainCacheServiceUtils = {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return entry.value as SharedCacheSchema[K]
|
||||
return entry.value as InferSharedCacheValue<K>
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type {
|
||||
InferSharedCacheValue,
|
||||
InferUseCacheValue,
|
||||
RendererPersistCacheKey,
|
||||
RendererPersistCacheSchema,
|
||||
UseCacheKey,
|
||||
InferUseCacheValue,
|
||||
SharedCacheKey,
|
||||
SharedCacheSchema
|
||||
SharedCacheSchema,
|
||||
UseCacheKey
|
||||
} from '@shared/data/cache/cacheSchemas'
|
||||
import { DefaultRendererPersistCache, DefaultSharedCache } from '@shared/data/cache/cacheSchemas'
|
||||
import type { CacheEntry, CacheSubscriber } from '@shared/data/cache/cacheTypes'
|
||||
@@ -176,20 +177,23 @@ export const createMockCacheService = (
|
||||
|
||||
// ============ Shared Cache (Type-safe) ============
|
||||
|
||||
getShared: vi.fn(<K extends SharedCacheKey>(key: K): SharedCacheSchema[K] | undefined => {
|
||||
getShared: vi.fn(<K extends SharedCacheKey>(key: K): InferSharedCacheValue<K> | undefined => {
|
||||
const entry = sharedCache.get(key)
|
||||
// For fixed schema keys, fall back to the schema default. Template
|
||||
// instances miss this lookup at runtime and return undefined.
|
||||
const fallback = DefaultSharedCache[key as keyof SharedCacheSchema] as InferSharedCacheValue<K> | undefined
|
||||
if (entry === undefined) {
|
||||
return DefaultSharedCache[key]
|
||||
return fallback
|
||||
}
|
||||
if (isExpired(entry)) {
|
||||
sharedCache.delete(key)
|
||||
notifySubscribers(key)
|
||||
return DefaultSharedCache[key]
|
||||
return fallback
|
||||
}
|
||||
return entry.value
|
||||
return entry.value as InferSharedCacheValue<K>
|
||||
}),
|
||||
|
||||
setShared: vi.fn(<K extends SharedCacheKey>(key: K, value: SharedCacheSchema[K], ttl?: number): void => {
|
||||
setShared: vi.fn(<K extends SharedCacheKey>(key: K, value: InferSharedCacheValue<K>, ttl?: number): void => {
|
||||
const entry: CacheEntry = {
|
||||
value,
|
||||
expireAt: ttl ? Date.now() + ttl : undefined
|
||||
@@ -229,61 +233,6 @@ export const createMockCacheService = (
|
||||
return entry?.expireAt !== undefined
|
||||
}),
|
||||
|
||||
// ============ Shared Cache (Casual - Dynamic Keys) ============
|
||||
|
||||
getSharedCasual: vi.fn(<T>(key: string): T | undefined => {
|
||||
const entry = sharedCache.get(key)
|
||||
if (entry === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (isExpired(entry)) {
|
||||
sharedCache.delete(key)
|
||||
notifySubscribers(key)
|
||||
return undefined
|
||||
}
|
||||
return entry.value as T
|
||||
}),
|
||||
|
||||
setSharedCasual: vi.fn(<T>(key: string, value: T, ttl?: number): void => {
|
||||
const entry: CacheEntry = {
|
||||
value,
|
||||
expireAt: ttl ? Date.now() + ttl : undefined
|
||||
}
|
||||
sharedCache.set(key, entry)
|
||||
notifySubscribers(key)
|
||||
}),
|
||||
|
||||
hasSharedCasual: vi.fn((key: string): boolean => {
|
||||
const entry = sharedCache.get(key)
|
||||
if (entry === undefined) {
|
||||
return false
|
||||
}
|
||||
if (isExpired(entry)) {
|
||||
sharedCache.delete(key)
|
||||
notifySubscribers(key)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}),
|
||||
|
||||
deleteSharedCasual: vi.fn((key: string): boolean => {
|
||||
if (activeHookCounts.get(key)) {
|
||||
console.error(`Cannot delete key "${key}" as it's being used by useSharedCache hook`)
|
||||
return false
|
||||
}
|
||||
const existed = sharedCache.has(key)
|
||||
sharedCache.delete(key)
|
||||
if (existed) {
|
||||
notifySubscribers(key)
|
||||
}
|
||||
return true
|
||||
}),
|
||||
|
||||
hasSharedTTLCasual: vi.fn((key: string): boolean => {
|
||||
const entry = sharedCache.get(key)
|
||||
return entry?.expireAt !== undefined
|
||||
}),
|
||||
|
||||
// ============ Persist Cache ============
|
||||
|
||||
getPersist: vi.fn(<K extends RendererPersistCacheKey>(key: K): RendererPersistCacheSchema[K] => {
|
||||
@@ -401,6 +350,17 @@ export const createMockCacheService = (
|
||||
sharedCacheReadyCallbacks.forEach((cb) => cb())
|
||||
sharedCacheReadyCallbacks.length = 0
|
||||
}
|
||||
},
|
||||
|
||||
// Test scaffold: seed a shared cache entry with an arbitrary string key,
|
||||
// bypassing SharedCacheKey type-safety. Used by setInitialState.
|
||||
_setSharedEntry: (key: string, value: unknown, ttl?: number) => {
|
||||
const entry: CacheEntry = {
|
||||
value,
|
||||
expireAt: ttl ? Date.now() + ttl : undefined
|
||||
}
|
||||
sharedCache.set(key, entry)
|
||||
notifySubscribers(key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,12 +420,12 @@ export const MockCacheService = {
|
||||
}
|
||||
|
||||
// ============ Shared Cache (Type-safe) ============
|
||||
getShared<K extends SharedCacheKey>(key: K): SharedCacheSchema[K] | undefined {
|
||||
return mockCacheService.getShared(key)
|
||||
getShared<K extends SharedCacheKey>(key: K): InferSharedCacheValue<K> | undefined {
|
||||
return mockCacheService.getShared(key) as InferSharedCacheValue<K> | undefined
|
||||
}
|
||||
|
||||
setShared<K extends SharedCacheKey>(key: K, value: SharedCacheSchema[K], ttl?: number): void {
|
||||
return mockCacheService.setShared(key, value, ttl)
|
||||
setShared<K extends SharedCacheKey>(key: K, value: InferSharedCacheValue<K>, ttl?: number): void {
|
||||
return mockCacheService.setShared(key, value as never, ttl)
|
||||
}
|
||||
|
||||
hasShared<K extends SharedCacheKey>(key: K): boolean {
|
||||
@@ -480,27 +440,6 @@ export const MockCacheService = {
|
||||
return mockCacheService.hasSharedTTL(key)
|
||||
}
|
||||
|
||||
// ============ Shared Cache (Casual) ============
|
||||
getSharedCasual<T>(key: string): T | undefined {
|
||||
return mockCacheService.getSharedCasual(key) as T | undefined
|
||||
}
|
||||
|
||||
setSharedCasual<T>(key: string, value: T, ttl?: number): void {
|
||||
return mockCacheService.setSharedCasual(key, value, ttl)
|
||||
}
|
||||
|
||||
hasSharedCasual(key: string): boolean {
|
||||
return mockCacheService.hasSharedCasual(key)
|
||||
}
|
||||
|
||||
deleteSharedCasual(key: string): boolean {
|
||||
return mockCacheService.deleteSharedCasual(key)
|
||||
}
|
||||
|
||||
hasSharedTTLCasual(key: string): boolean {
|
||||
return mockCacheService.hasSharedTTLCasual(key)
|
||||
}
|
||||
|
||||
// ============ Persist Cache ============
|
||||
getPersist<K extends RendererPersistCacheKey>(key: K): RendererPersistCacheSchema[K] {
|
||||
return mockCacheService.getPersist(key)
|
||||
@@ -581,7 +520,7 @@ export const MockCacheUtils = {
|
||||
mockCacheService.setCasual(key, value, ttl)
|
||||
})
|
||||
state.shared?.forEach(([key, value, ttl]) => {
|
||||
mockCacheService.setSharedCasual(key, value, ttl)
|
||||
mockCacheService._setSharedEntry(key, value, ttl)
|
||||
})
|
||||
state.persist?.forEach(([key, value]) => {
|
||||
mockCacheService.setPersist(key, value)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type {
|
||||
InferSharedCacheValue,
|
||||
InferUseCacheValue,
|
||||
RendererPersistCacheKey,
|
||||
RendererPersistCacheSchema,
|
||||
UseCacheKey,
|
||||
UseCacheSchema,
|
||||
InferUseCacheValue,
|
||||
SharedCacheKey,
|
||||
SharedCacheSchema
|
||||
SharedCacheSchema,
|
||||
UseCacheKey,
|
||||
UseCacheSchema
|
||||
} from '@shared/data/cache/cacheSchemas'
|
||||
import { DefaultRendererPersistCache, DefaultUseCache, DefaultSharedCache } from '@shared/data/cache/cacheSchemas'
|
||||
import { vi } from 'vitest'
|
||||
@@ -164,19 +165,21 @@ export const mockUseCache = vi.fn(
|
||||
export const mockUseSharedCache = vi.fn(
|
||||
<K extends SharedCacheKey>(
|
||||
key: K,
|
||||
initValue?: SharedCacheSchema[K]
|
||||
): [SharedCacheSchema[K], (value: SharedCacheSchema[K]) => void] => {
|
||||
initValue?: InferSharedCacheValue<K>
|
||||
): [InferSharedCacheValue<K>, (value: InferSharedCacheValue<K>) => void] => {
|
||||
// Get current value
|
||||
let currentValue = mockSharedCache.get(key)
|
||||
if (currentValue === undefined) {
|
||||
currentValue = initValue ?? DefaultSharedCache[key]
|
||||
// Fixed keys look up in DefaultSharedCache; template instances return undefined.
|
||||
const schemaDefault = DefaultSharedCache[key as keyof SharedCacheSchema]
|
||||
currentValue = initValue ?? schemaDefault
|
||||
if (currentValue !== undefined) {
|
||||
mockSharedCache.set(key, currentValue)
|
||||
}
|
||||
}
|
||||
|
||||
// Mock setValue function
|
||||
const setValue = vi.fn((value: SharedCacheSchema[K]) => {
|
||||
const setValue = vi.fn((value: InferSharedCacheValue<K>) => {
|
||||
mockSharedCache.set(key, value)
|
||||
notifySharedSubscribers(key)
|
||||
})
|
||||
@@ -274,7 +277,7 @@ export const MockUseCacheUtils = {
|
||||
/**
|
||||
* Set shared cache value for testing
|
||||
*/
|
||||
setSharedCacheValue: <K extends SharedCacheKey>(key: K, value: SharedCacheSchema[K]) => {
|
||||
setSharedCacheValue: <K extends SharedCacheKey>(key: K, value: InferSharedCacheValue<K>) => {
|
||||
mockSharedCache.set(key, value)
|
||||
notifySharedSubscribers(key)
|
||||
},
|
||||
@@ -282,8 +285,8 @@ export const MockUseCacheUtils = {
|
||||
/**
|
||||
* Get shared cache value
|
||||
*/
|
||||
getSharedCacheValue: <K extends SharedCacheKey>(key: K): SharedCacheSchema[K] => {
|
||||
return mockSharedCache.get(key) ?? DefaultSharedCache[key]
|
||||
getSharedCacheValue: <K extends SharedCacheKey>(key: K): InferSharedCacheValue<K> => {
|
||||
return mockSharedCache.get(key) ?? DefaultSharedCache[key as keyof SharedCacheSchema]
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -366,18 +369,19 @@ export const MockUseCacheUtils = {
|
||||
*/
|
||||
mockSharedCacheReturn: <K extends SharedCacheKey>(
|
||||
key: K,
|
||||
value: SharedCacheSchema[K],
|
||||
setValue?: (value: SharedCacheSchema[K]) => void
|
||||
value: InferSharedCacheValue<K>,
|
||||
setValue?: (value: InferSharedCacheValue<K>) => void
|
||||
) => {
|
||||
mockUseSharedCache.mockImplementation((cacheKey, initValue) => {
|
||||
mockUseSharedCache.mockImplementation(((cacheKey: K, initValue: InferSharedCacheValue<K> | undefined) => {
|
||||
if (cacheKey === key) {
|
||||
return [value, setValue || vi.fn()]
|
||||
}
|
||||
|
||||
// Default behavior for other keys
|
||||
const defaultValue = mockSharedCache.get(cacheKey) ?? initValue ?? DefaultSharedCache[cacheKey]
|
||||
const defaultValue =
|
||||
mockSharedCache.get(cacheKey) ?? initValue ?? DefaultSharedCache[cacheKey as keyof SharedCacheSchema]
|
||||
return [defaultValue, vi.fn()]
|
||||
})
|
||||
}) as never)
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user