- Add FeatureFlaggedRunButton wrapper component - Add ExperimentalRunButton with JSON-driven styling - Extend useFeatureFlags to support remoteConfig and variant payloads - Support 4 experimental button variants (bold-gradient, animated, playful, minimal) - Add documentation for feature flag payload structure NOTE: This is a DEMO and should NOT be merged
7.1 KiB
Feature Flags System Explanation
Overview
The useFeatureFlag hook (actually named useFeatureFlags) is a Vue 3 composable that provides reactive access to server-side feature flags received via WebSocket from the backend. It enables capability negotiation between frontend and backend, allowing the UI to adapt based on what features the server supports.
Architecture Flow
1. Frontend connects via WebSocket
2. Frontend sends client feature flags (first message)
3. Backend responds with server feature flags
4. Frontend stores flags in api.serverFeatureFlags
5. Components use useFeatureFlags() to access flags reactively
Core Implementation
1. The useFeatureFlags Composable
Location: src/composables/useFeatureFlags.ts
The composable returns two things:
A. Predefined flags Object
A reactive object with getter properties for commonly-used feature flags:
const { flags } = useFeatureFlags()
// Access predefined flags
flags.supportsPreviewMetadata // boolean | undefined
flags.maxUploadSize // number | undefined
flags.supportsManagerV4 // boolean | undefined
flags.modelUploadButtonEnabled // boolean (checks remoteConfig first)
flags.assetUpdateOptionsEnabled // boolean (checks remoteConfig first)
Key Points:
- Uses Vue's
reactive()to make the object reactive - Each getter calls
api.getServerFeature()which reads fromapi.serverFeatureFlags - Some flags (like
modelUploadButtonEnabled) checkremoteConfigfirst (from/api/featuresendpoint) before falling back to WebSocket flags - Returns a
readonly()wrapper to prevent external mutation
B. Generic featureFlag Function
A function that creates a computed ref for any feature flag path:
const { featureFlag } = useFeatureFlags()
// Create a reactive computed ref for any flag
const myFlag = featureFlag('custom.feature.path', false) // defaultValue is optional
// myFlag is a ComputedRef that updates when serverFeatureFlags changes
Key Points:
- Accepts any string path (supports dot notation for nested values)
- Returns a
computed()ref that automatically updates when flags change - Generic type parameter allows type safety:
featureFlag<boolean>('flag', false)
2. The Underlying API Layer
Location: src/scripts/api.ts
The ComfyApi class manages feature flags:
class ComfyApi {
// Stores flags received from backend
serverFeatureFlags: Record<string, unknown> = {}
// Retrieves a flag value using dot notation
getServerFeature<T>(featureName: string, defaultValue?: T): T {
return get(this.serverFeatureFlags, featureName, defaultValue) as T
}
}
How Flags Are Received:
- WebSocket connection is established
- Frontend sends client feature flags as first message
- Backend responds with a
feature_flagsmessage type - The message handler stores it:
this.serverFeatureFlags = msg.data
The get Function:
- Uses
es-toolkit/compat'sgetfunction (lodash-style) - Supports dot notation:
'extension.manager.supports_v4'accesses nested objects - Returns
defaultValueif the path doesn't exist
3. Remote Config Integration
Location: src/platform/remoteConfig/remoteConfig.ts
Some flags check remoteConfig first (loaded from /api/features endpoint):
// Example from modelUploadButtonEnabled
return (
remoteConfig.value.model_upload_button_enabled ?? // Check remote config first
api.getServerFeature(ServerFeatureFlag.MODEL_UPLOAD_BUTTON_ENABLED, false) // Fallback
)
Why Two Sources?
remoteConfig: Fetched via HTTP at app startup, can be updated without WebSocket- WebSocket flags: Real-time capability negotiation, updated on reconnection
Usage Patterns
Pattern 1: Using Predefined Flags
import { useFeatureFlags } from '@/composables/useFeatureFlags'
const { flags } = useFeatureFlags()
// In template
if (flags.supportsPreviewMetadata) {
// Use enhanced preview feature
}
// In script
const maxSize = flags.maxUploadSize ?? 100 * 1024 * 1024 // Default 100MB
Pattern 2: Using Generic featureFlag Function
import { useFeatureFlags } from '@/composables/useFeatureFlags'
const { featureFlag } = useFeatureFlags()
// Create a reactive computed ref
const customFeature = featureFlag<boolean>('extension.custom.feature', false)
// Use in template (automatically reactive)
// <div v-if="customFeature">New Feature UI</div>
// Use in script
watch(customFeature, (enabled) => {
if (enabled) {
// Feature was enabled
}
})
Pattern 3: Direct API Access (Non-Reactive)
import { api } from '@/scripts/api'
// Direct access (not reactive, use sparingly)
if (api.serverSupportsFeature('supports_preview_metadata')) {
// Feature is supported
}
const maxSize = api.getServerFeature('max_upload_size', 100 * 1024 * 1024)
Reactivity Explained
The composable is reactive because:
- Predefined flags: Use
reactive()with getters, so whenapi.serverFeatureFlagschanges, Vue's reactivity system detects it - Generic featureFlag: Returns
computed(), which automatically tracksapi.getServerFeature()calls and re-evaluates when flags change - WebSocket updates: When flags are updated via WebSocket,
api.serverFeatureFlagsis reassigned, triggering reactivity
Adding New Feature Flags
Step 1: Add to Enum (if it's a core flag)
// In useFeatureFlags.ts
export enum ServerFeatureFlag {
// ... existing flags
MY_NEW_FEATURE = 'my_new_feature'
}
Step 2: Add to flags Object (if commonly used)
// In useFeatureFlags.ts flags object
get myNewFeature() {
return api.getServerFeature(ServerFeatureFlag.MY_NEW_FEATURE, false)
}
Step 3: Use in Components
const { flags } = useFeatureFlags()
if (flags.myNewFeature) {
// Use the feature
}
OR use the generic function without modifying the composable:
const { featureFlag } = useFeatureFlags()
const myFeature = featureFlag('my_new_feature', false)
Important Notes
- Flags are server-driven: The backend controls which flags are available
- Default values: Always provide sensible defaults when using
getServerFeature() - Reactivity: The composable ensures UI updates automatically when flags change (e.g., on WebSocket reconnection)
- Type safety: Use TypeScript generics with
featureFlag<T>()for type safety - Dot notation: Feature flags can be nested, use dot notation:
'extension.manager.supports_v4' - Remote config priority: Some flags check
remoteConfigfirst, then fall back to WebSocket flags
Testing
See tests-ui/tests/composables/useFeatureFlags.test.ts for examples of:
- Mocking
api.getServerFeature() - Testing reactive behavior
- Testing default values
- Testing nested paths
Related Files
src/composables/useFeatureFlags.ts- The main composablesrc/scripts/api.ts- API layer withgetServerFeature()methodsrc/platform/remoteConfig/remoteConfig.ts- Remote config integrationdocs/FEATURE_FLAGS.md- Full system documentationtests-ui/tests/composables/useFeatureFlags.test.ts- Unit tests