mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-07 13:59:28 +08:00
fix(cache): enforce dot-separated naming for template keys
- Update template key pattern to use dots instead of colons
(e.g., 'scroll.position.${id}' not 'scroll.position:${id}')
- Template keys follow same naming convention as fixed keys
- Add example template keys to schema for testing
- Add comprehensive type tests for template key inference
- Update mock files to support template key types
- Update documentation with correct template key examples
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
fb51df99d0
commit
2093452e69
@ -52,9 +52,11 @@ 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:topic-123'`)
|
||||
- **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)
|
||||
|
||||
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`
|
||||
|
||||
## Data Categories
|
||||
|
||||
### Performance Cache (Memory tier)
|
||||
@ -126,8 +128,8 @@ For detailed code examples and API usage, see [Cache Usage Guide](./cache-usage.
|
||||
| Type | Example Schema | Example Usage | Type Inference |
|
||||
|------|----------------|---------------|----------------|
|
||||
| Fixed key | `'app.user.avatar': string` | `get('app.user.avatar')` | Automatic |
|
||||
| Template key | `'scroll.position:${id}': number` | `get('scroll.position:topic-123')` | Automatic |
|
||||
| Casual key | N/A | `getCasual<T>('my.key')` | Manual |
|
||||
| Template key | `'scroll.position.${id}': number` | `get('scroll.position.topic123')` | Automatic |
|
||||
| Casual key | N/A | `getCasual<T>('my.custom.key')` | Manual |
|
||||
|
||||
### API Reference
|
||||
|
||||
|
||||
@ -163,13 +163,15 @@ 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
|
||||
cacheService.getCasual('scroll.position.topic123') // 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.
|
||||
|
||||
**Important**: 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`
|
||||
|
||||
#### Defining Template Keys
|
||||
|
||||
```typescript
|
||||
@ -179,15 +181,16 @@ export type UseCacheSchema = {
|
||||
'app.user.avatar': string
|
||||
|
||||
// Template keys - use ${variable} for dynamic segments
|
||||
'scroll.position:${topicId}': number
|
||||
'entity.cache:${type}:${id}': EntityData
|
||||
// Must follow dot-separated pattern like fixed keys
|
||||
'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 }
|
||||
'scroll.position.${topicId}': 0,
|
||||
'entity.cache.${type}_${id}': { loaded: false }
|
||||
}
|
||||
```
|
||||
|
||||
@ -195,15 +198,15 @@ export const DefaultUseCache: UseCacheSchema = {
|
||||
|
||||
```typescript
|
||||
// TypeScript infers the value type from schema
|
||||
const [scrollPos, setScrollPos] = useCache('scroll.position:topic-123')
|
||||
const [scrollPos, setScrollPos] = useCache('scroll.position.topic123')
|
||||
// scrollPos is inferred as `number`
|
||||
|
||||
const [entity, setEntity] = useCache('entity.cache:user:456')
|
||||
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
|
||||
cacheService.set('scroll.position.mytopic', 150) // OK: value must be number
|
||||
cacheService.set('scroll.position.mytopic', 'hi') // Error: type mismatch
|
||||
```
|
||||
|
||||
#### Template Key Benefits
|
||||
@ -221,9 +224,9 @@ cacheService.set('scroll.position:my-topic', 'hi') // Error: type mismatch
|
||||
| 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}\`)` |
|
||||
| Dynamic keys with known pattern | Template key | `useCache('scroll.position.topic123')` |
|
||||
| Entity caching by ID | Template key | `get('entity.cache.user_456')` |
|
||||
| Completely dynamic keys | Casual | `getCasual<T>(\`custom.dynamic.${x}\`)` |
|
||||
| UI state | Type-safe | `useSharedCache('window.layout')` |
|
||||
|
||||
## Common Patterns
|
||||
@ -342,13 +345,13 @@ const [data, setData] = useCache('myFeature.data')
|
||||
export type UseCacheSchema = {
|
||||
// Existing keys...
|
||||
// Template key with dynamic segment
|
||||
'scroll.position:${topicId}': number
|
||||
'scroll.position.${topicId}': number
|
||||
}
|
||||
|
||||
export const DefaultUseCache: UseCacheSchema = {
|
||||
// Existing defaults...
|
||||
// Default shared by all instances of this template
|
||||
'scroll.position:${topicId}': 0
|
||||
'scroll.position.${topicId}': 0
|
||||
}
|
||||
```
|
||||
|
||||
@ -356,27 +359,28 @@ export const DefaultUseCache: UseCacheSchema = {
|
||||
|
||||
```typescript
|
||||
// TypeScript infers number from template pattern
|
||||
const [scrollPos, setScrollPos] = useCache(`scroll.position:${topicId}`)
|
||||
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')
|
||||
const [pos1, setPos1] = useCache('scroll.position.topic123')
|
||||
const [pos2, setPos2] = useCache('scroll.position.conversationabc')
|
||||
```
|
||||
|
||||
### Key Naming Convention
|
||||
|
||||
All keys (fixed and template) must follow the naming convention:
|
||||
All keys (fixed and template) must follow the same naming convention:
|
||||
|
||||
- **Format**: `namespace.sub.key_name` or `namespace.key:${variable}`
|
||||
- **Format**: `namespace.sub.key_name` (template `${xxx}` treated as a literal string segment)
|
||||
- **Rules**:
|
||||
- Start with lowercase letter
|
||||
- Use lowercase letters, numbers, and underscores
|
||||
- Separate segments with dots (`.`)
|
||||
- Use colons (`:`) before template placeholders
|
||||
- Template placeholders `${xxx}` are treated as literal string segments
|
||||
- **Examples**:
|
||||
- ✅ `app.user.avatar`
|
||||
- ✅ `scroll.position:${id}`
|
||||
- ✅ `cache.entity:${type}:${id}`
|
||||
- ✅ `scroll.position.${id}`
|
||||
- ✅ `entity.cache.${type}_${id}`
|
||||
- ❌ `scroll.position:${id}` (colon not allowed)
|
||||
- ❌ `UserAvatar` (no dots)
|
||||
- ❌ `App.User` (uppercase)
|
||||
|
||||
|
||||
@ -174,7 +174,9 @@ export default defineConfig([
|
||||
// Schema key naming convention (cache & preferences)
|
||||
// Supports both fixed keys and template keys:
|
||||
// - Fixed: 'app.user.avatar', 'chat.multi_select_mode'
|
||||
// - Template: 'scroll.position:${topicId}', 'cache:${type}:${id}'
|
||||
// - Template: 'scroll.position.${topicId}', 'entity.cache.${type}_${id}'
|
||||
// Template keys must follow the same dot-separated pattern as fixed keys.
|
||||
// When ${xxx} placeholders are treated as literal strings, the key must match: xxx.yyy.zzz_www
|
||||
{
|
||||
files: ['packages/shared/data/cache/cacheSchemas.ts', 'packages/shared/data/preference/preferenceSchemas.ts'],
|
||||
plugins: {
|
||||
@ -185,12 +187,12 @@ export default defineConfig([
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description:
|
||||
'Enforce schema key naming convention: namespace.sub.key_name or namespace.key:${variable}',
|
||||
'Enforce schema key naming convention: namespace.sub.key_name (template placeholders treated as literal strings)',
|
||||
recommended: true
|
||||
},
|
||||
messages: {
|
||||
invalidKey:
|
||||
'Schema key "{{key}}" must follow format: namespace.sub.key_name (e.g., app.user.avatar) or with template: namespace.key:${variable} (e.g., scroll.position:${id}).',
|
||||
'Schema key "{{key}}" must follow format: namespace.sub.key_name (e.g., app.user.avatar, scroll.position.${id}). Template ${xxx} is treated as a literal string segment.',
|
||||
invalidTemplateVar:
|
||||
'Template variable in "{{key}}" must be a valid identifier (e.g., ${id}, ${topicId}).'
|
||||
}
|
||||
@ -199,17 +201,14 @@ export default defineConfig([
|
||||
/**
|
||||
* Validates a schema key for correct naming convention.
|
||||
*
|
||||
* Supports two formats:
|
||||
* 1. Fixed keys: lowercase segments separated by dots
|
||||
* Example: 'app.user.avatar', 'chat.multi_select_mode'
|
||||
* Both fixed keys and template keys must follow the same pattern:
|
||||
* - Lowercase segments separated by dots
|
||||
* - Each segment: starts with letter, contains letters/numbers/underscores
|
||||
* - At least two segments (must have at least one dot)
|
||||
*
|
||||
* 2. Template keys: fixed prefix + template placeholders
|
||||
* Example: 'scroll.position:${id}', 'cache:${type}:${id}'
|
||||
*
|
||||
* Template placeholder rules:
|
||||
* - Must use ${variableName} syntax
|
||||
* - Variable name must be valid identifier (start with letter, alphanumeric + underscore)
|
||||
* - Empty placeholders like ${} are invalid
|
||||
* Template keys: ${xxx} placeholders are treated as literal string segments.
|
||||
* Example valid: 'scroll.position.${id}', 'entity.cache.${type}_${id}'
|
||||
* Example invalid: 'cache:${type}' (colon not allowed), '${id}' (no dot)
|
||||
*
|
||||
* @param {string} key - The schema key to validate
|
||||
* @returns {{ valid: boolean, error?: 'invalidKey' | 'invalidTemplateVar' }}
|
||||
@ -219,12 +218,7 @@ export default defineConfig([
|
||||
const hasTemplate = key.includes('${')
|
||||
|
||||
if (hasTemplate) {
|
||||
// Template key validation
|
||||
// Must have at least one dot-separated segment before any template or colon
|
||||
// Example valid: 'scroll.position:${id}', 'cache:${type}:${id}'
|
||||
// Example invalid: '${id}', ':${id}'
|
||||
|
||||
// Extract and validate all template variables
|
||||
// Validate template variable names first
|
||||
const templateVarPattern = /\$\{([^}]*)\}/g
|
||||
let match
|
||||
while ((match = templateVarPattern.exec(key)) !== null) {
|
||||
@ -235,17 +229,14 @@ export default defineConfig([
|
||||
}
|
||||
}
|
||||
|
||||
// Replace template placeholders with a marker to validate the structure
|
||||
const keyWithoutTemplates = key.replace(/\$\{[^}]+\}/g, '__TEMPLATE__')
|
||||
// Replace template placeholders with a valid segment marker
|
||||
// Use 'x' as placeholder since it's a valid segment character
|
||||
const keyWithoutTemplates = key.replace(/\$\{[^}]+\}/g, 'x')
|
||||
|
||||
// Template key structure:
|
||||
// - Must start with a valid segment (lowercase letters, numbers, underscores)
|
||||
// - Segments separated by dots or colons
|
||||
// - Must have at least one dot-separated segment
|
||||
// - Can end with template placeholder
|
||||
const templateKeyPattern = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*(:[a-z0-9_]*|:__TEMPLATE__)*$/
|
||||
|
||||
if (!templateKeyPattern.test(keyWithoutTemplates)) {
|
||||
// Template key must follow the same pattern as fixed keys
|
||||
// when ${xxx} is treated as a literal string
|
||||
const fixedKeyPattern = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/
|
||||
if (!fixedKeyPattern.test(keyWithoutTemplates)) {
|
||||
return { valid: false, error: 'invalidKey' }
|
||||
}
|
||||
|
||||
|
||||
71
packages/shared/data/cache/cacheSchemas.ts
vendored
71
packages/shared/data/cache/cacheSchemas.ts
vendored
@ -5,32 +5,35 @@ import type * as CacheValueTypes from './cacheValueTypes'
|
||||
*
|
||||
* ## Key Naming Convention
|
||||
*
|
||||
* All cache keys MUST follow the format: `namespace.sub.key_name`
|
||||
* All cache keys (fixed and template) MUST follow the format: `namespace.sub.key_name`
|
||||
*
|
||||
* Rules:
|
||||
* - At least 2 segments separated by dots (.)
|
||||
* - Each segment uses lowercase letters, numbers, and underscores only
|
||||
* - Pattern: /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/
|
||||
* - Template placeholders `${xxx}` are treated as literal string segments
|
||||
*
|
||||
* Examples:
|
||||
* - 'app.user.avatar' (valid)
|
||||
* - 'chat.multi_select_mode' (valid)
|
||||
* - 'minapp.opened_keep_alive' (valid)
|
||||
* - 'scroll.position.${topicId}' (valid template key)
|
||||
* - 'userAvatar' (invalid - missing dot separator)
|
||||
* - 'App.user' (invalid - uppercase not allowed)
|
||||
* - 'scroll.position:${id}' (invalid - colon not allowed)
|
||||
*
|
||||
* ## Template Key Support
|
||||
*
|
||||
* Template keys allow type-safe dynamic keys using template literal syntax.
|
||||
* Define in schema with `${variable}` placeholder, use with actual values.
|
||||
* Template keys follow the same dot-separated pattern as fixed keys.
|
||||
*
|
||||
* Examples:
|
||||
* - Schema: `'scroll.position:${topicId}': number`
|
||||
* - Usage: `useCache('scroll.position:topic-123')` -> infers `number` type
|
||||
* - Schema: `'scroll.position.${topicId}': number`
|
||||
* - Usage: `useCache('scroll.position.topic123')` -> infers `number` type
|
||||
*
|
||||
* Multiple placeholders are supported:
|
||||
* - Schema: `'cache:${type}:${id}': CacheData`
|
||||
* - Usage: `useCache('cache:user:456')` -> infers `CacheData` type
|
||||
* - Schema: `'entity.cache.${type}_${id}': CacheData`
|
||||
* - Usage: `useCache('entity.cache.user_456')` -> infers `CacheData` type
|
||||
*
|
||||
* This convention is enforced by ESLint rule: data-schema-key/valid-key
|
||||
*/
|
||||
@ -50,9 +53,9 @@ import type * as CacheValueTypes from './cacheValueTypes'
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* type Test1 = IsTemplateKey<'scroll:${id}'> // true
|
||||
* type Test2 = IsTemplateKey<'cache:${a}:${b}'> // true
|
||||
* type Test3 = IsTemplateKey<'app.user.avatar'> // false
|
||||
* type Test1 = IsTemplateKey<'scroll.position.${id}'> // true
|
||||
* type Test2 = IsTemplateKey<'entity.cache.${a}_${b}'> // true
|
||||
* type Test3 = IsTemplateKey<'app.user.avatar'> // false
|
||||
* ```
|
||||
*/
|
||||
export type IsTemplateKey<K extends string> = K extends `${string}\${${string}}${string}` ? true : false
|
||||
@ -69,11 +72,11 @@ export type IsTemplateKey<K extends string> = K extends `${string}\${${string}}$
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* type Test1 = ExpandTemplateKey<'scroll:${id}'>
|
||||
* // Result: `scroll:${string}` (matches 'scroll:123', 'scroll:abc', etc.)
|
||||
* type Test1 = ExpandTemplateKey<'scroll.position.${id}'>
|
||||
* // Result: `scroll.position.${string}` (matches 'scroll.position.123', etc.)
|
||||
*
|
||||
* type Test2 = ExpandTemplateKey<'cache:${type}:${id}'>
|
||||
* // Result: `cache:${string}:${string}` (matches 'cache:user:123', etc.)
|
||||
* type Test2 = ExpandTemplateKey<'entity.cache.${type}_${id}'>
|
||||
* // Result: `entity.cache.${string}_${string}` (matches 'entity.cache.user_123', etc.)
|
||||
*
|
||||
* type Test3 = ExpandTemplateKey<'app.user.avatar'>
|
||||
* // Result: 'app.user.avatar' (unchanged for non-template keys)
|
||||
@ -94,8 +97,8 @@ export type ExpandTemplateKey<T extends string> = T extends `${infer Prefix}\${$
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* type Test1 = ProcessKey<'scroll:${id}'> // `scroll:${string}`
|
||||
* type Test2 = ProcessKey<'app.user.avatar'> // 'app.user.avatar'
|
||||
* type Test1 = ProcessKey<'scroll.position.${id}'> // `scroll.position.${string}`
|
||||
* type Test2 = ProcessKey<'app.user.avatar'> // 'app.user.avatar'
|
||||
* ```
|
||||
*/
|
||||
export type ProcessKey<K extends string> = IsTemplateKey<K> extends true ? ExpandTemplateKey<K> : K
|
||||
@ -135,6 +138,10 @@ export type UseCacheSchema = {
|
||||
'agent.active_id': string | null
|
||||
'agent.session.active_id_map': Record<string, string | null>
|
||||
'agent.session.waiting_id_map': Record<string, boolean>
|
||||
|
||||
// Template key examples (for testing and demonstration)
|
||||
'scroll.position.${topicId}': number
|
||||
'entity.cache.${type}_${id}': { loaded: boolean; data: unknown }
|
||||
}
|
||||
|
||||
export const DefaultUseCache: UseCacheSchema = {
|
||||
@ -173,7 +180,11 @@ export const DefaultUseCache: UseCacheSchema = {
|
||||
// Agent management
|
||||
'agent.active_id': null,
|
||||
'agent.session.active_id_map': {},
|
||||
'agent.session.waiting_id_map': {}
|
||||
'agent.session.waiting_id_map': {},
|
||||
|
||||
// Template key examples (for testing and demonstration)
|
||||
'scroll.position.${topicId}': 0,
|
||||
'entity.cache.${type}_${id}': { loaded: false, data: null }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -218,7 +229,7 @@ export type SharedCacheKey = keyof SharedCacheSchema
|
||||
*
|
||||
* This type expands all schema keys using ProcessKey, which:
|
||||
* - Keeps fixed keys unchanged (e.g., 'app.user.avatar')
|
||||
* - Expands template keys to match patterns (e.g., 'scroll:${id}' -> `scroll:${string}`)
|
||||
* - Expands template keys to match patterns (e.g., 'scroll.position.${id}' -> `scroll.position.${string}`)
|
||||
*
|
||||
* The resulting union type allows TypeScript to accept any concrete key
|
||||
* that matches either a fixed key or an expanded template pattern.
|
||||
@ -227,17 +238,17 @@ export type SharedCacheKey = keyof SharedCacheSchema
|
||||
* ```typescript
|
||||
* // Given schema:
|
||||
* // 'app.user.avatar': string
|
||||
* // 'scroll.position:${topicId}': number
|
||||
* // 'scroll.position.${topicId}': number
|
||||
*
|
||||
* // UseCacheKey becomes: 'app.user.avatar' | `scroll.position:${string}`
|
||||
* // UseCacheKey becomes: 'app.user.avatar' | `scroll.position.${string}`
|
||||
*
|
||||
* // Valid keys:
|
||||
* const k1: UseCacheKey = 'app.user.avatar' // fixed key
|
||||
* const k2: UseCacheKey = 'scroll.position:123' // matches template
|
||||
* const k3: UseCacheKey = 'scroll.position:abc' // matches template
|
||||
* const k1: UseCacheKey = 'app.user.avatar' // fixed key
|
||||
* const k2: UseCacheKey = 'scroll.position.123' // matches template
|
||||
* const k3: UseCacheKey = 'scroll.position.abc' // matches template
|
||||
*
|
||||
* // Invalid keys:
|
||||
* const k4: UseCacheKey = 'unknown.key' // error: not in schema
|
||||
* const k4: UseCacheKey = 'unknown.key' // error: not in schema
|
||||
* ```
|
||||
*/
|
||||
export type UseCacheKey = {
|
||||
@ -264,12 +275,12 @@ export type UseCacheKey = {
|
||||
* ```typescript
|
||||
* // Given schema:
|
||||
* // 'app.user.avatar': string
|
||||
* // 'scroll.position:${topicId}': number
|
||||
* // 'scroll.position.${topicId}': number
|
||||
*
|
||||
* type T1 = InferUseCacheValue<'app.user.avatar'> // string
|
||||
* type T2 = InferUseCacheValue<'scroll.position:123'> // number
|
||||
* type T3 = InferUseCacheValue<'scroll.position:abc'> // number
|
||||
* type T4 = InferUseCacheValue<'unknown.key'> // never
|
||||
* type T2 = InferUseCacheValue<'scroll.position.123'> // number
|
||||
* type T3 = InferUseCacheValue<'scroll.position.abc'> // number
|
||||
* type T4 = InferUseCacheValue<'unknown.key'> // never
|
||||
* ```
|
||||
*/
|
||||
export type InferUseCacheValue<K extends string> = {
|
||||
@ -291,15 +302,15 @@ export type InferUseCacheValue<K extends string> = {
|
||||
* ```typescript
|
||||
* // Given schema:
|
||||
* // 'app.user.avatar': string
|
||||
* // 'scroll.position:${topicId}': number
|
||||
* // 'scroll.position.${topicId}': number
|
||||
*
|
||||
* // These cause compile-time errors (key matches schema):
|
||||
* getCasual('app.user.avatar') // Error: never
|
||||
* getCasual('scroll.position:123') // Error: never (matches template)
|
||||
* getCasual('scroll.position.123') // Error: never (matches template)
|
||||
*
|
||||
* // These are allowed (key doesn't match any schema pattern):
|
||||
* getCasual('my.custom.key') // OK
|
||||
* getCasual('dynamic:xyz:456') // OK
|
||||
* getCasual('other.dynamic.key') // OK
|
||||
* ```
|
||||
*/
|
||||
export type UseCacheCasualKey<K extends string> = K extends UseCacheKey ? never : K
|
||||
|
||||
@ -105,7 +105,10 @@ export class CacheService {
|
||||
*
|
||||
* Supports both fixed keys and template keys:
|
||||
* - Fixed keys: `get('app.user.avatar')`
|
||||
* - Template keys: `get('scroll.position:topic-123')` (matches schema `'scroll.position:${id}'`)
|
||||
* - Template keys: `get('scroll.position.topic123')` (matches schema `'scroll.position.${id}'`)
|
||||
*
|
||||
* Template keys follow the same dot-separated pattern as fixed keys.
|
||||
* When ${xxx} is treated as a literal string, the key matches: xxx.yyy.zzz_www
|
||||
*
|
||||
* DESIGN NOTE: Returns `undefined` when cache miss or TTL expired.
|
||||
* This is intentional - developers need to know when a value doesn't exist
|
||||
@ -122,8 +125,8 @@ export class CacheService {
|
||||
* // Fixed key - handle undefined explicitly
|
||||
* const avatar = cacheService.get('app.user.avatar') ?? ''
|
||||
*
|
||||
* // Template key (schema: 'scroll.position:${id}': number)
|
||||
* const scrollPos = cacheService.get('scroll.position:topic-123') ?? 0
|
||||
* // Template key (schema: 'scroll.position.${id}': number)
|
||||
* const scrollPos = cacheService.get('scroll.position.topic123') ?? 0
|
||||
* ```
|
||||
*/
|
||||
get<K extends UseCacheKey>(key: K): InferUseCacheValue<K> | undefined {
|
||||
@ -181,7 +184,9 @@ export class CacheService {
|
||||
*
|
||||
* Supports both fixed keys and template keys:
|
||||
* - Fixed keys: `set('app.user.avatar', 'url')`
|
||||
* - Template keys: `set('scroll.position:topic-123', 100)`
|
||||
* - Template keys: `set('scroll.position.topic123', 100)`
|
||||
*
|
||||
* Template keys follow the same dot-separated pattern as fixed keys.
|
||||
*
|
||||
* @template K - The cache key type (inferred from UseCacheKey, supports template patterns)
|
||||
* @param key - Schema-defined cache key (fixed or matching template pattern)
|
||||
@ -193,8 +198,8 @@ export class CacheService {
|
||||
* // Fixed key
|
||||
* cacheService.set('app.user.avatar', 'https://example.com/avatar.png')
|
||||
*
|
||||
* // Template key (schema: 'scroll.position:${id}': number)
|
||||
* cacheService.set('scroll.position:topic-123', 150)
|
||||
* // Template key (schema: 'scroll.position.${id}': number)
|
||||
* cacheService.set('scroll.position.topic123', 150)
|
||||
*
|
||||
* // With TTL (expires after 30 seconds)
|
||||
* cacheService.set('chat.generating', true, 30000)
|
||||
|
||||
149
src/renderer/src/data/hooks/__tests__/useCache.types.test.ts
Normal file
149
src/renderer/src/data/hooks/__tests__/useCache.types.test.ts
Normal file
@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Type-level tests for template key type inference
|
||||
*
|
||||
* These tests verify compile-time type behavior of the cache system:
|
||||
* 1. Template key type inference works correctly
|
||||
* 2. Casual API blocks schema keys (including template patterns)
|
||||
* 3. Value types are correctly inferred from schema
|
||||
*/
|
||||
|
||||
import type {
|
||||
ExpandTemplateKey,
|
||||
InferUseCacheValue,
|
||||
IsTemplateKey,
|
||||
ProcessKey,
|
||||
UseCacheCasualKey,
|
||||
UseCacheKey
|
||||
} from '@shared/data/cache/cacheSchemas'
|
||||
import { describe, expect, expectTypeOf, it } from 'vitest'
|
||||
|
||||
describe('Template Key Type Utilities', () => {
|
||||
describe('IsTemplateKey', () => {
|
||||
it('should detect template keys as true', () => {
|
||||
// Using expectTypeOf for type-level assertions
|
||||
const templateResult1: IsTemplateKey<'scroll.position.${id}'> = true
|
||||
const templateResult2: IsTemplateKey<'entity.cache.${type}_${id}'> = true
|
||||
expect(templateResult1).toBe(true)
|
||||
expect(templateResult2).toBe(true)
|
||||
})
|
||||
|
||||
it('should detect fixed keys as false', () => {
|
||||
const fixedResult1: IsTemplateKey<'app.user.avatar'> = false
|
||||
const fixedResult2: IsTemplateKey<'chat.generating'> = false
|
||||
expect(fixedResult1).toBe(false)
|
||||
expect(fixedResult2).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ExpandTemplateKey', () => {
|
||||
it('should expand single placeholder', () => {
|
||||
// Type assertion: 'scroll.position.topic123' should extend the expanded type
|
||||
type Expanded = ExpandTemplateKey<'scroll.position.${id}'>
|
||||
const key1: Expanded = 'scroll.position.topic123'
|
||||
const key2: Expanded = 'scroll.position.abc'
|
||||
expect(key1).toBe('scroll.position.topic123')
|
||||
expect(key2).toBe('scroll.position.abc')
|
||||
})
|
||||
|
||||
it('should expand multiple placeholders', () => {
|
||||
type Expanded = ExpandTemplateKey<'entity.cache.${type}_${id}'>
|
||||
const key1: Expanded = 'entity.cache.user_123'
|
||||
const key2: Expanded = 'entity.cache.post_456'
|
||||
expect(key1).toBe('entity.cache.user_123')
|
||||
expect(key2).toBe('entity.cache.post_456')
|
||||
})
|
||||
|
||||
it('should leave fixed keys unchanged', () => {
|
||||
type Expanded = ExpandTemplateKey<'app.user.avatar'>
|
||||
const key: Expanded = 'app.user.avatar'
|
||||
expect(key).toBe('app.user.avatar')
|
||||
})
|
||||
})
|
||||
|
||||
describe('ProcessKey', () => {
|
||||
it('should expand template keys', () => {
|
||||
type Processed = ProcessKey<'scroll.position.${topicId}'>
|
||||
const key: Processed = 'scroll.position.topic123'
|
||||
expect(key).toBe('scroll.position.topic123')
|
||||
})
|
||||
|
||||
it('should keep fixed keys unchanged', () => {
|
||||
type Processed = ProcessKey<'app.user.avatar'>
|
||||
const key: Processed = 'app.user.avatar'
|
||||
expect(key).toBe('app.user.avatar')
|
||||
})
|
||||
})
|
||||
|
||||
describe('UseCacheKey', () => {
|
||||
it('should include fixed keys', () => {
|
||||
const key1: UseCacheKey = 'app.user.avatar'
|
||||
const key2: UseCacheKey = 'chat.generating'
|
||||
expect(key1).toBe('app.user.avatar')
|
||||
expect(key2).toBe('chat.generating')
|
||||
})
|
||||
|
||||
it('should match template patterns', () => {
|
||||
const key1: UseCacheKey = 'scroll.position.topic123'
|
||||
const key2: UseCacheKey = 'scroll.position.abc-def'
|
||||
const key3: UseCacheKey = 'entity.cache.user_456'
|
||||
expect(key1).toBe('scroll.position.topic123')
|
||||
expect(key2).toBe('scroll.position.abc-def')
|
||||
expect(key3).toBe('entity.cache.user_456')
|
||||
})
|
||||
})
|
||||
|
||||
describe('InferUseCacheValue', () => {
|
||||
it('should infer value type for fixed keys', () => {
|
||||
// These type assertions verify the type system works
|
||||
const avatarType: InferUseCacheValue<'app.user.avatar'> = 'test'
|
||||
const generatingType: InferUseCacheValue<'chat.generating'> = true
|
||||
expectTypeOf(avatarType).toBeString()
|
||||
expectTypeOf(generatingType).toBeBoolean()
|
||||
})
|
||||
|
||||
it('should infer value type for template key instances', () => {
|
||||
const scrollType: InferUseCacheValue<'scroll.position.topic123'> = 100
|
||||
const entityType: InferUseCacheValue<'entity.cache.user_456'> = { loaded: true, data: null }
|
||||
expectTypeOf(scrollType).toBeNumber()
|
||||
expectTypeOf(entityType).toMatchTypeOf<{ loaded: boolean; data: unknown }>()
|
||||
})
|
||||
|
||||
it('should return never for unknown keys', () => {
|
||||
// Unknown key should infer to never
|
||||
type UnknownValue = InferUseCacheValue<'unknown.key.here'>
|
||||
expectTypeOf<UnknownValue>().toBeNever()
|
||||
})
|
||||
})
|
||||
|
||||
describe('UseCacheCasualKey', () => {
|
||||
it('should block fixed schema keys', () => {
|
||||
// Fixed keys should resolve to never
|
||||
type BlockedFixed = UseCacheCasualKey<'app.user.avatar'>
|
||||
expectTypeOf<BlockedFixed>().toBeNever()
|
||||
})
|
||||
|
||||
it('should block template pattern matches', () => {
|
||||
// Keys matching template patterns should resolve to never
|
||||
type BlockedTemplate = UseCacheCasualKey<'scroll.position.topic123'>
|
||||
expectTypeOf<BlockedTemplate>().toBeNever()
|
||||
})
|
||||
|
||||
it('should allow non-schema keys', () => {
|
||||
// Non-schema keys should pass through
|
||||
type AllowedKey = UseCacheCasualKey<'my.custom.key'>
|
||||
const key: AllowedKey = 'my.custom.key'
|
||||
expect(key).toBe('my.custom.key')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Runtime template key detection', () => {
|
||||
it('should correctly detect template keys', () => {
|
||||
const isTemplate = (key: string) => key.includes('${') && key.includes('}')
|
||||
|
||||
expect(isTemplate('scroll.position.${id}')).toBe(true)
|
||||
expect(isTemplate('entity.cache.${type}_${id}')).toBe(true)
|
||||
expect(isTemplate('app.user.avatar')).toBe(false)
|
||||
expect(isTemplate('chat.generating')).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -26,8 +26,8 @@ const logger = loggerService.withContext('useCache')
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* isTemplateKey('scroll.position:${id}') // true
|
||||
* isTemplateKey('app.user.avatar') // false
|
||||
* isTemplateKey('scroll.position.${id}') // true
|
||||
* isTemplateKey('app.user.avatar') // false
|
||||
* ```
|
||||
*/
|
||||
function isTemplateKey(key: string): boolean {
|
||||
@ -38,17 +38,21 @@ function isTemplateKey(key: string): boolean {
|
||||
* Converts a template key pattern into a RegExp for matching concrete keys.
|
||||
*
|
||||
* Each `${variable}` placeholder is replaced with a pattern that matches
|
||||
* any non-empty string of word characters, dots, and hyphens.
|
||||
* any non-empty string of word characters (letters, numbers, underscores, hyphens).
|
||||
*
|
||||
* @param template - The template key pattern (e.g., 'scroll.position:${id}')
|
||||
* Template keys follow the same dot-separated pattern as fixed keys.
|
||||
* When ${xxx} is treated as a literal string, the key matches: xxx.yyy.zzz_www
|
||||
*
|
||||
* @param template - The template key pattern (e.g., 'scroll.position.${id}')
|
||||
* @returns A RegExp that matches concrete keys for this template
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const regex = templateToRegex('scroll.position:${id}')
|
||||
* regex.test('scroll.position:topic-123') // true
|
||||
* regex.test('scroll.position:') // false
|
||||
* regex.test('other.key:123') // false
|
||||
* const regex = templateToRegex('scroll.position.${id}')
|
||||
* regex.test('scroll.position.topic123') // true
|
||||
* regex.test('scroll.position.topic-123') // true
|
||||
* regex.test('scroll.position.') // false
|
||||
* regex.test('other.key.123') // false
|
||||
* ```
|
||||
*/
|
||||
function templateToRegex(template: string): RegExp {
|
||||
@ -62,8 +66,9 @@ function templateToRegex(template: string): RegExp {
|
||||
})
|
||||
|
||||
// Replace ${...} placeholders with a pattern matching non-empty strings
|
||||
// Allows: word chars, dots, hyphens, underscores, colons
|
||||
const pattern = escaped.replace(/\$\{[^}]+\}/g, '([\\w.\\-_:]+)')
|
||||
// Allows: word chars (letters, numbers, underscores) and hyphens
|
||||
// Does NOT allow dots or colons since those are structural separators
|
||||
const pattern = escaped.replace(/\$\{[^}]+\}/g, '([\\w\\-]+)')
|
||||
|
||||
return new RegExp(`^${pattern}$`)
|
||||
}
|
||||
@ -79,10 +84,10 @@ function templateToRegex(template: string): RegExp {
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Given schema has 'app.user.avatar' and 'scroll.position:${id}'
|
||||
* // Given schema has 'app.user.avatar' and 'scroll.position.${id}'
|
||||
*
|
||||
* findMatchingUseCacheSchemaKey('app.user.avatar') // 'app.user.avatar'
|
||||
* findMatchingUseCacheSchemaKey('scroll.position:123') // 'scroll.position:${id}'
|
||||
* findMatchingUseCacheSchemaKey('scroll.position.123') // 'scroll.position.${id}'
|
||||
* findMatchingUseCacheSchemaKey('unknown.key') // undefined
|
||||
* ```
|
||||
*/
|
||||
@ -119,10 +124,10 @@ function findMatchingUseCacheSchemaKey(key: string): keyof UseCacheSchema | unde
|
||||
* ```typescript
|
||||
* // Given schema:
|
||||
* // 'app.user.avatar': '' (default)
|
||||
* // 'scroll.position:${id}': 0 (default)
|
||||
* // 'scroll.position.${id}': 0 (default)
|
||||
*
|
||||
* getUseCacheDefaultValue('app.user.avatar') // ''
|
||||
* getUseCacheDefaultValue('scroll.position:123') // 0
|
||||
* getUseCacheDefaultValue('scroll.position.123') // 0
|
||||
* getUseCacheDefaultValue('unknown.key') // undefined
|
||||
* ```
|
||||
*/
|
||||
@ -142,7 +147,10 @@ function getUseCacheDefaultValue<K extends UseCacheKey>(key: K): InferUseCacheVa
|
||||
*
|
||||
* Supports both fixed keys and template keys:
|
||||
* - Fixed keys: `useCache('app.user.avatar')`
|
||||
* - Template keys: `useCache('scroll.position:topic-123')` (matches schema `'scroll.position:${id}'`)
|
||||
* - Template keys: `useCache('scroll.position.topic123')` (matches schema `'scroll.position.${id}'`)
|
||||
*
|
||||
* Template keys follow the same dot-separated pattern as fixed keys.
|
||||
* When ${xxx} is treated as a literal string, the key matches: xxx.yyy.zzz_www
|
||||
*
|
||||
* @template K - The cache key type (inferred from UseCacheKey)
|
||||
* @param key - Cache key from the predefined schema (fixed or matching template pattern)
|
||||
@ -154,8 +162,8 @@ function getUseCacheDefaultValue<K extends UseCacheKey>(key: K): InferUseCacheVa
|
||||
* // Fixed key usage
|
||||
* const [avatar, setAvatar] = useCache('app.user.avatar')
|
||||
*
|
||||
* // Template key usage (schema: 'scroll.position:${id}': number)
|
||||
* const [scrollPos, setScrollPos] = useCache('scroll.position:topic-123')
|
||||
* // Template key usage (schema: 'scroll.position.${id}': number)
|
||||
* const [scrollPos, setScrollPos] = useCache('scroll.position.topic123')
|
||||
* // TypeScript infers scrollPos as number
|
||||
*
|
||||
* // With custom initial value
|
||||
|
||||
@ -2,11 +2,11 @@ import type {
|
||||
RendererPersistCacheKey,
|
||||
RendererPersistCacheSchema,
|
||||
UseCacheKey,
|
||||
UseCacheSchema,
|
||||
InferUseCacheValue,
|
||||
SharedCacheKey,
|
||||
SharedCacheSchema
|
||||
} from '@shared/data/cache/cacheSchemas'
|
||||
import { DefaultRendererPersistCache, DefaultUseCache, DefaultSharedCache } 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'
|
||||
|
||||
@ -66,20 +66,20 @@ export const createMockCacheService = (
|
||||
const mockCacheService = {
|
||||
// ============ Memory Cache (Type-safe) ============
|
||||
|
||||
get: vi.fn(<K extends UseCacheKey>(key: K): UseCacheSchema[K] => {
|
||||
get: vi.fn(<K extends UseCacheKey>(key: K): InferUseCacheValue<K> | undefined => {
|
||||
const entry = memoryCache.get(key)
|
||||
if (entry === undefined) {
|
||||
return DefaultUseCache[key]
|
||||
return undefined
|
||||
}
|
||||
if (isExpired(entry)) {
|
||||
memoryCache.delete(key)
|
||||
notifySubscribers(key)
|
||||
return DefaultUseCache[key]
|
||||
return undefined
|
||||
}
|
||||
return entry.value
|
||||
return entry.value as InferUseCacheValue<K>
|
||||
}),
|
||||
|
||||
set: vi.fn(<K extends UseCacheKey>(key: K, value: UseCacheSchema[K], ttl?: number): void => {
|
||||
set: vi.fn(<K extends UseCacheKey>(key: K, value: InferUseCacheValue<K>, ttl?: number): void => {
|
||||
const entry: CacheEntry = {
|
||||
value,
|
||||
expireAt: ttl ? Date.now() + ttl : undefined
|
||||
@ -409,12 +409,12 @@ export const MockCacheService = {
|
||||
}
|
||||
|
||||
// ============ Memory Cache (Type-safe) ============
|
||||
get<K extends UseCacheKey>(key: K): UseCacheSchema[K] {
|
||||
return mockCacheService.get(key)
|
||||
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: UseCacheSchema[K], ttl?: number): void {
|
||||
return mockCacheService.set(key, value, ttl)
|
||||
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 {
|
||||
|
||||
@ -3,6 +3,7 @@ import type {
|
||||
RendererPersistCacheSchema,
|
||||
UseCacheKey,
|
||||
UseCacheSchema,
|
||||
InferUseCacheValue,
|
||||
SharedCacheKey,
|
||||
SharedCacheSchema
|
||||
} from '@shared/data/cache/cacheSchemas'
|
||||
@ -14,14 +15,14 @@ import { vi } from 'vitest'
|
||||
* Provides comprehensive mocks for all cache management hooks
|
||||
*/
|
||||
|
||||
// Mock cache state storage
|
||||
const mockMemoryCache = new Map<UseCacheKey, any>()
|
||||
// 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 as UseCacheKey, value)
|
||||
mockMemoryCache.set(key, value)
|
||||
})
|
||||
|
||||
Object.entries(DefaultSharedCache).forEach(([key, value]) => {
|
||||
@ -32,13 +33,13 @@ Object.entries(DefaultRendererPersistCache).forEach(([key, value]) => {
|
||||
mockPersistCache.set(key as RendererPersistCacheKey, value)
|
||||
})
|
||||
|
||||
// Mock subscribers for cache changes
|
||||
const mockMemorySubscribers = new Map<UseCacheKey, Set<() => void>>()
|
||||
// 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: UseCacheKey) => {
|
||||
const notifyMemorySubscribers = (key: string) => {
|
||||
const subscribers = mockMemorySubscribers.get(key)
|
||||
if (subscribers) {
|
||||
subscribers.forEach((callback) => {
|
||||
@ -77,25 +78,78 @@ const notifyPersistSubscribers = (key: RendererPersistCacheKey) => {
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 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?: UseCacheSchema[K]
|
||||
): [UseCacheSchema[K], (value: UseCacheSchema[K]) => void] => {
|
||||
initValue?: InferUseCacheValue<K>
|
||||
): [InferUseCacheValue<K>, (value: InferUseCacheValue<K>) => void] => {
|
||||
// Get current value
|
||||
let currentValue = mockMemoryCache.get(key)
|
||||
if (currentValue === undefined) {
|
||||
currentValue = initValue ?? DefaultUseCache[key]
|
||||
currentValue = initValue ?? getDefaultValue(key)
|
||||
if (currentValue !== undefined) {
|
||||
mockMemoryCache.set(key, currentValue)
|
||||
}
|
||||
}
|
||||
|
||||
// Mock setValue function
|
||||
const setValue = vi.fn((value: UseCacheSchema[K]) => {
|
||||
const setValue = vi.fn((value: InferUseCacheValue<K>) => {
|
||||
mockMemoryCache.set(key, value)
|
||||
notifyMemorySubscribers(key)
|
||||
})
|
||||
@ -185,7 +239,7 @@ export const MockUseCacheUtils = {
|
||||
mockPersistCache.clear()
|
||||
|
||||
Object.entries(DefaultUseCache).forEach(([key, value]) => {
|
||||
mockMemoryCache.set(key as UseCacheKey, value)
|
||||
mockMemoryCache.set(key, value)
|
||||
})
|
||||
|
||||
Object.entries(DefaultSharedCache).forEach(([key, value]) => {
|
||||
@ -205,7 +259,7 @@ export const MockUseCacheUtils = {
|
||||
/**
|
||||
* Set cache value for testing (memory cache)
|
||||
*/
|
||||
setCacheValue: <K extends UseCacheKey>(key: K, value: UseCacheSchema[K]) => {
|
||||
setCacheValue: <K extends UseCacheKey>(key: K, value: InferUseCacheValue<K>) => {
|
||||
mockMemoryCache.set(key, value)
|
||||
notifyMemorySubscribers(key)
|
||||
},
|
||||
@ -213,8 +267,8 @@ export const MockUseCacheUtils = {
|
||||
/**
|
||||
* Get cache value (memory cache)
|
||||
*/
|
||||
getCacheValue: <K extends UseCacheKey>(key: K): UseCacheSchema[K] => {
|
||||
return mockMemoryCache.get(key) ?? DefaultUseCache[key]
|
||||
getCacheValue: <K extends UseCacheKey>(key: K): InferUseCacheValue<K> | undefined => {
|
||||
return mockMemoryCache.get(key) ?? getDefaultValue(key)
|
||||
},
|
||||
|
||||
/**
|
||||
@ -283,7 +337,7 @@ export const MockUseCacheUtils = {
|
||||
/**
|
||||
* Simulate cache change from external source
|
||||
*/
|
||||
simulateExternalCacheChange: <K extends UseCacheKey>(key: K, value: UseCacheSchema[K]) => {
|
||||
simulateExternalCacheChange: <K extends UseCacheKey>(key: K, value: InferUseCacheValue<K>) => {
|
||||
mockMemoryCache.set(key, value)
|
||||
notifyMemorySubscribers(key)
|
||||
},
|
||||
@ -293,17 +347,17 @@ export const MockUseCacheUtils = {
|
||||
*/
|
||||
mockCacheReturn: <K extends UseCacheKey>(
|
||||
key: K,
|
||||
value: UseCacheSchema[K],
|
||||
setValue?: (value: UseCacheSchema[K]) => void
|
||||
value: InferUseCacheValue<K>,
|
||||
setValue?: (value: InferUseCacheValue<K>) => void
|
||||
) => {
|
||||
mockUseCache.mockImplementation((cacheKey, initValue) => {
|
||||
if (cacheKey === key) {
|
||||
return [value, setValue || vi.fn()]
|
||||
return [value, setValue || vi.fn()] as any
|
||||
}
|
||||
|
||||
// Default behavior for other keys
|
||||
const defaultValue = mockMemoryCache.get(cacheKey) ?? initValue ?? DefaultUseCache[cacheKey]
|
||||
return [defaultValue, vi.fn()]
|
||||
const defaultValue = mockMemoryCache.get(cacheKey) ?? initValue ?? getDefaultValue(cacheKey)
|
||||
return [defaultValue, vi.fn()] as any
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user