Skip to content

feat: add init/shutdown and events #436

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

Merged
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
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