mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-27 10:14:06 +00:00
* docs: add Claude documentation for settings and feature flags * docs: update services README.md * docs: update stores README.md
571 lines
20 KiB
Markdown
571 lines
20 KiB
Markdown
# Services
|
|
|
|
This directory contains the service layer for the ComfyUI frontend application. Services encapsulate application logic and functionality into organized, reusable modules.
|
|
|
|
## Table of Contents
|
|
|
|
- [Overview](#overview)
|
|
- [Service Architecture](#service-architecture)
|
|
- [Core Services](#core-services)
|
|
- [Service Development Guidelines](#service-development-guidelines)
|
|
- [Common Design Patterns](#common-design-patterns)
|
|
|
|
## 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.
|
|
|
|
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.
|
|
|
|
Services help organize related functionality into cohesive units, making the codebase more maintainable and testable. By centralizing related operations in services, the application achieves better separation of concerns, with UI components focusing on presentation and services handling functional operations.
|
|
|
|
## Service Architecture
|
|
|
|
The service layer in ComfyUI follows these architectural principles:
|
|
|
|
1. **Domain-driven**: Each service focuses on a specific domain of the application
|
|
2. **Stateless when possible**: Services generally avoid maintaining internal state
|
|
3. **Reusable**: Services can be used across multiple components
|
|
4. **Testable**: Services are designed for easy unit testing
|
|
5. **Isolated**: Services have clear boundaries and dependencies
|
|
|
|
While services can interact with both UI components and stores (centralized state), they primarily focus on implementing functionality rather than managing state. The following diagram illustrates how services fit into the application architecture:
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ UI Components │
|
|
└────────────────────────────┬────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ Composables │
|
|
└────────────────────────────┬────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ Services │
|
|
│ │
|
|
│ (Application Functionality) │
|
|
└────────────────────────────┬────────────────────────────┘
|
|
│
|
|
┌───────────┴───────────┐
|
|
▼ ▼
|
|
┌───────────────────────────┐ ┌─────────────────────────┐
|
|
│ Stores │ │ External APIs │
|
|
│ (Centralized State) │ │ │
|
|
└───────────────────────────┘ └─────────────────────────┘
|
|
```
|
|
|
|
## Core Services
|
|
|
|
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 |
|
|
| releaseService.ts | Manages application release information and updates | System |
|
|
| subgraphService.ts | Handles subgraph operations and navigation | Graph |
|
|
| workflowService.ts | Handles workflow operations (save, load, execute) | Workflows |
|
|
|
|
### Gateway Services
|
|
Located in `services/gateway/`:
|
|
|
|
| Service | Description |
|
|
|---------|-------------|
|
|
| registrySearchGateway.ts | Gateway for registry search operations |
|
|
|
|
### Provider Services
|
|
Located in `services/providers/`:
|
|
|
|
| Service | Description |
|
|
|---------|-------------|
|
|
| algoliaSearchProvider.ts | Implements search functionality using Algolia |
|
|
| registrySearchProvider.ts | Provides registry search capabilities |
|
|
|
|
## Service Development Guidelines
|
|
|
|
In ComfyUI, services can be implemented using two approaches:
|
|
|
|
### 1. Class-based Services
|
|
|
|
For complex services with state management and multiple methods, class-based services are used:
|
|
|
|
```typescript
|
|
export class NodeSearchService {
|
|
// Service state
|
|
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 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, 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 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,
|
|
refreshData
|
|
}
|
|
}
|
|
```
|
|
|
|
### Service Pattern Comparison
|
|
|
|
| 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
|
|
|
|
Here's a template for creating a new composable-style service:
|
|
|
|
```typescript
|
|
/**
|
|
* Service for managing [domain/functionality]
|
|
*/
|
|
export function useExampleService() {
|
|
// Private state/functionality
|
|
const cache = new Map()
|
|
|
|
/**
|
|
* Description of what this method does
|
|
* @param param1 Description of parameter
|
|
* @returns Description of return value
|
|
*/
|
|
async function performOperation(param1: string) {
|
|
try {
|
|
// Implementation
|
|
return result
|
|
} catch (error) {
|
|
// Error handling
|
|
console.error(`Operation failed: ${error.message}`)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// Return public API
|
|
return {
|
|
performOperation
|
|
}
|
|
}
|
|
```
|
|
|
|
## Common Design Patterns
|
|
|
|
Services in ComfyUI frequently use the following design patterns:
|
|
|
|
### Caching and Request Deduplication
|
|
|
|
```typescript
|
|
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())
|
|
.then(data => {
|
|
cache.set(key, data)
|
|
pendingRequests.delete(key)
|
|
return data
|
|
})
|
|
|
|
pendingRequests.set(key, requestPromise)
|
|
return requestPromise
|
|
}
|
|
|
|
return { fetchData }
|
|
}
|
|
```
|
|
|
|
### Factory Pattern
|
|
|
|
```typescript
|
|
export function useNodeFactory() {
|
|
function createNode(type: string, config: Record<string, any>) {
|
|
// Create node based on type and configuration
|
|
switch (type) {
|
|
case 'basic':
|
|
return { /* basic node implementation */ }
|
|
case 'complex':
|
|
return { /* complex node implementation */ }
|
|
default:
|
|
throw new Error(`Unknown node type: ${type}`)
|
|
}
|
|
}
|
|
|
|
return { createNode }
|
|
}
|
|
```
|
|
|
|
### Facade Pattern
|
|
|
|
```typescript
|
|
export function useWorkflowService(
|
|
apiService,
|
|
graphService,
|
|
storageService
|
|
) {
|
|
// Provides a simple interface to complex subsystems
|
|
async function saveWorkflow(name: string) {
|
|
const graphData = graphService.serializeGraph()
|
|
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) |