Skip to content

Commit 76e5143

Browse files
committed
Accept a validateContext as option instead
1 parent b46a766 commit 76e5143

File tree

1 file changed

+75
-10
lines changed

1 file changed

+75
-10
lines changed

packages/web/src/open-feature.ts

Lines changed: 75 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,33 @@ 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+
}
133149

134150
if (context) {
135151
// synonymously setting context prior to provider initialization.
@@ -141,7 +157,7 @@ export class OpenFeatureAPI
141157
}
142158
}
143159

144-
await this.setAwaitableProvider(domain, provider);
160+
await this.setAwaitableProvider(domain, provider, skipInitialization);
145161
}
146162

147163
/**
@@ -161,7 +177,7 @@ export class OpenFeatureAPI
161177
* @param {SetProviderOptions} [options] Options for setting the provider.
162178
* @returns {this} OpenFeature API
163179
*/
164-
setProvider<T extends SetProviderOptions>(provider: Provider, context: T extends { delayed: true } ? Record<never, never> : EvaluationContext, options?: T): this;
180+
setProvider(provider: Provider, context: EvaluationContext, options?: SetProviderOptions): this;
165181
/**
166182
* Sets the provider for flag evaluations of providers with the given name.
167183
* Setting a provider supersedes the current provider used in new and existing clients bound to the same domain.
@@ -179,7 +195,7 @@ export class OpenFeatureAPI
179195
* @param {SetProviderOptions} [options] Options for setting the provider.
180196
* @returns {this} OpenFeature API
181197
*/
182-
setProvider<T extends SetProviderOptions>(domain: string, provider: Provider, context: T extends { delayed: true } ? Record<never, never> : EvaluationContext, options?: T): this;
198+
setProvider(domain: string, provider: Provider, context: EvaluationContext, options?: SetProviderOptions): this;
183199
setProvider(
184200
domainOrProvider?: string | Provider,
185201
providerContextOrUndefined?: Provider | EvaluationContext,
@@ -197,6 +213,26 @@ export class OpenFeatureAPI
197213
? objectOrUndefined<SetProviderOptions>(optionsOrUndefined)
198214
: objectOrUndefined<SetProviderOptions>(contextOptionsOrUndefined);
199215

216+
let skipInitialization = false;
217+
let validateContextError: unknown;
218+
if (typeof options?.validateContext === 'function' && context) {
219+
try {
220+
skipInitialization = !options.validateContext(context);
221+
if (skipInitialization) {
222+
this._logger.debug(
223+
`Skipping provider initialization during setProvider for domain '${domain ?? 'default'}' due to validateContext returning false.`,
224+
);
225+
}
226+
} catch (err) {
227+
// capture the error to move the provider to ERROR state after setting it
228+
validateContextError = err;
229+
skipInitialization = true;
230+
this._logger.debug(
231+
`Skipping provider initialization during setProvider for domain '${domain ?? 'default'}' due to validateContext throwing an error.`,
232+
);
233+
}
234+
}
235+
200236
if (context) {
201237
// synonymously setting context prior to provider initialization.
202238
// No context change event will be emitted.
@@ -207,7 +243,34 @@ export class OpenFeatureAPI
207243
}
208244
}
209245

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

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

328+
// TODO: We need to store and call `validateContext` here if provided in `setProvider` options
329+
265330
if (domain) {
266331
const wrapper = this._domainScopedProviders.get(domain);
267332
if (wrapper) {

0 commit comments

Comments
 (0)