Skip to content

Commit 6625918

Browse files
authored
feat!: use node style events, add initialize (#379)
Adds and `initialize` handler, and uses node-style event emitters. --------- Signed-off-by: Todd Baert <[email protected]>
1 parent 2ff7135 commit 6625918

File tree

5 files changed

+106
-29
lines changed

5 files changed

+106
-29
lines changed

package-lock.json

Lines changed: 31 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"@openfeature/flagd-provider": "~0.7.0",
3636
"@rollup/plugin-alias": "^4.0.3",
3737
"@rollup/plugin-typescript": "^11.0.0",
38+
"@types/events": "^3.0.0",
3839
"@types/jest": "^29.0.0",
3940
"@types/node": "^18.0.3",
4041
"@typescript-eslint/eslint-plugin": "^5.23.0",
@@ -47,6 +48,7 @@
4748
"eslint-plugin-import": "^2.26.0",
4849
"eslint-plugin-jest": "^27.0.1",
4950
"eslint-plugin-jsdoc": "^39.3.6",
51+
"events": "^3.3.0",
5052
"jest": "^29.4.3",
5153
"jest-config": "^29.4.3",
5254
"jest-cucumber": "^3.0.1",

packages/client/src/client.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { OpenFeatureError } from '@openfeature/shared';
1+
import { EvaluationContext, OpenFeatureError } from '@openfeature/shared';
22
import { SafeLogger } from '@openfeature/shared';
33
import {
44
ApiEvents,
@@ -15,8 +15,9 @@ import {
1515
ResolutionDetails,
1616
StandardResolutionReasons
1717
} from '@openfeature/shared';
18+
import { EventEmitter } from 'events';
1819
import { OpenFeature } from './open-feature';
19-
import { Client, EventProvider, FlagEvaluationOptions, Hook, Provider } from './types';
20+
import { Client, FlagEvaluationOptions, Hook, Provider } from './types';
2021

2122
type OpenFeatureClientOptions = {
2223
name?: string;
@@ -35,9 +36,11 @@ export class OpenFeatureClient implements Client {
3536
private _handlerWrappers: HandlerWrapper[] = [];
3637

3738
constructor(
38-
// we always want the client to use the current provider,
39-
// so pass a function to always access the currently registered one.
40-
private readonly providerAccessor: () => Provider & Partial<EventProvider>,
39+
// functions are passed here to make sure that these values are always up to date,
40+
// and so we don't have to make these public properties on the API class.
41+
private readonly providerAccessor: () => Provider,
42+
private readonly providerReady: () => boolean,
43+
apiEvents: () => EventEmitter,
4144
private readonly globalLogger: () => Logger,
4245
options: OpenFeatureClientOptions,
4346
) {
@@ -47,12 +50,14 @@ export class OpenFeatureClient implements Client {
4750
} as const;
4851

4952
this.attachListeners();
50-
window.dispatchEvent(new CustomEvent(ApiEvents.ProviderChanged));
53+
apiEvents().on(ApiEvents.ProviderChanged, () => {
54+
this.attachListeners();
55+
});
5156
}
5257

5358
addHandler(eventType: ProviderEvents, handler: Handler): void {
5459
this._handlerWrappers.push({eventType, handler});
55-
if (eventType === ProviderEvents.Ready && this.providerAccessor().ready) {
60+
if (eventType === ProviderEvents.Ready && this.providerReady()) {
5661
// run immediately, we're ready.
5762
handler();
5863
}
@@ -166,6 +171,7 @@ export class OpenFeatureClient implements Client {
166171
resolver: (
167172
flagKey: string,
168173
defaultValue: T,
174+
context: EvaluationContext,
169175
logger: Logger
170176
) => ResolutionDetails<T>,
171177
defaultValue: T,
@@ -182,7 +188,6 @@ export class OpenFeatureClient implements Client {
182188
];
183189
const allHooksReversed = [...allHooks].reverse();
184190

185-
// merge global and client contexts
186191
const context = {
187192
...OpenFeature.getContext(),
188193
};
@@ -203,7 +208,7 @@ export class OpenFeatureClient implements Client {
203208
this.beforeHooks(allHooks, hookContext, options);
204209

205210
// run the referenced resolver, binding the provider.
206-
const resolution = resolver.call(this._provider, flagKey, defaultValue, this._logger);
211+
const resolution = resolver.call(this._provider, flagKey, defaultValue, context, this._logger);
207212

208213
const evaluationDetails = {
209214
...resolution,
@@ -289,7 +294,7 @@ export class OpenFeatureClient implements Client {
289294
}
290295
}
291296

292-
private get _provider(): Provider & Partial<EventProvider> {
297+
private get _provider(): Provider {
293298
return this.providerAccessor();
294299
}
295300

@@ -300,7 +305,7 @@ export class OpenFeatureClient implements Client {
300305
private attachListeners() {
301306
// iterate over the event types
302307
Object.values(ProviderEvents)
303-
.forEach(eventType => window.addEventListener(eventType, () => {
308+
.forEach(eventType => this._provider.events?.on(eventType, () => {
304309
// on each event type, fire the associated handlers
305310
this._handlerWrappers
306311
.filter(wrapper => wrapper.eventType === eventType)

packages/client/src/open-feature.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { NOOP_PROVIDER } from './no-op-provider';
2-
import { Logger, OpenFeatureCommonAPI, SafeLogger } from '@openfeature/shared';
3-
import { ApiEvents, EvaluationContext, FlagValue, ProviderMetadata } from '@openfeature/shared';
1+
import { ApiEvents, EvaluationContext, FlagValue, Logger, OpenFeatureCommonAPI, ProviderEvents, ProviderMetadata, SafeLogger } from '@openfeature/shared';
2+
import { EventEmitter } from 'events';
43
import { OpenFeatureClient } from './client';
4+
import { NOOP_PROVIDER } from './no-op-provider';
55
import { Client, Hook, Provider } from './types';
66

77
// use a symbol as a key for the global singleton
@@ -14,10 +14,11 @@ const _globalThis = globalThis as OpenFeatureGlobal;
1414

1515
export class OpenFeatureAPI extends OpenFeatureCommonAPI {
1616

17+
private _apiEvents = new EventEmitter();
18+
private _providerReady = false;
1719
protected _hooks: Hook[] = [];
1820
protected _provider: Provider = NOOP_PROVIDER;
1921

20-
2122
// eslint-disable-next-line @typescript-eslint/no-empty-function
2223
private constructor() {
2324
super();
@@ -79,7 +80,24 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI {
7980
if (this._provider !== provider) {
8081
const oldProvider = this._provider;
8182
this._provider = provider;
82-
window.dispatchEvent(new CustomEvent(ApiEvents.ProviderChanged));
83+
this._providerReady = false;
84+
85+
if (!this._provider.events) {
86+
this._provider.events = new EventEmitter();
87+
}
88+
89+
if (typeof this._provider?.initialize === 'function') {
90+
this._provider.initialize?.(this._context)?.then(() => {
91+
this._providerReady = true;
92+
this._provider.events?.emit(ProviderEvents.Ready);
93+
})?.catch(() => {
94+
this._provider.events?.emit(ProviderEvents.Error);
95+
});
96+
} else {
97+
this._providerReady = true;
98+
this._provider.events?.emit(ProviderEvents.Ready);
99+
}
100+
this._apiEvents.emit(ApiEvents.ProviderChanged);
83101
oldProvider?.onClose?.();
84102
}
85103
return this;
@@ -91,7 +109,11 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI {
91109

92110
getClient(name?: string, version?: string): Client {
93111
return new OpenFeatureClient(
112+
// functions are passed here to make sure that these values are always up to date,
113+
// and so we don't have to make these public properties on the API class.
94114
() => this._provider,
115+
() => this._providerReady,
116+
() => this._apiEvents,
95117
() => this._logger,
96118
{ name, version },
97119
);

packages/client/src/types.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
ProviderMetadata,
1717
ResolutionDetails,
1818
} from '@openfeature/shared';
19+
import EventEmitter from 'events';
1920

2021
/**
2122
* Interface that providers must implement to resolve flag values for their particular
@@ -33,18 +34,43 @@ export interface Provider extends CommonProvider {
3334
*/
3435
readonly hooks?: Hook[];
3536

36-
// client vs global context?
37+
/**
38+
* An event emitter for ProviderEvents.
39+
* @see ProviderEvents
40+
*/
41+
events?: EventEmitter;
42+
43+
/**
44+
* A handler function to reconcile changes when the static context.
45+
* Called by the SDK when the context is changed.
46+
*
47+
* @param oldContext
48+
* @param newContext
49+
*/
3750
onContextChange?(oldContext: EvaluationContext, newContext: EvaluationContext): Promise<void>
3851

3952
// TODO: move to common Provider type when we want close in server
4053
onClose?(): Promise<void>;
4154

55+
// TODO: move to common Provider type when we want close in server
56+
/**
57+
* A handler function used to setup the provider.
58+
* Called by the SDK after the provider is set.
59+
* When the returned promise resolves, the SDK fires the ProviderEvents.Ready event.
60+
* If the returned promise rejects, the SDK fires the ProviderEvents.Error event.
61+
* Use this function to perform any context-dependent setup within the provider.
62+
*
63+
* @param context
64+
*/
65+
initialize?(context: EvaluationContext): Promise<void>;
66+
4267
/**
4368
* Resolve a boolean flag and its evaluation details.
4469
*/
4570
resolveBooleanEvaluation(
4671
flagKey: string,
4772
defaultValue: boolean,
73+
context: EvaluationContext,
4874
logger: Logger
4975
): ResolutionDetails<boolean>;
5076

@@ -54,6 +80,7 @@ export interface Provider extends CommonProvider {
5480
resolveStringEvaluation(
5581
flagKey: string,
5682
defaultValue: string,
83+
context: EvaluationContext,
5784
logger: Logger
5885
): ResolutionDetails<string>;
5986

@@ -63,6 +90,7 @@ export interface Provider extends CommonProvider {
6390
resolveNumberEvaluation(
6491
flagKey: string,
6592
defaultValue: number,
93+
context: EvaluationContext,
6694
logger: Logger
6795
): ResolutionDetails<number>;
6896

@@ -72,6 +100,7 @@ export interface Provider extends CommonProvider {
72100
resolveObjectEvaluation<T extends JsonValue>(
73101
flagKey: string,
74102
defaultValue: T,
103+
context: EvaluationContext,
75104
logger: Logger
76105
): ResolutionDetails<T>;
77106
}
@@ -340,8 +369,4 @@ export interface GlobalApi
340369
* @returns {GlobalApi} OpenFeature API
341370
*/
342371
setProvider(provider: Provider): GlobalApi;
343-
}
344-
345-
export interface EventProvider {
346-
readonly ready: boolean;
347372
}

0 commit comments

Comments
 (0)