Skip to content
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
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);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chose this name because the context object is actually frozen (immutable here) after the before hooks have run...


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