|
| 1 | +# ComfyUI Feature Flags System |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +The ComfyUI feature flags system enables capability negotiation between frontend and backend, allowing both sides to communicate their supported features and adapt behavior accordingly. This ensures backward compatibility while enabling progressive enhancement of features. |
| 6 | + |
| 7 | +## System Architecture |
| 8 | + |
| 9 | +### High-Level Flow |
| 10 | + |
| 11 | +```mermaid |
| 12 | +sequenceDiagram |
| 13 | + participant Frontend |
| 14 | + participant WebSocket |
| 15 | + participant Backend |
| 16 | + participant FeatureFlags Module |
| 17 | +
|
| 18 | + Frontend->>WebSocket: Connect |
| 19 | + WebSocket-->>Frontend: Connection established |
| 20 | + |
| 21 | + Note over Frontend: First message must be feature flags |
| 22 | + Frontend->>WebSocket: Send client feature flags |
| 23 | + WebSocket->>Backend: Receive feature flags |
| 24 | + Backend->>FeatureFlags Module: Store client capabilities |
| 25 | + |
| 26 | + Backend->>FeatureFlags Module: Get server features |
| 27 | + FeatureFlags Module-->>Backend: Return server capabilities |
| 28 | + Backend->>WebSocket: Send server feature flags |
| 29 | + WebSocket-->>Frontend: Receive server features |
| 30 | + |
| 31 | + Note over Frontend,Backend: Both sides now know each other's capabilities |
| 32 | + |
| 33 | + Frontend->>Frontend: Store server features |
| 34 | + Frontend->>Frontend: Components use useFeatureFlags() |
| 35 | +``` |
| 36 | + |
| 37 | +### Component Architecture |
| 38 | + |
| 39 | +```mermaid |
| 40 | +graph TB |
| 41 | + subgraph Frontend |
| 42 | + A[clientFeatureFlags.json] --> B[api.ts] |
| 43 | + B --> C[WebSocket Handler] |
| 44 | + D[useFeatureFlags composable] --> B |
| 45 | + E[Vue Components] --> D |
| 46 | + end |
| 47 | + |
| 48 | + subgraph Backend |
| 49 | + F[feature_flags.py] --> G[SERVER_FEATURE_FLAGS] |
| 50 | + H[server.py WebSocket] --> F |
| 51 | + I[Feature Consumers] --> F |
| 52 | + end |
| 53 | + |
| 54 | + C <--> H |
| 55 | + |
| 56 | + style A fill:#f9f,stroke:#333,stroke-width:2px |
| 57 | + style G fill:#f9f,stroke:#333,stroke-width:2px |
| 58 | + style D fill:#9ff,stroke:#333,stroke-width:2px |
| 59 | +``` |
| 60 | + |
| 61 | +## Feature Flag Structure |
| 62 | + |
| 63 | +Feature flags are organized as a flat dictionary at the top level, with extensions nested under an `extension` object: |
| 64 | + |
| 65 | +### Naming Convention |
| 66 | + |
| 67 | +- **Core features**: Top-level keys (e.g., `"async_execution"`, `"supports_batch_queue"`) |
| 68 | +- **Client features**: Top-level keys (e.g., `"supports_preview_metadata"`) |
| 69 | +- **Extensions**: Nested under `"extension"` object (e.g., `extension.manager`) |
| 70 | + |
| 71 | +### Structure Example |
| 72 | + |
| 73 | +```json |
| 74 | +{ |
| 75 | + "async_execution": true, |
| 76 | + "supports_batch_queue": false, |
| 77 | + "supports_preview_metadata": true, |
| 78 | + "supports_websocket_v2": false, |
| 79 | + "max_upload_size": 104857600, |
| 80 | + "extension": { |
| 81 | + "manager": { |
| 82 | + "supports_v4": true, |
| 83 | + "supports_ai_search": false |
| 84 | + } |
| 85 | + } |
| 86 | +} |
| 87 | +``` |
| 88 | + |
| 89 | +## Implementation Details |
| 90 | + |
| 91 | +### Backend Implementation |
| 92 | + |
| 93 | +```mermaid |
| 94 | +classDiagram |
| 95 | + class FeatureFlagsModule { |
| 96 | + +SERVER_FEATURE_FLAGS: Dict |
| 97 | + +get_server_features() Dict |
| 98 | + +supports_feature(sockets_metadata, sid, feature_name) bool |
| 99 | + +get_connection_feature(sockets_metadata, sid, feature_name, default) Any |
| 100 | + } |
| 101 | + |
| 102 | + class PromptServer { |
| 103 | + -sockets_metadata: Dict |
| 104 | + +websocket_handler() |
| 105 | + +send() |
| 106 | + } |
| 107 | + |
| 108 | + class FeatureConsumer { |
| 109 | + <<interface>> |
| 110 | + +check_feature() |
| 111 | + +use_feature() |
| 112 | + } |
| 113 | + |
| 114 | + PromptServer --> FeatureFlagsModule |
| 115 | + FeatureConsumer --> FeatureFlagsModule |
| 116 | +``` |
| 117 | + |
| 118 | +### Frontend Implementation |
| 119 | + |
| 120 | +The `useFeatureFlags` composable provides reactive access to feature flags, meaning components will automatically update when feature flags change (e.g., during WebSocket reconnection). |
| 121 | + |
| 122 | +```mermaid |
| 123 | +classDiagram |
| 124 | + class ComfyApi { |
| 125 | + +serverFeatureFlags: Record~string, unknown~ |
| 126 | + +getClientFeatureFlags() Record |
| 127 | + +serverSupportsFeature(name) boolean |
| 128 | + +getServerFeature(name, default) T |
| 129 | + } |
| 130 | + |
| 131 | + class useFeatureFlags { |
| 132 | + +serverSupports(name) boolean |
| 133 | + +getServerFeature(name, default) T |
| 134 | + +createServerFeatureFlag(name) ComputedRef |
| 135 | + +extension: ExtensionFlags |
| 136 | + } |
| 137 | + |
| 138 | + class VueComponent { |
| 139 | + <<component>> |
| 140 | + +setup() |
| 141 | + } |
| 142 | + |
| 143 | + ComfyApi <-- useFeatureFlags |
| 144 | + VueComponent --> useFeatureFlags |
| 145 | +``` |
| 146 | + |
| 147 | +## Examples |
| 148 | + |
| 149 | +### 1. Preview Metadata Support |
| 150 | + |
| 151 | +```mermaid |
| 152 | +graph LR |
| 153 | + A[Preview Generation] --> B{supports_preview_metadata?} |
| 154 | + B -->|Yes| C[Send metadata with preview] |
| 155 | + B -->|No| D[Send preview only] |
| 156 | + |
| 157 | + C --> E[Enhanced preview with node info] |
| 158 | + D --> F[Basic preview image] |
| 159 | +``` |
| 160 | + |
| 161 | +**Backend Usage:** |
| 162 | +```python |
| 163 | +# Check if client supports preview metadata |
| 164 | +if feature_flags.supports_feature( |
| 165 | + self.server_instance.sockets_metadata, |
| 166 | + self.server_instance.client_id, |
| 167 | + "supports_preview_metadata" |
| 168 | +): |
| 169 | + # Send enhanced preview with metadata |
| 170 | + metadata = { |
| 171 | + "node_id": node_id, |
| 172 | + "prompt_id": prompt_id, |
| 173 | + "display_node_id": display_node_id, |
| 174 | + "parent_node_id": parent_node_id, |
| 175 | + "real_node_id": real_node_id, |
| 176 | + } |
| 177 | + self.server_instance.send_sync( |
| 178 | + BinaryEventTypes.PREVIEW_IMAGE_WITH_METADATA, |
| 179 | + (image, metadata), |
| 180 | + self.server_instance.client_id, |
| 181 | + ) |
| 182 | +``` |
| 183 | + |
| 184 | +### 2. Max Upload Size |
| 185 | + |
| 186 | +```mermaid |
| 187 | +graph TB |
| 188 | + A[Client File Upload] --> B[Check max_upload_size] |
| 189 | + B --> C{File size OK?} |
| 190 | + C -->|Yes| D[Upload file] |
| 191 | + C -->|No| E[Show error] |
| 192 | + |
| 193 | + F[Backend] --> G[Set from CLI args] |
| 194 | + G --> H[Convert MB to bytes] |
| 195 | + H --> I[Include in feature flags] |
| 196 | +``` |
| 197 | + |
| 198 | +**Backend Configuration:** |
| 199 | +```python |
| 200 | +# In feature_flags.py |
| 201 | +SERVER_FEATURE_FLAGS = { |
| 202 | + "supports_preview_metadata": True, |
| 203 | + "max_upload_size": args.max_upload_size * 1024 * 1024, # Convert MB to bytes |
| 204 | +} |
| 205 | +``` |
| 206 | + |
| 207 | +**Frontend Usage:** |
| 208 | +```typescript |
| 209 | +const { getServerFeature } = useFeatureFlags() |
| 210 | +const maxUploadSize = getServerFeature('max_upload_size', 100 * 1024 * 1024) // Default 100MB |
| 211 | +``` |
| 212 | + |
| 213 | +## Using Feature Flags |
| 214 | + |
| 215 | +### Frontend Access Patterns |
| 216 | + |
| 217 | +1. **Direct API access:** |
| 218 | +```typescript |
| 219 | +// Check boolean feature |
| 220 | +if (api.serverSupportsFeature('supports_preview_metadata')) { |
| 221 | + // Feature is supported |
| 222 | +} |
| 223 | + |
| 224 | +// Get feature value with default |
| 225 | +const maxSize = api.getServerFeature('max_upload_size', 100 * 1024 * 1024) |
| 226 | +``` |
| 227 | + |
| 228 | +2. **Using the composable (recommended for reactive components):** |
| 229 | +```typescript |
| 230 | +const { serverSupports, getServerFeature, extension } = useFeatureFlags() |
| 231 | + |
| 232 | +// Check feature support |
| 233 | +if (serverSupports('supports_preview_metadata')) { |
| 234 | + // Use enhanced previews |
| 235 | +} |
| 236 | + |
| 237 | +// Use reactive convenience properties (automatically update if flags change) |
| 238 | +if (extension.manager.supportsV4.value) { |
| 239 | + // Use V4 manager API |
| 240 | +} |
| 241 | +``` |
| 242 | + |
| 243 | +3. **Reactive usage in templates:** |
| 244 | +```vue |
| 245 | +<template> |
| 246 | + <div v-if="featureFlags.extension.manager.supportsV4"> |
| 247 | + <!-- V4-specific UI --> |
| 248 | + </div> |
| 249 | + <div v-else> |
| 250 | + <!-- Legacy UI --> |
| 251 | + </div> |
| 252 | +</template> |
| 253 | +
|
| 254 | +<script setup> |
| 255 | +import { useFeatureFlags } from '@/composables/useFeatureFlags' |
| 256 | +const featureFlags = useFeatureFlags() |
| 257 | +</script> |
| 258 | +``` |
| 259 | + |
| 260 | +### Backend Access Patterns |
| 261 | + |
| 262 | +```python |
| 263 | +# Check if a specific client supports a feature |
| 264 | +if feature_flags.supports_feature( |
| 265 | + sockets_metadata, |
| 266 | + client_id, |
| 267 | + "supports_preview_metadata" |
| 268 | +): |
| 269 | + # Client supports this feature |
| 270 | + |
| 271 | +# Get feature value with default |
| 272 | +max_size = feature_flags.get_connection_feature( |
| 273 | + sockets_metadata, |
| 274 | + client_id, |
| 275 | + "max_upload_size", |
| 276 | + 100 * 1024 * 1024 # Default 100MB |
| 277 | +) |
| 278 | +``` |
| 279 | + |
| 280 | +## Adding New Feature Flags |
| 281 | + |
| 282 | +### Backend |
| 283 | + |
| 284 | +1. **For server capabilities**, add to `SERVER_FEATURE_FLAGS` in `comfy_api/feature_flags.py`: |
| 285 | +```python |
| 286 | +SERVER_FEATURE_FLAGS = { |
| 287 | + "supports_preview_metadata": True, |
| 288 | + "max_upload_size": args.max_upload_size * 1024 * 1024, |
| 289 | + "your_new_feature": True, # Add your flag |
| 290 | +} |
| 291 | +``` |
| 292 | + |
| 293 | +2. **Use in your code:** |
| 294 | +```python |
| 295 | +if feature_flags.supports_feature(sockets_metadata, sid, "your_new_feature"): |
| 296 | + # Feature-specific code |
| 297 | +``` |
| 298 | + |
| 299 | +### Frontend |
| 300 | + |
| 301 | +1. **For client capabilities**, add to `src/config/clientFeatureFlags.json`: |
| 302 | +```json |
| 303 | +{ |
| 304 | + "supports_preview_metadata": false, |
| 305 | + "your_new_feature": true |
| 306 | +} |
| 307 | +``` |
| 308 | + |
| 309 | +2. **For extension features**, update the composable to add convenience accessors: |
| 310 | +```typescript |
| 311 | +// In useFeatureFlags.ts |
| 312 | +const extension = { |
| 313 | + manager: { |
| 314 | + supportsV4: computed(() => getServerFeature('extension.manager.supports_v4', false)) |
| 315 | + }, |
| 316 | + yourExtension: { |
| 317 | + supportsNewFeature: computed(() => getServerFeature('extension.yourExtension.supports_new_feature', false)) |
| 318 | + } |
| 319 | +} |
| 320 | + |
| 321 | +return { |
| 322 | + // ... existing returns |
| 323 | + extension |
| 324 | +} |
| 325 | +``` |
| 326 | + |
| 327 | +## Testing Feature Flags |
| 328 | + |
| 329 | +```mermaid |
| 330 | +graph LR |
| 331 | + A[Test Scenarios] --> B[Both support feature] |
| 332 | + A --> C[Only frontend supports] |
| 333 | + A --> D[Only backend supports] |
| 334 | + A --> E[Neither supports] |
| 335 | + |
| 336 | + B --> F[Feature enabled] |
| 337 | + C --> G[Feature disabled] |
| 338 | + D --> H[Feature disabled] |
| 339 | + E --> I[Feature disabled] |
| 340 | +``` |
| 341 | + |
| 342 | +Test your feature flags with different combinations: |
| 343 | +- Frontend with flag + Backend with flag = Feature works |
| 344 | +- Frontend with flag + Backend without = Graceful degradation |
| 345 | +- Frontend without + Backend with flag = No feature usage |
| 346 | +- Neither has flag = Default behavior |
| 347 | + |
| 348 | +### Example Test |
| 349 | + |
| 350 | +```typescript |
| 351 | +// In tests-ui/tests/api.featureFlags.test.ts |
| 352 | +it('should handle preview metadata based on feature flag', () => { |
| 353 | + // Mock server supports feature |
| 354 | + api.serverFeatureFlags = { supports_preview_metadata: true } |
| 355 | + |
| 356 | + expect(api.serverSupportsFeature('supports_preview_metadata')).toBe(true) |
| 357 | + |
| 358 | + // Mock server doesn't support feature |
| 359 | + api.serverFeatureFlags = {} |
| 360 | + |
| 361 | + expect(api.serverSupportsFeature('supports_preview_metadata')).toBe(false) |
| 362 | +}) |
0 commit comments