Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

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

Merged
merged 10 commits into from
Nov 21, 2023
41 changes: 17 additions & 24 deletions packages/client/src/client/open-feature-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
EventHandler,
FlagValue,
FlagValueType,
Hook,
HookContext,
JsonValue,
Logger,
Expand All @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
}

Expand All @@ -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);
}
Expand All @@ -109,15 +109,15 @@ 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,
// this isolates providers from our restricted string generic argument.
this._provider.resolveStringEvaluation as () => EvaluationDetails<T>,
defaultValue,
'string',
options
options,
);
}

Expand All @@ -128,30 +128,30 @@ 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,
// this isolates providers from our restricted number generic argument.
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;
}

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);
}
Expand All @@ -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

Expand Down Expand Up @@ -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) {
Expand Down
14 changes: 7 additions & 7 deletions packages/client/src/evaluation/evaluation.ts
Original file line number Diff line number Diff line change
@@ -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;
}

Expand All @@ -25,7 +25,7 @@ export interface Features {
getBooleanDetails(
flagKey: string,
defaultValue: boolean,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): EvaluationDetails<boolean>;

/**
Expand Down Expand Up @@ -53,7 +53,7 @@ export interface Features {
getStringDetails<T extends string = string>(
flagKey: string,
defaultValue: T,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): EvaluationDetails<T>;

/**
Expand Down Expand Up @@ -81,7 +81,7 @@ export interface Features {
getNumberDetails<T extends number = number>(
flagKey: string,
defaultValue: T,
options?: FlagEvaluationOptions
options?: FlagEvaluationOptions,
): EvaluationDetails<T>;

/**
Expand All @@ -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>;
}
3 changes: 3 additions & 0 deletions packages/client/src/hooks/hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { BaseHook, FlagValue } from '@openfeature/core';

export type Hook = BaseHook<FlagValue, void, void>;
1 change: 1 addition & 0 deletions packages/client/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './hook';
1 change: 1 addition & 0 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export * from './provider';
export * from './evaluation';
export * from './open-feature';
export * from './events';
export * from './hooks';
export * from '@openfeature/core';
7 changes: 4 additions & 3 deletions packages/client/src/open-feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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();
Expand Down Expand Up @@ -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);
}
})
}),
);
}

Expand All @@ -75,7 +76,7 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider> implements Ma
() => this.getProviderForClient(name),
() => this.buildAndCacheEventEmitterForClient(name),
() => this._logger,
{ name, version }
{ name, version },
);
}

Expand Down
11 changes: 6 additions & 5 deletions packages/client/src/provider/provider.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -30,7 +31,7 @@ export interface Provider extends CommonProvider {
flagKey: string,
defaultValue: boolean,
context: EvaluationContext,
logger: Logger
logger: Logger,
): ResolutionDetails<boolean>;

/**
Expand All @@ -40,7 +41,7 @@ export interface Provider extends CommonProvider {
flagKey: string,
defaultValue: string,
context: EvaluationContext,
logger: Logger
logger: Logger,
): ResolutionDetails<string>;

/**
Expand All @@ -50,7 +51,7 @@ export interface Provider extends CommonProvider {
flagKey: string,
defaultValue: number,
context: EvaluationContext,
logger: Logger
logger: Logger,
): ResolutionDetails<number>;

/**
Expand All @@ -60,6 +61,6 @@ export interface Provider extends CommonProvider {
flagKey: string,
defaultValue: T,
context: EvaluationContext,
logger: Logger
logger: Logger,
): ResolutionDetails<T>;
}
75 changes: 1 addition & 74 deletions packages/client/test/hooks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import {
Client,
FlagValueType,
EvaluationContext,
Hook,
GeneralError,
OpenFeature,
Hook,
} from '../src';

const BOOLEAN_VALUE = true;
Expand Down Expand Up @@ -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, {
Expand All @@ -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, {
Expand Down
Loading
Loading