[docs] update CLAUDE.md and selected README.md files (#5293)

* docs: add Claude documentation for settings and feature flags

* docs: update services README.md

* docs: update stores README.md
This commit is contained in:
Arjan Singh
2025-09-01 15:31:44 -07:00
committed by GitHub
parent b592c9015e
commit 481e3b593a
5 changed files with 807 additions and 93 deletions

View File

@@ -82,6 +82,44 @@ When referencing Comfy-Org repos:
2. Use GitHub API for branches/PRs/metadata
3. Curl GitHub website if needed
## Settings and Feature Flags Quick Reference
### Settings Usage
```typescript
const settingStore = useSettingStore()
const value = settingStore.get('Comfy.SomeSetting') // Get setting
await settingStore.set('Comfy.SomeSetting', newValue) // Update setting
```
### Dynamic Defaults
```typescript
{
id: 'Comfy.Example.Setting',
defaultValue: () => window.innerWidth < 1024 ? 'small' : 'large' // Runtime context
}
```
### Version-Based Defaults
```typescript
{
id: 'Comfy.Example.Feature',
defaultValue: 'legacy',
defaultsByInstallVersion: { '1.25.0': 'enhanced' } // Gradual rollout
}
```
### Feature Flags
```typescript
if (api.serverSupportsFeature('feature_name')) { // Check capability
// Use enhanced feature
}
const value = api.getServerFeature('config_name', defaultValue) // Get config
```
**Documentation:**
- Settings system: `docs/SETTINGS.md`
- Feature flags system: `docs/FEATURE_FLAGS.md`
## Common Pitfalls
- NEVER use `any` type - use proper TypeScript types

293
docs/SETTINGS.md Normal file
View File

@@ -0,0 +1,293 @@
# Settings System
## Overview
ComfyUI frontend uses a comprehensive settings system for user preferences with support for dynamic defaults, version-based rollouts, and environment-aware configuration.
### Settings Architecture
- Settings are defined as `SettingParams` in `src/constants/coreSettings.ts`
- Registered at app startup, loaded/saved via `useSettingStore` (Pinia)
- Persisted per user via backend `/settings` endpoint
- If a value hasn't been set by the user, the store returns the computed default
```typescript
// From src/stores/settingStore.ts:105-122
function getDefaultValue<K extends keyof Settings>(
key: K
): Settings[K] | undefined {
const param = getSettingById(key)
if (param === undefined) return
const versionedDefault = getVersionedDefaultValue(key, param)
if (versionedDefault) {
return versionedDefault
}
return typeof param.defaultValue === 'function'
? param.defaultValue()
: param.defaultValue
}
```
### Settings Registration Process
Settings are registered after server values are loaded:
```typescript
// From src/components/graph/GraphCanvas.vue:311-315
CORE_SETTINGS.forEach((setting) => {
settingStore.addSetting(setting)
})
await newUserService().initializeIfNewUser(settingStore)
```
## Dynamic and Environment-Based Defaults
### Computed Defaults
You can compute defaults dynamically using function defaults that access runtime context:
```typescript
// From src/constants/coreSettings.ts:94-101
{
id: 'Comfy.Sidebar.Size',
// Default to small if the window is less than 1536px(2xl) wide
defaultValue: () => (window.innerWidth < 1536 ? 'small' : 'normal')
}
```
```typescript
// From src/constants/coreSettings.ts:306
{
id: 'Comfy.Locale',
defaultValue: () => navigator.language.split('-')[0] || 'en'
}
```
### Version-Based Defaults
You can vary defaults by installed frontend version using `defaultsByInstallVersion`:
```typescript
// From src/stores/settingStore.ts:129-150
function getVersionedDefaultValue<K extends keyof Settings, TValue = Settings[K]>(
key: K,
param: SettingParams<TValue> | undefined
): TValue | null {
const defaultsByInstallVersion = param?.defaultsByInstallVersion
if (defaultsByInstallVersion && key !== 'Comfy.InstalledVersion') {
const installedVersion = get('Comfy.InstalledVersion')
if (installedVersion) {
const sortedVersions = Object.keys(defaultsByInstallVersion).sort(
(a, b) => compareVersions(b, a)
)
for (const version of sortedVersions) {
if (!isSemVer(version)) continue
if (compareVersions(installedVersion, version) >= 0) {
const versionedDefault = defaultsByInstallVersion[version]
return typeof versionedDefault === 'function'
? versionedDefault()
: versionedDefault
}
}
}
}
return null
}
```
Example versioned defaults from codebase:
```typescript
// From src/constants/coreSettings.ts:38-40
{
id: 'Comfy.Graph.LinkReleaseAction',
defaultValue: LinkReleaseTriggerAction.CONTEXT_MENU,
defaultsByInstallVersion: {
'1.24.1': LinkReleaseTriggerAction.SEARCH_BOX
}
}
// Another versioned default example
{
id: 'Comfy.Graph.LinkReleaseAction.Shift',
defaultValue: LinkReleaseTriggerAction.SEARCH_BOX,
defaultsByInstallVersion: {
'1.24.1': LinkReleaseTriggerAction.CONTEXT_MENU
}
}
```
### Real Examples from Codebase
Here are actual settings showing different patterns:
```typescript
// Number setting with validation
{
id: 'LiteGraph.Node.TooltipDelay',
name: 'Tooltip Delay',
type: 'number',
attrs: {
min: 100,
max: 3000,
step: 50
},
defaultValue: 500,
versionAdded: '1.9.0'
}
// Hidden system setting for tracking
{
id: 'Comfy.InstalledVersion',
name: 'The frontend version that was running when the user first installed ComfyUI',
type: 'hidden',
defaultValue: null,
versionAdded: '1.24.0'
}
// Slider with complex tooltip
{
id: 'LiteGraph.Canvas.LowQualityRenderingZoomThreshold',
name: 'Low quality rendering zoom threshold',
tooltip: 'Zoom level threshold for performance mode. Lower values (0.1) = quality at all zoom levels. Higher values (1.0) = performance mode even when zoomed in.',
type: 'slider',
attrs: {
min: 0.1,
max: 1.0,
step: 0.05
},
defaultValue: 0.5
}
```
### New User Version Capture
The initial installed version is captured for new users to ensure versioned defaults remain stable:
```typescript
// From src/services/newUserService.ts:49-53
await settingStore.set(
'Comfy.InstalledVersion',
__COMFYUI_FRONTEND_VERSION__
)
```
## Practical Patterns for Environment-Based Defaults
### Dynamic Default Patterns
```typescript
// Device-based default
{
id: 'Comfy.Example.MobileDefault',
type: 'boolean',
defaultValue: () => /Mobile/i.test(navigator.userAgent)
}
// Environment-based default
{
id: 'Comfy.Example.DevMode',
type: 'boolean',
defaultValue: () => import.meta.env.DEV
}
// Window size based
{
id: 'Comfy.Example.CompactUI',
type: 'boolean',
defaultValue: () => window.innerWidth < 1024
}
```
### Version-Based Rollout Pattern
```typescript
{
id: 'Comfy.Example.NewFeature',
type: 'combo',
options: ['legacy', 'enhanced'],
defaultValue: 'legacy',
defaultsByInstallVersion: {
'1.25.0': 'enhanced'
}
}
```
## Settings Persistence and Access
### API Interaction
Values are stored per user via the backend. The store writes through API and falls back to defaults when not set:
```typescript
// From src/stores/settingStore.ts:73-75
onChange(settingsById.value[key], newValue, oldValue)
settingValues.value[key] = newValue
await api.storeSetting(key, newValue)
```
### Usage in Components
```typescript
const settingStore = useSettingStore()
// Get setting value (returns computed default if not set by user)
const value = settingStore.get('Comfy.SomeSetting')
// Update setting value
await settingStore.set('Comfy.SomeSetting', newValue)
```
## Advanced Settings Features
### Migration and Backward Compatibility
Settings support migration from deprecated values:
```typescript
// From src/stores/settingStore.ts:68-69, 172-175
const newValue = tryMigrateDeprecatedValue(
settingsById.value[key],
clonedValue
)
// Migration happens during addSetting for existing values:
if (settingValues.value[setting.id] !== undefined) {
settingValues.value[setting.id] = tryMigrateDeprecatedValue(
setting,
settingValues.value[setting.id]
)
}
```
### onChange Callbacks
Settings can define onChange callbacks that receive the setting definition, new value, and old value:
```typescript
// From src/stores/settingStore.ts:73, 177
onChange(settingsById.value[key], newValue, oldValue) // During set()
onChange(setting, get(setting.id), undefined) // During addSetting()
```
### Settings UI and Categories
Settings are automatically grouped for UI based on their `category` or derived from `id`:
```typescript
{
id: 'Comfy.Sidebar.Size',
category: ['Appearance', 'Sidebar', 'Size'],
// UI will group this under Appearance > Sidebar > Size
}
```
## Related Documentation
- Feature flag system: `docs/FEATURE_FLAGS.md`
- Settings schema for backend: `src/schemas/apiSchema.ts` (zSettings)
- Server configuration (separate from user settings): `src/constants/serverConfig.ts`
## Summary
- **Settings**: User preferences with dynamic/versioned defaults, persisted per user
- **Environment Defaults**: Use function defaults to read runtime context (window, navigator, env)
- **Version Rollouts**: Use `defaultsByInstallVersion` for gradual feature releases
- **API Interaction**: Settings persist to `/settings` endpoint via `storeSetting()`

View File

@@ -0,0 +1,82 @@
# Settings and Feature Flags Sequence Diagram
This diagram shows the flow of settings initialization, default resolution, persistence, and feature flags exchange.
This diagram accurately reflects the actual implementation in the ComfyUI frontend codebase.
```mermaid
sequenceDiagram
participant User as User
participant Vue as Vue Component
participant Store as SettingStore (Pinia)
participant API as ComfyApi (WebSocket/REST)
participant Backend as Backend
participant NewUserSvc as NewUserService
Note over Vue,Store: App startup (GraphCanvas.vue)
Vue->>Store: loadSettingValues()
Store->>API: getSettings()
API->>Backend: GET /settings
Backend-->>API: settings map (per-user)
API-->>Store: settings map
Store-->>Vue: loaded
Vue->>Store: register CORE_SETTINGS (addSetting for each)
loop For each setting registration
Store->>Store: tryMigrateDeprecatedValue(existing value)
Store->>Store: onChange(setting, currentValue, undefined)
end
Note over Vue,NewUserSvc: New user detection
Vue->>NewUserSvc: initializeIfNewUser(settingStore)
NewUserSvc->>NewUserSvc: checkIsNewUser(settingStore)
alt New user detected
NewUserSvc->>Store: set("Comfy.InstalledVersion", __COMFYUI_FRONTEND_VERSION__)
Store->>Store: tryMigrateDeprecatedValue(newValue)
Store->>Store: onChange(setting, newValue, oldValue)
Store->>API: storeSetting(key, newValue)
API->>Backend: POST /settings/{id}
else Existing user
Note over NewUserSvc: Skip setting installed version
end
Note over Vue,Store: Component reads a setting
Vue->>Store: get(key)
Store->>Store: exists(key)?
alt User value exists
Store-->>Vue: return stored user value
else Not set by user
Store->>Store: getVersionedDefaultValue(key)
alt Versioned default matched (defaultsByInstallVersion)
Store-->>Vue: return versioned default
else No version match
Store->>Store: evaluate defaultValue (function or constant)
Note over Store: defaultValue can use window size,<br/>locale, env, etc.
Store-->>Vue: return computed default
end
end
Note over User,Store: User updates a setting
User->>Vue: changes setting in UI
Vue->>Store: set(key, newValue)
Store->>Store: tryMigrateDeprecatedValue(newValue)
Store->>Store: check if newValue === oldValue (early return if same)
Store->>Store: onChange(setting, newValue, oldValue)
Store->>Store: update settingValues[key]
Store->>API: storeSetting(key, newValue)
API->>Backend: POST /settings/{id}
Backend-->>API: 200 OK
API-->>Store: ack
Note over API,Backend: Feature Flags WebSocket Exchange
API->>Backend: WS connect
API->>Backend: send { type: "feature_flags", data: clientFeatureFlags.json }
Backend-->>API: WS send { type: "feature_flags", data: server flags }
API->>API: store serverFeatureFlags = data
Note over Vue,API: Feature flag consumption in UI/logic
Vue->>API: serverSupportsFeature(name)
API-->>Vue: boolean (true only if flag === true)
Vue->>API: getServerFeature(name, default)
API-->>Vue: value or default
```

View File

@@ -12,7 +12,7 @@ This directory contains the service layer for the ComfyUI frontend application.
## Overview
Services in ComfyUI provide organized modules that implement the application's functionality and logic. They handle operations such as API communication, workflow management, user settings, and other essential features.
Services in ComfyUI provide organized modules that implement the application's functionality and logic. They handle operations such as API communication, workflow management, user settings, and other essential features.
The term "business logic" in this context refers to the code that implements the core functionality and behavior of the application - the rules, processes, and operations that make ComfyUI work as expected, separate from the UI display code.
@@ -57,21 +57,25 @@ While services can interact with both UI components and stores (centralized stat
## Core Services
The following table lists ALL services in the system as of 2025-01-30:
The following table lists ALL services in the system as of 2025-09-01:
### Main Services
| Service | Description | Category |
|---------|-------------|----------|
| audioService.ts | Manages audio recording and WAV encoding functionality | Media |
| autoQueueService.ts | Manages automatic queue execution | Execution |
| colorPaletteService.ts | Handles color palette management and customization | UI |
| comfyManagerService.ts | Manages ComfyUI application packages and updates | Manager |
| comfyRegistryService.ts | Handles registration and discovery of ComfyUI extensions | Registry |
| customerEventsService.ts | Handles customer event tracking and audit logs | Analytics |
| dialogService.ts | Provides dialog and modal management | UI |
| extensionService.ts | Manages extension registration and lifecycle | Extensions |
| keybindingService.ts | Handles keyboard shortcuts and keybindings | Input |
| litegraphService.ts | Provides utilities for working with the LiteGraph library | Graph |
| load3dService.ts | Manages 3D model loading and visualization | 3D |
| mediaCacheService.ts | Manages media file caching with blob storage and cleanup | Media |
| newUserService.ts | Handles new user initialization and onboarding | System |
| nodeHelpService.ts | Provides node documentation and help | Nodes |
| nodeOrganizationService.ts | Handles node organization and categorization | Nodes |
| nodeSearchService.ts | Implements node search functionality | Search |
@@ -105,47 +109,82 @@ For complex services with state management and multiple methods, class-based ser
```typescript
export class NodeSearchService {
// Service state
private readonly nodeFuseSearch: FuseSearch<ComfyNodeDefImpl>
private readonly filters: Record<string, FuseFilter<ComfyNodeDefImpl, string>>
public readonly nodeFuseSearch: FuseSearch<ComfyNodeDefImpl>
public readonly inputTypeFilter: FuseFilter<ComfyNodeDefImpl, string>
public readonly outputTypeFilter: FuseFilter<ComfyNodeDefImpl, string>
public readonly nodeCategoryFilter: FuseFilter<ComfyNodeDefImpl, string>
public readonly nodeSourceFilter: FuseFilter<ComfyNodeDefImpl, string>
constructor(data: ComfyNodeDefImpl[]) {
// Initialize state
this.nodeFuseSearch = new FuseSearch(data, { /* options */ })
// Setup filters
this.filters = {
inputType: new FuseFilter<ComfyNodeDefImpl, string>(/* options */),
category: new FuseFilter<ComfyNodeDefImpl, string>(/* options */)
}
// Initialize search index
this.nodeFuseSearch = new FuseSearch(data, {
fuseOptions: {
keys: ['name', 'display_name'],
includeScore: true,
threshold: 0.3,
shouldSort: false,
useExtendedSearch: true
},
createIndex: true,
advancedScoring: true
})
// Setup individual filters
const fuseOptions = { includeScore: true, threshold: 0.3, shouldSort: true }
this.inputTypeFilter = new FuseFilter<ComfyNodeDefImpl, string>(data, {
id: 'input',
name: 'Input Type',
invokeSequence: 'i',
getItemOptions: (node) => Object.values(node.inputs).map((input) => input.type),
fuseOptions
})
// Additional filters initialized similarly...
}
public searchNode(query: string, filters: FuseFilterWithValue[] = []): ComfyNodeDefImpl[] {
// Implementation
return results
public searchNode(
query: string,
filters: FuseFilterWithValue<ComfyNodeDefImpl, string>[] = []
): ComfyNodeDefImpl[] {
const matchedNodes = this.nodeFuseSearch.search(query)
return matchedNodes.filter((node) => {
return filters.every((filterAndValue) => {
const { filterDef, value } = filterAndValue
return filterDef.matches(node, value, { wildcard: '*' })
})
})
}
get nodeFilters(): FuseFilter<ComfyNodeDefImpl, string>[] {
return [
this.inputTypeFilter,
this.outputTypeFilter,
this.nodeCategoryFilter,
this.nodeSourceFilter
]
}
}
```
### 2. Composable-style Services
For simpler services or those that need to integrate with Vue's reactivity system, we prefer using composable-style services:
For services that need to integrate with Vue's reactivity system or handle API interactions, we use composable-style services:
```typescript
export function useNodeSearchService(initialData: ComfyNodeDefImpl[]) {
// State (reactive if needed)
const data = ref(initialData)
// Search functionality
function searchNodes(query: string) {
// Implementation
return results
}
// Additional methods
function refreshData(newData: ComfyNodeDefImpl[]) {
data.value = newData
}
// Return public API
return {
searchNodes,
@@ -154,12 +193,35 @@ export function useNodeSearchService(initialData: ComfyNodeDefImpl[]) {
}
```
When deciding between these approaches, consider:
### Service Pattern Comparison
1. **Stateful vs. Stateless**: For stateful services, classes often provide clearer encapsulation
2. **Reactivity needs**: If the service needs to be reactive, composable-style services integrate better with Vue's reactivity system
3. **Complexity**: For complex services with many methods and internal state, classes can provide better organization
4. **Testing**: Both approaches can be tested effectively, but composables may be simpler to test with Vue Test Utils
| Aspect | Class-Based Services | Composable-Style Services | Bootstrap Services | Shared State Services |
|--------|---------------------|---------------------------|-------------------|---------------------|
| **Count** | 4 services | 18+ services | 1 service | 1 service |
| **Export Pattern** | `export class ServiceName` | `export function useServiceName()` | `export function setupX()` | `export function serviceFactory()` |
| **Instantiation** | `new ServiceName(data)` | `useServiceName()` | Direct function call | Direct function call |
| **Best For** | Complex data structures, search algorithms, expensive initialization | Vue integration, API calls, reactive state | One-time app initialization | Singleton-like shared state |
| **State Management** | Encapsulated private/public properties | External stores + reactive refs | Event listeners, side effects | Module-level state |
| **Vue Integration** | Manual integration needed | Native reactivity support | N/A | Varies |
| **Examples** | `NodeSearchService`, `Load3dService` | `workflowService`, `dialogService` | `autoQueueService` | `newUserService` |
### Decision Criteria
When choosing between these approaches, consider:
1. **Data Structure Complexity**: Classes work well for services managing multiple related data structures (search indices, filters, complex state)
2. **Initialization Cost**: Classes are ideal when expensive setup should happen once and be controlled by instantiation
3. **Vue Integration**: Composables integrate seamlessly with Vue's reactivity system and stores
4. **API Interactions**: Composables handle async operations and API calls more naturally
5. **State Management**: Classes provide strong encapsulation; composables work better with external state management
6. **Application Bootstrap**: Bootstrap services handle one-time app initialization, event listener setup, and side effects
7. **Singleton Behavior**: Shared state services provide module-level state that persists across multiple function calls
**Current Usage Patterns:**
- **Class-based services (4)**: Complex data processing, search algorithms, expensive initialization
- **Composable-style services (18+)**: UI interactions, API calls, store integration, reactive state management
- **Bootstrap services (1)**: One-time application initialization and event handler setup
- **Shared state services (1)**: Singleton-like behavior with module-level state management
### Service Template
@@ -172,7 +234,7 @@ Here's a template for creating a new composable-style service:
export function useExampleService() {
// Private state/functionality
const cache = new Map()
/**
* Description of what this method does
* @param param1 Description of parameter
@@ -188,7 +250,7 @@ export function useExampleService() {
throw error
}
}
// Return public API
return {
performOperation
@@ -206,16 +268,16 @@ Services in ComfyUI frequently use the following design patterns:
export function useCachedService() {
const cache = new Map()
const pendingRequests = new Map()
async function fetchData(key: string) {
// Check cache first
if (cache.has(key)) return cache.get(key)
// Check if request is already in progress
if (pendingRequests.has(key)) {
return pendingRequests.get(key)
}
// Perform new request
const requestPromise = fetch(`/api/${key}`)
.then(response => response.json())
@@ -224,11 +286,11 @@ export function useCachedService() {
pendingRequests.delete(key)
return data
})
pendingRequests.set(key, requestPromise)
return requestPromise
}
return { fetchData }
}
```
@@ -248,7 +310,7 @@ export function useNodeFactory() {
throw new Error(`Unknown node type: ${type}`)
}
}
return { createNode }
}
```
@@ -267,11 +329,243 @@ export function useWorkflowService(
const storagePath = await storageService.getPath(name)
return apiService.saveData(storagePath, graphData)
}
return { saveWorkflow }
}
```
## Testing Services
Services in ComfyUI can be tested effectively using different approaches depending on their implementation pattern.
### Testing Class-Based Services
**Setup Requirements:**
```typescript
// Manual instantiation required
const mockData = [/* test data */]
const service = new NodeSearchService(mockData)
```
**Characteristics:**
- Requires constructor argument preparation
- State is encapsulated within the class instance
- Direct method calls on the instance
- Good isolation - each test gets a fresh instance
**Example:**
```typescript
describe('NodeSearchService', () => {
let service: NodeSearchService
beforeEach(() => {
const mockNodes = [/* mock node definitions */]
service = new NodeSearchService(mockNodes)
})
test('should search nodes by query', () => {
const results = service.searchNode('test query')
expect(results).toHaveLength(2)
})
test('should apply filters correctly', () => {
const filters = [{ filterDef: service.inputTypeFilter, value: 'IMAGE' }]
const results = service.searchNode('*', filters)
expect(results.every(node => /* has IMAGE input */)).toBe(true)
})
})
```
### Testing Composable-Style Services
**Setup Requirements:**
```typescript
// Direct function call, no instantiation
const { saveWorkflow, loadWorkflow } = useWorkflowService()
```
**Characteristics:**
- No instantiation needed
- Integrates naturally with Vue Test Utils
- Easy mocking of reactive dependencies
- External store dependencies need mocking
**Example:**
```typescript
describe('useWorkflowService', () => {
beforeEach(() => {
// Mock external dependencies
vi.mock('@/stores/settingStore', () => ({
useSettingStore: () => ({
get: vi.fn().mockReturnValue(true),
set: vi.fn()
})
}))
vi.mock('@/stores/toastStore', () => ({
useToastStore: () => ({
add: vi.fn()
})
}))
})
test('should save workflow with prompt', async () => {
const { saveWorkflow } = useWorkflowService()
await saveWorkflow('test-workflow')
// Verify interactions with mocked dependencies
expect(mockSettingStore.get).toHaveBeenCalledWith('Comfy.PromptFilename')
})
})
```
### Testing Bootstrap Services
**Focus on Setup Behavior:**
```typescript
describe('autoQueueService', () => {
beforeEach(() => {
// Mock global dependencies
vi.mock('@/scripts/api', () => ({
api: {
addEventListener: vi.fn()
}
}))
vi.mock('@/scripts/app', () => ({
app: {
queuePrompt: vi.fn()
}
}))
})
test('should setup event listeners', () => {
setupAutoQueueHandler()
expect(mockApi.addEventListener).toHaveBeenCalledWith('graphChanged', expect.any(Function))
})
test('should handle graph changes when auto-queue enabled', () => {
setupAutoQueueHandler()
// Simulate graph change event
const graphChangeHandler = mockApi.addEventListener.mock.calls[0][1]
graphChangeHandler()
expect(mockApp.queuePrompt).toHaveBeenCalled()
})
})
```
### Testing Shared State Services
**Focus on Shared State Behavior:**
```typescript
describe('newUserService', () => {
beforeEach(() => {
// Reset module state between tests
vi.resetModules()
})
test('should return consistent API across calls', () => {
const service1 = newUserService()
const service2 = newUserService()
// Same functions returned (shared behavior)
expect(service1.isNewUser).toBeDefined()
expect(service2.isNewUser).toBeDefined()
})
test('should share state between service instances', async () => {
const service1 = newUserService()
const service2 = newUserService()
// Initialize through one instance
const mockSettingStore = { set: vi.fn() }
await service1.initializeIfNewUser(mockSettingStore)
// State should be shared
expect(service2.isNewUser()).toBe(true) // or false, depending on mock
})
})
```
### Common Testing Patterns
**Mocking External Dependencies:**
```typescript
// Mock stores
vi.mock('@/stores/settingStore', () => ({
useSettingStore: () => ({
get: vi.fn(),
set: vi.fn()
})
}))
// Mock API calls
vi.mock('@/scripts/api', () => ({
api: {
get: vi.fn().mockResolvedValue({ data: 'mock' }),
post: vi.fn().mockResolvedValue({ success: true })
}
}))
// Mock Vue composables
vi.mock('vue', () => ({
ref: vi.fn((val) => ({ value: val })),
reactive: vi.fn((obj) => obj)
}))
```
**Async Testing:**
```typescript
test('should handle async operations', async () => {
const service = useMyService()
const result = await service.performAsyncOperation()
expect(result).toBeTruthy()
})
test('should handle concurrent requests', async () => {
const service = useMyService()
const promises = [
service.loadData('key1'),
service.loadData('key2')
]
const results = await Promise.all(promises)
expect(results).toHaveLength(2)
})
```
**Error Handling:**
```typescript
test('should handle service errors gracefully', async () => {
const service = useMyService()
// Mock API to throw error
mockApi.get.mockRejectedValue(new Error('Network error'))
await expect(service.fetchData()).rejects.toThrow('Network error')
})
test('should provide meaningful error messages', async () => {
const service = useMyService()
const consoleSpy = vi.spyOn(console, 'error').mockImplementation()
await service.handleError('test error')
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('test error'))
})
```
### Testing Best Practices
1. **Isolate Dependencies**: Always mock external dependencies (stores, APIs, DOM)
2. **Reset State**: Use `beforeEach` to ensure clean test state
3. **Test Error Paths**: Don't just test happy paths - test error scenarios
4. **Mock Timers**: Use `vi.useFakeTimers()` for time-dependent services
5. **Test Async Properly**: Use `async/await` and proper promise handling
For more detailed information about the service layer pattern and its applications, refer to:
- [Service Layer Pattern](https://en.wikipedia.org/wiki/Service_layer_pattern)
- [Service-Orientation](https://en.wikipedia.org/wiki/Service-orientation)

View File

@@ -100,57 +100,64 @@ The following diagram illustrates the store architecture and data flow:
## Core Stores
The following table lists ALL stores in the system as of 2025-01-30:
The following table lists ALL 46 store instances in the system as of 2025-09-01:
### Main Stores
| Store | Description | Category |
|-------|-------------|----------|
| aboutPanelStore.ts | Manages the About panel state and badges | UI |
| apiKeyAuthStore.ts | Handles API key authentication | Auth |
| comfyManagerStore.ts | Manages ComfyUI application state | Core |
| comfyRegistryStore.ts | Handles extensions registry | Registry |
| commandStore.ts | Manages commands and command execution | Core |
| dialogStore.ts | Controls dialog/modal display and state | UI |
| domWidgetStore.ts | Manages DOM widget state | Widgets |
| electronDownloadStore.ts | Handles Electron-specific download operations | Platform |
| executionStore.ts | Tracks workflow execution state | Execution |
| extensionStore.ts | Manages extension registration and state | Extensions |
| firebaseAuthStore.ts | Handles Firebase authentication | Auth |
| graphStore.ts | Manages the graph canvas state | Core |
| imagePreviewStore.ts | Controls image preview functionality | Media |
| keybindingStore.ts | Manages keyboard shortcuts | Input |
| maintenanceTaskStore.ts | Handles system maintenance tasks | System |
| menuItemStore.ts | Handles menu items and their state | UI |
| modelStore.ts | Manages AI models information | Models |
| modelToNodeStore.ts | Maps models to compatible nodes | Models |
| nodeBookmarkStore.ts | Manages node bookmarks and favorites | Nodes |
| nodeDefStore.ts | Manages node definitions | Nodes |
| queueStore.ts | Handles the execution queue | Execution |
| releaseStore.ts | Manages application release information | System |
| serverConfigStore.ts | Handles server configuration | Config |
| settingStore.ts | Manages application settings | Config |
| subgraphNavigationStore.ts | Handles subgraph navigation state | Navigation |
| systemStatsStore.ts | Tracks system performance statistics | System |
| toastStore.ts | Manages toast notifications | UI |
| userFileStore.ts | Manages user file operations | Files |
| userStore.ts | Manages user data and preferences | User |
| versionCompatibilityStore.ts | Manages frontend/backend version compatibility warnings | Core |
| widgetStore.ts | Manages widget configurations | Widgets |
| workflowStore.ts | Handles workflow data and operations | Workflows |
| workflowTemplatesStore.ts | Manages workflow templates | Workflows |
| workspaceStore.ts | Manages overall workspace state | Workspace |
| File | Store | Description | Category |
|------|-------|-------------|----------|
| aboutPanelStore.ts | useAboutPanelStore | Manages the About panel state and badges | UI |
| apiKeyAuthStore.ts | useApiKeyAuthStore | Handles API key authentication | Auth |
| comfyManagerStore.ts | useComfyManagerStore | Manages ComfyUI application state | Core |
| comfyManagerStore.ts | useManagerProgressDialogStore | Manages manager progress dialog state | UI |
| comfyRegistryStore.ts | useComfyRegistryStore | Handles extensions registry | Registry |
| commandStore.ts | useCommandStore | Manages commands and command execution | Core |
| dialogStore.ts | useDialogStore | Controls dialog/modal display and state | UI |
| domWidgetStore.ts | useDomWidgetStore | Manages DOM widget state | Widgets |
| electronDownloadStore.ts | useElectronDownloadStore | Handles Electron-specific download operations | Platform |
| executionStore.ts | useExecutionStore | Tracks workflow execution state | Execution |
| extensionStore.ts | useExtensionStore | Manages extension registration and state | Extensions |
| firebaseAuthStore.ts | useFirebaseAuthStore | Handles Firebase authentication | Auth |
| graphStore.ts | useTitleEditorStore | Manages title editing for nodes and groups | UI |
| graphStore.ts | useCanvasStore | Manages the graph canvas state and interactions | Core |
| helpCenterStore.ts | useHelpCenterStore | Manages help center visibility and state | UI |
| imagePreviewStore.ts | useNodeOutputStore | Manages node outputs and execution results | Media |
| keybindingStore.ts | useKeybindingStore | Manages keyboard shortcuts | Input |
| maintenanceTaskStore.ts | useMaintenanceTaskStore | Handles system maintenance tasks | System |
| menuItemStore.ts | useMenuItemStore | Handles menu items and their state | UI |
| modelStore.ts | useModelStore | Manages AI models information | Models |
| modelToNodeStore.ts | useModelToNodeStore | Maps models to compatible nodes | Models |
| nodeBookmarkStore.ts | useNodeBookmarkStore | Manages node bookmarks and favorites | Nodes |
| nodeDefStore.ts | useNodeDefStore | Manages node definitions and schemas | Nodes |
| nodeDefStore.ts | useNodeFrequencyStore | Tracks node usage frequency | Nodes |
| queueStore.ts | useQueueStore | Manages execution queue and task history | Execution |
| queueStore.ts | useQueuePendingTaskCountStore | Tracks pending task counts | Execution |
| queueStore.ts | useQueueSettingsStore | Manages queue execution settings | Execution |
| releaseStore.ts | useReleaseStore | Manages application release information | System |
| serverConfigStore.ts | useServerConfigStore | Handles server configuration | Config |
| settingStore.ts | useSettingStore | Manages application settings | Config |
| subgraphNavigationStore.ts | useSubgraphNavigationStore | Handles subgraph navigation state | Navigation |
| systemStatsStore.ts | useSystemStatsStore | Tracks system performance statistics | System |
| toastStore.ts | useToastStore | Manages toast notifications | UI |
| userFileStore.ts | useUserFileStore | Manages user file operations | Files |
| userStore.ts | useUserStore | Manages user data and preferences | User |
| versionCompatibilityStore.ts | useVersionCompatibilityStore | Manages frontend/backend version compatibility warnings | Core |
| widgetStore.ts | useWidgetStore | Manages widget configurations | Widgets |
| workflowStore.ts | useWorkflowStore | Handles workflow data and operations | Workflows |
| workflowStore.ts | useWorkflowBookmarkStore | Manages workflow bookmarks and favorites | Workflows |
| workflowTemplatesStore.ts | useWorkflowTemplatesStore | Manages workflow templates | Workflows |
| workspaceStore.ts | useWorkspaceStore | Manages overall workspace state | Workspace |
### Workspace Stores
Located in `stores/workspace/`:
| Store | Description |
|-------|-------------|
| bottomPanelStore.ts | Controls bottom panel visibility and state |
| colorPaletteStore.ts | Manages color palette configurations |
| nodeHelpStore.ts | Handles node help and documentation display |
| searchBoxStore.ts | Manages search box functionality |
| sidebarTabStore.ts | Controls sidebar tab states and navigation |
| File | Store | Description | Category |
|------|-------|-------------|----------|
| bottomPanelStore.ts | useBottomPanelStore | Controls bottom panel visibility and state | UI |
| colorPaletteStore.ts | useColorPaletteStore | Manages color palette configurations | UI |
| nodeHelpStore.ts | useNodeHelpStore | Handles node help and documentation display | UI |
| searchBoxStore.ts | useSearchBoxStore | Manages search box functionality | UI |
| sidebarTabStore.ts | useSidebarTabStore | Controls sidebar tab states and navigation | UI |
## Store Development Guidelines
@@ -189,7 +196,7 @@ export const useExampleStore = defineStore('example', () => {
async function fetchItems() {
isLoading.value = true
error.value = null
try {
const response = await fetch('/api/items')
const data = await response.json()
@@ -207,11 +214,11 @@ export const useExampleStore = defineStore('example', () => {
items,
isLoading,
error,
// Getters
itemCount,
hasError,
// Actions
addItem,
fetchItems
@@ -238,7 +245,7 @@ export const useDataStore = defineStore('data', () => {
async function fetchData() {
loading.value = true
try {
const result = await api.getData()
const result = await api.getExtensions()
data.value = result
} catch (err) {
error.value = err.message
@@ -266,21 +273,21 @@ import { useOtherStore } from './otherStore'
export const useComposedStore = defineStore('composed', () => {
const otherStore = useOtherStore()
const { someData } = storeToRefs(otherStore)
// Local state
const localState = ref(0)
// Computed value based on other store
const derivedValue = computed(() => {
return computeFromOtherData(someData.value, localState.value)
})
// Action that uses another store
async function complexAction() {
await otherStore.someAction()
localState.value += 1
}
return {
localState,
derivedValue,
@@ -299,20 +306,20 @@ export const usePreferencesStore = defineStore('preferences', () => {
// Load from localStorage if available
const theme = ref(localStorage.getItem('theme') || 'light')
const fontSize = ref(parseInt(localStorage.getItem('fontSize') || '14'))
// Save to localStorage when changed
watch(theme, (newTheme) => {
localStorage.setItem('theme', newTheme)
})
watch(fontSize, (newSize) => {
localStorage.setItem('fontSize', newSize.toString())
})
function setTheme(newTheme) {
theme.value = newTheme
}
return {
theme,
fontSize,
@@ -347,7 +354,7 @@ describe('useExampleStore', () => {
// Create a fresh pinia instance and make it active
setActivePinia(createPinia())
store = useExampleStore()
// Clear all mocks
vi.clearAllMocks()
})
@@ -363,14 +370,14 @@ describe('useExampleStore', () => {
expect(store.items).toEqual(['test'])
expect(store.itemCount).toBe(1)
})
it('should fetch items', async () => {
// Setup mock response
vi.mocked(api.getData).mockResolvedValue(['item1', 'item2'])
// Call the action
await store.fetchItems()
// Verify state changes
expect(store.isLoading).toBe(false)
expect(store.items).toEqual(['item1', 'item2'])