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:
parent
00a6d2efb4
commit
a7d0b954dc
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
import { BaseHook, FlagValue } from '@openfeature/core';
|
||||
|
||||
export type Hook = BaseHook<FlagValue, void, void>;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './hook';
|
||||
|
|
@ -3,4 +3,5 @@ export * from './provider';
|
|||
export * from './evaluation';
|
||||
export * from './open-feature';
|
||||
export * from './events';
|
||||
export * from './hooks';
|
||||
export * from '@openfeature/core';
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export * from './open-feature-event-emitter';
|
||||
export * from './open-feature-event-emitter';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
>;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './hook';
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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>>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue