Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3dd3e26
[backport cloud/1.33] feat: open template via URL in linear mode (#6968)
comfy-pr-bot Nov 27, 2025
31d8422
[backport cloud/1.33] fix: don't use registry when only checking for …
christian-byrne Nov 27, 2025
334404a
[backport cloud/1.33] fix: remove LOD from vue nodes (#6983)
christian-byrne Nov 27, 2025
0cd0218
[backport cloud/1.33] fix: Vue Node <-> Litegraph node height offset …
christian-byrne Nov 27, 2025
896867b
[backport cloud/1.33] fix: add filter for combo widgets (#7003)
comfy-pr-bot Nov 27, 2025
550ca0c
[backport cloud/1.33] Remove app.graph usage from widgetInput code (#…
comfy-pr-bot Nov 28, 2025
637c199
[backport cloud/1.33] [fix] Re-encode cloud-subscription video to VP9…
comfy-pr-bot Nov 28, 2025
22aea29
[backport cloud/1.33] [feat] Show "Finished in" duration for complete…
comfy-pr-bot Nov 28, 2025
72a2581
[backport cloud/1.33] feat(api-nodes-pricing): add prices for ByteDan…
comfy-pr-bot Nov 29, 2025
7b589b5
[backport cloud/1.33] mark vue nodes menu toggle with beta tag (#7052)
comfy-pr-bot Nov 30, 2025
3856e0d
[backport cloud/1.33] fix: loader node widget value shows placeholder…
comfy-pr-bot Nov 30, 2025
ffa55cb
[backport cloud/1.33] Simplify Vue node resize to bottom-right corner…
christian-byrne Dec 1, 2025
3920413
[backport cloud/1.33] [fix] Prevent drag activation during Vue node r…
comfy-pr-bot Dec 1, 2025
c8a1df3
[backport cloud/1.33] feat(api-nodes-pricing): add prices for Kling O…
comfy-pr-bot Dec 1, 2025
b663244
[backport cloud/1.33] fix: normalize path separators in comfyAPIPlugi…
comfy-pr-bot Dec 3, 2025
6c34085
[backport cloud/1.33] cloud: increase feature flag polling interval t…
comfy-pr-bot Dec 3, 2025
d314172
[feat] Add remote config support for model upload and asset update fe…
luke-mino-altherr Dec 4, 2025
0b0af89
[demo] PostHog feature flag integration for Run button experiment
luke-mino-altherr Dec 5, 2025
8d90716
[demo] Add feature flag support to CloudRunButtonWrapper
luke-mino-altherr Dec 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 229 additions & 0 deletions FEATURE_FLAGS_EXPLANATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
# 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:

```typescript
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 from `api.serverFeatureFlags`
- Some flags (like `modelUploadButtonEnabled`) check `remoteConfig` first (from `/api/features` endpoint) 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:

```typescript
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:

```typescript
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:**
1. WebSocket connection is established
2. Frontend sends client feature flags as first message
3. Backend responds with a `feature_flags` message type
4. The message handler stores it: `this.serverFeatureFlags = msg.data`

**The `get` Function:**
- Uses `es-toolkit/compat`'s `get` function (lodash-style)
- Supports dot notation: `'extension.manager.supports_v4'` accesses nested objects
- Returns `defaultValue` if 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):

```typescript
// 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

```typescript
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

```typescript
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)

```typescript
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:

1. **Predefined flags**: Use `reactive()` with getters, so when `api.serverFeatureFlags` changes, Vue's reactivity system detects it
2. **Generic featureFlag**: Returns `computed()`, which automatically tracks `api.getServerFeature()` calls and re-evaluates when flags change
3. **WebSocket updates**: When flags are updated via WebSocket, `api.serverFeatureFlags` is reassigned, triggering reactivity

## Adding New Feature Flags

### Step 1: Add to Enum (if it's a core flag)

```typescript
// In useFeatureFlags.ts
export enum ServerFeatureFlag {
// ... existing flags
MY_NEW_FEATURE = 'my_new_feature'
}
```

### Step 2: Add to flags Object (if commonly used)

```typescript
// In useFeatureFlags.ts flags object
get myNewFeature() {
return api.getServerFeature(ServerFeatureFlag.MY_NEW_FEATURE, false)
}
```

### Step 3: Use in Components

```typescript
const { flags } = useFeatureFlags()
if (flags.myNewFeature) {
// Use the feature
}
```

**OR** use the generic function without modifying the composable:

```typescript
const { featureFlag } = useFeatureFlags()
const myFeature = featureFlag('my_new_feature', false)
```

## Important Notes

1. **Flags are server-driven**: The backend controls which flags are available
2. **Default values**: Always provide sensible defaults when using `getServerFeature()`
3. **Reactivity**: The composable ensures UI updates automatically when flags change (e.g., on WebSocket reconnection)
4. **Type safety**: Use TypeScript generics with `featureFlag<T>()` for type safety
5. **Dot notation**: Feature flags can be nested, use dot notation: `'extension.manager.supports_v4'`
6. **Remote config priority**: Some flags check `remoteConfig` first, 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 composable
- `src/scripts/api.ts` - API layer with `getServerFeature()` method
- `src/platform/remoteConfig/remoteConfig.ts` - Remote config integration
- `docs/FEATURE_FLAGS.md` - Full system documentation
- `tests-ui/tests/composables/useFeatureFlags.test.ts` - Unit tests

Loading