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' ;
@@ -13,11 +13,11 @@ export type FeatureFlagValue = boolean | string | number;
1313export enum FeatureFlagKey {
1414 WelcomeTitle = 'glensWelcomeTitle' ,
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,64 @@ 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` ) ;
79- return defaultValue ;
80- }
64+ getFlag < T extends FeatureFlagValue > ( key : FeatureFlagKey , defaultValue : T ) : T {
65+ return ( this . _flags [ key ] ?? defaultValue ) as T ;
8166 }
8267
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- }
68+ getAllFlags ( ) : FeatureFlagMap {
69+ return this . _flags ;
10970 }
11071
111- private async loadClient ( ) : Promise < IConfigCatClient | undefined > {
112- using scope = maybeStartScopedLogger ( `${ getLoggableName ( this ) } .loadClient` ) ;
72+ /**
73+ * Fetches fresh config from the API, evaluates all flags via ConfigCat SDK,
74+ * and stores the resolved flag map in globalState for the next activation.
75+ * Fire-and-forget — errors are logged but never propagated.
76+ */
77+ private async fetchAndCacheFlags ( ) : Promise < void > {
78+ using scope = maybeStartScopedLogger ( `${ getLoggableName ( this ) } .fetchAndCacheFlags` ) ;
11379
11480 try {
11581 const response = await fetch ( this . container . urls . getGkApiUrl ( 'feature-flags' , 'config' ) , {
11682 headers : { Accept : 'application/json' } ,
11783 } ) ;
11884
11985 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 ;
86+ Logger . debug ( scope , `Failed to fetch feature flags config (${ response . status } ${ response . statusText } )` ) ;
87+ return ;
12588 }
12689
12790 const configJson = await response . text ( ) ;
12891 if ( ! configJson ) {
129- Logger . debug ( scope , 'Feature flags config response was empty; using defaults ' ) ;
130- return undefined ;
92+ Logger . debug ( scope , 'Feature flags config response was empty' ) ;
93+ return ;
13194 }
13295
133- return await this . createClient ( configJson ) ;
96+ const flags = await this . evaluateFlags ( configJson ) ;
97+ if ( flags != null ) {
98+ await this . container . storage . store ( 'featureFlags:flags' , flags ) ;
99+ }
134100 } catch ( ex ) {
135- Logger . debug ( ex , scope , 'Failed to fetch feature flags config; using defaults' ) ;
136- return undefined ;
101+ Logger . debug ( ex , scope , 'Failed to fetch and cache feature flags' ) ;
137102 }
138103 }
139104
140- private async createClient ( configJson : string ) : Promise < IConfigCatClient | undefined > {
141- using scope = maybeStartScopedLogger ( `${ getLoggableName ( this ) } .createClient` ) ;
105+ /**
106+ * Creates a temporary ConfigCat client to evaluate all flags from the given config JSON,
107+ * then disposes it immediately.
108+ */
109+ private async evaluateFlags ( configJson : string ) : Promise < FeatureFlagMap | undefined > {
110+ using scope = maybeStartScopedLogger ( `${ getLoggableName ( this ) } .evaluateFlags` ) ;
142111
143112 const [ sdkResult , projectConfigResult ] = await Promise . allSettled ( [
144113 import ( /* webpackChunkName: "feature-flags" */ '@configcat/sdk' ) ,
@@ -149,26 +118,44 @@ export class ConfigCatFeatureFlagService implements FeatureFlagService {
149118 const projectConfigModule = getSettledValue ( projectConfigResult ) ;
150119
151120 if ( sdk == null || projectConfigModule == null ) {
152- Logger . debug ( scope , 'Failed to load ConfigCat SDK modules; using defaults ' ) ;
121+ Logger . debug ( scope , 'Failed to load ConfigCat SDK modules' ) ;
153122 return undefined ;
154123 }
155124
125+ let client : IConfigCatClient | undefined ;
156126 try {
157127 const cache = new PrefetchedConfigCache (
158128 this . serializeProjectConfig ( configJson , sdk . deserializeConfig , projectConfigModule ) ,
159129 ) ;
160- const client = sdk . getClient ( localSdkKey , sdk . PollingMode . ManualPoll , {
130+ client = sdk . getClient ( localSdkKey , sdk . PollingMode . ManualPoll , {
161131 cache : cache ,
162- defaultUser : { identifier : vscodeEnv . machineId , country : 'ES' } ,
132+ defaultUser : { identifier : vscodeEnv . machineId } ,
163133 offline : true ,
164134 } ) ;
165135
166136 await client . waitForReady ( ) ;
167137 await client . forceRefreshAsync ( ) ;
168- return client ;
138+
139+ const values = await client . getAllValuesAsync ( ) ;
140+ const flags : Partial < Record < FeatureFlagKey , FeatureFlagValue > > = { } ;
141+
142+ for ( const { settingKey, settingValue } of values ) {
143+ if (
144+ isFeatureFlagKey ( settingKey ) &&
145+ ( typeof settingValue === 'boolean' ||
146+ typeof settingValue === 'number' ||
147+ typeof settingValue === 'string' )
148+ ) {
149+ flags [ settingKey ] = settingValue ;
150+ }
151+ }
152+
153+ return flags ;
169154 } catch ( ex ) {
170- Logger . debug ( ex , scope , 'Failed to initialize ConfigCat feature flag client; using defaults ' ) ;
155+ Logger . debug ( ex , scope , 'Failed to evaluate feature flags ' ) ;
171156 return undefined ;
157+ } finally {
158+ client ?. dispose ( ) ;
172159 }
173160 }
174161
0 commit comments