Skip to content

Commit 6990b63

Browse files
committed
Accept a validateContext to skip initialization
1 parent 472b399 commit 6990b63

File tree

2 files changed

+114
-25
lines changed

2 files changed

+114
-25
lines changed

packages/shared/src/open-feature.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -228,13 +228,15 @@ export abstract class OpenFeatureCommonAPI<
228228
abstract setProviderAndWait(
229229
clientOrProvider?: string | P,
230230
providerContextOrUndefined?: P | EvaluationContext,
231-
contextOrUndefined?: EvaluationContext,
231+
contextOptionsOrUndefined?: EvaluationContext | Record<never, never>,
232+
optionsOrUndefined?: Record<never, never>,
232233
): Promise<void>;
233234

234235
abstract setProvider(
235236
clientOrProvider?: string | P,
236237
providerContextOrUndefined?: P | EvaluationContext,
237-
contextOrUndefined?: EvaluationContext,
238+
contextOptionsOrUndefined?: EvaluationContext | Record<never, never>,
239+
optionsOrUndefined?: Record<never, never>,
238240
): this;
239241

240242
protected initializeProviderForDomain(wrapper: ProviderWrapper<P, AnyProviderStatus>, domain?: string): Promise<void> | void {
@@ -285,7 +287,7 @@ export abstract class OpenFeatureCommonAPI<
285287
});
286288
}
287289

288-
protected setAwaitableProvider(domainOrProvider?: string | P, providerOrUndefined?: P): Promise<void> | void {
290+
protected setAwaitableProvider(domainOrProvider?: string | P, providerOrUndefined?: P, skipInitialization = false): Promise<void> | void {
289291
const domain = stringOrUndefined(domainOrProvider);
290292
const provider = objectOrUndefined<P>(domainOrProvider) ?? objectOrUndefined<P>(providerOrUndefined);
291293

@@ -318,17 +320,19 @@ export abstract class OpenFeatureCommonAPI<
318320
this._statusEnumType,
319321
);
320322

321-
// initialize the provider if it's not already registered and it implements "initialize"
322-
if (!this.allProviders.includes(provider)) {
323-
initializationPromise = this.initializeProviderForDomain(wrappedProvider, domain);
324-
}
323+
if (!skipInitialization) {
324+
// initialize the provider if it's not already registered and it implements "initialize"
325+
if (!this.allProviders.includes(provider)) {
326+
initializationPromise = this.initializeProviderForDomain(wrappedProvider, domain);
327+
}
325328

326-
if (!initializationPromise) {
327-
wrappedProvider.status = this._statusEnumType.READY;
328-
emitters.forEach((emitter) => {
329-
emitter?.emit(AllProviderEvents.Ready, { clientName: domain, domain, providerName });
330-
});
331-
this._apiEmitter?.emit(AllProviderEvents.Ready, { clientName: domain, domain, providerName });
329+
if (!initializationPromise) {
330+
wrappedProvider.status = this._statusEnumType.READY;
331+
emitters.forEach((emitter) => {
332+
emitter?.emit(AllProviderEvents.Ready, { clientName: domain, domain, providerName });
333+
});
334+
this._apiEmitter?.emit(AllProviderEvents.Ready, { clientName: domain, domain, providerName });
335+
}
332336
}
333337

334338
if (domain) {

packages/web/src/open-feature.ts

Lines changed: 97 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@ import type { Hook } from './hooks';
1616
import type { Provider} from './provider';
1717
import { NOOP_PROVIDER, ProviderStatus } from './provider';
1818

19+
interface ProviderOptions {
20+
/**
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.
26+
*/
27+
validateContext?: (context: EvaluationContext) => boolean;
28+
}
29+
1930
// use a symbol as a key for the global singleton
2031
const GLOBAL_OPENFEATURE_API_KEY = Symbol.for('@openfeature/web-sdk/api');
2132

@@ -86,10 +97,11 @@ export class OpenFeatureAPI
8697
* Setting a provider supersedes the current provider used in new and existing unbound clients.
8798
* @param {Provider} provider The provider responsible for flag evaluations.
8899
* @param {EvaluationContext} context The evaluation context to use for flag evaluations.
100+
* @param {ProviderOptions} [options] Options for setting the provider.
89101
* @returns {Promise<void>}
90102
* @throws {Error} If the provider throws an exception during initialization.
91103
*/
92-
setProviderAndWait(provider: Provider, context: EvaluationContext): Promise<void>;
104+
setProviderAndWait(provider: Provider, context: EvaluationContext, options?: ProviderOptions): Promise<void>;
93105
/**
94106
* Sets the provider that OpenFeature will use for flag evaluations on clients bound to the same domain.
95107
* A promise is returned that resolves when the provider is ready.
@@ -107,24 +119,41 @@ export class OpenFeatureAPI
107119
* @param {string} domain The name to identify the client
108120
* @param {Provider} provider The provider responsible for flag evaluations.
109121
* @param {EvaluationContext} context The evaluation context to use for flag evaluations.
122+
* @param {ProviderOptions} [options] Options for setting the provider.
110123
* @returns {Promise<void>}
111124
* @throws {Error} If the provider throws an exception during initialization.
112125
*/
113-
setProviderAndWait(domain: string, provider: Provider, context: EvaluationContext): Promise<void>;
126+
setProviderAndWait(domain: string, provider: Provider, context: EvaluationContext, options?: ProviderOptions): Promise<void>;
114127
async setProviderAndWait(
115128
clientOrProvider?: string | Provider,
116129
providerContextOrUndefined?: Provider | EvaluationContext,
117-
contextOrUndefined?: EvaluationContext,
130+
contextOptionsOrUndefined?: EvaluationContext | ProviderOptions,
131+
optionsOrUndefined?: ProviderOptions,
118132
): Promise<void> {
119133
const domain = stringOrUndefined(clientOrProvider);
120134
const provider = domain
121135
? objectOrUndefined<Provider>(providerContextOrUndefined)
122136
: objectOrUndefined<Provider>(clientOrProvider);
123137
const context = domain
124-
? objectOrUndefined<EvaluationContext>(contextOrUndefined)
138+
? objectOrUndefined<EvaluationContext>(contextOptionsOrUndefined)
125139
: objectOrUndefined<EvaluationContext>(providerContextOrUndefined);
140+
const options = domain
141+
? objectOrUndefined<ProviderOptions>(optionsOrUndefined)
142+
: objectOrUndefined<ProviderOptions>(contextOptionsOrUndefined);
126143

144+
let skipInitialization = false;
127145
if (context) {
146+
// validate the context to decide if we should initialize the provider with it.
147+
if (typeof options?.validateContext === 'function') {
148+
// allow any error to propagate here to reject the promise.
149+
skipInitialization = !options.validateContext(context);
150+
if (skipInitialization) {
151+
this._logger.debug(
152+
`Skipping provider initialization during setProviderAndWait for domain '${domain ?? 'default'}' due to validateContext returning false.`,
153+
);
154+
}
155+
}
156+
128157
// synonymously setting context prior to provider initialization.
129158
// No context change event will be emitted.
130159
if (domain) {
@@ -134,7 +163,7 @@ export class OpenFeatureAPI
134163
}
135164
}
136165

137-
await this.setAwaitableProvider(domain, provider);
166+
await this.setAwaitableProvider(domain, provider, skipInitialization);
138167
}
139168

140169
/**
@@ -150,10 +179,11 @@ export class OpenFeatureAPI
150179
* This provider will be used by domainless clients and clients associated with domains to which no provider is bound.
151180
* Setting a provider supersedes the current provider used in new and existing unbound clients.
152181
* @param {Provider} provider The provider responsible for flag evaluations.
153-
* @param context {EvaluationContext} The evaluation context to use for flag evaluations.
182+
* @param {EvaluationContext} context The evaluation context to use for flag evaluations. Ignored if 'delayed' option is set.
183+
* @param {ProviderOptions} [options] Options for setting the provider.
154184
* @returns {this} OpenFeature API
155185
*/
156-
setProvider(provider: Provider, context: EvaluationContext): this;
186+
setProvider(provider: Provider, context: EvaluationContext, options?: ProviderOptions): this;
157187
/**
158188
* Sets the provider for flag evaluations of providers with the given name.
159189
* Setting a provider supersedes the current provider used in new and existing clients bound to the same domain.
@@ -167,24 +197,50 @@ export class OpenFeatureAPI
167197
* Setting a provider supersedes the current provider used in new and existing clients bound to the same domain.
168198
* @param {string} domain The name to identify the client
169199
* @param {Provider} provider The provider responsible for flag evaluations.
170-
* @param context {EvaluationContext} The evaluation context to use for flag evaluations.
200+
* @param {EvaluationContext} context The evaluation context to use for flag evaluations. Ignored if 'delayed' option is set.
201+
* @param {ProviderOptions} [options] Options for setting the provider.
171202
* @returns {this} OpenFeature API
172203
*/
173-
setProvider(domain: string, provider: Provider, context: EvaluationContext): this;
204+
setProvider(domain: string, provider: Provider, context: EvaluationContext, options?: ProviderOptions): this;
174205
setProvider(
175206
domainOrProvider?: string | Provider,
176207
providerContextOrUndefined?: Provider | EvaluationContext,
177-
contextOrUndefined?: EvaluationContext,
208+
contextOptionsOrUndefined?: EvaluationContext | ProviderOptions,
209+
optionsOrUndefined?: ProviderOptions,
178210
): this {
179211
const domain = stringOrUndefined(domainOrProvider);
180212
const provider = domain
181213
? objectOrUndefined<Provider>(providerContextOrUndefined)
182214
: objectOrUndefined<Provider>(domainOrProvider);
183215
const context = domain
184-
? objectOrUndefined<EvaluationContext>(contextOrUndefined)
216+
? objectOrUndefined<EvaluationContext>(contextOptionsOrUndefined)
185217
: objectOrUndefined<EvaluationContext>(providerContextOrUndefined);
218+
const options = domain
219+
? objectOrUndefined<ProviderOptions>(optionsOrUndefined)
220+
: objectOrUndefined<ProviderOptions>(contextOptionsOrUndefined);
186221

222+
let skipInitialization = false;
223+
let validateContextError: unknown;
187224
if (context) {
225+
// validate the context to decide if we should initialize the provider with it.
226+
if (typeof options?.validateContext === 'function') {
227+
try {
228+
skipInitialization = !options.validateContext(context);
229+
if (skipInitialization) {
230+
this._logger.debug(
231+
`Skipping provider initialization during setProvider for domain '${domain ?? 'default'}' due to validateContext returning false.`,
232+
);
233+
}
234+
} catch (err) {
235+
// capture the error to move the provider to ERROR state after setting it.
236+
validateContextError = err;
237+
skipInitialization = true;
238+
this._logger.debug(
239+
`Skipping provider initialization during setProvider for domain '${domain ?? 'default'}' due to validateContext throwing an error.`,
240+
);
241+
}
242+
}
243+
188244
// synonymously setting context prior to provider initialization.
189245
// No context change event will be emitted.
190246
if (domain) {
@@ -194,7 +250,34 @@ export class OpenFeatureAPI
194250
}
195251
}
196252

197-
const maybePromise = this.setAwaitableProvider(domain, provider);
253+
const maybePromise = this.setAwaitableProvider(domain, provider, skipInitialization);
254+
255+
// If there was a validation error with the context, move the newly created provider to ERROR state.
256+
// We know we've skipped initialization if this happens, so no need to worry about the promise changing the state later.
257+
if (validateContextError) {
258+
const wrapper = domain
259+
? this._domainScopedProviders.get(domain)
260+
: this._defaultProvider;
261+
if (wrapper) {
262+
wrapper.status = this._statusEnumType.ERROR;
263+
const providerName = wrapper.provider?.metadata?.name || 'unnamed-provider';
264+
this.getAssociatedEventEmitters(domain).forEach((emitter) => {
265+
emitter?.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+
});
272+
this._apiEmitter?.emit(ProviderEvents.Error, {
273+
clientName: domain,
274+
domain,
275+
providerName,
276+
message: `Error validating context during setProvider: ${validateContextError instanceof Error ? validateContextError.message : String(validateContextError)}`,
277+
});
278+
this._logger.error('Error validating context during setProvider:', validateContextError);
279+
}
280+
}
198281

199282
// The setProvider method doesn't return a promise so we need to catch and
200283
// log any errors that occur during provider initialization to avoid having
@@ -249,6 +332,8 @@ export class OpenFeatureAPI
249332
const domain = stringOrUndefined(domainOrContext);
250333
const context = objectOrUndefined<T>(domainOrContext) ?? objectOrUndefined(contextOrUndefined) ?? {};
251334

335+
// TODO: We need to store and call `validateContext` here if provided in `setProvider` options
336+
252337
if (domain) {
253338
const wrapper = this._domainScopedProviders.get(domain);
254339
if (wrapper) {

0 commit comments

Comments
 (0)