Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: typesafe event emitter #490

Merged
merged 4 commits into from
Jul 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions packages/client/src/client/open-feature-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class OpenFeatureClient implements Client {
};
}

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

Expand All @@ -63,20 +63,20 @@ export class OpenFeatureClient implements Client {
}
}

removeHandler(notificationType: ProviderEvents, handler: EventHandler): void {
removeHandler<T extends ProviderEvents>(notificationType: T, handler: EventHandler<T>): void {
this.emitterAccessor().removeHandler(notificationType, handler);
}

getHandlers(eventType: ProviderEvents) {
return this.emitterAccessor().getHandlers(eventType);
}

setLogger(logger: Logger): OpenFeatureClient {
setLogger(logger: Logger): this {
this._clientLogger = new SafeLogger(logger);
return this;
}

addHooks(...hooks: Hook<FlagValue>[]): OpenFeatureClient {
addHooks(...hooks: Hook<FlagValue>[]): this {
this._hooks = [...this._hooks, ...hooks];
return this;
}
Expand All @@ -85,7 +85,7 @@ export class OpenFeatureClient implements Client {
return this._hooks;
}

clearHooks(): OpenFeatureClient {
clearHooks(): this {
this._hooks = [];
return this;
}
Expand Down
12 changes: 6 additions & 6 deletions packages/client/test/events.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
EventDetails,
JsonValue,
NOOP_PROVIDER,
OpenFeature,
Expand All @@ -9,6 +8,7 @@ import {
ProviderMetadata,
ProviderStatus,
ResolutionDetails,
StaleEvent,
} from '../src';
import { v4 as uuid } from 'uuid';

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

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

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

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

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

client.addHandler(ProviderEvents.Stale, (givenDetails?: EventDetails) => {
client.addHandler(ProviderEvents.Stale, (givenDetails) => {
expect(givenDetails?.message).toEqual(details.message);
done();
});
Expand Down
8 changes: 4 additions & 4 deletions packages/server/src/client/open-feature-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
JsonValue,
Logger,
OpenFeatureError,
OpenFeatureEventEmitter,
InternalEventEmitter,
ProviderEvents,
ProviderStatus,
ResolutionDetails,
Expand Down Expand Up @@ -38,7 +38,7 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
// we always want the client to use the current provider,
// so pass a function to always access the currently registered one.
private readonly providerAccessor: () => Provider,
private readonly emitterAccessor: () => OpenFeatureEventEmitter,
private readonly emitterAccessor: () => InternalEventEmitter,
private readonly globalLogger: () => Logger,
private readonly options: OpenFeatureClientOptions,
context: EvaluationContext = {}
Expand All @@ -54,7 +54,7 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
};
}

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

Expand All @@ -68,7 +68,7 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
}
}

removeHandler(eventType: ProviderEvents, handler: EventHandler) {
removeHandler<T extends ProviderEvents>(eventType: T, handler: EventHandler<T>) {
this.emitterAccessor().removeHandler(eventType, handler);
}

Expand Down
10 changes: 1 addition & 9 deletions packages/server/src/provider/provider.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
import {
CommonProvider,
EvaluationContext,
HookHints,
JsonValue,
Logger,
ResolutionDetails,
} from '@openfeature/shared';
import { Hook } from '@openfeature/shared';
import { CommonProvider, EvaluationContext, Hook, JsonValue, Logger, ResolutionDetails } from '@openfeature/shared';

/**
* Interface that providers must implement to resolve flag values for their particular
Expand Down
14 changes: 7 additions & 7 deletions packages/server/test/events.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
EventDetails,
JsonValue,
OpenFeature,
OpenFeatureEventEmitter,
Expand All @@ -8,9 +7,10 @@ import {
ProviderMetadata,
ProviderStatus,
ResolutionDetails,
NOOP_PROVIDER,
StaleEvent,
} from '../src';
import { v4 as uuid } from 'uuid';
import { NOOP_PROVIDER } from '../src';

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

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

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

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

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

client.addHandler(ProviderEvents.Stale, (givenDetails?: EventDetails) => {
client.addHandler(ProviderEvents.Stale, (givenDetails) => {
expect(givenDetails?.message).toEqual(details.message);
done();
});
Expand Down
32 changes: 26 additions & 6 deletions packages/shared/src/events/eventing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,34 @@ export type EventMetadata = {
[key: string]: string | boolean | number;
};

export type EventDetails = {
export type CommonEventDetails = {
clientName?: string;
};

type CommonEventProps = {
message?: string;
flagsChanged?: string[];
metadata?: EventMetadata;
};

export type EventHandler = (eventDetails?: EventDetails) => Promise<unknown> | unknown;
export type ReadyEvent = CommonEventProps;
export type ErrorEvent = CommonEventProps;
export type StaleEvent = CommonEventProps;
export type ConfigChangeEvent = CommonEventProps & { flagsChanged?: string[] };

type EventMap = {
[ProviderEvents.Ready]: ReadyEvent;
[ProviderEvents.Error]: ErrorEvent;
[ProviderEvents.Stale]: StaleEvent;
[ProviderEvents.ConfigurationChanged]: ConfigChangeEvent;
};

export type EventContext<
T extends ProviderEvents,
U extends Record<string, unknown> = Record<string, unknown>
> = EventMap[T] & U;

export type EventDetails<T extends ProviderEvents> = EventContext<T> & CommonEventDetails;
export type EventHandler<T extends ProviderEvents> = (eventDetails?: EventDetails<T>) => Promise<unknown> | unknown;

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

/**
* Removes a handler for the given provider event type.
* @param {ProviderEvents} eventType The provider event type to remove the listener for
* @param {EventHandler} handler The handler to remove for the provider event type
*/
removeHandler(eventType: ProviderEvents, handler: EventHandler): void;
removeHandler<T extends ProviderEvents>(eventType: T, handler: EventHandler<T>): void;

/**
* Gets the current handlers for the given provider event type.
* @param {ProviderEvents} eventType The provider event type to get the current handlers for
* @returns {EventHandler[]} The handlers currently attached to the given provider event type
*/
getHandlers(eventType: ProviderEvents): EventHandler[];
getHandlers<T extends ProviderEvents>(eventType: T): EventHandler<T>[];
}
41 changes: 30 additions & 11 deletions packages/shared/src/events/open-feature-event-emitter.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Logger, ManageLogger, SafeLogger } from '../logger';
import EventEmitter from 'events';
import { ProviderEvents } from './events';
import { EventDetails, EventHandler } from './eventing';
import { EventContext, EventDetails, EventHandler, CommonEventDetails } from './eventing';

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

Expand All @@ -14,24 +17,24 @@ export class OpenFeatureEventEmitter implements ManageLogger<OpenFeatureEventEmi
});
}

emit(eventType: ProviderEvents, context?: EventDetails): void {
emit<T extends ProviderEvents>(eventType: T, context?: EventContext<T, AdditionalContext>): void {
this.eventEmitter.emit(eventType, context);
}

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

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

if (!asyncHandler) {
return;
Expand All @@ -49,8 +52,8 @@ export class OpenFeatureEventEmitter implements ManageLogger<OpenFeatureEventEmi
}
}

getHandlers(eventType: ProviderEvents): EventHandler[] {
return this.eventEmitter.listeners(eventType) as EventHandler[];
getHandlers<T extends ProviderEvents>(eventType: T): EventHandler<T>[] {
return this.eventEmitter.listeners(eventType) as EventHandler<T>[];
}

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

private get _logger() {
return this._eventLogger || this.globalLogger?.();
return this._eventLogger ?? this.globalLogger?.();
}
}

/**
* The OpenFeatureEventEmitter can be used by provider developers to emit
* events at various parts of the provider lifecycle.
*
* NOTE: Ready and error events are automatically emitted by the SDK based on
* the result of the initialize method.
*/
export class OpenFeatureEventEmitter extends GenericEventEmitter {};

/**
* The InternalEventEmitter should only be used within the SDK. It extends the
* OpenFeatureEventEmitter to include additional properties that can be included
* in the event details.
*/
export class InternalEventEmitter extends GenericEventEmitter<CommonEventDetails> {};
Loading