Skip to content

Commit 424f6de

Browse files
[feat] Add reactive feature flags foundation (#4817)
1 parent 2cf4dab commit 424f6de

File tree

3 files changed

+162
-4
lines changed

3 files changed

+162
-4
lines changed

src/composables/useFeatureFlags.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { computed, reactive, readonly } from 'vue'
2+
3+
import { api } from '@/scripts/api'
4+
5+
/**
6+
* Known server feature flags (top-level, not extensions)
7+
*/
8+
export enum ServerFeatureFlag {
9+
SUPPORTS_PREVIEW_METADATA = 'supports_preview_metadata',
10+
MAX_UPLOAD_SIZE = 'max_upload_size'
11+
}
12+
13+
/**
14+
* Composable for reactive access to feature flags
15+
*/
16+
export function useFeatureFlags() {
17+
// Create reactive state that tracks server feature flags
18+
const flags = reactive({
19+
get supportsPreviewMetadata() {
20+
return api.getServerFeature(ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA)
21+
},
22+
get maxUploadSize() {
23+
return api.getServerFeature(ServerFeatureFlag.MAX_UPLOAD_SIZE)
24+
}
25+
})
26+
27+
// Create a reactive computed for any feature flag
28+
const featureFlag = <T = unknown>(featurePath: string, defaultValue?: T) => {
29+
return computed(() => api.getServerFeature(featurePath, defaultValue))
30+
}
31+
32+
return {
33+
flags: readonly(flags),
34+
featureFlag
35+
}
36+
}

src/scripts/api.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import axios from 'axios'
2+
import get from 'lodash/get'
23

34
import defaultClientFeatureFlags from '@/config/clientFeatureFlags.json'
45
import type {
@@ -1082,21 +1083,21 @@ export class ComfyApi extends EventTarget {
10821083

10831084
/**
10841085
* Checks if the server supports a specific feature.
1085-
* @param featureName The name of the feature to check
1086+
* @param featureName The name of the feature to check (supports dot notation for nested values)
10861087
* @returns true if the feature is supported, false otherwise
10871088
*/
10881089
serverSupportsFeature(featureName: string): boolean {
1089-
return this.serverFeatureFlags[featureName] === true
1090+
return get(this.serverFeatureFlags, featureName) === true
10901091
}
10911092

10921093
/**
10931094
* Gets a server feature flag value.
1094-
* @param featureName The name of the feature to get
1095+
* @param featureName The name of the feature to get (supports dot notation for nested values)
10951096
* @param defaultValue The default value if the feature is not found
10961097
* @returns The feature value or default
10971098
*/
10981099
getServerFeature<T = unknown>(featureName: string, defaultValue?: T): T {
1099-
return (this.serverFeatureFlags[featureName] ?? defaultValue) as T
1100+
return get(this.serverFeatureFlags, featureName, defaultValue) as T
11001101
}
11011102

11021103
/**
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
import { isReactive, isReadonly } from 'vue'
3+
4+
import {
5+
ServerFeatureFlag,
6+
useFeatureFlags
7+
} from '@/composables/useFeatureFlags'
8+
import { api } from '@/scripts/api'
9+
10+
// Mock the API module
11+
vi.mock('@/scripts/api', () => ({
12+
api: {
13+
getServerFeature: vi.fn()
14+
}
15+
}))
16+
17+
describe('useFeatureFlags', () => {
18+
beforeEach(() => {
19+
vi.clearAllMocks()
20+
})
21+
22+
describe('flags object', () => {
23+
it('should provide reactive readonly flags', () => {
24+
const { flags } = useFeatureFlags()
25+
26+
expect(isReadonly(flags)).toBe(true)
27+
expect(isReactive(flags)).toBe(true)
28+
})
29+
30+
it('should access supportsPreviewMetadata', () => {
31+
vi.mocked(api.getServerFeature).mockImplementation(
32+
(path, defaultValue) => {
33+
if (path === ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA)
34+
return true as any
35+
return defaultValue
36+
}
37+
)
38+
39+
const { flags } = useFeatureFlags()
40+
expect(flags.supportsPreviewMetadata).toBe(true)
41+
expect(api.getServerFeature).toHaveBeenCalledWith(
42+
ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA
43+
)
44+
})
45+
46+
it('should access maxUploadSize', () => {
47+
vi.mocked(api.getServerFeature).mockImplementation(
48+
(path, defaultValue) => {
49+
if (path === ServerFeatureFlag.MAX_UPLOAD_SIZE)
50+
return 209715200 as any // 200MB
51+
return defaultValue
52+
}
53+
)
54+
55+
const { flags } = useFeatureFlags()
56+
expect(flags.maxUploadSize).toBe(209715200)
57+
expect(api.getServerFeature).toHaveBeenCalledWith(
58+
ServerFeatureFlag.MAX_UPLOAD_SIZE
59+
)
60+
})
61+
62+
it('should return undefined when features are not available and no default provided', () => {
63+
vi.mocked(api.getServerFeature).mockImplementation(
64+
(_path, defaultValue) => defaultValue as any
65+
)
66+
67+
const { flags } = useFeatureFlags()
68+
expect(flags.supportsPreviewMetadata).toBeUndefined()
69+
expect(flags.maxUploadSize).toBeUndefined()
70+
})
71+
})
72+
73+
describe('featureFlag', () => {
74+
it('should create reactive computed for custom feature flags', () => {
75+
vi.mocked(api.getServerFeature).mockImplementation(
76+
(path, defaultValue) => {
77+
if (path === 'custom.feature') return 'custom-value' as any
78+
return defaultValue
79+
}
80+
)
81+
82+
const { featureFlag } = useFeatureFlags()
83+
const customFlag = featureFlag('custom.feature', 'default')
84+
85+
expect(customFlag.value).toBe('custom-value')
86+
expect(api.getServerFeature).toHaveBeenCalledWith(
87+
'custom.feature',
88+
'default'
89+
)
90+
})
91+
92+
it('should handle nested paths', () => {
93+
vi.mocked(api.getServerFeature).mockImplementation(
94+
(path, defaultValue) => {
95+
if (path === 'extension.custom.nested.feature') return true as any
96+
return defaultValue
97+
}
98+
)
99+
100+
const { featureFlag } = useFeatureFlags()
101+
const nestedFlag = featureFlag('extension.custom.nested.feature', false)
102+
103+
expect(nestedFlag.value).toBe(true)
104+
})
105+
106+
it('should work with ServerFeatureFlag enum', () => {
107+
vi.mocked(api.getServerFeature).mockImplementation(
108+
(path, defaultValue) => {
109+
if (path === ServerFeatureFlag.MAX_UPLOAD_SIZE)
110+
return 104857600 as any
111+
return defaultValue
112+
}
113+
)
114+
115+
const { featureFlag } = useFeatureFlags()
116+
const maxUploadSize = featureFlag(ServerFeatureFlag.MAX_UPLOAD_SIZE)
117+
118+
expect(maxUploadSize.value).toBe(104857600)
119+
})
120+
})
121+
})

0 commit comments

Comments
 (0)