Skip to content

Commit

Permalink
feat!: remove ctx transformer, add provider hooks (#148)
Browse files Browse the repository at this point in the history
Add provider hooks, update ordering

BREAKING CHANGE: context transformer and related interfaces removed.

Signed-off-by: Todd Baert <[email protected]>

Signed-off-by: Todd Baert <[email protected]>
  • Loading branch information
toddbaert authored Aug 10, 2022
1 parent fde134f commit 260432c
Show file tree
Hide file tree
Showing 5 changed files with 689 additions and 613 deletions.
29 changes: 9 additions & 20 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {
FlagValueType,
Hook,
HookContext,
Provider,
ResolutionDetails,
TransformingProvider,
} from './types';

type OpenFeatureClientOptions = {
Expand All @@ -27,7 +27,7 @@ export class OpenFeatureClient implements Client {
constructor(
// we always want the client to use the current provider,
// so pass a function to always access the currently registered one.
private readonly providerAccessor: () => TransformingProvider<unknown>,
private readonly providerAccessor: () => Provider,
options: OpenFeatureClientOptions,
context: EvaluationContext = {}
) {
Expand Down Expand Up @@ -148,7 +148,7 @@ export class OpenFeatureClient implements Client {
resolver: (
flagKey: string,
defaultValue: T,
transformedContext: unknown,
context: EvaluationContext,
options: FlagEvaluationOptions | undefined
) => Promise<ResolutionDetails<T>>,
defaultValue: T,
Expand All @@ -158,13 +158,14 @@ export class OpenFeatureClient implements Client {
): Promise<EvaluationDetails<T>> {
// merge global, client, and evaluation context

const allHooks = [...OpenFeature.hooks, ...this.hooks, ...(options.hooks || [])];
const allHooks = [...OpenFeature.hooks, ...this.hooks, ...(options.hooks || []), ...(this.provider.hooks || [])];
const allHooksReversed = [...allHooks].reverse();

// merge global and client contexts
const globalAndClientContext = {
const mergedContext = {
...OpenFeature.context,
...this.context,
...invocationContext,
};

// this reference cannot change during the course of evaluation
Expand All @@ -175,26 +176,14 @@ export class OpenFeatureClient implements Client {
flagValueType: flagType,
clientMetadata: this.metadata,
providerMetadata: OpenFeature.providerMetadata,
context: globalAndClientContext,
context: mergedContext,
};

try {
const mergedHookContext = await this.beforeHooks(allHooks, hookContext, options);

// merge context in order: global, client, hook, invocation
const mergedContext = {
...mergedHookContext,
...invocationContext,
};

// if a transformer is defined, run it to prepare the context
const transformedContext =
typeof this.provider.contextTransformer === 'function'
? await this.provider.contextTransformer(mergedContext)
: mergedContext;
const frozenContext = await this.beforeHooks(allHooks, hookContext, options);

// run the referenced resolver, binding the provider.
const resolution = await resolver.call(this.provider, flagKey, defaultValue, transformedContext, options);
const resolution = await resolver.call(this.provider, flagKey, defaultValue, frozenContext, options);

const evaluationDetails = {
...resolution,
Expand Down
15 changes: 3 additions & 12 deletions src/open-feature.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
import { OpenFeatureClient } from './client';
import { NOOP_PROVIDER } from './no-op-provider';
import {
Client,
EvaluationContext,
EvaluationLifeCycle,
FlagValue,
Hook,
NonTransformingProvider,
Provider,
TransformingProvider,
} from './types';
import { Client, EvaluationContext, EvaluationLifeCycle, FlagValue, 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 @@ -36,7 +27,7 @@ class OpenFeatureAPI implements EvaluationLifeCycle {
}

getClient(name?: string, version?: string, context?: EvaluationContext): Client {
return new OpenFeatureClient(() => this._provider as TransformingProvider<unknown>, { name, version }, context);
return new OpenFeatureClient(() => this._provider, { name, version }, context);
}

get providerMetadata() {
Expand All @@ -51,7 +42,7 @@ class OpenFeatureAPI implements EvaluationLifeCycle {
return this._hooks;
}

setProvider(provider: TransformingProvider<unknown> | NonTransformingProvider) {
setProvider(provider: Provider) {
this._provider = provider;
}

Expand Down
48 changes: 13 additions & 35 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,12 @@ export interface Features {
}

/**
* Function which transforms the EvaluationContext to a type useful for the provider.
* Interface that providers must implement to resolve flag values for their particular
* backend or vendor.
*
* Implementation for resolving all the required flag types must be defined.
*/
export type ContextTransformer<T = unknown> = (context: EvaluationContext) => T;

interface GenericProvider<T> {
export interface Provider extends Pick<Partial<EvaluationLifeCycle>, 'hooks'> {
readonly metadata: ProviderMetadata;

/**
Expand All @@ -117,7 +118,7 @@ interface GenericProvider<T> {
resolveBooleanEvaluation(
flagKey: string,
defaultValue: boolean,
transformedContext: T,
context: EvaluationContext,
options: FlagEvaluationOptions | undefined
): Promise<ResolutionDetails<boolean>>;

Expand All @@ -127,7 +128,7 @@ interface GenericProvider<T> {
resolveStringEvaluation(
flagKey: string,
defaultValue: string,
transformedContext: T,
context: EvaluationContext,
options: FlagEvaluationOptions | undefined
): Promise<ResolutionDetails<string>>;

Expand All @@ -137,7 +138,7 @@ interface GenericProvider<T> {
resolveNumberEvaluation(
flagKey: string,
defaultValue: number,
transformedContext: T,
context: EvaluationContext,
options: FlagEvaluationOptions | undefined
): Promise<ResolutionDetails<number>>;

Expand All @@ -147,40 +148,17 @@ interface GenericProvider<T> {
resolveObjectEvaluation<U extends object>(
flagKey: string,
defaultValue: U,
transformedContext: T,
context: EvaluationContext,
options: FlagEvaluationOptions | undefined
): Promise<ResolutionDetails<U>>;
}

export type NonTransformingProvider = GenericProvider<EvaluationContext>;

export interface TransformingProvider<T> extends GenericProvider<T> {
contextTransformer: ContextTransformer<Promise<T> | T> | undefined;
}

/**
* Interface that providers must implement to resolve flag values for their particular
* backend or vendor.
*
* Implementation for resolving all the required flag types must be defined.
*
* Additionally, a ContextTransformer function that transforms the OpenFeature context to the requisite user/context/attribute representation (typeof T)
* may also be implemented. This function will run immediately before the flag value resolver functions, appropriately transforming the context.
*/
export type Provider<T extends EvaluationContext | unknown = EvaluationContext> = T extends EvaluationContext
? NonTransformingProvider
: TransformingProvider<T>;

export interface EvaluationLifeCycle {
addHooks(...hooks: Hook[]): void;
get hooks(): Hook[];
clearHooks(): void;
}

export interface ProviderOptions<T = unknown> {
contextTransformer?: ContextTransformer<T>;
}

export enum StandardResolutionReasons {
/**
* Indicates that the feature flag is targeting
Expand All @@ -204,20 +182,20 @@ export enum StandardResolutionReasons {
* similar functions in the Client */
DEFAULT = 'DEFAULT',
/**
* Indicates that the feature flag evaluated to a
* Indicates that the feature flag evaluated to a
* static value, for example, the default value for the flag
*
*
* Note: Typically means that no dynamic evaluation has been
* executed for the feature flag
*/
STATIC = 'STATIC',
STATIC = 'STATIC',
/**
* Indicates an unknown issue occurred during evaluation
*/
UNKNOWN = 'UNKNOWN',
/**
* Indicates that an error occurred during evaluation
*
*
* Note: The `errorCode`-field contains the details of this error
*/
ERROR = 'ERROR',
Expand Down
Loading

0 comments on commit 260432c

Please sign in to comment.