Skip to content

Commit 92e3a72

Browse files
authored
feat: typesafe event emitter (#490)
Signed-off-by: Michael Beemer <[email protected]>
1 parent 5725194 commit 92e3a72

File tree

9 files changed

+118
-82
lines changed

9 files changed

+118
-82
lines changed

packages/client/src/client/open-feature-client.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export class OpenFeatureClient implements Client {
4949
};
5050
}
5151

52-
addHandler(eventType: ProviderEvents, handler: EventHandler): void {
52+
addHandler<T extends ProviderEvents>(eventType: T, handler: EventHandler<T>): void {
5353
this.emitterAccessor().addHandler(eventType, handler);
5454
const providerReady = !this._provider.status || this._provider.status === ProviderStatus.READY;
5555

@@ -63,20 +63,20 @@ export class OpenFeatureClient implements Client {
6363
}
6464
}
6565

66-
removeHandler(notificationType: ProviderEvents, handler: EventHandler): void {
66+
removeHandler<T extends ProviderEvents>(notificationType: T, handler: EventHandler<T>): void {
6767
this.emitterAccessor().removeHandler(notificationType, handler);
6868
}
6969

7070
getHandlers(eventType: ProviderEvents) {
7171
return this.emitterAccessor().getHandlers(eventType);
7272
}
7373

74-
setLogger(logger: Logger): OpenFeatureClient {
74+
setLogger(logger: Logger): this {
7575
this._clientLogger = new SafeLogger(logger);
7676
return this;
7777
}
7878

79-
addHooks(...hooks: Hook<FlagValue>[]): OpenFeatureClient {
79+
addHooks(...hooks: Hook<FlagValue>[]): this {
8080
this._hooks = [...this._hooks, ...hooks];
8181
return this;
8282
}
@@ -85,7 +85,7 @@ export class OpenFeatureClient implements Client {
8585
return this._hooks;
8686
}
8787

88-
clearHooks(): OpenFeatureClient {
88+
clearHooks(): this {
8989
this._hooks = [];
9090
return this;
9191
}

packages/client/test/events.spec.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
EventDetails,
32
JsonValue,
43
NOOP_PROVIDER,
54
OpenFeature,
@@ -9,6 +8,7 @@ import {
98
ProviderMetadata,
109
ProviderStatus,
1110
ResolutionDetails,
11+
StaleEvent,
1212
} from '../src';
1313
import { v4 as uuid } from 'uuid';
1414

@@ -300,7 +300,7 @@ describe('Events', () => {
300300
const provider = new MockProvider({ failOnInit: true });
301301
const client = OpenFeature.getClient(clientId);
302302

303-
client.addHandler(ProviderEvents.Error, (details?: EventDetails) => {
303+
client.addHandler(ProviderEvents.Error, (details) => {
304304
expect(details?.message).toBeDefined();
305305
done();
306306
});
@@ -327,7 +327,7 @@ describe('Events', () => {
327327
const provider = new MockProvider();
328328
const client = OpenFeature.getClient(clientId);
329329

330-
client.addHandler(ProviderEvents.Ready, (details?: EventDetails) => {
330+
client.addHandler(ProviderEvents.Ready, (details) => {
331331
expect(details?.clientName).toEqual(clientId);
332332
done();
333333
});
@@ -339,7 +339,7 @@ describe('Events', () => {
339339
const provider = new MockProvider();
340340
const client = OpenFeature.getClient(clientId);
341341

342-
client.addHandler(ProviderEvents.Ready, (details?: EventDetails) => {
342+
client.addHandler(ProviderEvents.Ready, (details) => {
343343
expect(details?.clientName).toEqual(clientId);
344344
done();
345345
});
@@ -350,11 +350,11 @@ describe('Events', () => {
350350

351351
describe('Requirement 5.2.4', () => {
352352
it('The handler function accepts a event details parameter.', (done) => {
353-
const details: EventDetails = { message: 'message' };
353+
const details: StaleEvent = { message: 'message' };
354354
const provider = new MockProvider();
355355
const client = OpenFeature.getClient(clientId);
356356

357-
client.addHandler(ProviderEvents.Stale, (givenDetails?: EventDetails) => {
357+
client.addHandler(ProviderEvents.Stale, (givenDetails) => {
358358
expect(givenDetails?.message).toEqual(details.message);
359359
done();
360360
});

packages/server/src/client/open-feature-client.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
JsonValue,
1111
Logger,
1212
OpenFeatureError,
13-
OpenFeatureEventEmitter,
13+
InternalEventEmitter,
1414
ProviderEvents,
1515
ProviderStatus,
1616
ResolutionDetails,
@@ -38,7 +38,7 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
3838
// we always want the client to use the current provider,
3939
// so pass a function to always access the currently registered one.
4040
private readonly providerAccessor: () => Provider,
41-
private readonly emitterAccessor: () => OpenFeatureEventEmitter,
41+
private readonly emitterAccessor: () => InternalEventEmitter,
4242
private readonly globalLogger: () => Logger,
4343
private readonly options: OpenFeatureClientOptions,
4444
context: EvaluationContext = {}
@@ -54,7 +54,7 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
5454
};
5555
}
5656

57-
addHandler(eventType: ProviderEvents, handler: EventHandler): void {
57+
addHandler<T extends ProviderEvents>(eventType: T, handler: EventHandler<T>): void {
5858
this.emitterAccessor().addHandler(eventType, handler);
5959
const providerReady = !this._provider.status || this._provider.status === ProviderStatus.READY;
6060

@@ -68,7 +68,7 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
6868
}
6969
}
7070

71-
removeHandler(eventType: ProviderEvents, handler: EventHandler) {
71+
removeHandler<T extends ProviderEvents>(eventType: T, handler: EventHandler<T>) {
7272
this.emitterAccessor().removeHandler(eventType, handler);
7373
}
7474

packages/server/src/provider/provider.ts

+1-9
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,4 @@
1-
import {
2-
CommonProvider,
3-
EvaluationContext,
4-
HookHints,
5-
JsonValue,
6-
Logger,
7-
ResolutionDetails,
8-
} from '@openfeature/shared';
9-
import { Hook } from '@openfeature/shared';
1+
import { CommonProvider, EvaluationContext, Hook, JsonValue, Logger, ResolutionDetails } from '@openfeature/shared';
102

113
/**
124
* Interface that providers must implement to resolve flag values for their particular

packages/server/test/events.spec.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
EventDetails,
32
JsonValue,
43
OpenFeature,
54
OpenFeatureEventEmitter,
@@ -8,9 +7,10 @@ import {
87
ProviderMetadata,
98
ProviderStatus,
109
ResolutionDetails,
10+
NOOP_PROVIDER,
11+
StaleEvent,
1112
} from '../src';
1213
import { v4 as uuid } from 'uuid';
13-
import { NOOP_PROVIDER } from '../src';
1414

1515
class MockProvider implements Provider {
1616
readonly metadata: ProviderMetadata;
@@ -303,7 +303,7 @@ describe('Events', () => {
303303
const provider = new MockProvider({ failOnInit: true });
304304
const client = OpenFeature.getClient(clientId);
305305

306-
client.addHandler(ProviderEvents.Error, (details?: EventDetails) => {
306+
client.addHandler(ProviderEvents.Error, (details) => {
307307
expect(details?.message).toBeDefined();
308308
done();
309309
});
@@ -330,7 +330,7 @@ describe('Events', () => {
330330
const provider = new MockProvider();
331331
const client = OpenFeature.getClient(clientId);
332332

333-
client.addHandler(ProviderEvents.Ready, (details?: EventDetails) => {
333+
client.addHandler(ProviderEvents.Ready, (details) => {
334334
expect(details?.clientName).toEqual(clientId);
335335
done();
336336
});
@@ -342,7 +342,7 @@ describe('Events', () => {
342342
const provider = new MockProvider();
343343
const client = OpenFeature.getClient(clientId);
344344

345-
client.addHandler(ProviderEvents.Ready, (details?: EventDetails) => {
345+
client.addHandler(ProviderEvents.Ready, (details) => {
346346
expect(details?.clientName).toEqual(clientId);
347347
done();
348348
});
@@ -353,11 +353,11 @@ describe('Events', () => {
353353

354354
describe('Requirement 5.2.4', () => {
355355
it('The handler function accepts a event details parameter.', (done) => {
356-
const details: EventDetails = { message: 'message' };
356+
const details: StaleEvent = { message: 'message' };
357357
const provider = new MockProvider();
358358
const client = OpenFeature.getClient(clientId);
359359

360-
client.addHandler(ProviderEvents.Stale, (givenDetails?: EventDetails) => {
360+
client.addHandler(ProviderEvents.Stale, (givenDetails) => {
361361
expect(givenDetails?.message).toEqual(details.message);
362362
done();
363363
});

packages/shared/src/events/eventing.ts

+26-6
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,34 @@ export type EventMetadata = {
44
[key: string]: string | boolean | number;
55
};
66

7-
export type EventDetails = {
7+
export type CommonEventDetails = {
88
clientName?: string;
9+
};
10+
11+
type CommonEventProps = {
912
message?: string;
10-
flagsChanged?: string[];
1113
metadata?: EventMetadata;
1214
};
1315

14-
export type EventHandler = (eventDetails?: EventDetails) => Promise<unknown> | unknown;
16+
export type ReadyEvent = CommonEventProps;
17+
export type ErrorEvent = CommonEventProps;
18+
export type StaleEvent = CommonEventProps;
19+
export type ConfigChangeEvent = CommonEventProps & { flagsChanged?: string[] };
20+
21+
type EventMap = {
22+
[ProviderEvents.Ready]: ReadyEvent;
23+
[ProviderEvents.Error]: ErrorEvent;
24+
[ProviderEvents.Stale]: StaleEvent;
25+
[ProviderEvents.ConfigurationChanged]: ConfigChangeEvent;
26+
};
27+
28+
export type EventContext<
29+
T extends ProviderEvents,
30+
U extends Record<string, unknown> = Record<string, unknown>
31+
> = EventMap[T] & U;
32+
33+
export type EventDetails<T extends ProviderEvents> = EventContext<T> & CommonEventDetails;
34+
export type EventHandler<T extends ProviderEvents> = (eventDetails?: EventDetails<T>) => Promise<unknown> | unknown;
1535

1636
export interface Eventing {
1737
/**
@@ -20,19 +40,19 @@ export interface Eventing {
2040
* @param {ProviderEvents} eventType The provider event type to listen to
2141
* @param {EventHandler} handler The handler to run on occurrence of the event type
2242
*/
23-
addHandler(eventType: ProviderEvents, handler: EventHandler): void;
43+
addHandler<T extends ProviderEvents>(eventType: T, handler: EventHandler<T>): void;
2444

2545
/**
2646
* Removes a handler for the given provider event type.
2747
* @param {ProviderEvents} eventType The provider event type to remove the listener for
2848
* @param {EventHandler} handler The handler to remove for the provider event type
2949
*/
30-
removeHandler(eventType: ProviderEvents, handler: EventHandler): void;
50+
removeHandler<T extends ProviderEvents>(eventType: T, handler: EventHandler<T>): void;
3151

3252
/**
3353
* Gets the current handlers for the given provider event type.
3454
* @param {ProviderEvents} eventType The provider event type to get the current handlers for
3555
* @returns {EventHandler[]} The handlers currently attached to the given provider event type
3656
*/
37-
getHandlers(eventType: ProviderEvents): EventHandler[];
57+
getHandlers<T extends ProviderEvents>(eventType: T): EventHandler<T>[];
3858
}

packages/shared/src/events/open-feature-event-emitter.ts

+30-11
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { Logger, ManageLogger, SafeLogger } from '../logger';
22
import EventEmitter from 'events';
33
import { ProviderEvents } from './events';
4-
import { EventDetails, EventHandler } from './eventing';
4+
import { EventContext, EventDetails, EventHandler, CommonEventDetails } from './eventing';
55

6-
export class OpenFeatureEventEmitter implements ManageLogger<OpenFeatureEventEmitter> {
7-
private readonly _handlers = new WeakMap<EventHandler, EventHandler>();
6+
abstract class GenericEventEmitter<AdditionalContext extends Record<string, unknown> = Record<string, unknown>>
7+
implements ManageLogger<GenericEventEmitter<AdditionalContext>>
8+
{
9+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
10+
private readonly _handlers = new WeakMap<EventHandler<any>, EventHandler<any>>();
811
private readonly eventEmitter = new EventEmitter({ captureRejections: true });
912
private _eventLogger?: Logger;
1013

@@ -14,24 +17,24 @@ export class OpenFeatureEventEmitter implements ManageLogger<OpenFeatureEventEmi
1417
});
1518
}
1619

17-
emit(eventType: ProviderEvents, context?: EventDetails): void {
20+
emit<T extends ProviderEvents>(eventType: T, context?: EventContext<T, AdditionalContext>): void {
1821
this.eventEmitter.emit(eventType, context);
1922
}
2023

21-
addHandler(eventType: ProviderEvents, handler: EventHandler): void {
24+
addHandler<T extends ProviderEvents>(eventType: T, handler: EventHandler<T>): void {
2225
// The handlers have to be wrapped with an async function because if a synchronous functions throws an error,
2326
// the other handlers will not run.
24-
const asyncHandler = async (context?: EventDetails) => {
27+
const asyncHandler = async (context?: EventDetails<T>) => {
2528
await handler(context);
2629
};
2730
// The async handler has to be written to the map, because we need to get the wrapper function when deleting a listener
2831
this._handlers.set(handler, asyncHandler);
2932
this.eventEmitter.on(eventType, asyncHandler);
3033
}
3134

32-
removeHandler(eventType: ProviderEvents, handler: EventHandler): void {
35+
removeHandler<T extends ProviderEvents>(eventType: T, handler: EventHandler<T>): void {
3336
// Get the wrapper function for this handler, to delete it from the event emitter
34-
const asyncHandler = this._handlers.get(handler);
37+
const asyncHandler = this._handlers.get(handler) as EventHandler<T> | undefined;
3538

3639
if (!asyncHandler) {
3740
return;
@@ -49,8 +52,8 @@ export class OpenFeatureEventEmitter implements ManageLogger<OpenFeatureEventEmi
4952
}
5053
}
5154

52-
getHandlers(eventType: ProviderEvents): EventHandler[] {
53-
return this.eventEmitter.listeners(eventType) as EventHandler[];
55+
getHandlers<T extends ProviderEvents>(eventType: T): EventHandler<T>[] {
56+
return this.eventEmitter.listeners(eventType) as EventHandler<T>[];
5457
}
5558

5659
setLogger(logger: Logger): this {
@@ -59,6 +62,22 @@ export class OpenFeatureEventEmitter implements ManageLogger<OpenFeatureEventEmi
5962
}
6063

6164
private get _logger() {
62-
return this._eventLogger || this.globalLogger?.();
65+
return this._eventLogger ?? this.globalLogger?.();
6366
}
6467
}
68+
69+
/**
70+
* The OpenFeatureEventEmitter can be used by provider developers to emit
71+
* events at various parts of the provider lifecycle.
72+
*
73+
* NOTE: Ready and error events are automatically emitted by the SDK based on
74+
* the result of the initialize method.
75+
*/
76+
export class OpenFeatureEventEmitter extends GenericEventEmitter {};
77+
78+
/**
79+
* The InternalEventEmitter should only be used within the SDK. It extends the
80+
* OpenFeatureEventEmitter to include additional properties that can be included
81+
* in the event details.
82+
*/
83+
export class InternalEventEmitter extends GenericEventEmitter<CommonEventDetails> {};

0 commit comments

Comments
 (0)