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
4 changes: 4 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
1.2.0 (November 7, 2025)
- Updated @openfeature/server-sdk to 1.20.0
- Updated @splitsoftware/splitio to 11.8.0

1.1.0 (September 12, 2025)
- Updated @openfeature/server-sdk to 1.19.0
- Updated @splitsoftware/splitio to 11.4.1
Expand Down
44 changes: 22 additions & 22 deletions package-lock.json

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

10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@splitsoftware/openfeature-js-split-provider",
"version": "1.1.0",
"version": "1.2.0",
"description": "Split OpenFeature Provider",
"files": [
"README.md",
Expand Down Expand Up @@ -35,13 +35,13 @@
}
},
"peerDependencies": {
"@openfeature/server-sdk": "^1.19.0",
"@splitsoftware/splitio": "^11.4.1"
"@openfeature/server-sdk": "^1.20.0",
"@splitsoftware/splitio": "^11.8.0"
},
"devDependencies": {
"@eslint/js": "^9.35.0",
"@openfeature/server-sdk": "^1.19.0",
"@splitsoftware/splitio": "^11.4.1",
"@openfeature/server-sdk": "^1.20.0",
"@splitsoftware/splitio": "^11.8.0",
"@types/jest": "^30.0.0",
"@types/node": "^24.3.1",
"copyfiles": "^2.4.1",
Expand Down
90 changes: 46 additions & 44 deletions src/lib/js-split-provider.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
EvaluationContext,
FlagNotFoundError,
InvalidContextError,
JsonValue,
OpenFeatureEventEmitter,
ParseError,
Expand All @@ -20,7 +19,8 @@ type SplitProviderOptions = {
}

type Consumer = {
key: string | undefined;
targetingKey: string | undefined;
trafficType: string;
attributes: SplitIO.Attributes;
};

Expand All @@ -31,9 +31,9 @@ export class OpenFeatureSplitProvider implements Provider {
metadata = {
name: 'split',
};
private initialized: Promise<void>;
private client: SplitIO.IClient | SplitIO.IAsyncClient;

private client: SplitIO.IClient | SplitIO.IAsyncClient;
private trafficType: string;
public readonly events = new OpenFeatureEventEmitter();

private getSplitClient(options: SplitProviderOptions | string | SplitIO.ISDK | SplitIO.IAsyncSDK) {
Expand All @@ -53,27 +53,15 @@ export class OpenFeatureSplitProvider implements Provider {
}

constructor(options: SplitProviderOptions | string | SplitIO.ISDK | SplitIO.IAsyncSDK) {

// Asume 'user' as default traffic type'
this.trafficType = 'user';
this.client = this.getSplitClient(options);

this.client.on(this.client.Event.SDK_UPDATE, () => {
this.events.emit(ProviderEvents.ConfigurationChanged)
});
this.initialized = new Promise((resolve) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((this.client as any).__getStatus().isReady) {
console.log(`${this.metadata.name} provider initialized`);
resolve();
} else {
this.client.on(this.client.Event.SDK_READY, () => {
console.log(`${this.metadata.name} provider initialized`);
resolve();
});
}
});
this.events.emit(ProviderEvents.ConfigurationChanged);
});
}

async resolveBooleanEvaluation(
public async resolveBooleanEvaluation(
flagKey: string,
_: boolean,
context: EvaluationContext
Expand All @@ -95,7 +83,7 @@ export class OpenFeatureSplitProvider implements Provider {
throw new ParseError(`Invalid boolean value for ${treatment}`);
}

async resolveStringEvaluation(
public async resolveStringEvaluation(
flagKey: string,
_: string,
context: EvaluationContext
Expand All @@ -107,7 +95,7 @@ export class OpenFeatureSplitProvider implements Provider {
return details;
}

async resolveNumberEvaluation(
public async resolveNumberEvaluation(
flagKey: string,
_: number,
context: EvaluationContext
Expand All @@ -119,7 +107,7 @@ export class OpenFeatureSplitProvider implements Provider {
return { ...details, value: this.parseValidNumber(details.value) };
}

async resolveObjectEvaluation<U extends JsonValue>(
public async resolveObjectEvaluation<U extends JsonValue>(
flagKey: string,
_: U,
context: EvaluationContext
Expand All @@ -135,7 +123,7 @@ export class OpenFeatureSplitProvider implements Provider {
flagKey: string,
consumer: Consumer
): Promise<ResolutionDetails<string>> {
if (!consumer.key) {
if (!consumer.targetingKey) {
throw new TargetingKeyMissingError(
'The Split provider requires a targeting key.'
);
Expand All @@ -146,9 +134,12 @@ export class OpenFeatureSplitProvider implements Provider {
);
}

await this.initialized;
await new Promise((resolve, reject) => {
this.readinessHandler(resolve, reject);
});

const { treatment: value, config }: SplitIO.TreatmentWithConfig = await this.client.getTreatmentWithConfig(
consumer.key,
consumer.targetingKey,
flagKey,
consumer.attributes
);
Expand All @@ -171,23 +162,14 @@ export class OpenFeatureSplitProvider implements Provider {
details: TrackingEventDetails
): Promise<void> {

// targetingKey is always required
const { targetingKey } = context;
if (targetingKey == null || targetingKey === '')
throw new TargetingKeyMissingError('Missing targetingKey, required to track');

// eventName is always required
if (trackingEventName == null || trackingEventName === '')
throw new ParseError('Missing eventName, required to track');

// trafficType is always required
const ttVal = context['trafficType'];
const trafficType =
ttVal != null && typeof ttVal === 'string' && ttVal.trim() !== ''
? ttVal
: null;
if (trafficType == null || trafficType === '')
throw new InvalidContextError('Missing trafficType variable, required to track');
// targetingKey is always required
const { targetingKey, trafficType } = this.transformContext(context);
if (targetingKey == null || targetingKey === '')
throw new TargetingKeyMissingError('Missing targetingKey, required to track');

let value;
let properties: SplitIO.Properties = {};
Expand All @@ -203,15 +185,20 @@ export class OpenFeatureSplitProvider implements Provider {
this.client.track(targetingKey, trafficType, trackingEventName, value, properties);
}

async onClose?(): Promise<void> {
public async onClose?(): Promise<void> {
return this.client.destroy();
}

//Transform the context into an object useful for the Split API, an key string with arbitrary Split 'Attributes'.
private transformContext(context: EvaluationContext): Consumer {
const { targetingKey, ...attributes } = context;
const { targetingKey, trafficType: ttVal, ...attributes } = context;
const trafficType =
ttVal != null && typeof ttVal === 'string' && ttVal.trim() !== ''
? ttVal
: this.trafficType;
return {
key: targetingKey,
targetingKey,
trafficType,
// Stringify context objects include date.
attributes: JSON.parse(JSON.stringify(attributes)),
};
Expand Down Expand Up @@ -247,4 +234,19 @@ export class OpenFeatureSplitProvider implements Provider {
throw new ParseError(`Error parsing ${stringValue} as JSON, ${err}`);
}
}
}

private async readinessHandler(onSdkReady: (params?: unknown) => void, onSdkTimedOut: () => void): Promise<void> {

const clientStatus = this.client.getStatus();
if (clientStatus.isReady) {
onSdkReady();
} else {
if (clientStatus.hasTimedout) {
onSdkTimedOut();
} else {
this.client.on(this.client.Event.SDK_READY_TIMED_OUT, onSdkTimedOut);
}
this.client.on(this.client.Event.SDK_READY, onSdkReady);
}
}
}