Skip to content
Draft
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
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/shared/src/events/generic-event-emitter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Logger, ManageLogger} from '../logger';
import type { Logger, ManageLogger } from '../logger';
import { SafeLogger } from '../logger';
import type { ProviderEventEmitter } from './provider-event-emitter';
import type { EventContext, EventDetails, EventHandler } from './eventing';
Expand Down
3 changes: 2 additions & 1 deletion packages/web/src/client/client.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { ClientMetadata, EvaluationLifeCycle, Eventing, ManageLogger } from '@openfeature/core';
import type { Features } from '../evaluation';
import type { Features, ContextChangeSubscriptions } from '../evaluation';
import type { ProviderStatus } from '../provider';
import type { ProviderEvents } from '../events';
import type { Tracking } from '../tracking';

export interface Client
extends EvaluationLifeCycle<Client>,
Features,
ContextChangeSubscriptions,
ManageLogger<Client>,
Eventing<ProviderEvents>,
Tracking {
Expand Down
130 changes: 125 additions & 5 deletions packages/web/src/client/internal/open-feature-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import {
MapHookData,
} from '@openfeature/core';
import type { FlagEvaluationOptions } from '../../evaluation';
import type { ProviderEvents } from '../../events';
import { EvaluationDetailsWithSubscription } from '../../evaluation';
import { ProviderEvents } from '../../events';
import type { InternalEventEmitter } from '../../events/internal/internal-event-emitter';
import type { Hook } from '../../hooks';
import type { Provider } from '../../provider';
Expand Down Expand Up @@ -136,7 +137,13 @@ export class OpenFeatureClient implements Client {
defaultValue: boolean,
options?: FlagEvaluationOptions,
): EvaluationDetails<boolean> {
return this.evaluate<boolean>(flagKey, this._provider.resolveBooleanEvaluation, defaultValue, 'boolean', options);
return this.evaluateWithSubscription<boolean>(
flagKey,
this._provider.resolveBooleanEvaluation,
defaultValue,
'boolean',
options,
);
}

getStringValue<T extends string = string>(flagKey: string, defaultValue: T, options?: FlagEvaluationOptions): T {
Expand All @@ -148,7 +155,7 @@ export class OpenFeatureClient implements Client {
defaultValue: T,
options?: FlagEvaluationOptions,
): EvaluationDetails<T> {
return this.evaluate<T>(
return this.evaluateWithSubscription<T>(
flagKey,
// this isolates providers from our restricted string generic argument.
this._provider.resolveStringEvaluation as () => EvaluationDetails<T>,
Expand All @@ -167,7 +174,7 @@ export class OpenFeatureClient implements Client {
defaultValue: T,
options?: FlagEvaluationOptions,
): EvaluationDetails<T> {
return this.evaluate<T>(
return this.evaluateWithSubscription<T>(
flagKey,
// this isolates providers from our restricted number generic argument.
this._provider.resolveNumberEvaluation as () => EvaluationDetails<T>,
Expand All @@ -190,7 +197,109 @@ export class OpenFeatureClient implements Client {
defaultValue: T,
options?: FlagEvaluationOptions,
): EvaluationDetails<T> {
return this.evaluate<T>(flagKey, this._provider.resolveObjectEvaluation, defaultValue, 'object', options);
return this.evaluateWithSubscription<T>(
flagKey,
this._provider.resolveObjectEvaluation,
defaultValue,
'object',
options,
);
}

onBooleanContextChanged(
flagKey: string,
defaultValue: boolean,
callback: (newDetails: EvaluationDetails<boolean>, oldDetails: EvaluationDetails<boolean>) => void,
options?: FlagEvaluationOptions,
): () => void {
return this.subscribeToContextChanges(flagKey, defaultValue, 'boolean', callback, options);
}

onStringContextChanged(
flagKey: string,
defaultValue: string,
callback: (newDetails: EvaluationDetails<string>, oldDetails: EvaluationDetails<string>) => void,
options?: FlagEvaluationOptions,
): () => void {
return this.subscribeToContextChanges(flagKey, defaultValue, 'string', callback, options);
}

onNumberContextChanged(
flagKey: string,
defaultValue: number,
callback: (newDetails: EvaluationDetails<number>, oldDetails: EvaluationDetails<number>) => void,
options?: FlagEvaluationOptions,
): () => void {
return this.subscribeToContextChanges(flagKey, defaultValue, 'number', callback, options);
}

onObjectContextChanged<T extends JsonValue = JsonValue>(
flagKey: string,
defaultValue: T,
callback: (newDetails: EvaluationDetails<T>, oldDetails: EvaluationDetails<T>) => void,
options?: FlagEvaluationOptions,
): () => void {
return this.subscribeToContextChanges(flagKey, defaultValue, 'object', callback, options);
}

private subscribeToContextChanges<T extends FlagValue>(
flagKey: string,
defaultValue: T,
flagType: FlagValueType,
callback: (newDetails: EvaluationDetails<T>, oldDetails: EvaluationDetails<T>) => void,
options?: FlagEvaluationOptions,
): () => void {
let currentDetails: EvaluationDetails<T>;

switch (flagType) {
case 'boolean':
currentDetails = this.getBooleanDetails(flagKey, defaultValue as boolean, options) as EvaluationDetails<T>;
break;
case 'string':
currentDetails = this.getStringDetails(flagKey, defaultValue as string, options) as EvaluationDetails<T>;
break;
case 'number':
currentDetails = this.getNumberDetails(flagKey, defaultValue as number, options) as EvaluationDetails<T>;
break;
case 'object':
currentDetails = this.getObjectDetails(flagKey, defaultValue as JsonValue, options) as EvaluationDetails<T>;
break;
default:
throw new Error(`Unsupported flag type: ${flagType}`);
}

callback(currentDetails, { ...currentDetails });

const handler = () => {
const oldDetails = { ...currentDetails };
let newDetails: EvaluationDetails<T>;

switch (flagType) {
case 'boolean':
newDetails = this.getBooleanDetails(flagKey, defaultValue as boolean, options) as EvaluationDetails<T>;
break;
case 'string':
newDetails = this.getStringDetails(flagKey, defaultValue as string, options) as EvaluationDetails<T>;
break;
case 'number':
newDetails = this.getNumberDetails(flagKey, defaultValue as number, options) as EvaluationDetails<T>;
break;
case 'object':
newDetails = this.getObjectDetails(flagKey, defaultValue as JsonValue, options) as EvaluationDetails<T>;
break;
default:
return;
}

currentDetails = newDetails;
callback(newDetails, oldDetails);
};

this.addHandler(ProviderEvents.ContextChanged, handler, {});

return () => {
this.removeHandler(ProviderEvents.ContextChanged, handler);
};
}

track(occurrenceKey: string, occurrenceDetails: TrackingEventDetails = {}): void {
Expand All @@ -211,6 +320,17 @@ export class OpenFeatureClient implements Client {
}
}

private evaluateWithSubscription<T extends FlagValue>(
flagKey: string,
resolver: (flagKey: string, defaultValue: T, context: EvaluationContext, logger: Logger) => ResolutionDetails<T>,
defaultValue: T,
flagType: FlagValueType,
options: FlagEvaluationOptions = {},
): EvaluationDetails<T> {
const details = this.evaluate<T>(flagKey, resolver, defaultValue, flagType, options);
return new EvaluationDetailsWithSubscription(this, flagKey, defaultValue, flagType, details, options);
}

private evaluate<T extends FlagValue>(
flagKey: string,
resolver: (flagKey: string, defaultValue: T, context: EvaluationContext, logger: Logger) => ResolutionDetails<T>,
Expand Down
104 changes: 104 additions & 0 deletions packages/web/src/evaluation/evaluation-details-with-subscription.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import type { EvaluationDetails, ErrorCode, FlagValue, FlagValueType, JsonValue } from '@openfeature/core';
import type { Client } from '../client';
import { ProviderEvents } from '../events';
import type { FlagEvaluationOptions } from './evaluation';

export class EvaluationDetailsWithSubscription<T extends FlagValue> implements EvaluationDetails<T> {
private _details: EvaluationDetails<T>;
private readonly _flagKey: string;
private readonly _defaultValue: T;
private readonly _flagType: FlagValueType;
private readonly _options?: FlagEvaluationOptions;

constructor(
private readonly client: Client,
flagKey: string,
defaultValue: T,
flagType: FlagValueType,
initialDetails: EvaluationDetails<T>,
options?: FlagEvaluationOptions,
) {
this._details = initialDetails;
this._flagKey = flagKey;
this._defaultValue = defaultValue;
this._flagType = flagType;
this._options = options;
}

get flagKey(): string {
return this._details.flagKey;
}

get value(): T {
return this._details.value;
}

get variant(): string | undefined {
return this._details.variant;
}

get flagMetadata(): Readonly<Record<string, string | number | boolean>> {
return this._details.flagMetadata;
}

get reason(): string | undefined {
return this._details.reason;
}

get errorCode(): ErrorCode | undefined {
return this._details.errorCode;
}

get errorMessage(): string | undefined {
return this._details.errorMessage;
}

onContextChanged(callback: (newDetails: EvaluationDetails<T>, oldDetails: EvaluationDetails<T>) => void): () => void {
const handler = () => {
const oldDetails = { ...this._details };
let newDetails: EvaluationDetails<T>;

switch (this._flagType) {
case 'boolean':
newDetails = this.client.getBooleanDetails(
this._flagKey,
this._defaultValue as boolean,
this._options,
) as EvaluationDetails<T>;
break;
case 'string':
newDetails = this.client.getStringDetails(
this._flagKey,
this._defaultValue as string,
this._options,
) as EvaluationDetails<T>;
break;
case 'number':
newDetails = this.client.getNumberDetails(
this._flagKey,
this._defaultValue as number,
this._options,
) as EvaluationDetails<T>;
break;
case 'object':
newDetails = this.client.getObjectDetails(
this._flagKey,
this._defaultValue as JsonValue,
this._options,
) as EvaluationDetails<T>;
break;
default:
return;
}

this._details = newDetails;
callback(newDetails, oldDetails);
};

this.client.addHandler(ProviderEvents.ContextChanged, handler);

return () => {
this.client.removeHandler(ProviderEvents.ContextChanged, handler);
};
}
}
Loading
Loading