fix: make hooks in client sdk only return void (#671)

<!-- Please use this template for your pull request. -->
<!-- Please use the sections that you need and delete other sections -->

## This PR

Fixes return type of hooks in client SDK. 

Maybe we should make this more clear in the spec @toddbaert.

### Related Issues
Fixes #630 

### Notes
<!-- any additional notes for this PR -->

### Follow-up Tasks
<!-- anything that is related to this PR but not done here should be
noted under this section -->
<!-- if there is a need for a new issue, please link it here -->

### How to test
<!-- if applicable, add testing instructions under this section -->

---------

Signed-off-by: Lukas Reining <lukas.reining@codecentric.de>
Co-authored-by: Todd Baert <todd.baert@dynatrace.com>
This commit is contained in:
Lukas Reining 2023-11-21 17:31:33 +01:00 committed by GitHub
parent 00a6d2efb4
commit a7d0b954dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 130 additions and 193 deletions

View File

@ -6,7 +6,6 @@ import {
EventHandler,
FlagValue,
FlagValueType,
Hook,
HookContext,
JsonValue,
Logger,
@ -22,6 +21,7 @@ import { OpenFeature } from '../open-feature';
import { Provider } from '../provider';
import { InternalEventEmitter } from '../events/internal/internal-event-emitter';
import { Client } from './client';
import { Hook } from '../hooks';
type OpenFeatureClientOptions = {
name?: string;
@ -38,7 +38,7 @@ export class OpenFeatureClient implements Client {
private readonly providerAccessor: () => Provider,
private readonly emitterAccessor: () => InternalEventEmitter,
private readonly globalLogger: () => Logger,
private readonly options: OpenFeatureClientOptions
private readonly options: OpenFeatureClientOptions,
) {}
get metadata(): ClientMetadata {
@ -76,12 +76,12 @@ export class OpenFeatureClient implements Client {
return this;
}
addHooks(...hooks: Hook<FlagValue>[]): this {
addHooks(...hooks: Hook[]): this {
this._hooks = [...this._hooks, ...hooks];
return this;
}
getHooks(): Hook<FlagValue>[] {
getHooks(): Hook[] {
return this._hooks;
}
@ -97,7 +97,7 @@ export class OpenFeatureClient implements Client {
getBooleanDetails(
flagKey: string,
defaultValue: boolean,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): EvaluationDetails<boolean> {
return this.evaluate<boolean>(flagKey, this._provider.resolveBooleanEvaluation, defaultValue, 'boolean', options);
}
@ -109,7 +109,7 @@ export class OpenFeatureClient implements Client {
getStringDetails<T extends string = string>(
flagKey: string,
defaultValue: T,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): EvaluationDetails<T> {
return this.evaluate<T>(
flagKey,
@ -117,7 +117,7 @@ export class OpenFeatureClient implements Client {
this._provider.resolveStringEvaluation as () => EvaluationDetails<T>,
defaultValue,
'string',
options
options,
);
}
@ -128,7 +128,7 @@ export class OpenFeatureClient implements Client {
getNumberDetails<T extends number = number>(
flagKey: string,
defaultValue: T,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): EvaluationDetails<T> {
return this.evaluate<T>(
flagKey,
@ -136,14 +136,14 @@ export class OpenFeatureClient implements Client {
this._provider.resolveNumberEvaluation as () => EvaluationDetails<T>,
defaultValue,
'number',
options
options,
);
}
getObjectValue<T extends JsonValue = JsonValue>(
flagKey: string,
defaultValue: T,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): T {
return this.getObjectDetails(flagKey, defaultValue, options).value;
}
@ -151,7 +151,7 @@ export class OpenFeatureClient implements Client {
getObjectDetails<T extends JsonValue = JsonValue>(
flagKey: string,
defaultValue: T,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): EvaluationDetails<T> {
return this.evaluate<T>(flagKey, this._provider.resolveObjectEvaluation, defaultValue, 'object', options);
}
@ -161,7 +161,7 @@ export class OpenFeatureClient implements Client {
resolver: (flagKey: string, defaultValue: T, context: EvaluationContext, logger: Logger) => ResolutionDetails<T>,
defaultValue: T,
flagType: FlagValueType,
options: FlagEvaluationOptions = {}
options: FlagEvaluationOptions = {},
): EvaluationDetails<T> {
// merge global, client, and evaluation context
@ -224,26 +224,19 @@ export class OpenFeatureClient implements Client {
}
private beforeHooks(hooks: Hook[], hookContext: HookContext, options: FlagEvaluationOptions) {
Object.freeze(hookContext);
Object.freeze(hookContext.context);
for (const hook of hooks) {
// freeze the hookContext
Object.freeze(hookContext);
// use Object.assign to avoid modification of frozen hookContext
Object.assign(hookContext.context, {
...hookContext.context,
...hook?.before?.(hookContext, Object.freeze(options.hookHints)),
});
hook?.before?.(hookContext, Object.freeze(options.hookHints));
}
// after before hooks, freeze the EvaluationContext.
return Object.freeze(hookContext.context);
}
private afterHooks(
hooks: Hook[],
hookContext: HookContext,
evaluationDetails: EvaluationDetails<FlagValue>,
options: FlagEvaluationOptions
options: FlagEvaluationOptions,
) {
// run "after" hooks sequentially
for (const hook of hooks) {

View File

@ -1,7 +1,7 @@
import { EvaluationDetails, Hook, HookHints, JsonValue } from '@openfeature/core';
import { EvaluationDetails, BaseHook, HookHints, JsonValue } from '@openfeature/core';
export interface FlagEvaluationOptions {
hooks?: Hook[];
hooks?: BaseHook[];
hookHints?: HookHints;
}
@ -25,7 +25,7 @@ export interface Features {
getBooleanDetails(
flagKey: string,
defaultValue: boolean,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): EvaluationDetails<boolean>;
/**
@ -53,7 +53,7 @@ export interface Features {
getStringDetails<T extends string = string>(
flagKey: string,
defaultValue: T,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): EvaluationDetails<T>;
/**
@ -81,7 +81,7 @@ export interface Features {
getNumberDetails<T extends number = number>(
flagKey: string,
defaultValue: T,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): EvaluationDetails<T>;
/**
@ -107,12 +107,12 @@ export interface Features {
getObjectDetails(
flagKey: string,
defaultValue: JsonValue,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): EvaluationDetails<JsonValue>;
getObjectDetails<T extends JsonValue = JsonValue>(
flagKey: string,
defaultValue: T,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): EvaluationDetails<T>;
}

View File

@ -0,0 +1,3 @@
import { BaseHook, FlagValue } from '@openfeature/core';
export type Hook = BaseHook<FlagValue, void, void>;

View File

@ -0,0 +1 @@
export * from './hook';

View File

@ -3,4 +3,5 @@ export * from './provider';
export * from './evaluation';
export * from './open-feature';
export * from './events';
export * from './hooks';
export * from '@openfeature/core';

View File

@ -2,6 +2,7 @@ import { EvaluationContext, ManageContext, OpenFeatureCommonAPI } from '@openfea
import { Client, OpenFeatureClient } from './client';
import { NOOP_PROVIDER, Provider } from './provider';
import { OpenFeatureEventEmitter } from './events';
import { Hook } from './hooks';
// use a symbol as a key for the global singleton
const GLOBAL_OPENFEATURE_API_KEY = Symbol.for('@openfeature/web-sdk/api');
@ -11,7 +12,7 @@ type OpenFeatureGlobal = {
};
const _globalThis = globalThis as OpenFeatureGlobal;
export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider> implements ManageContext<Promise<void>> {
export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider, Hook> implements ManageContext<Promise<void>> {
protected _events = new OpenFeatureEventEmitter();
protected _defaultProvider: Provider = NOOP_PROVIDER;
protected _createEventEmitter = () => new OpenFeatureEventEmitter();
@ -49,7 +50,7 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider> implements Ma
} catch (err) {
this._logger?.error(`Error running context change handler of provider ${provider.metadata.name}:`, err);
}
})
}),
);
}
@ -75,7 +76,7 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider> implements Ma
() => this.getProviderForClient(name),
() => this.buildAndCacheEventEmitterForClient(name),
() => this._logger,
{ name, version }
{ name, version },
);
}

View File

@ -1,4 +1,5 @@
import { CommonProvider, EvaluationContext, Hook, JsonValue, Logger, ResolutionDetails } from '@openfeature/core';
import { CommonProvider, EvaluationContext, JsonValue, Logger, ResolutionDetails } from '@openfeature/core';
import { Hook } from '../hooks';
/**
* Interface that providers must implement to resolve flag values for their particular
@ -30,7 +31,7 @@ export interface Provider extends CommonProvider {
flagKey: string,
defaultValue: boolean,
context: EvaluationContext,
logger: Logger
logger: Logger,
): ResolutionDetails<boolean>;
/**
@ -40,7 +41,7 @@ export interface Provider extends CommonProvider {
flagKey: string,
defaultValue: string,
context: EvaluationContext,
logger: Logger
logger: Logger,
): ResolutionDetails<string>;
/**
@ -50,7 +51,7 @@ export interface Provider extends CommonProvider {
flagKey: string,
defaultValue: number,
context: EvaluationContext,
logger: Logger
logger: Logger,
): ResolutionDetails<number>;
/**
@ -60,6 +61,6 @@ export interface Provider extends CommonProvider {
flagKey: string,
defaultValue: T,
context: EvaluationContext,
logger: Logger
logger: Logger,
): ResolutionDetails<T>;
}

View File

@ -4,9 +4,9 @@ import {
Client,
FlagValueType,
EvaluationContext,
Hook,
GeneralError,
OpenFeature,
Hook,
} from '../src';
const BOOLEAN_VALUE = true;
@ -107,27 +107,6 @@ describe('Hooks', () => {
});
describe('Requirement 4.1.4', () => {
describe('before', () => {
it('evaluationContext must be mutable', (done) => {
client.getBooleanValue(FLAG_KEY, false, {
hooks: [
{
before: (hookContext) => {
try {
// evaluation context is mutable in before, so this should work.
hookContext.context.newBeforeProp = 'new!';
expect(hookContext.context.newBeforeProp).toBeTruthy();
done();
} catch (err) {
done(err);
}
},
},
],
});
});
});
describe('after', () => {
it('evaluationContext must be immutable', (done) => {
client.getBooleanValue(FLAG_KEY, false, {
@ -151,58 +130,6 @@ describe('Hooks', () => {
});
});
describe('4.3.2', () => {
it('"before" must run before flag resolution', async () => {
await client.getBooleanValue(FLAG_KEY, false, {
hooks: [
{
before: () => {
// add a prop to the context.
return { beforeRan: true };
},
},
],
});
expect(MOCK_PROVIDER.resolveBooleanEvaluation).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
// ensure property was added by the time the flag resolution occurred.
expect.objectContaining({
beforeRan: true,
}),
expect.anything()
);
});
});
describe('Requirement 4.3.3', () => {
it('EvaluationContext must be passed to next "before" hook', (done) => {
client.getBooleanValue(FLAG_KEY, false, {
hooks: [
{
before: () => {
// add a prop to the context.
return { beforeRan: true };
},
},
{
before: (hookContext) => {
// ensure added prop exists in next hook
try {
expect(hookContext.context.beforeRan).toBeTruthy();
done();
} catch (err) {
done(err);
}
return { beforeRan: true };
},
},
],
});
});
});
describe('Requirement 4.3.5', () => {
it('"after" must run after flag evaluation', (done) => {
client.getBooleanValue(FLAG_KEY, false, {

View File

@ -6,7 +6,6 @@ import {
EventHandler,
FlagValue,
FlagValueType,
Hook,
HookContext,
JsonValue,
Logger,
@ -16,13 +15,14 @@ import {
ResolutionDetails,
SafeLogger,
StandardResolutionReasons,
statusMatchesEvent
statusMatchesEvent,
} from '@openfeature/core';
import { FlagEvaluationOptions } from '../evaluation';
import { OpenFeature } from '../open-feature';
import { Provider } from '../provider';
import { Client } from './client';
import { InternalEventEmitter } from '../events/internal/internal-event-emitter';
import { Hook } from '../hooks';
type OpenFeatureClientOptions = {
name?: string;
@ -41,7 +41,7 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
private readonly emitterAccessor: () => InternalEventEmitter,
private readonly globalLogger: () => Logger,
private readonly options: OpenFeatureClientOptions,
context: EvaluationContext = {}
context: EvaluationContext = {},
) {
this._context = context;
}
@ -90,12 +90,12 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
return this._context;
}
addHooks(...hooks: Hook<FlagValue>[]): OpenFeatureClient {
addHooks(...hooks: Hook[]): OpenFeatureClient {
this._hooks = [...this._hooks, ...hooks];
return this;
}
getHooks(): Hook<FlagValue>[] {
getHooks(): Hook[] {
return this._hooks;
}
@ -108,7 +108,7 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
flagKey: string,
defaultValue: boolean,
context?: EvaluationContext,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): Promise<boolean> {
return (await this.getBooleanDetails(flagKey, defaultValue, context, options)).value;
}
@ -117,7 +117,7 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
flagKey: string,
defaultValue: boolean,
context?: EvaluationContext,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): Promise<EvaluationDetails<boolean>> {
return this.evaluate<boolean>(
flagKey,
@ -125,7 +125,7 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
defaultValue,
'boolean',
context,
options
options,
);
}
@ -133,7 +133,7 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
flagKey: string,
defaultValue: T,
context?: EvaluationContext,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): Promise<T> {
return (await this.getStringDetails<T>(flagKey, defaultValue, context, options)).value;
}
@ -142,7 +142,7 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
flagKey: string,
defaultValue: T,
context?: EvaluationContext,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): Promise<EvaluationDetails<T>> {
return this.evaluate<T>(
flagKey,
@ -151,7 +151,7 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
defaultValue,
'string',
context,
options
options,
);
}
@ -159,7 +159,7 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
flagKey: string,
defaultValue: T,
context?: EvaluationContext,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): Promise<T> {
return (await this.getNumberDetails(flagKey, defaultValue, context, options)).value;
}
@ -168,7 +168,7 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
flagKey: string,
defaultValue: T,
context?: EvaluationContext,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): Promise<EvaluationDetails<T>> {
return this.evaluate<T>(
flagKey,
@ -177,7 +177,7 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
defaultValue,
'number',
context,
options
options,
);
}
@ -185,7 +185,7 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
flagKey: string,
defaultValue: T,
context?: EvaluationContext,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): Promise<T> {
return (await this.getObjectDetails(flagKey, defaultValue, context, options)).value;
}
@ -194,7 +194,7 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
flagKey: string,
defaultValue: T,
context?: EvaluationContext,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): Promise<EvaluationDetails<T>> {
return this.evaluate<T>(flagKey, this._provider.resolveObjectEvaluation, defaultValue, 'object', context, options);
}
@ -205,12 +205,12 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
flagKey: string,
defaultValue: T,
context: EvaluationContext,
logger: Logger
logger: Logger,
) => Promise<ResolutionDetails<T>>,
defaultValue: T,
flagType: FlagValueType,
invocationContext: EvaluationContext = {},
options: FlagEvaluationOptions = {}
options: FlagEvaluationOptions = {},
): Promise<EvaluationDetails<T>> {
// merge global, client, and evaluation context
@ -296,7 +296,7 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
hooks: Hook[],
hookContext: HookContext,
evaluationDetails: EvaluationDetails<FlagValue>,
options: FlagEvaluationOptions
options: FlagEvaluationOptions,
) {
// run "after" hooks sequentially
for (const hook of hooks) {

View File

@ -1,4 +1,5 @@
import { EvaluationContext, EvaluationDetails, Hook, HookHints, JsonValue } from '@openfeature/core';
import { EvaluationContext, EvaluationDetails, HookHints, JsonValue } from '@openfeature/core';
import { Hook } from '../hooks';
export interface FlagEvaluationOptions {
hooks?: Hook[];
@ -18,7 +19,7 @@ export interface Features {
flagKey: string,
defaultValue: boolean,
context?: EvaluationContext,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): Promise<boolean>;
/**
@ -33,7 +34,7 @@ export interface Features {
flagKey: string,
defaultValue: boolean,
context?: EvaluationContext,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): Promise<EvaluationDetails<boolean>>;
/**
@ -49,14 +50,14 @@ export interface Features {
flagKey: string,
defaultValue: string,
context?: EvaluationContext,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): Promise<string>;
getStringValue<T extends string = string>(
flagKey: string,
defaultValue: T,
context?: EvaluationContext,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): Promise<T>;
/**
@ -72,14 +73,14 @@ export interface Features {
flagKey: string,
defaultValue: string,
context?: EvaluationContext,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): Promise<EvaluationDetails<string>>;
getStringDetails<T extends string = string>(
flagKey: string,
defaultValue: T,
context?: EvaluationContext,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): Promise<EvaluationDetails<T>>;
/**
@ -95,14 +96,14 @@ export interface Features {
flagKey: string,
defaultValue: number,
context?: EvaluationContext,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): Promise<number>;
getNumberValue<T extends number = number>(
flagKey: string,
defaultValue: T,
context?: EvaluationContext,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): Promise<T>;
/**
@ -118,14 +119,14 @@ export interface Features {
flagKey: string,
defaultValue: number,
context?: EvaluationContext,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): Promise<EvaluationDetails<number>>;
getNumberDetails<T extends number = number>(
flagKey: string,
defaultValue: T,
context?: EvaluationContext,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): Promise<EvaluationDetails<T>>;
/**
@ -141,14 +142,14 @@ export interface Features {
flagKey: string,
defaultValue: JsonValue,
context?: EvaluationContext,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): Promise<JsonValue>;
getObjectValue<T extends JsonValue = JsonValue>(
flagKey: string,
defaultValue: T,
context?: EvaluationContext,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): Promise<T>;
/**
@ -164,13 +165,13 @@ export interface Features {
flagKey: string,
defaultValue: JsonValue,
context?: EvaluationContext,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): Promise<EvaluationDetails<JsonValue>>;
getObjectDetails<T extends JsonValue = JsonValue>(
flagKey: string,
defaultValue: T,
context?: EvaluationContext,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): Promise<EvaluationDetails<T>>;
}

View File

@ -1 +1 @@
export * from './open-feature-event-emitter';
export * from './open-feature-event-emitter';

View File

@ -0,0 +1,7 @@
import { BaseHook, EvaluationContext, FlagValue } from '@openfeature/core';
export type Hook = BaseHook<
FlagValue,
Promise<EvaluationContext | Promise<void>> | EvaluationContext | void,
Promise<void> | void
>;

View File

@ -0,0 +1 @@
export * from './hook';

View File

@ -4,4 +4,5 @@ export * from './evaluation';
export * from './open-feature';
export * from './transaction-context';
export * from './events';
export * from './hooks';
export * from '@openfeature/core';

View File

@ -14,6 +14,7 @@ import {
} from './transaction-context';
import { Client, OpenFeatureClient } from './client';
import { OpenFeatureEventEmitter } from './events';
import { Hook } from './hooks';
// use a symbol as a key for the global singleton
const GLOBAL_OPENFEATURE_API_KEY = Symbol.for('@openfeature/js-sdk/api');
@ -24,13 +25,13 @@ type OpenFeatureGlobal = {
const _globalThis = globalThis as OpenFeatureGlobal;
export class OpenFeatureAPI
extends OpenFeatureCommonAPI<Provider>
extends OpenFeatureCommonAPI<Provider, Hook>
implements ManageContext<OpenFeatureAPI>, ManageTransactionContextPropagator<OpenFeatureCommonAPI<Provider>>
{
protected _events = new OpenFeatureEventEmitter();
protected _defaultProvider: Provider = NOOP_PROVIDER;
protected _createEventEmitter = () => new OpenFeatureEventEmitter();
private _transactionContextPropagator: TransactionContextPropagator = NOOP_TRANSACTION_CONTEXT_PROPAGATOR;
private constructor() {
@ -100,7 +101,7 @@ export class OpenFeatureAPI
getClient(
nameOrContext?: string | EvaluationContext,
versionOrContext?: string | EvaluationContext,
contextOrUndefined?: EvaluationContext
contextOrUndefined?: EvaluationContext,
): Client {
const name = stringOrUndefined(nameOrContext);
const version = stringOrUndefined(versionOrContext);
@ -114,7 +115,7 @@ export class OpenFeatureAPI
() => this.buildAndCacheEventEmitterForClient(name),
() => this._logger,
{ name, version },
context
context,
);
}
@ -127,7 +128,7 @@ export class OpenFeatureAPI
}
setTransactionContextPropagator(
transactionContextPropagator: TransactionContextPropagator
transactionContextPropagator: TransactionContextPropagator,
): OpenFeatureCommonAPI<Provider> {
const baseMessage = 'Invalid TransactionContextPropagator, will not be set: ';
if (typeof transactionContextPropagator?.getTransactionContext !== 'function') {

View File

@ -1,4 +1,5 @@
import { CommonProvider, EvaluationContext, Hook, JsonValue, Logger, ResolutionDetails } from '@openfeature/core';
import { CommonProvider, EvaluationContext, JsonValue, Logger, ResolutionDetails } from '@openfeature/core';
import { Hook } from '../hooks';
/**
* Interface that providers must implement to resolve flag values for their particular
@ -22,7 +23,7 @@ export interface Provider extends CommonProvider {
flagKey: string,
defaultValue: boolean,
context: EvaluationContext,
logger: Logger
logger: Logger,
): Promise<ResolutionDetails<boolean>>;
/**
@ -32,7 +33,7 @@ export interface Provider extends CommonProvider {
flagKey: string,
defaultValue: string,
context: EvaluationContext,
logger: Logger
logger: Logger,
): Promise<ResolutionDetails<string>>;
/**
@ -42,7 +43,7 @@ export interface Provider extends CommonProvider {
flagKey: string,
defaultValue: number,
context: EvaluationContext,
logger: Logger
logger: Logger,
): Promise<ResolutionDetails<number>>;
/**
@ -52,6 +53,6 @@ export interface Provider extends CommonProvider {
flagKey: string,
defaultValue: T,
context: EvaluationContext,
logger: Logger
logger: Logger,
): Promise<ResolutionDetails<T>>;
}

View File

@ -167,7 +167,7 @@ describe('Hooks', () => {
expect.objectContaining({
beforeRan: true,
}),
expect.anything()
expect.anything(),
);
});
});
@ -252,7 +252,7 @@ describe('Hooks', () => {
[invocationPropToOverwrite434]: true,
[hookProp434]: true,
}),
expect.anything()
expect.anything(),
);
});
});
@ -903,7 +903,7 @@ describe('Hooks', () => {
return new Promise<EvaluationContext>((resolve) =>
setTimeout(() => {
resolve({ beforeRan: true });
}, 100)
}, 100),
);
}),
after: jest.fn((hookContext) => {
@ -940,7 +940,7 @@ describe('Hooks', () => {
return new Promise<EvaluationContext>((resolve, reject) =>
setTimeout(() => {
reject();
}, 100)
}, 100),
);
}),
error: jest.fn(() => {

View File

@ -1,4 +1,4 @@
import { OpenFeature, Hook, Logger, Provider, DefaultLogger, SafeLogger } from '../src';
import { OpenFeature, BaseHook, Logger, Provider, DefaultLogger, SafeLogger } from '../src';
class MockedLogger implements Logger {
error = jest.fn();
@ -12,7 +12,7 @@ const AFTER_HOOK_LOG_MESSAGE = 'in after hook';
const ERROR_HOOK_LOG_MESSAGE = 'in error hook';
const FINALLY_HOOK_LOG_MESSAGE = 'in finally hook';
const MOCK_HOOK: Hook = {
const MOCK_HOOK: BaseHook = {
before: jest.fn((hookContext) => hookContext.logger.info(BEFORE_HOOK_LOG_MESSAGE)),
after: jest.fn((hookContext) => hookContext.logger.info(AFTER_HOOK_LOG_MESSAGE)),
error: jest.fn((hookContext) => hookContext.logger.info(ERROR_HOOK_LOG_MESSAGE)),
@ -93,7 +93,7 @@ describe('Logger', () => {
const safeLogger = new SafeLogger({} as Logger);
expect(errorSpy).toBeCalledWith(
expect.objectContaining({ message: 'The provided logger is missing the error method.' })
expect.objectContaining({ message: 'The provided logger is missing the error method.' }),
);
// Checking the private logger
expect(safeLogger['logger']).toBeInstanceOf(DefaultLogger);

View File

@ -1,4 +1,4 @@
import { Hook } from './hook';
import { BaseHook } from './hook';
import { FlagValue } from '../evaluation';
export interface EvaluationLifeCycle<T> {
@ -9,16 +9,16 @@ export interface EvaluationLifeCycle<T> {
* Hooks registered on the global API object run with all evaluations.
* Hooks registered on the client run with all evaluations on that client.
* @template T The type of the receiver
* @param {Hook<FlagValue>[]} hooks A list of hooks that should always run
* @param {BaseHook[]} hooks A list of hooks that should always run
* @returns {T} The receiver (this object)
*/
addHooks(...hooks: Hook<FlagValue>[]): T;
addHooks(...hooks: BaseHook[]): T;
/**
* Access all the hooks that are registered on this receiver.
* @returns {Hook<FlagValue>[]} A list of the client hooks
* @returns {BaseHook<FlagValue>[]} A list of the client hooks
*/
getHooks(): Hook<FlagValue>[];
getHooks(): BaseHook[];
/**
* Clears all the hooks that are registered on this receiver.

View File

@ -1,17 +1,14 @@
import { BeforeHookContext, HookContext, HookHints } from './hooks';
import { EvaluationContext, EvaluationDetails, FlagValue } from '../evaluation';
import { EvaluationDetails, FlagValue } from '../evaluation';
export interface Hook<T extends FlagValue = FlagValue> {
export interface BaseHook<T extends FlagValue = FlagValue, BeforeHookReturn = unknown, HooksReturn = unknown> {
/**
* Runs before flag values are resolved from the provider.
* If an EvaluationContext is returned, it will be merged with the pre-existing EvaluationContext.
* @param hookContext
* @param hookHints
*/
before?(
hookContext: BeforeHookContext,
hookHints?: HookHints
): Promise<EvaluationContext | void> | EvaluationContext | void;
before?(hookContext: BeforeHookContext, hookHints?: HookHints): BeforeHookReturn;
/**
* Runs after flag values are successfully resolved from the provider.
@ -22,8 +19,8 @@ export interface Hook<T extends FlagValue = FlagValue> {
after?(
hookContext: Readonly<HookContext<T>>,
evaluationDetails: EvaluationDetails<T>,
hookHints?: HookHints
): Promise<void> | void;
hookHints?: HookHints,
): HooksReturn;
/**
* Runs in the event of an unhandled error or promise rejection during flag resolution, or any attached hooks.
@ -31,7 +28,7 @@ export interface Hook<T extends FlagValue = FlagValue> {
* @param error
* @param hookHints
*/
error?(hookContext: Readonly<HookContext<T>>, error: unknown, hookHints?: HookHints): Promise<void> | void;
error?(hookContext: Readonly<HookContext<T>>, error: unknown, hookHints?: HookHints): HooksReturn;
/**
* Runs after all other hook stages, regardless of success or error.
@ -39,5 +36,5 @@ export interface Hook<T extends FlagValue = FlagValue> {
* @param hookContext
* @param hookHints
*/
finally?(hookContext: Readonly<HookContext<T>>, hookHints?: HookHints): Promise<void> | void;
finally?(hookContext: Readonly<HookContext<T>>, hookHints?: HookHints): HooksReturn;
}

View File

@ -1,22 +1,22 @@
import { GeneralError } from './errors';
import { EvaluationContext, FlagValue } from './evaluation';
import { EvaluationContext } from './evaluation';
import { EventDetails, EventHandler, Eventing, ProviderEvents, statusMatchesEvent } from './events';
import { GenericEventEmitter } from './events';
import { isDefined } from './filter';
import { EvaluationLifeCycle, Hook } from './hooks';
import { EvaluationLifeCycle, BaseHook } from './hooks';
import { DefaultLogger, Logger, ManageLogger, SafeLogger } from './logger';
import { CommonProvider, ProviderMetadata, ProviderStatus } from './provider';
import { objectOrUndefined, stringOrUndefined } from './type-guards';
import { Paradigm } from './types';
export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProvider>
export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProvider, H extends BaseHook = BaseHook>
implements Eventing, EvaluationLifeCycle<OpenFeatureCommonAPI<P>>, ManageLogger<OpenFeatureCommonAPI<P>>
{
protected abstract _createEventEmitter(): GenericEventEmitter;
protected abstract _defaultProvider: P;
protected abstract readonly _events: GenericEventEmitter;
protected _hooks: Hook[] = [];
protected _hooks: H[] = [];
protected _context: EvaluationContext = {};
protected _logger: Logger = new DefaultLogger();
@ -30,12 +30,12 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
this._runsOn = category;
}
addHooks(...hooks: Hook<FlagValue>[]): this {
addHooks(...hooks: H[]): this {
this._hooks = [...this._hooks, ...hooks];
return this;
}
getHooks(): Hook<FlagValue>[] {
getHooks(): H[] {
return this._hooks;
}
@ -184,7 +184,7 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
if (typeof provider.initialize === 'function' && provider.status === undefined) {
const activeLogger = this._logger || console;
activeLogger.warn(
`Provider ${providerName} implements 'initialize' but not 'status'. Please implement 'status'.`
`Provider ${providerName} implements 'initialize' but not 'status'. Please implement 'status'.`,
);
}
@ -251,10 +251,11 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
this._clientEvents.set(name, newEmitter);
const clientProvider = this.getProviderForClient(name);
Object.values<ProviderEvents>(ProviderEvents).forEach((eventType) =>
clientProvider.events?.addHandler(eventType, async (details) => {
newEmitter.emit(eventType, { ...details, clientName: name, providerName: clientProvider.metadata.name });
})
Object.values<ProviderEvents>(ProviderEvents).forEach(
(eventType) =>
clientProvider.events?.addHandler(eventType, async (details) => {
newEmitter.emit(eventType, { ...details, clientName: name, providerName: clientProvider.metadata.name });
}),
);
return newEmitter;
@ -280,7 +281,7 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
oldProvider: P,
newProvider: P,
clientName: string | undefined,
emitters: (GenericEventEmitter | undefined)[]
emitters: (GenericEventEmitter | undefined)[],
) {
this._clientEventHandlers
.get(clientName)
@ -321,7 +322,7 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
} catch (err) {
this.handleShutdownError(provider, err);
}
})
}),
);
}