1- import type { deserializeConfig , IConfigCatCache , IConfigCatClient , SettingTypeOf } from '@configcat/sdk' ;
1+ import type { deserializeConfig , IConfigCatCache , IConfigCatClient } from '@configcat/sdk' ;
22import type * as FeatureFlagProjectConfigModule from '@configcat/sdk/lib/esm/ProjectConfig.js' ;
33import { env as vscodeEnv } from 'vscode' ;
44import { fetch } from '@env/fetch.js' ;
@@ -11,13 +11,13 @@ type DeserializeFeatureFlagConfig = typeof deserializeConfig;
1111
1212export type FeatureFlagValue = boolean | string | number ;
1313export enum FeatureFlagKey {
14- WelcomeTitle = 'glensWelcomeTitle ' ,
14+ WelcomeTitleVariant = 'glensWelcomeTitleVariant ' ,
1515}
16- export type FeatureFlagMap = Partial < Record < FeatureFlagKey , FeatureFlagValue > > ;
16+ export type FeatureFlagMap = Readonly < Partial < Record < FeatureFlagKey , FeatureFlagValue > > > ;
1717export interface FeatureFlagService {
1818 dispose ( ) : void ;
19- getFlag < T extends FeatureFlagValue > ( key : FeatureFlagKey , defaultValue : T ) : Promise < FeatureFlagValue > ;
20- getAllFlags ( ) : Promise < FeatureFlagMap > ;
19+ getFlag < T extends FeatureFlagValue > ( key : FeatureFlagKey , defaultValue : T ) : T ;
20+ getAllFlags ( ) : FeatureFlagMap ;
2121}
2222
2323const _featureFlagKeys : ReadonlySet < string > = new Set < FeatureFlagKey > ( Object . values ( FeatureFlagKey ) ) ;
@@ -50,95 +50,68 @@ class PrefetchedConfigCache implements IConfigCatCache {
5050}
5151
5252export class ConfigCatFeatureFlagService implements FeatureFlagService {
53- private readonly _client : Promise < IConfigCatClient | undefined > ;
53+ private readonly _flags : FeatureFlagMap ;
5454
5555 constructor ( private readonly container : Container ) {
56- this . _client = this . loadClient ( ) ;
57- }
56+ this . _flags = Object . freeze ( this . container . storage . get ( 'featureFlags:flags' ) ?? { } ) ;
5857
59- dispose ( ) : void {
60- void this . _client . then (
61- client => {
62- client ?. dispose ( ) ;
63- } ,
64- ( ) => { } ,
65- ) ;
58+ // Fire background fetch to evaluate flags and store them for the NEXT activation
59+ void this . fetchAndCacheFlags ( ) ;
6660 }
6761
68- async getFlag < T extends FeatureFlagValue > ( key : FeatureFlagKey , defaultValue : T ) : Promise < SettingTypeOf < T > | T > {
69- using scope = maybeStartScopedLogger ( `${ getLoggableName ( this ) } .getFlag` ) ;
70-
71- const client = await this . _client ;
72- if ( client == null ) return defaultValue ;
62+ dispose ( ) : void { }
7363
74- try {
75- const details = await client . getValueDetailsAsync < T > ( key , defaultValue ) ;
76- return details . value ;
77- } catch ( ex ) {
78- Logger . debug ( ex , scope , `Failed to evaluate feature flag '${ key } '; return default value` ) ;
64+ getFlag < T extends FeatureFlagValue > ( key : FeatureFlagKey , defaultValue : T ) : T {
65+ const value = this . _flags [ key ] ;
66+ if ( value == null || typeof value !== typeof defaultValue ) {
7967 return defaultValue ;
8068 }
69+ return value as T ;
8170 }
8271
83- async getAllFlags ( ) : Promise < FeatureFlagMap > {
84- using scope = maybeStartScopedLogger ( `${ getLoggableName ( this ) } .getAllFlags` ) ;
85-
86- const client = await this . _client ;
87- if ( client == null ) return { } ;
88-
89- try {
90- const values = await client . getAllValuesAsync ( ) ;
91- const flags : FeatureFlagMap = { } ;
92-
93- for ( const { settingKey, settingValue } of values ) {
94- if (
95- isFeatureFlagKey ( settingKey ) &&
96- ( typeof settingValue === 'boolean' ||
97- typeof settingValue === 'number' ||
98- typeof settingValue === 'string' )
99- ) {
100- flags [ settingKey ] = settingValue ;
101- }
102- }
103-
104- return flags ;
105- } catch ( ex ) {
106- Logger . debug ( ex , scope , 'Failed to evaluate feature flags; returning no flags' ) ;
107- return { } ;
108- }
72+ getAllFlags ( ) : FeatureFlagMap {
73+ return this . _flags ;
10974 }
11075
111- private async loadClient ( ) : Promise < IConfigCatClient | undefined > {
112- using scope = maybeStartScopedLogger ( `${ getLoggableName ( this ) } .loadClient` ) ;
76+ /**
77+ * Fetches fresh config from the API, evaluates all flags via ConfigCat SDK,
78+ * and stores the resolved flag map in globalState for the next activation.
79+ * Fire-and-forget — errors are logged but never propagated.
80+ */
81+ private async fetchAndCacheFlags ( ) : Promise < void > {
82+ using scope = maybeStartScopedLogger ( `${ getLoggableName ( this ) } .fetchAndCacheFlags` ) ;
11383
11484 try {
11585 const response = await fetch ( this . container . urls . getGkApiUrl ( 'feature-flags' , 'config' ) , {
11686 headers : { Accept : 'application/json' } ,
11787 } ) ;
11888
11989 if ( ! response . ok ) {
120- Logger . debug (
121- scope ,
122- `Failed to fetch feature flags config (${ response . status } ${ response . statusText } ); using defaults` ,
123- ) ;
124- return undefined ;
90+ Logger . debug ( scope , `Failed to fetch feature flags config (${ response . status } ${ response . statusText } )` ) ;
91+ return ;
12592 }
12693
12794 const configJson = await response . text ( ) ;
12895 if ( ! configJson ) {
129- Logger . debug ( scope , 'Feature flags config response was empty; using defaults ' ) ;
130- return undefined ;
96+ Logger . debug ( scope , 'Feature flags config response was empty' ) ;
97+ return ;
13198 }
13299
133- return await this . createClient ( configJson ) ;
100+ const flags = await this . evaluateFlags ( configJson ) ;
101+ if ( flags != null ) {
102+ await this . container . storage . store ( 'featureFlags:flags' , flags ) ;
103+ }
134104 } catch ( ex ) {
135- Logger . debug ( ex , scope , 'Failed to fetch feature flags config; using defaults' ) ;
136- return undefined ;
105+ Logger . debug ( ex , scope , 'Failed to fetch and cache feature flags' ) ;
137106 }
138107 }
139108
140- private async createClient ( configJson : string ) : Promise < IConfigCatClient | undefined > {
141- using scope = maybeStartScopedLogger ( `${ getLoggableName ( this ) } .createClient` ) ;
109+ /**
110+ * Creates a temporary ConfigCat client to evaluate all flags from the given config JSON,
111+ * then disposes it immediately.
112+ */
113+ private async evaluateFlags ( configJson : string ) : Promise < FeatureFlagMap | undefined > {
114+ using scope = maybeStartScopedLogger ( `${ getLoggableName ( this ) } .evaluateFlags` ) ;
142115
143116 const [ sdkResult , projectConfigResult ] = await Promise . allSettled ( [
144117 import ( /* webpackChunkName: "feature-flags" */ '@configcat/sdk' ) ,
@@ -149,26 +122,44 @@ export class ConfigCatFeatureFlagService implements FeatureFlagService {
149122 const projectConfigModule = getSettledValue ( projectConfigResult ) ;
150123
151124 if ( sdk == null || projectConfigModule == null ) {
152- Logger . debug ( scope , 'Failed to load ConfigCat SDK modules; using defaults ' ) ;
125+ Logger . debug ( scope , 'Failed to load ConfigCat SDK modules' ) ;
153126 return undefined ;
154127 }
155128
129+ let client : IConfigCatClient | undefined ;
156130 try {
157131 const cache = new PrefetchedConfigCache (
158132 this . serializeProjectConfig ( configJson , sdk . deserializeConfig , projectConfigModule ) ,
159133 ) ;
160- const client = sdk . getClient ( localSdkKey , sdk . PollingMode . ManualPoll , {
134+ client = sdk . getClient ( localSdkKey , sdk . PollingMode . ManualPoll , {
161135 cache : cache ,
162- defaultUser : { identifier : vscodeEnv . machineId , country : 'ES' } ,
136+ defaultUser : { identifier : vscodeEnv . machineId } ,
163137 offline : true ,
164138 } ) ;
165139
166140 await client . waitForReady ( ) ;
167141 await client . forceRefreshAsync ( ) ;
168- return client ;
142+
143+ const values = await client . getAllValuesAsync ( ) ;
144+ const flags : Partial < Record < FeatureFlagKey , FeatureFlagValue > > = { } ;
145+
146+ for ( const { settingKey, settingValue } of values ) {
147+ if (
148+ isFeatureFlagKey ( settingKey ) &&
149+ ( typeof settingValue === 'boolean' ||
150+ typeof settingValue === 'number' ||
151+ typeof settingValue === 'string' )
152+ ) {
153+ flags [ settingKey ] = settingValue ;
154+ }
155+ }
156+
157+ return flags ;
169158 } catch ( ex ) {
170- Logger . debug ( ex , scope , 'Failed to initialize ConfigCat feature flag client; using defaults ' ) ;
159+ Logger . debug ( ex , scope , 'Failed to evaluate feature flags ' ) ;
171160 return undefined ;
161+ } finally {
162+ client ?. dispose ( ) ;
172163 }
173164 }
174165
0 commit comments