mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-05 13:47:59 +08:00
useCache, useSharedCache and usePersistCache setters now accept a React-style functional updater `(prev) => next` in addition to a concrete value. The updater resolves `prev` from the latest stored value at write time rather than the render-time snapshot, making read-modify-write correct across an `await` — the root cause of the keep-alive overwrite race behind #16460. `prev` is typed shallow-readonly (`ReadonlyValue<T>`), so mutating it in place and returning the same reference — which the CacheService `isEqual` short-circuit would otherwise swallow silently — is a compile error. Concrete-value calls are unchanged, so existing consumers keep compiling. The renderer useCache mock mirrors the functional branch with the same default fallback; docs and hook tests updated. Consumer call-site adoption lands separately.
499 lines
15 KiB
TypeScript
499 lines
15 KiB
TypeScript
import type {
|
|
InferSharedCacheValue,
|
|
InferUseCacheValue,
|
|
RendererPersistCacheKey,
|
|
RendererPersistCacheSchema,
|
|
SharedCacheKey,
|
|
SharedCacheSchema,
|
|
UseCacheKey,
|
|
UseCacheSchema
|
|
} from '@shared/data/cache/cacheSchemas'
|
|
import { DefaultRendererPersistCache, DefaultUseCache, DefaultSharedCache } from '@shared/data/cache/cacheSchemas'
|
|
import { vi } from 'vitest'
|
|
|
|
/**
|
|
* Mock useCache hooks for testing
|
|
* Provides comprehensive mocks for all cache management hooks
|
|
*/
|
|
|
|
/**
|
|
* Setter input mirroring production's `CacheSetStateAction` (value or updater).
|
|
* Lightweight: `prev` is plain (not readonly) — the mock only needs to resolve
|
|
* functional updaters so functional-update call sites run under the mock.
|
|
*/
|
|
type SetAction<V> = V | ((prev: V) => V)
|
|
|
|
// Mock cache state storage (using string for memory cache to support template keys)
|
|
const mockMemoryCache = new Map<string, any>()
|
|
const mockSharedCache = new Map<SharedCacheKey, any>()
|
|
const mockPersistCache = new Map<RendererPersistCacheKey, any>()
|
|
|
|
// Initialize caches with defaults
|
|
Object.entries(DefaultUseCache).forEach(([key, value]) => {
|
|
mockMemoryCache.set(key, value)
|
|
})
|
|
|
|
Object.entries(DefaultSharedCache).forEach(([key, value]) => {
|
|
mockSharedCache.set(key as SharedCacheKey, value)
|
|
})
|
|
|
|
Object.entries(DefaultRendererPersistCache).forEach(([key, value]) => {
|
|
mockPersistCache.set(key as RendererPersistCacheKey, value)
|
|
})
|
|
|
|
// Mock subscribers for cache changes (using string for memory to support template keys)
|
|
const mockMemorySubscribers = new Map<string, Set<() => void>>()
|
|
const mockSharedSubscribers = new Map<SharedCacheKey, Set<() => void>>()
|
|
const mockPersistSubscribers = new Map<RendererPersistCacheKey, Set<() => void>>()
|
|
|
|
// Helper functions to notify subscribers
|
|
const notifyMemorySubscribers = (key: string) => {
|
|
const subscribers = mockMemorySubscribers.get(key)
|
|
if (subscribers) {
|
|
subscribers.forEach((callback) => {
|
|
try {
|
|
callback()
|
|
} catch (error) {
|
|
console.warn('Mock useCache: Memory subscriber callback error:', error)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
const notifySharedSubscribers = (key: SharedCacheKey) => {
|
|
const subscribers = mockSharedSubscribers.get(key)
|
|
if (subscribers) {
|
|
subscribers.forEach((callback) => {
|
|
try {
|
|
callback()
|
|
} catch (error) {
|
|
console.warn('Mock useCache: Shared subscriber callback error:', error)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
const notifyPersistSubscribers = (key: RendererPersistCacheKey) => {
|
|
const subscribers = mockPersistSubscribers.get(key)
|
|
if (subscribers) {
|
|
subscribers.forEach((callback) => {
|
|
try {
|
|
callback()
|
|
} catch (error) {
|
|
console.warn('Mock useCache: Persist subscriber callback error:', error)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ============ Template Key Utilities ============
|
|
|
|
/**
|
|
* Checks if a schema key is a template key (contains ${...} placeholder).
|
|
*/
|
|
const isTemplateKey = (key: string): boolean => {
|
|
return key.includes('${') && key.includes('}')
|
|
}
|
|
|
|
/**
|
|
* Converts a template key pattern into a RegExp for matching concrete keys.
|
|
*/
|
|
const templateToRegex = (template: string): RegExp => {
|
|
const escaped = template.replace(/[.*+?^${}()|[\]\\]/g, (match) => {
|
|
if (match === '$' || match === '{' || match === '}') {
|
|
return match
|
|
}
|
|
return '\\' + match
|
|
})
|
|
const pattern = escaped.replace(/\$\{[^}]+\}/g, '([\\w\\-]+)')
|
|
return new RegExp(`^${pattern}$`)
|
|
}
|
|
|
|
/**
|
|
* Finds the schema key that matches a given concrete key.
|
|
*/
|
|
const findMatchingSchemaKey = (key: string): keyof UseCacheSchema | undefined => {
|
|
if (key in DefaultUseCache) {
|
|
return key as keyof UseCacheSchema
|
|
}
|
|
const schemaKeys = Object.keys(DefaultUseCache) as Array<keyof UseCacheSchema>
|
|
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 cache key from the schema.
|
|
*/
|
|
const getDefaultValue = <K extends UseCacheKey>(key: K): InferUseCacheValue<K> | undefined => {
|
|
const schemaKey = findMatchingSchemaKey(key)
|
|
if (schemaKey) {
|
|
return DefaultUseCache[schemaKey] as InferUseCacheValue<K>
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
/**
|
|
* Mock useCache hook (memory cache)
|
|
*/
|
|
export const mockUseCache = vi.fn(
|
|
<K extends UseCacheKey>(
|
|
key: K,
|
|
initValue?: InferUseCacheValue<K>
|
|
): [InferUseCacheValue<K>, (value: SetAction<InferUseCacheValue<K>>) => void] => {
|
|
// Get current value
|
|
let currentValue = mockMemoryCache.get(key)
|
|
if (currentValue === undefined) {
|
|
currentValue = initValue ?? getDefaultValue(key)
|
|
if (currentValue !== undefined) {
|
|
mockMemoryCache.set(key, currentValue)
|
|
}
|
|
}
|
|
|
|
// Mock setValue function (mirrors production: resolves functional updaters
|
|
// against the latest stored value with the same default fallback)
|
|
const setValue = vi.fn((value: SetAction<InferUseCacheValue<K>>) => {
|
|
if (typeof value === 'function') {
|
|
const prev = (mockMemoryCache.get(key) ?? getDefaultValue(key)) as InferUseCacheValue<K>
|
|
mockMemoryCache.set(key, value(prev))
|
|
} else {
|
|
mockMemoryCache.set(key, value)
|
|
}
|
|
notifyMemorySubscribers(key)
|
|
})
|
|
|
|
return [currentValue, setValue]
|
|
}
|
|
)
|
|
|
|
/**
|
|
* Mock useSharedCache hook (shared cache)
|
|
*/
|
|
export const mockUseSharedCache = vi.fn(
|
|
<K extends SharedCacheKey>(
|
|
key: K,
|
|
initValue?: InferSharedCacheValue<K>
|
|
): [InferSharedCacheValue<K>, (value: SetAction<InferSharedCacheValue<K>>) => void] => {
|
|
// Get current value
|
|
let currentValue = mockSharedCache.get(key)
|
|
if (currentValue === undefined) {
|
|
// 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 (mirrors production functional-updater handling)
|
|
const setValue = vi.fn((value: SetAction<InferSharedCacheValue<K>>) => {
|
|
if (typeof value === 'function') {
|
|
const prev = (mockSharedCache.get(key) ??
|
|
DefaultSharedCache[key as keyof SharedCacheSchema]) as InferSharedCacheValue<K>
|
|
mockSharedCache.set(key, value(prev))
|
|
} else {
|
|
mockSharedCache.set(key, value)
|
|
}
|
|
notifySharedSubscribers(key)
|
|
})
|
|
|
|
return [currentValue, setValue]
|
|
}
|
|
)
|
|
|
|
/**
|
|
* Mock usePersistCache hook (persistent cache)
|
|
*/
|
|
export const mockUsePersistCache = vi.fn(
|
|
<K extends RendererPersistCacheKey>(
|
|
key: K,
|
|
initValue?: RendererPersistCacheSchema[K]
|
|
): [RendererPersistCacheSchema[K], (value: SetAction<RendererPersistCacheSchema[K]>) => void] => {
|
|
// Get current value
|
|
let currentValue = mockPersistCache.get(key)
|
|
if (currentValue === undefined) {
|
|
currentValue = initValue ?? DefaultRendererPersistCache[key]
|
|
if (currentValue !== undefined) {
|
|
mockPersistCache.set(key, currentValue)
|
|
}
|
|
}
|
|
|
|
// Mock setValue function (mirrors production functional-updater handling)
|
|
const setValue = vi.fn((value: SetAction<RendererPersistCacheSchema[K]>) => {
|
|
if (typeof value === 'function') {
|
|
const prev = (mockPersistCache.get(key) ?? DefaultRendererPersistCache[key]) as RendererPersistCacheSchema[K]
|
|
mockPersistCache.set(key, value(prev))
|
|
} else {
|
|
mockPersistCache.set(key, value)
|
|
}
|
|
notifyPersistSubscribers(key)
|
|
})
|
|
|
|
return [currentValue, setValue]
|
|
}
|
|
)
|
|
|
|
/**
|
|
* Export all mocks as a unified module
|
|
*/
|
|
export const MockUseCache = {
|
|
useCache: mockUseCache,
|
|
useSharedCache: mockUseSharedCache,
|
|
usePersistCache: mockUsePersistCache
|
|
}
|
|
|
|
/**
|
|
* Utility functions for testing
|
|
*/
|
|
export const MockUseCacheUtils = {
|
|
/**
|
|
* Reset all hook mock call counts and state
|
|
*/
|
|
resetMocks: () => {
|
|
mockUseCache.mockClear()
|
|
mockUseSharedCache.mockClear()
|
|
mockUsePersistCache.mockClear()
|
|
|
|
// Reset caches to defaults
|
|
mockMemoryCache.clear()
|
|
mockSharedCache.clear()
|
|
mockPersistCache.clear()
|
|
|
|
Object.entries(DefaultUseCache).forEach(([key, value]) => {
|
|
mockMemoryCache.set(key, value)
|
|
})
|
|
|
|
Object.entries(DefaultSharedCache).forEach(([key, value]) => {
|
|
mockSharedCache.set(key as SharedCacheKey, value)
|
|
})
|
|
|
|
Object.entries(DefaultRendererPersistCache).forEach(([key, value]) => {
|
|
mockPersistCache.set(key as RendererPersistCacheKey, value)
|
|
})
|
|
|
|
// Clear subscribers
|
|
mockMemorySubscribers.clear()
|
|
mockSharedSubscribers.clear()
|
|
mockPersistSubscribers.clear()
|
|
},
|
|
|
|
/**
|
|
* Set cache value for testing (memory cache)
|
|
*/
|
|
setCacheValue: <K extends UseCacheKey>(key: K, value: InferUseCacheValue<K>) => {
|
|
mockMemoryCache.set(key, value)
|
|
notifyMemorySubscribers(key)
|
|
},
|
|
|
|
/**
|
|
* Get cache value (memory cache)
|
|
*/
|
|
getCacheValue: <K extends UseCacheKey>(key: K): InferUseCacheValue<K> | undefined => {
|
|
return mockMemoryCache.get(key) ?? getDefaultValue(key)
|
|
},
|
|
|
|
/**
|
|
* Set shared cache value for testing
|
|
*/
|
|
setSharedCacheValue: <K extends SharedCacheKey>(key: K, value: InferSharedCacheValue<K>) => {
|
|
mockSharedCache.set(key, value)
|
|
notifySharedSubscribers(key)
|
|
},
|
|
|
|
/**
|
|
* Get shared cache value
|
|
*/
|
|
getSharedCacheValue: <K extends SharedCacheKey>(key: K): InferSharedCacheValue<K> => {
|
|
return mockSharedCache.get(key) ?? DefaultSharedCache[key as keyof SharedCacheSchema]
|
|
},
|
|
|
|
/**
|
|
* Set persist cache value for testing
|
|
*/
|
|
setPersistCacheValue: <K extends RendererPersistCacheKey>(key: K, value: RendererPersistCacheSchema[K]) => {
|
|
mockPersistCache.set(key, value)
|
|
notifyPersistSubscribers(key)
|
|
},
|
|
|
|
/**
|
|
* Get persist cache value
|
|
*/
|
|
getPersistCacheValue: <K extends RendererPersistCacheKey>(key: K): RendererPersistCacheSchema[K] => {
|
|
return mockPersistCache.get(key) ?? DefaultRendererPersistCache[key]
|
|
},
|
|
|
|
/**
|
|
* Set multiple cache values at once
|
|
*/
|
|
setMultipleCacheValues: (values: {
|
|
memory?: Array<[UseCacheKey, any]>
|
|
shared?: Array<[SharedCacheKey, any]>
|
|
persist?: Array<[RendererPersistCacheKey, any]>
|
|
}) => {
|
|
values.memory?.forEach(([key, value]) => {
|
|
mockMemoryCache.set(key, value)
|
|
notifyMemorySubscribers(key)
|
|
})
|
|
|
|
values.shared?.forEach(([key, value]) => {
|
|
mockSharedCache.set(key, value)
|
|
notifySharedSubscribers(key)
|
|
})
|
|
|
|
values.persist?.forEach(([key, value]) => {
|
|
mockPersistCache.set(key, value)
|
|
notifyPersistSubscribers(key)
|
|
})
|
|
},
|
|
|
|
/**
|
|
* Get all cache values
|
|
*/
|
|
getAllCacheValues: () => ({
|
|
memory: Object.fromEntries(mockMemoryCache.entries()),
|
|
shared: Object.fromEntries(mockSharedCache.entries()),
|
|
persist: Object.fromEntries(mockPersistCache.entries())
|
|
}),
|
|
|
|
/**
|
|
* Simulate cache change from external source
|
|
*/
|
|
simulateExternalCacheChange: <K extends UseCacheKey>(key: K, value: InferUseCacheValue<K>) => {
|
|
mockMemoryCache.set(key, value)
|
|
notifyMemorySubscribers(key)
|
|
},
|
|
|
|
/**
|
|
* Mock cache hook to return specific value for a key
|
|
*/
|
|
mockCacheReturn: <K extends UseCacheKey>(
|
|
key: K,
|
|
value: InferUseCacheValue<K>,
|
|
setValue?: (value: InferUseCacheValue<K>) => void
|
|
) => {
|
|
mockUseCache.mockImplementation((cacheKey, initValue) => {
|
|
if (cacheKey === key) {
|
|
return [value, setValue || vi.fn()] as any
|
|
}
|
|
|
|
// Default behavior for other keys
|
|
const defaultValue = mockMemoryCache.get(cacheKey) ?? initValue ?? getDefaultValue(cacheKey)
|
|
return [defaultValue, vi.fn()] as any
|
|
})
|
|
},
|
|
|
|
/**
|
|
* Mock shared cache hook to return specific value for a key
|
|
*/
|
|
mockSharedCacheReturn: <K extends SharedCacheKey>(
|
|
key: K,
|
|
value: InferSharedCacheValue<K>,
|
|
setValue?: (value: InferSharedCacheValue<K>) => void
|
|
) => {
|
|
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 as keyof SharedCacheSchema]
|
|
return [defaultValue, vi.fn()]
|
|
}) as never)
|
|
},
|
|
|
|
/**
|
|
* Mock persist cache hook to return specific value for a key
|
|
*/
|
|
mockPersistCacheReturn: <K extends RendererPersistCacheKey>(
|
|
key: K,
|
|
value: RendererPersistCacheSchema[K],
|
|
setValue?: (value: RendererPersistCacheSchema[K]) => void
|
|
) => {
|
|
mockUsePersistCache.mockImplementation((cacheKey, initValue) => {
|
|
if (cacheKey === key) {
|
|
return [value, setValue || vi.fn()]
|
|
}
|
|
|
|
// Default behavior for other keys
|
|
const defaultValue = mockPersistCache.get(cacheKey) ?? initValue ?? DefaultRendererPersistCache[cacheKey]
|
|
return [defaultValue, vi.fn()]
|
|
})
|
|
},
|
|
|
|
/**
|
|
* Add subscriber for cache changes (for testing subscription behavior)
|
|
*/
|
|
addMemorySubscriber: (key: UseCacheKey, callback: () => void): (() => void) => {
|
|
if (!mockMemorySubscribers.has(key)) {
|
|
mockMemorySubscribers.set(key, new Set())
|
|
}
|
|
mockMemorySubscribers.get(key)!.add(callback)
|
|
|
|
return () => {
|
|
const subscribers = mockMemorySubscribers.get(key)
|
|
if (subscribers) {
|
|
subscribers.delete(callback)
|
|
if (subscribers.size === 0) {
|
|
mockMemorySubscribers.delete(key)
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Add subscriber for shared cache changes
|
|
*/
|
|
addSharedSubscriber: (key: SharedCacheKey, callback: () => void): (() => void) => {
|
|
if (!mockSharedSubscribers.has(key)) {
|
|
mockSharedSubscribers.set(key, new Set())
|
|
}
|
|
mockSharedSubscribers.get(key)!.add(callback)
|
|
|
|
return () => {
|
|
const subscribers = mockSharedSubscribers.get(key)
|
|
if (subscribers) {
|
|
subscribers.delete(callback)
|
|
if (subscribers.size === 0) {
|
|
mockSharedSubscribers.delete(key)
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Add subscriber for persist cache changes
|
|
*/
|
|
addPersistSubscriber: (key: RendererPersistCacheKey, callback: () => void): (() => void) => {
|
|
if (!mockPersistSubscribers.has(key)) {
|
|
mockPersistSubscribers.set(key, new Set())
|
|
}
|
|
mockPersistSubscribers.get(key)!.add(callback)
|
|
|
|
return () => {
|
|
const subscribers = mockPersistSubscribers.get(key)
|
|
if (subscribers) {
|
|
subscribers.delete(callback)
|
|
if (subscribers.size === 0) {
|
|
mockPersistSubscribers.delete(key)
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get subscriber counts for debugging
|
|
*/
|
|
getSubscriberCounts: () => ({
|
|
memory: Array.from(mockMemorySubscribers.entries()).map(([key, subs]) => [key, subs.size]),
|
|
shared: Array.from(mockSharedSubscribers.entries()).map(([key, subs]) => [key, subs.size]),
|
|
persist: Array.from(mockPersistSubscribers.entries()).map(([key, subs]) => [key, subs.size])
|
|
})
|
|
}
|