Skip to content

Commit 3e3ed0f

Browse files
committed
Accept a validateContext as option instead
1 parent b46a766 commit 3e3ed0f

File tree

1 file changed

+80
-10
lines changed

1 file changed

+80
-10
lines changed

packages/web/src/open-feature.ts

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@ import { NOOP_PROVIDER, ProviderStatus } from './provider';
1818

1919
interface SetProviderOptions {
2020
/**
21-
* If true, delays provider initialization until the first context change.
21+
* If provided, will be used to check if the current context is valid during initialization and context changes.
22+
* When calling `setProvider`, returning `false` will skip provider initialization. Throwing will move the provider to the ERROR state.
23+
* When calling `setProviderAndWait`, returning `false` will skip provider initialization. Throwing will reject the promise.
24+
* TODO: When calling `setContext`, returning `false` will skip provider context change handling. Throwing will move the provider to the ERROR state.
25+
* @param context The evaluation context to validate.
2226
*/
23-
delayed?: boolean;
27+
validateContext?: (context: EvaluationContext) => boolean;
2428
}
2529

2630
// use a symbol as a key for the global singleton
@@ -93,10 +97,11 @@ export class OpenFeatureAPI
9397
* Setting a provider supersedes the current provider used in new and existing unbound clients.
9498
* @param {Provider} provider The provider responsible for flag evaluations.
9599
* @param {EvaluationContext} context The evaluation context to use for flag evaluations.
100+
* @param {SetProviderOptions} [options] Options for setting the provider.
96101
* @returns {Promise<void>}
97102
* @throws {Error} If the provider throws an exception during initialization.
98103
*/
99-
setProviderAndWait(provider: Provider, context: EvaluationContext): Promise<void>;
104+
setProviderAndWait(provider: Provider, context: EvaluationContext, options?: SetProviderOptions): Promise<void>;
100105
/**
101106
* Sets the provider that OpenFeature will use for flag evaluations on clients bound to the same domain.
102107
* A promise is returned that resolves when the provider is ready.
@@ -114,22 +119,38 @@ export class OpenFeatureAPI
114119
* @param {string} domain The name to identify the client
115120
* @param {Provider} provider The provider responsible for flag evaluations.
116121
* @param {EvaluationContext} context The evaluation context to use for flag evaluations.
122+
* @param {SetProviderOptions} [options] Options for setting the provider.
117123
* @returns {Promise<void>}
118124
* @throws {Error} If the provider throws an exception during initialization.
119125
*/
120-
setProviderAndWait(domain: string, provider: Provider, context: EvaluationContext): Promise<void>;
126+
setProviderAndWait(domain: string, provider: Provider, context: EvaluationContext, options?: SetProviderOptions): Promise<void>;
121127
async setProviderAndWait(
122128
clientOrProvider?: string | Provider,
123129
providerContextOrUndefined?: Provider | EvaluationContext,
124-
contextOrUndefined?: EvaluationContext,
130+
contextOptionsOrUndefined?: EvaluationContext | SetProviderOptions,
131+
optionsOrUndefined?: SetProviderOptions,
125132
): Promise<void> {
126133
const domain = stringOrUndefined(clientOrProvider);
127134
const provider = domain
128135
? objectOrUndefined<Provider>(providerContextOrUndefined)
129136
: objectOrUndefined<Provider>(clientOrProvider);
130137
const context = domain
131-
? objectOrUndefined<EvaluationContext>(contextOrUndefined)
138+
? objectOrUndefined<EvaluationContext>(contextOptionsOrUndefined)
132139
: objectOrUndefined<EvaluationContext>(providerContextOrUndefined);
140+
const options = domain
141+
? objectOrUndefined<SetProviderOptions>(optionsOrUndefined)
142+
: objectOrUndefined<SetProviderOptions>(contextOptionsOrUndefined);
143+
144+
let skipInitialization = false;
145+
if (typeof options?.validateContext === 'function' && context) {
146+
// allow any error to propagate here to reject the promise
147+
skipInitialization = !options.validateContext(context);
148+
if (skipInitialization) {
149+
this._logger.debug(
150+
`Skipping provider initialization during setProviderAndWait for domain '${domain ?? 'default'}' due to validateContext returning false.`,
151+
);
152+
}
153+
}
133154

134155
if (context) {
135156
// synonymously setting context prior to provider initialization.
@@ -141,7 +162,7 @@ export class OpenFeatureAPI
141162
}
142163
}
143164

144-
await this.setAwaitableProvider(domain, provider);
165+
await this.setAwaitableProvider(domain, provider, skipInitialization);
145166
}
146167

147168
/**
@@ -161,7 +182,7 @@ export class OpenFeatureAPI
161182
* @param {SetProviderOptions} [options] Options for setting the provider.
162183
* @returns {this} OpenFeature API
163184
*/
164-
setProvider<T extends SetProviderOptions>(provider: Provider, context: T extends { delayed: true } ? Record<never, never> : EvaluationContext, options?: T): this;
185+
setProvider(provider: Provider, context: EvaluationContext, options?: SetProviderOptions): this;
165186
/**
166187
* Sets the provider for flag evaluations of providers with the given name.
167188
* Setting a provider supersedes the current provider used in new and existing clients bound to the same domain.
@@ -179,7 +200,7 @@ export class OpenFeatureAPI
179200
* @param {SetProviderOptions} [options] Options for setting the provider.
180201
* @returns {this} OpenFeature API
181202
*/
182-
setProvider<T extends SetProviderOptions>(domain: string, provider: Provider, context: T extends { delayed: true } ? Record<never, never> : EvaluationContext, options?: T): this;
203+
setProvider(domain: string, provider: Provider, context: EvaluationContext, options?: SetProviderOptions): this;
183204
setProvider(
184205
domainOrProvider?: string | Provider,
185206
providerContextOrUndefined?: Provider | EvaluationContext,
@@ -197,6 +218,26 @@ export class OpenFeatureAPI
197218
? objectOrUndefined<SetProviderOptions>(optionsOrUndefined)
198219
: objectOrUndefined<SetProviderOptions>(contextOptionsOrUndefined);
199220

221+
let skipInitialization = false;
222+
let validateContextError: unknown;
223+
if (typeof options?.validateContext === 'function' && context) {
224+
try {
225+
skipInitialization = !options.validateContext(context);
226+
if (skipInitialization) {
227+
this._logger.debug(
228+
`Skipping provider initialization during setProvider for domain '${domain ?? 'default'}' due to validateContext returning false.`,
229+
);
230+
}
231+
} catch (err) {
232+
// capture the error to move the provider to ERROR state after setting it
233+
validateContextError = err;
234+
skipInitialization = true;
235+
this._logger.debug(
236+
`Skipping provider initialization during setProvider for domain '${domain ?? 'default'}' due to validateContext throwing an error.`,
237+
);
238+
}
239+
}
240+
200241
if (context) {
201242
// synonymously setting context prior to provider initialization.
202243
// No context change event will be emitted.
@@ -207,7 +248,34 @@ export class OpenFeatureAPI
207248
}
208249
}
209250

210-
const maybePromise = this.setAwaitableProvider(domain, provider, options?.delayed);
251+
const maybePromise = this.setAwaitableProvider(domain, provider, skipInitialization);
252+
253+
// If there was a validation error, move the provider to ERROR state.
254+
// In this situation we should never have a promise returned from initialization.
255+
if (validateContextError) {
256+
const wrapper = domain
257+
? this._domainScopedProviders.get(domain)
258+
: this._defaultProvider;
259+
if (wrapper) {
260+
wrapper.status = this._statusEnumType.ERROR;
261+
const providerName = wrapper.provider?.metadata?.name || 'unnamed-provider';
262+
this.getAssociatedEventEmitters(domain).forEach((emitter) => {
263+
emitter?.emit(ProviderEvents.Error, {
264+
clientName: domain,
265+
domain,
266+
providerName,
267+
message: `Error validating context during setProvider: ${validateContextError instanceof Error ? validateContextError.message : String(validateContextError)}`,
268+
});
269+
});
270+
this._apiEmitter?.emit(ProviderEvents.Error, {
271+
clientName: domain,
272+
domain,
273+
providerName,
274+
message: `Error validating context during setProvider: ${validateContextError instanceof Error ? validateContextError.message : String(validateContextError)}`,
275+
});
276+
this._logger.error('Error validating context during setProvider:', validateContextError);
277+
}
278+
}
211279

212280
// The setProvider method doesn't return a promise so we need to catch and
213281
// log any errors that occur during provider initialization to avoid having
@@ -262,6 +330,8 @@ export class OpenFeatureAPI
262330
const domain = stringOrUndefined(domainOrContext);
263331
const context = objectOrUndefined<T>(domainOrContext) ?? objectOrUndefined(contextOrUndefined) ?? {};
264332

333+
// TODO: We need to store and call `validateContext` here if provided in `setProvider` options
334+
265335
if (domain) {
266336
const wrapper = this._domainScopedProviders.get(domain);
267337
if (wrapper) {

0 commit comments

Comments
 (0)