mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-05 13:47:59 +08:00
Move all renderer source from src/renderer/src/* up one level to
src/renderer/*, removing the redundant nested src directory.
- Update path aliases (@renderer, @types, @logger, @data) and TanStack
Router paths in electron.vite.config.ts; update tsconfig.{json,web,node}
path mappings and include globs.
- Fix Vite root-relative script paths in the 8 renderer HTML entries.
- Update cross-process relative imports in main/preload (language,
apiServer models, preload index) to drop the /src segment.
- Switch renderer test imports of the logger mock to the @test-mocks alias.
- Update hardcoded renderer paths in scripts and their fixtures, lint
configs (eslint/oxlint/biome), CODEOWNERS, docs, and the data-classify tool.
- Convert deep (../../+) relative imports within the renderer to the
@renderer alias (69 files, 108 imports); keep single-level relatives.
- Fix doc links broken by the move and correct one pre-existing broken
link in naming-conventions.md.
562 lines
16 KiB
TypeScript
562 lines
16 KiB
TypeScript
import type {
|
|
InferSharedCacheValue,
|
|
InferUseCacheValue,
|
|
RendererPersistCacheKey,
|
|
RendererPersistCacheSchema,
|
|
SharedCacheKey,
|
|
SharedCacheSchema,
|
|
UseCacheKey
|
|
} from '@shared/data/cache/cacheSchemas'
|
|
import { DefaultRendererPersistCache, DefaultSharedCache } from '@shared/data/cache/cacheSchemas'
|
|
import type { CacheEntry, CacheSubscriber } from '@shared/data/cache/cacheTypes'
|
|
import { vi } from 'vitest'
|
|
|
|
/**
|
|
* Mock CacheService for testing
|
|
* Provides a comprehensive mock of the three-layer cache system
|
|
* Matches the actual CacheService interface from src/renderer/data/CacheService.ts
|
|
*/
|
|
|
|
/**
|
|
* Create a mock CacheService with realistic behavior
|
|
*/
|
|
export const createMockCacheService = (
|
|
options: {
|
|
initialMemoryCache?: Map<string, CacheEntry>
|
|
initialSharedCache?: Map<string, CacheEntry>
|
|
initialPersistCache?: Map<RendererPersistCacheKey, any>
|
|
} = {}
|
|
) => {
|
|
// Mock cache storage with CacheEntry structure (includes TTL support)
|
|
const memoryCache = new Map<string, CacheEntry>(options.initialMemoryCache || [])
|
|
const sharedCache = new Map<string, CacheEntry>(options.initialSharedCache || [])
|
|
const persistCache = new Map<RendererPersistCacheKey, any>(options.initialPersistCache || [])
|
|
|
|
// Active hooks tracking
|
|
const activeHookCounts = new Map<string, number>()
|
|
|
|
// Mock subscribers
|
|
const subscribers = new Map<string, Set<CacheSubscriber>>()
|
|
|
|
// Shared cache ready state
|
|
let sharedCacheReady = true
|
|
const sharedCacheReadyCallbacks: Array<() => void> = []
|
|
|
|
// Helper function to check TTL expiration
|
|
const isExpired = (entry: CacheEntry): boolean => {
|
|
if (entry.expireAt && Date.now() > entry.expireAt) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Helper function to notify subscribers
|
|
const notifySubscribers = (key: string) => {
|
|
const keySubscribers = subscribers.get(key)
|
|
if (keySubscribers) {
|
|
keySubscribers.forEach((callback) => {
|
|
try {
|
|
callback()
|
|
} catch (error) {
|
|
console.warn('Mock CacheService: Subscriber callback error:', error)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
const mockCacheService = {
|
|
// ============ Memory Cache (Type-safe) ============
|
|
|
|
get: vi.fn(<K extends UseCacheKey>(key: K): InferUseCacheValue<K> | undefined => {
|
|
const entry = memoryCache.get(key)
|
|
if (entry === undefined) {
|
|
return undefined
|
|
}
|
|
if (isExpired(entry)) {
|
|
memoryCache.delete(key)
|
|
notifySubscribers(key)
|
|
return undefined
|
|
}
|
|
return entry.value as InferUseCacheValue<K>
|
|
}),
|
|
|
|
set: vi.fn(<K extends UseCacheKey>(key: K, value: InferUseCacheValue<K>, ttl?: number): void => {
|
|
const entry: CacheEntry = {
|
|
value,
|
|
expireAt: ttl ? Date.now() + ttl : undefined
|
|
}
|
|
memoryCache.set(key, entry)
|
|
notifySubscribers(key)
|
|
}),
|
|
|
|
has: vi.fn(<K extends UseCacheKey>(key: K): boolean => {
|
|
const entry = memoryCache.get(key)
|
|
if (entry === undefined) {
|
|
return false
|
|
}
|
|
if (isExpired(entry)) {
|
|
memoryCache.delete(key)
|
|
notifySubscribers(key)
|
|
return false
|
|
}
|
|
return true
|
|
}),
|
|
|
|
delete: vi.fn(<K extends UseCacheKey>(key: K): boolean => {
|
|
if (activeHookCounts.get(key)) {
|
|
console.error(`Cannot delete key "${key}" as it's being used by useCache hook`)
|
|
return false
|
|
}
|
|
const existed = memoryCache.has(key)
|
|
memoryCache.delete(key)
|
|
if (existed) {
|
|
notifySubscribers(key)
|
|
}
|
|
return true
|
|
}),
|
|
|
|
hasTTL: vi.fn(<K extends UseCacheKey>(key: K): boolean => {
|
|
const entry = memoryCache.get(key)
|
|
return entry?.expireAt !== undefined
|
|
}),
|
|
|
|
// ============ Memory Cache (Casual - Dynamic Keys) ============
|
|
|
|
getCasual: vi.fn(<T>(key: string): T | undefined => {
|
|
const entry = memoryCache.get(key)
|
|
if (entry === undefined) {
|
|
return undefined
|
|
}
|
|
if (isExpired(entry)) {
|
|
memoryCache.delete(key)
|
|
notifySubscribers(key)
|
|
return undefined
|
|
}
|
|
return entry.value as T
|
|
}),
|
|
|
|
setCasual: vi.fn(<T>(key: string, value: T, ttl?: number): void => {
|
|
const entry: CacheEntry = {
|
|
value,
|
|
expireAt: ttl ? Date.now() + ttl : undefined
|
|
}
|
|
memoryCache.set(key, entry)
|
|
notifySubscribers(key)
|
|
}),
|
|
|
|
hasCasual: vi.fn((key: string): boolean => {
|
|
const entry = memoryCache.get(key)
|
|
if (entry === undefined) {
|
|
return false
|
|
}
|
|
if (isExpired(entry)) {
|
|
memoryCache.delete(key)
|
|
notifySubscribers(key)
|
|
return false
|
|
}
|
|
return true
|
|
}),
|
|
|
|
deleteCasual: vi.fn((key: string): boolean => {
|
|
if (activeHookCounts.get(key)) {
|
|
console.error(`Cannot delete key "${key}" as it's being used by useCache hook`)
|
|
return false
|
|
}
|
|
const existed = memoryCache.has(key)
|
|
memoryCache.delete(key)
|
|
if (existed) {
|
|
notifySubscribers(key)
|
|
}
|
|
return true
|
|
}),
|
|
|
|
hasTTLCasual: vi.fn((key: string): boolean => {
|
|
const entry = memoryCache.get(key)
|
|
return entry?.expireAt !== undefined
|
|
}),
|
|
|
|
// ============ Shared Cache (Type-safe) ============
|
|
|
|
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 fallback
|
|
}
|
|
if (isExpired(entry)) {
|
|
sharedCache.delete(key)
|
|
notifySubscribers(key)
|
|
return fallback
|
|
}
|
|
return entry.value as InferSharedCacheValue<K>
|
|
}),
|
|
|
|
setShared: vi.fn(<K extends SharedCacheKey>(key: K, value: InferSharedCacheValue<K>, ttl?: number): void => {
|
|
const entry: CacheEntry = {
|
|
value,
|
|
expireAt: ttl ? Date.now() + ttl : undefined
|
|
}
|
|
sharedCache.set(key, entry)
|
|
notifySubscribers(key)
|
|
}),
|
|
|
|
hasShared: vi.fn(<K extends SharedCacheKey>(key: K): boolean => {
|
|
const entry = sharedCache.get(key)
|
|
if (entry === undefined) {
|
|
return false
|
|
}
|
|
if (isExpired(entry)) {
|
|
sharedCache.delete(key)
|
|
notifySubscribers(key)
|
|
return false
|
|
}
|
|
return true
|
|
}),
|
|
|
|
deleteShared: vi.fn(<K extends SharedCacheKey>(key: K): 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
|
|
}),
|
|
|
|
hasSharedTTL: vi.fn(<K extends SharedCacheKey>(key: K): boolean => {
|
|
const entry = sharedCache.get(key)
|
|
return entry?.expireAt !== undefined
|
|
}),
|
|
|
|
// ============ Persist Cache ============
|
|
|
|
getPersist: vi.fn(<K extends RendererPersistCacheKey>(key: K): RendererPersistCacheSchema[K] => {
|
|
if (persistCache.has(key)) {
|
|
return persistCache.get(key) as RendererPersistCacheSchema[K]
|
|
}
|
|
return DefaultRendererPersistCache[key]
|
|
}),
|
|
|
|
setPersist: vi.fn(<K extends RendererPersistCacheKey>(key: K, value: RendererPersistCacheSchema[K]): void => {
|
|
persistCache.set(key, value)
|
|
notifySubscribers(key)
|
|
}),
|
|
|
|
hasPersist: vi.fn((key: RendererPersistCacheKey): boolean => {
|
|
return persistCache.has(key)
|
|
}),
|
|
|
|
// ============ Hook Reference Management ============
|
|
|
|
registerHook: vi.fn((key: string): void => {
|
|
const currentCount = activeHookCounts.get(key) ?? 0
|
|
activeHookCounts.set(key, currentCount + 1)
|
|
}),
|
|
|
|
unregisterHook: vi.fn((key: string): void => {
|
|
const currentCount = activeHookCounts.get(key)
|
|
if (!currentCount) {
|
|
return
|
|
}
|
|
if (currentCount === 1) {
|
|
activeHookCounts.delete(key)
|
|
return
|
|
}
|
|
activeHookCounts.set(key, currentCount - 1)
|
|
}),
|
|
|
|
// ============ Shared Cache Ready State ============
|
|
|
|
isSharedCacheReady: vi.fn((): boolean => {
|
|
return sharedCacheReady
|
|
}),
|
|
|
|
onSharedCacheReady: vi.fn((callback: () => void): (() => void) => {
|
|
if (sharedCacheReady) {
|
|
callback()
|
|
return () => {}
|
|
}
|
|
sharedCacheReadyCallbacks.push(callback)
|
|
return () => {
|
|
const idx = sharedCacheReadyCallbacks.indexOf(callback)
|
|
if (idx >= 0) {
|
|
sharedCacheReadyCallbacks.splice(idx, 1)
|
|
}
|
|
}
|
|
}),
|
|
|
|
// ============ Subscription Management ============
|
|
|
|
subscribe: vi.fn((key: string, callback: CacheSubscriber): (() => void) => {
|
|
if (!subscribers.has(key)) {
|
|
subscribers.set(key, new Set())
|
|
}
|
|
subscribers.get(key)!.add(callback)
|
|
|
|
// Return unsubscribe function
|
|
return () => {
|
|
const keySubscribers = subscribers.get(key)
|
|
if (keySubscribers) {
|
|
keySubscribers.delete(callback)
|
|
if (keySubscribers.size === 0) {
|
|
subscribers.delete(key)
|
|
}
|
|
}
|
|
}
|
|
}),
|
|
|
|
notifySubscribers: vi.fn((key: string): void => {
|
|
notifySubscribers(key)
|
|
}),
|
|
|
|
// ============ Lifecycle ============
|
|
|
|
cleanup: vi.fn((): void => {
|
|
memoryCache.clear()
|
|
sharedCache.clear()
|
|
persistCache.clear()
|
|
activeHookCounts.clear()
|
|
subscribers.clear()
|
|
}),
|
|
|
|
// ============ Internal State Access for Testing ============
|
|
|
|
_getMockState: () => ({
|
|
memoryCache: new Map(memoryCache),
|
|
sharedCache: new Map(sharedCache),
|
|
persistCache: new Map(persistCache),
|
|
activeHookCounts: new Map(activeHookCounts),
|
|
subscribers: new Map(subscribers),
|
|
sharedCacheReady
|
|
}),
|
|
|
|
_resetMockState: () => {
|
|
memoryCache.clear()
|
|
sharedCache.clear()
|
|
persistCache.clear()
|
|
activeHookCounts.clear()
|
|
subscribers.clear()
|
|
sharedCacheReady = true
|
|
},
|
|
|
|
_setSharedCacheReady: (ready: boolean) => {
|
|
sharedCacheReady = ready
|
|
if (ready) {
|
|
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)
|
|
}
|
|
}
|
|
|
|
return mockCacheService
|
|
}
|
|
|
|
// Default mock instance
|
|
export const mockCacheService = createMockCacheService()
|
|
|
|
// Singleton instance mock
|
|
export const MockCacheService = {
|
|
CacheService: class MockCacheService {
|
|
static getInstance() {
|
|
return mockCacheService
|
|
}
|
|
|
|
// ============ Memory Cache (Type-safe) ============
|
|
get<K extends UseCacheKey>(key: K): InferUseCacheValue<K> | undefined {
|
|
return mockCacheService.get(key) as unknown as InferUseCacheValue<K> | undefined
|
|
}
|
|
|
|
set<K extends UseCacheKey>(key: K, value: InferUseCacheValue<K>, ttl?: number): void {
|
|
mockCacheService.set(key, value as unknown as InferUseCacheValue<UseCacheKey>, ttl)
|
|
}
|
|
|
|
has<K extends UseCacheKey>(key: K): boolean {
|
|
return mockCacheService.has(key)
|
|
}
|
|
|
|
delete<K extends UseCacheKey>(key: K): boolean {
|
|
return mockCacheService.delete(key)
|
|
}
|
|
|
|
hasTTL<K extends UseCacheKey>(key: K): boolean {
|
|
return mockCacheService.hasTTL(key)
|
|
}
|
|
|
|
// ============ Memory Cache (Casual) ============
|
|
getCasual<T>(key: string): T | undefined {
|
|
return mockCacheService.getCasual(key) as T | undefined
|
|
}
|
|
|
|
setCasual<T>(key: string, value: T, ttl?: number): void {
|
|
return mockCacheService.setCasual(key, value, ttl)
|
|
}
|
|
|
|
hasCasual(key: string): boolean {
|
|
return mockCacheService.hasCasual(key)
|
|
}
|
|
|
|
deleteCasual(key: string): boolean {
|
|
return mockCacheService.deleteCasual(key)
|
|
}
|
|
|
|
hasTTLCasual(key: string): boolean {
|
|
return mockCacheService.hasTTLCasual(key)
|
|
}
|
|
|
|
// ============ Shared Cache (Type-safe) ============
|
|
getShared<K extends SharedCacheKey>(key: K): InferSharedCacheValue<K> | undefined {
|
|
return mockCacheService.getShared(key) as InferSharedCacheValue<K> | undefined
|
|
}
|
|
|
|
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 {
|
|
return mockCacheService.hasShared(key)
|
|
}
|
|
|
|
deleteShared<K extends SharedCacheKey>(key: K): boolean {
|
|
return mockCacheService.deleteShared(key)
|
|
}
|
|
|
|
hasSharedTTL<K extends SharedCacheKey>(key: K): boolean {
|
|
return mockCacheService.hasSharedTTL(key)
|
|
}
|
|
|
|
// ============ Persist Cache ============
|
|
getPersist<K extends RendererPersistCacheKey>(key: K): RendererPersistCacheSchema[K] {
|
|
return mockCacheService.getPersist(key)
|
|
}
|
|
|
|
setPersist<K extends RendererPersistCacheKey>(key: K, value: RendererPersistCacheSchema[K]): void {
|
|
return mockCacheService.setPersist(key, value)
|
|
}
|
|
|
|
hasPersist(key: RendererPersistCacheKey): boolean {
|
|
return mockCacheService.hasPersist(key)
|
|
}
|
|
|
|
// ============ Hook Reference Management ============
|
|
registerHook(key: string): void {
|
|
return mockCacheService.registerHook(key)
|
|
}
|
|
|
|
unregisterHook(key: string): void {
|
|
return mockCacheService.unregisterHook(key)
|
|
}
|
|
|
|
// ============ Ready State ============
|
|
isSharedCacheReady(): boolean {
|
|
return mockCacheService.isSharedCacheReady()
|
|
}
|
|
|
|
onSharedCacheReady(callback: () => void): () => void {
|
|
return mockCacheService.onSharedCacheReady(callback)
|
|
}
|
|
|
|
// ============ Subscription ============
|
|
subscribe(key: string, callback: CacheSubscriber): () => void {
|
|
return mockCacheService.subscribe(key, callback)
|
|
}
|
|
|
|
notifySubscribers(key: string): void {
|
|
return mockCacheService.notifySubscribers(key)
|
|
}
|
|
|
|
// ============ Lifecycle ============
|
|
cleanup(): void {
|
|
return mockCacheService.cleanup()
|
|
}
|
|
},
|
|
cacheService: mockCacheService
|
|
}
|
|
|
|
/**
|
|
* Utility functions for testing
|
|
*/
|
|
export const MockCacheUtils = {
|
|
/**
|
|
* Reset all mock function call counts and state
|
|
*/
|
|
resetMocks: () => {
|
|
Object.values(mockCacheService).forEach((method) => {
|
|
if (vi.isMockFunction(method)) {
|
|
method.mockClear()
|
|
}
|
|
})
|
|
if ('_resetMockState' in mockCacheService) {
|
|
mockCacheService._resetMockState()
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Set initial cache state for testing
|
|
*/
|
|
setInitialState: (state: {
|
|
memory?: Array<[string, any, number?]> // [key, value, ttl?]
|
|
shared?: Array<[string, any, number?]>
|
|
persist?: Array<[RendererPersistCacheKey, any]>
|
|
}) => {
|
|
mockCacheService._resetMockState()
|
|
|
|
state.memory?.forEach(([key, value, ttl]) => {
|
|
mockCacheService.setCasual(key, value, ttl)
|
|
})
|
|
state.shared?.forEach(([key, value, ttl]) => {
|
|
mockCacheService._setSharedEntry(key, value, ttl)
|
|
})
|
|
state.persist?.forEach(([key, value]) => {
|
|
mockCacheService.setPersist(key, value)
|
|
})
|
|
},
|
|
|
|
/**
|
|
* Get current mock state for inspection
|
|
*/
|
|
getCurrentState: () => {
|
|
return mockCacheService._getMockState()
|
|
},
|
|
|
|
/**
|
|
* Simulate cache events for testing subscribers
|
|
*/
|
|
triggerCacheChange: (key: string, value: any, ttl?: number) => {
|
|
mockCacheService.setCasual(key, value, ttl)
|
|
},
|
|
|
|
/**
|
|
* Set shared cache ready state for testing
|
|
*/
|
|
setSharedCacheReady: (ready: boolean) => {
|
|
mockCacheService._setSharedCacheReady(ready)
|
|
},
|
|
|
|
/**
|
|
* Simulate TTL expiration by manipulating cache entries
|
|
*/
|
|
simulateTTLExpiration: (key: string) => {
|
|
const state = mockCacheService._getMockState()
|
|
const entry = state.memoryCache.get(key) || state.sharedCache.get(key)
|
|
if (entry) {
|
|
entry.expireAt = Date.now() - 1000 // Set to expired
|
|
}
|
|
}
|
|
}
|