Skip to content

Commit

Permalink
feat: add init/shutdown and events (#436)
Browse files Browse the repository at this point in the history
Signed-off-by: Lukas Reining <lukas.reining@codecentric.de>
Co-authored-by: Michael Beemer <beeme1mr@users.noreply.github.com>
Co-authored-by: Todd Baert <todd.baert@dynatrace.com>
  • Loading branch information
3 people authored May 30, 2023
1 parent 310c6ac commit 5d55ea1
Show file tree
Hide file tree
Showing 20 changed files with 1,239 additions and 324 deletions.
8 changes: 7 additions & 1 deletion jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ export default {
preset: 'ts-jest',
// Run tests from one or more projects
projects: [
{
displayName: 'shared',
testEnvironment: 'node',
preset: 'ts-jest',
testMatch: ['<rootDir>/packages/shared/test/**/*.spec.ts'],
},
{
displayName: 'server',
testEnvironment: 'node',
Expand Down Expand Up @@ -137,7 +143,7 @@ export default {
setupFiles: ['<rootDir>/packages/client/e2e/step-definitions/setup.ts'],
moduleNameMapper: {
'^uuid$': require.resolve('uuid'),
'^(.*)\\.js$': ['$1', '$1.js']
'^(.*)\\.js$': ['$1', '$1.js'],
},
},
],
Expand Down
5 changes: 1 addition & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,5 @@
"packages/server",
"packages/client",
"packages/shared"
],
"dependencies": {

}
]
}
6 changes: 3 additions & 3 deletions packages/client/e2e/step-definitions/evaluation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ const givenAnOpenfeatureClientIsRegisteredWithCacheDisabled = (

defineFeature(feature, (test) => {
beforeAll((done) => {
client.addHandler(ProviderEvents.Ready, () => {
client.addHandler(ProviderEvents.Ready, async () => {
setTimeout(() => {
done(); // TODO remove this once flagd provider properly implements readiness (for now, we add a 2s wait).
}, 2000);
});
});

afterAll(() => {
OpenFeature.close();
afterAll(async () => {
await OpenFeature.close();
});

test('Resolves boolean value', ({ given, when, then }) => {
Expand Down
27 changes: 15 additions & 12 deletions packages/client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,22 @@ import {
ErrorCode,
EvaluationContext,
EvaluationDetails,
EventHandler,
FlagValue,
FlagValueType,
HookContext,
JsonValue,
Logger,
OpenFeatureError,
OpenFeatureEventEmitter,
ProviderEvents,
ProviderStatus,
ResolutionDetails,
SafeLogger,
StandardResolutionReasons,
} from '@openfeature/shared';
import { OpenFeature } from './open-feature';
import {
Client,
FlagEvaluationOptions,
Handler,
Hook,
OpenFeatureEventEmitter,
Provider,
ProviderEvents,
} from './types';
import { Client, FlagEvaluationOptions, Hook, Provider } from './types';

type OpenFeatureClientOptions = {
name?: string;
Expand Down Expand Up @@ -51,16 +46,24 @@ export class OpenFeatureClient implements Client {
};
}

addHandler(eventType: ProviderEvents, handler: Handler): void {
this.events().on(eventType, handler);
addHandler(eventType: ProviderEvents, handler: EventHandler): void {
this.events().addHandler(eventType, handler);
const providerReady = !this._provider.status || this._provider.status === ProviderStatus.READY;

if (eventType === ProviderEvents.Ready && providerReady) {
// run immediately, we're ready.
handler();
handler({ clientName: this.metadata.name });
}
}

removeHandler(notificationType: ProviderEvents, handler: EventHandler) {
this.events().removeHandler(notificationType, handler);
}

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

setLogger(logger: Logger): OpenFeatureClient {
this._clientLogger = new SafeLogger(logger);
return this;
Expand Down
122 changes: 3 additions & 119 deletions packages/client/src/open-feature.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
import {
EvaluationContext,
FlagValue,
Logger,
OpenFeatureCommonAPI,
ProviderMetadata,
SafeLogger,
} from '@openfeature/shared';
import { EvaluationContext, FlagValue, Logger, OpenFeatureCommonAPI, SafeLogger } from '@openfeature/shared';
import { OpenFeatureClient } from './client';
import { NOOP_PROVIDER } from './no-op-provider';
import { Client, Hook, OpenFeatureEventEmitter, Provider, ProviderEvents } from './types';
import { objectOrUndefined, stringOrUndefined } from '@openfeature/shared/src/type-guards';
import { Client, Hook, Provider } from './types';

// use a symbol as a key for the global singleton
const GLOBAL_OPENFEATURE_API_KEY = Symbol.for('@openfeature/js.api');
Expand All @@ -19,12 +11,9 @@ type OpenFeatureGlobal = {
};
const _globalThis = globalThis as OpenFeatureGlobal;

export class OpenFeatureAPI extends OpenFeatureCommonAPI {
export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider> {
protected _hooks: Hook[] = [];
private readonly _events = new OpenFeatureEventEmitter();
protected _defaultProvider: Provider = NOOP_PROVIDER;
protected _clientProviders: Map<string, Provider> = new Map();
protected _clientEvents: Map<string | undefined, OpenFeatureEventEmitter> = new Map();

// eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {
Expand All @@ -47,14 +36,6 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI {
return instance;
}

/**
* Get metadata about registered provider.
* @returns {ProviderMetadata} Provider Metadata
*/
get providerMetadata(): ProviderMetadata {
return this._defaultProvider.metadata;
}

setLogger(logger: Logger): this {
this._logger = new SafeLogger(logger);
return this;
Expand All @@ -80,70 +61,6 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI {
await this._defaultProvider?.onContextChange?.(oldContext, context);
}

/**
* Sets the default provider for flag evaluations.
* This provider will be used by unnamed clients and named clients to which no provider is bound.
* Setting a provider supersedes the current provider used in new and existing clients without a name.
* @param {Provider} provider The provider responsible for flag evaluations.
* @returns {OpenFeatureAPI} OpenFeature API
*/
setProvider(provider: Provider): this;
/**
* Sets the provider that OpenFeature will use for flag evaluations of providers with the given name.
* Setting a provider supersedes the current provider used in new and existing clients with that name.
* @param {string} clientName The name to identify the client
* @param {Provider} provider The provider responsible for flag evaluations.
* @returns {OpenFeatureAPI} OpenFeature API
*/
setProvider(clientName: string, provider: Provider): this;
setProvider(clientOrProvider?: string | Provider, providerOrUndefined?: Provider): this {
const clientName = stringOrUndefined(clientOrProvider);
const provider = objectOrUndefined<Provider>(clientOrProvider) ?? objectOrUndefined<Provider>(providerOrUndefined);

if (!provider) {
return this;
}

const oldProvider = this.getProviderForClient(clientName);

// ignore no-ops
if (oldProvider === provider) {
return this;
}

if (clientName) {
this._clientProviders.set(clientName, provider);
} else {
this._defaultProvider = provider;
}

const clientEmitter = this.getEventEmitterForClient(clientName);
this.transferListeners(oldProvider, provider, clientEmitter);

if (typeof provider.initialize === 'function') {
provider
.initialize?.(this._context)
?.then(() => {
clientEmitter.emit(ProviderEvents.Ready);
this._events?.emit(ProviderEvents.Ready);
})
?.catch(() => {
clientEmitter.emit(ProviderEvents.Error);
this._events?.emit(ProviderEvents.Error);
});
} else {
clientEmitter.emit(ProviderEvents.Ready);
this._events?.emit(ProviderEvents.Ready);
}

oldProvider?.onClose?.();
return this;
}

async close(): Promise<void> {
await this?._defaultProvider?.onClose?.();
}

/**
* A factory function for creating new named OpenFeature clients. Clients can contain
* their own state (e.g. logger, hook, context). Multiple clients can be used
Expand All @@ -165,39 +82,6 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI {
{ name, version }
);
}

private getProviderForClient(name?: string): Provider {
if (!name) {
return this._defaultProvider;
}

return this._clientProviders.get(name) ?? this._defaultProvider;
}

private getEventEmitterForClient(name?: string): OpenFeatureEventEmitter {
const emitter = this._clientEvents.get(name);

if (emitter) {
return emitter;
}

const newEmitter = new OpenFeatureEventEmitter({});
this._clientEvents.set(name, newEmitter);
return newEmitter;
}

private transferListeners(oldProvider: Provider, newProvider: Provider, clientEmitter: OpenFeatureEventEmitter) {
oldProvider.events?.removeAllListeners();

// iterate over the event types
Object.values(ProviderEvents).forEach((eventType) =>
newProvider.events?.on(eventType, () => {
// on each event type, fire the associated handlers
clientEmitter.emit(eventType);
this._events.emit(eventType);
})
);
}
}

/**
Expand Down
63 changes: 1 addition & 62 deletions packages/client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
CommonProvider,
EvaluationContext,
EvaluationDetails,
Eventing,
FlagValue,
HookContext,
HookHints,
Expand All @@ -15,48 +16,6 @@ import {
ProviderMetadata,
ResolutionDetails,
} from '@openfeature/shared';
import { EventEmitter as OpenFeatureEventEmitter } from 'events';
export { OpenFeatureEventEmitter };

export enum ProviderEvents {
/**
* The provider is ready to evaluate flags.
*/
Ready = 'PROVIDER_READY',

/**
* The provider is in an error state.
*/
Error = 'PROVIDER_ERROR',

/**
* The flag configuration in the source-of-truth has changed.
*/
ConfigurationChanged = 'PROVIDER_CONFIGURATION_CHANGED',

/**
* The provider's cached state is not longer valid and may not be up-to-date with the source of truth.
*/
Stale = 'PROVIDER_STALE',
}

export interface EventData {
flagKeysChanged?: string[];
changeMetadata?: { [key: string]: boolean | string }; // similar to flag metadata
}

export interface Eventing {
addHandler(notificationType: string, handler: Handler): void;
}

export type EventContext = {
notificationType: string;
[key: string]: unknown;
};

export type Handler = (eventContext?: EventContext) => void;

export type EventCallbackMessage = (eventContext: EventContext) => void;

/**
* Interface that providers must implement to resolve flag values for their particular
Expand All @@ -73,12 +32,6 @@ export interface Provider extends CommonProvider {
*/
readonly hooks?: Hook[];

/**
* An event emitter for ProviderEvents.
* @see ProviderEvents
*/
events?: OpenFeatureEventEmitter;

/**
* A handler function to reconcile changes when the static context.
* Called by the SDK when the context is changed.
Expand All @@ -87,20 +40,6 @@ export interface Provider extends CommonProvider {
*/
onContextChange?(oldContext: EvaluationContext, newContext: EvaluationContext): Promise<void>;

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

// TODO: move to common Provider type when we want close in server
/**
* A handler function used to setup the provider.
* Called by the SDK after the provider is set.
* When the returned promise resolves, the SDK fires the ProviderEvents.Ready event.
* If the returned promise rejects, the SDK fires the ProviderEvents.Error event.
* Use this function to perform any context-dependent setup within the provider.
* @param context
*/
initialize?(context: EvaluationContext): Promise<void>;

/**
* Resolve a boolean flag and its evaluation details.
*/
Expand Down
Loading

0 comments on commit 5d55ea1

Please sign in to comment.