@splitsoftware/openfeature-js-split-provider 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGES.txt +8 -0
- package/README.md +25 -3
- package/es/lib/client-resolver.js +17 -0
- package/es/lib/context.js +16 -0
- package/es/lib/js-split-provider.js +57 -102
- package/es/lib/parsers.js +26 -0
- package/es/lib/readiness.js +64 -0
- package/es/lib/types.js +4 -0
- package/lib/lib/client-resolver.js +21 -0
- package/lib/lib/context.js +20 -0
- package/lib/lib/js-split-provider.js +58 -103
- package/lib/lib/parsers.js +31 -0
- package/lib/lib/readiness.js +70 -0
- package/lib/lib/types.js +7 -0
- package/package.json +7 -5
- package/src/__tests__/nodeSuites/client.spec.js +17 -2
- package/src/__tests__/nodeSuites/client_redis.spec.js +9 -10
- package/src/__tests__/nodeSuites/provider.spec.js +73 -0
- package/src/lib/__tests__/client-resolver.spec.ts +64 -0
- package/src/lib/__tests__/context.spec.ts +72 -0
- package/src/lib/__tests__/parsers.spec.ts +63 -0
- package/src/lib/__tests__/readiness.spec.ts +159 -0
- package/src/lib/client-resolver.ts +22 -0
- package/src/lib/context.ts +23 -0
- package/src/lib/js-split-provider.ts +111 -148
- package/src/lib/parsers.ts +32 -0
- package/src/lib/readiness.ts +103 -0
- package/src/lib/types.ts +32 -0
- package/types/lib/client-resolver.d.ts +7 -0
- package/types/lib/context.d.ts +7 -0
- package/types/lib/js-split-provider.d.ts +16 -18
- package/types/lib/parsers.d.ts +3 -0
- package/types/lib/readiness.d.ts +16 -0
- package/types/lib/types.d.ts +24 -0
package/CHANGES.txt
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
1.3.0 (March 2, 2026)
|
|
2
|
+
- Updated ConfigurationChanged event to forward SDK_UPDATE metadata from Split (flagsChanged, metadata with type and names)
|
|
3
|
+
- Requires @splitsoftware/splitio ^11.10.0 for SDK_UPDATE metadata support
|
|
4
|
+
|
|
5
|
+
1.2.0 (November 7, 2025)
|
|
6
|
+
- Updated @openfeature/server-sdk to 1.20.0
|
|
7
|
+
- Updated @splitsoftware/splitio to 11.8.0
|
|
8
|
+
|
|
1
9
|
1.1.0 (September 12, 2025)
|
|
2
10
|
- Updated @openfeature/server-sdk to 1.19.0
|
|
3
11
|
- Updated @splitsoftware/splitio to 11.4.1
|
package/README.md
CHANGED
|
@@ -30,7 +30,9 @@ const OpenFeatureSplitProvider = require('@splitsoftware/openfeature-js-split-pr
|
|
|
30
30
|
|
|
31
31
|
const authorizationKey = 'your auth key'
|
|
32
32
|
const provider = new OpenFeatureSplitProvider(authorizationKey);
|
|
33
|
-
OpenFeature.
|
|
33
|
+
await OpenFeature.setProviderAndWait(provider);
|
|
34
|
+
const client = OpenFeature.getClient('my-app');
|
|
35
|
+
// safe to evaluate
|
|
34
36
|
```
|
|
35
37
|
|
|
36
38
|
### Register the Split provider with OpenFeature using splitFactory
|
|
@@ -42,7 +44,9 @@ const OpenFeatureSplitProvider = require('@splitsoftware/openfeature-js-split-pr
|
|
|
42
44
|
const authorizationKey = 'your auth key'
|
|
43
45
|
const splitFactory = SplitFactory({core: {authorizationKey}});
|
|
44
46
|
const provider = new OpenFeatureSplitProvider(splitFactory);
|
|
45
|
-
OpenFeature.
|
|
47
|
+
await OpenFeature.setProviderAndWait(provider);
|
|
48
|
+
const client = OpenFeature.getClient('my-app');
|
|
49
|
+
// safe to evaluate
|
|
46
50
|
```
|
|
47
51
|
|
|
48
52
|
### Register the Split provider with OpenFeature using splitClient
|
|
@@ -54,7 +58,9 @@ const OpenFeatureSplitProvider = require('@splitsoftware/openfeature-js-split-pr
|
|
|
54
58
|
const authorizationKey = 'your auth key'
|
|
55
59
|
const splitClient = SplitFactory({core: {authorizationKey}}).client();
|
|
56
60
|
const provider = new OpenFeatureSplitProvider({splitClient});
|
|
57
|
-
OpenFeature.
|
|
61
|
+
await OpenFeature.setProviderAndWait(provider);
|
|
62
|
+
const client = OpenFeature.getClient('my-app');
|
|
63
|
+
// safe to evaluate
|
|
58
64
|
```
|
|
59
65
|
|
|
60
66
|
## Use of OpenFeature with Split
|
|
@@ -94,6 +100,22 @@ const booleanTreatment = await client.getBooleanDetails('boolFlag', false, conte
|
|
|
94
100
|
const config = booleanTreatment.flagMetadata.config
|
|
95
101
|
```
|
|
96
102
|
|
|
103
|
+
## Configuration changed event (SDK_UPDATE)
|
|
104
|
+
|
|
105
|
+
When the Split SDK emits the `SDK_UPDATE` **event** (flags or segments changed), the provider emits OpenFeature’s `ConfigurationChanged` and forwards the event metadata. The metadata shape matches [javascript-commons SdkUpdateMetadata](https://github.com/splitio/javascript-commons): `type` is `'FLAGS_UPDATE' | 'SEGMENTS_UPDATE'` and `names` is the list of flag or segment names that were updated. Handlers receive [Provider Event Details](https://openfeature.dev/specification/types#provider-event-details): `flagsChanged` (when `type === 'FLAGS_UPDATE'`, the `names` array) and `metadata` (`type` as string).
|
|
106
|
+
|
|
107
|
+
Requires `@splitsoftware/splitio` **11.10.0 or later** (metadata was added in 11.10.0).
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
const { OpenFeature, ProviderEvents } = require('@openfeature/server-sdk');
|
|
111
|
+
|
|
112
|
+
const client = OpenFeature.getClient();
|
|
113
|
+
client.addHandler(ProviderEvents.ConfigurationChanged, (eventDetails) => {
|
|
114
|
+
console.log('Flags changed:', eventDetails.flagsChanged);
|
|
115
|
+
console.log('Event metadata:', eventDetails.metadata);
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
97
119
|
## Tracking
|
|
98
120
|
|
|
99
121
|
To use track(eventName, context, details) you must provide:
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { SplitFactory } from '@splitsoftware/splitio';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves the Split client from the various supported constructor option shapes.
|
|
4
|
+
* Supports: API key (string), Split SDK/AsyncSDK (factory), or pre-built splitClient.
|
|
5
|
+
*/
|
|
6
|
+
export function getSplitClient(options) {
|
|
7
|
+
if (typeof options === 'string') {
|
|
8
|
+
const splitFactory = SplitFactory({ core: { authorizationKey: options } });
|
|
9
|
+
return splitFactory.client();
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
return options.client();
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return options.splitClient;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { DEFAULT_TRAFFIC_TYPE } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Transforms OpenFeature evaluation context into the consumer shape used by the Split API:
|
|
4
|
+
* targeting key, traffic type (with default), and remaining attributes.
|
|
5
|
+
*/
|
|
6
|
+
export function transformContext(context, defaultTrafficType = DEFAULT_TRAFFIC_TYPE) {
|
|
7
|
+
const { targetingKey, trafficType: ttVal, ...attributes } = context;
|
|
8
|
+
const trafficType = ttVal != null && typeof ttVal === 'string' && ttVal.trim() !== ''
|
|
9
|
+
? ttVal
|
|
10
|
+
: defaultTrafficType;
|
|
11
|
+
return {
|
|
12
|
+
targetingKey,
|
|
13
|
+
trafficType,
|
|
14
|
+
attributes: JSON.parse(JSON.stringify(attributes)),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -1,47 +1,43 @@
|
|
|
1
|
-
import { FlagNotFoundError,
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { FlagNotFoundError, OpenFeatureEventEmitter, ParseError, ProviderEvents, StandardResolutionReasons, TargetingKeyMissingError } from '@openfeature/server-sdk';
|
|
2
|
+
import { getSplitClient } from './client-resolver';
|
|
3
|
+
import { transformContext } from './context';
|
|
4
|
+
import { parseValidJsonObject, parseValidNumber } from './parsers';
|
|
5
|
+
import { attachReadyEventHandlers, waitUntilReady } from './readiness';
|
|
6
|
+
import { CONTROL_TREATMENT, CONTROL_VALUE_ERROR_MESSAGE, DEFAULT_TRAFFIC_TYPE, PROVIDER_NAME } from './types';
|
|
5
7
|
export class OpenFeatureSplitProvider {
|
|
6
|
-
getSplitClient(options) {
|
|
7
|
-
if (typeof (options) === 'string') {
|
|
8
|
-
const splitFactory = SplitFactory({ core: { authorizationKey: options } });
|
|
9
|
-
return splitFactory.client();
|
|
10
|
-
}
|
|
11
|
-
let splitClient;
|
|
12
|
-
try {
|
|
13
|
-
splitClient = options.client();
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
splitClient = options.splitClient;
|
|
17
|
-
}
|
|
18
|
-
return splitClient;
|
|
19
|
-
}
|
|
20
8
|
constructor(options) {
|
|
21
9
|
this.metadata = {
|
|
22
|
-
name:
|
|
10
|
+
name: PROVIDER_NAME,
|
|
23
11
|
};
|
|
12
|
+
this.runsOn = 'server';
|
|
24
13
|
this.events = new OpenFeatureEventEmitter();
|
|
25
|
-
this.
|
|
26
|
-
this.client
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
});
|
|
40
|
-
}
|
|
14
|
+
this.trafficType = DEFAULT_TRAFFIC_TYPE;
|
|
15
|
+
this.client = getSplitClient(options);
|
|
16
|
+
attachReadyEventHandlers(this.client, this.events, this.metadata.name);
|
|
17
|
+
this.client.on(this.client.Event.SDK_UPDATE, (updateMetadata) => {
|
|
18
|
+
const eventDetails = {
|
|
19
|
+
providerName: this.metadata.name,
|
|
20
|
+
...(updateMetadata
|
|
21
|
+
? {
|
|
22
|
+
metadata: { type: updateMetadata.type },
|
|
23
|
+
flagsChanged: updateMetadata.names || [],
|
|
24
|
+
}
|
|
25
|
+
: {}),
|
|
26
|
+
};
|
|
27
|
+
this.events.emit(ProviderEvents.ConfigurationChanged, eventDetails);
|
|
41
28
|
});
|
|
42
29
|
}
|
|
43
|
-
|
|
44
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Called by the SDK after the provider is set. Waits for the Split client to be ready.
|
|
32
|
+
* When this promise resolves, the SDK emits ProviderEvents.Ready.
|
|
33
|
+
*/
|
|
34
|
+
async initialize(_context) {
|
|
35
|
+
void _context;
|
|
36
|
+
await waitUntilReady(this.client, this.events, this.metadata.name);
|
|
37
|
+
}
|
|
38
|
+
async resolveBooleanEvaluation(flagKey, _, context, logger) {
|
|
39
|
+
void logger;
|
|
40
|
+
const details = await this.evaluateTreatment(flagKey, transformContext(context, this.trafficType));
|
|
45
41
|
const treatment = details.value.toLowerCase();
|
|
46
42
|
if (treatment === 'on' || treatment === 'true') {
|
|
47
43
|
return { ...details, value: true };
|
|
@@ -51,54 +47,48 @@ export class OpenFeatureSplitProvider {
|
|
|
51
47
|
}
|
|
52
48
|
throw new ParseError(`Invalid boolean value for ${treatment}`);
|
|
53
49
|
}
|
|
54
|
-
async resolveStringEvaluation(flagKey, _, context) {
|
|
55
|
-
|
|
56
|
-
return
|
|
50
|
+
async resolveStringEvaluation(flagKey, _, context, logger) {
|
|
51
|
+
void logger;
|
|
52
|
+
return this.evaluateTreatment(flagKey, transformContext(context, this.trafficType));
|
|
57
53
|
}
|
|
58
|
-
async resolveNumberEvaluation(flagKey, _, context) {
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
async resolveNumberEvaluation(flagKey, _, context, logger) {
|
|
55
|
+
void logger;
|
|
56
|
+
const details = await this.evaluateTreatment(flagKey, transformContext(context, this.trafficType));
|
|
57
|
+
return { ...details, value: parseValidNumber(details.value) };
|
|
61
58
|
}
|
|
62
|
-
async resolveObjectEvaluation(flagKey, _, context) {
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
async resolveObjectEvaluation(flagKey, _, context, logger) {
|
|
60
|
+
void logger;
|
|
61
|
+
const details = await this.evaluateTreatment(flagKey, transformContext(context, this.trafficType));
|
|
62
|
+
return { ...details, value: parseValidJsonObject(details.value) };
|
|
65
63
|
}
|
|
66
64
|
async evaluateTreatment(flagKey, consumer) {
|
|
67
|
-
if (!consumer.
|
|
65
|
+
if (!consumer.targetingKey) {
|
|
68
66
|
throw new TargetingKeyMissingError('The Split provider requires a targeting key.');
|
|
69
67
|
}
|
|
70
68
|
if (flagKey == null || flagKey === '') {
|
|
71
69
|
throw new FlagNotFoundError('flagKey must be a non-empty string');
|
|
72
70
|
}
|
|
73
|
-
await this.
|
|
74
|
-
const { treatment: value, config } = await this.client.getTreatmentWithConfig(consumer.
|
|
71
|
+
await waitUntilReady(this.client, this.events, this.metadata.name);
|
|
72
|
+
const { treatment: value, config } = await this.client.getTreatmentWithConfig(consumer.targetingKey, flagKey, consumer.attributes);
|
|
75
73
|
if (value === CONTROL_TREATMENT) {
|
|
76
74
|
throw new FlagNotFoundError(CONTROL_VALUE_ERROR_MESSAGE);
|
|
77
75
|
}
|
|
78
|
-
const flagMetadata = { config: config
|
|
79
|
-
|
|
80
|
-
value
|
|
76
|
+
const flagMetadata = Object.freeze({ config: config ?? '' });
|
|
77
|
+
return {
|
|
78
|
+
value,
|
|
81
79
|
variant: value,
|
|
82
|
-
flagMetadata
|
|
80
|
+
flagMetadata,
|
|
83
81
|
reason: StandardResolutionReasons.TARGETING_MATCH,
|
|
84
82
|
};
|
|
85
|
-
return details;
|
|
86
83
|
}
|
|
87
84
|
async track(trackingEventName, context, details) {
|
|
88
|
-
|
|
89
|
-
const { targetingKey } = context;
|
|
90
|
-
if (targetingKey == null || targetingKey === '')
|
|
91
|
-
throw new TargetingKeyMissingError('Missing targetingKey, required to track');
|
|
92
|
-
// eventName is always required
|
|
93
|
-
if (trackingEventName == null || trackingEventName === '')
|
|
85
|
+
if (trackingEventName == null || trackingEventName === '') {
|
|
94
86
|
throw new ParseError('Missing eventName, required to track');
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (trafficType == null || trafficType === '')
|
|
101
|
-
throw new InvalidContextError('Missing trafficType variable, required to track');
|
|
87
|
+
}
|
|
88
|
+
const { targetingKey, trafficType } = transformContext(context, this.trafficType);
|
|
89
|
+
if (targetingKey == null || targetingKey === '') {
|
|
90
|
+
throw new TargetingKeyMissingError('Missing targetingKey, required to track');
|
|
91
|
+
}
|
|
102
92
|
let value;
|
|
103
93
|
let properties = {};
|
|
104
94
|
if (details != null) {
|
|
@@ -114,39 +104,4 @@ export class OpenFeatureSplitProvider {
|
|
|
114
104
|
async onClose() {
|
|
115
105
|
return this.client.destroy();
|
|
116
106
|
}
|
|
117
|
-
//Transform the context into an object useful for the Split API, an key string with arbitrary Split 'Attributes'.
|
|
118
|
-
transformContext(context) {
|
|
119
|
-
const { targetingKey, ...attributes } = context;
|
|
120
|
-
return {
|
|
121
|
-
key: targetingKey,
|
|
122
|
-
// Stringify context objects include date.
|
|
123
|
-
attributes: JSON.parse(JSON.stringify(attributes)),
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
parseValidNumber(stringValue) {
|
|
127
|
-
if (stringValue === undefined) {
|
|
128
|
-
throw new ParseError(`Invalid 'undefined' value.`);
|
|
129
|
-
}
|
|
130
|
-
const result = Number.parseFloat(stringValue);
|
|
131
|
-
if (Number.isNaN(result)) {
|
|
132
|
-
throw new ParseError(`Invalid numeric value ${stringValue}`);
|
|
133
|
-
}
|
|
134
|
-
return result;
|
|
135
|
-
}
|
|
136
|
-
parseValidJsonObject(stringValue) {
|
|
137
|
-
if (stringValue === undefined) {
|
|
138
|
-
throw new ParseError(`Invalid 'undefined' JSON value.`);
|
|
139
|
-
}
|
|
140
|
-
// we may want to allow the parsing to be customized.
|
|
141
|
-
try {
|
|
142
|
-
const value = JSON.parse(stringValue);
|
|
143
|
-
if (typeof value !== 'object') {
|
|
144
|
-
throw new ParseError(`Flag value ${stringValue} had unexpected type ${typeof value}, expected "object"`);
|
|
145
|
-
}
|
|
146
|
-
return value;
|
|
147
|
-
}
|
|
148
|
-
catch (err) {
|
|
149
|
-
throw new ParseError(`Error parsing ${stringValue} as JSON, ${err}`);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
107
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ParseError } from '@openfeature/server-sdk';
|
|
2
|
+
export function parseValidNumber(stringValue) {
|
|
3
|
+
if (stringValue === undefined) {
|
|
4
|
+
throw new ParseError(`Invalid 'undefined' value.`);
|
|
5
|
+
}
|
|
6
|
+
const result = Number.parseFloat(stringValue);
|
|
7
|
+
if (Number.isNaN(result)) {
|
|
8
|
+
throw new ParseError(`Invalid numeric value ${stringValue}`);
|
|
9
|
+
}
|
|
10
|
+
return result;
|
|
11
|
+
}
|
|
12
|
+
export function parseValidJsonObject(stringValue) {
|
|
13
|
+
if (stringValue === undefined) {
|
|
14
|
+
throw new ParseError(`Invalid 'undefined' JSON value.`);
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const value = JSON.parse(stringValue);
|
|
18
|
+
if (typeof value !== 'object') {
|
|
19
|
+
throw new ParseError(`Flag value ${stringValue} had unexpected type ${typeof value}, expected "object"`);
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
throw new ParseError(`Error parsing ${stringValue} as JSON, ${err}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ProviderEvents } from '@openfeature/server-sdk';
|
|
2
|
+
import { PROVIDER_NAME } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Builds OpenFeature Ready event details including Split SDK ready metadata.
|
|
5
|
+
* Handles Split SDK not passing metadata (e.g. in consumer/Redis mode).
|
|
6
|
+
*/
|
|
7
|
+
function buildReadyEventDetails(providerName, splitMetadata, readyFromCache) {
|
|
8
|
+
const metadata = {
|
|
9
|
+
readyFromCache,
|
|
10
|
+
initialCacheLoad: splitMetadata?.initialCacheLoad ?? false,
|
|
11
|
+
};
|
|
12
|
+
if (splitMetadata?.lastUpdateTimestamp != null) {
|
|
13
|
+
metadata.lastUpdateTimestamp = splitMetadata.lastUpdateTimestamp;
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
providerName: providerName || PROVIDER_NAME,
|
|
17
|
+
metadata,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Registers Split SDK_READY and SDK_READY_FROM_CACHE listeners and forwards them
|
|
22
|
+
* as OpenFeature ProviderEvents.Ready with event metadata (initialCacheLoad, lastUpdateTimestamp, readyFromCache).
|
|
23
|
+
* If the client is already ready when attaching (e.g. localhost or reused client), emits Ready once
|
|
24
|
+
* with best-effort metadata so handlers always receive at least one Ready when the client is ready.
|
|
25
|
+
*/
|
|
26
|
+
export function attachReadyEventHandlers(client, events, providerName = PROVIDER_NAME) {
|
|
27
|
+
client.on(client.Event.SDK_READY_FROM_CACHE, (splitMetadata) => {
|
|
28
|
+
events.emit(ProviderEvents.Ready, buildReadyEventDetails(providerName, splitMetadata, true));
|
|
29
|
+
});
|
|
30
|
+
client.on(client.Event.SDK_READY, (splitMetadata) => {
|
|
31
|
+
events.emit(ProviderEvents.Ready, buildReadyEventDetails(providerName, splitMetadata, false));
|
|
32
|
+
});
|
|
33
|
+
const status = client.getStatus();
|
|
34
|
+
if (status.isReady) {
|
|
35
|
+
events.emit(ProviderEvents.Ready, buildReadyEventDetails(providerName, undefined, false));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Returns a promise that resolves when the Split client is ready (SDK_READY),
|
|
40
|
+
* or rejects if the client has timed out (SDK_READY_TIMED_OUT).
|
|
41
|
+
* Used to gate evaluations until the SDK has synchronized with the backend.
|
|
42
|
+
*/
|
|
43
|
+
export function waitUntilReady(client, events, providerName = PROVIDER_NAME) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const status = client.getStatus();
|
|
46
|
+
if (status.isReady) {
|
|
47
|
+
emitReadyEvent(client, events, providerName);
|
|
48
|
+
resolve();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (status.hasTimedout) {
|
|
52
|
+
reject();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
client.on(client.Event.SDK_READY_TIMED_OUT, reject);
|
|
56
|
+
client.on(client.Event.SDK_READY, () => {
|
|
57
|
+
emitReadyEvent(client, events, providerName);
|
|
58
|
+
resolve();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export function emitReadyEvent(client, events, providerName = PROVIDER_NAME) {
|
|
63
|
+
events.emit(ProviderEvents.Ready, buildReadyEventDetails(providerName, undefined, false));
|
|
64
|
+
}
|
package/es/lib/types.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getSplitClient = void 0;
|
|
4
|
+
const splitio_1 = require("@splitsoftware/splitio");
|
|
5
|
+
/**
|
|
6
|
+
* Resolves the Split client from the various supported constructor option shapes.
|
|
7
|
+
* Supports: API key (string), Split SDK/AsyncSDK (factory), or pre-built splitClient.
|
|
8
|
+
*/
|
|
9
|
+
function getSplitClient(options) {
|
|
10
|
+
if (typeof options === 'string') {
|
|
11
|
+
const splitFactory = (0, splitio_1.SplitFactory)({ core: { authorizationKey: options } });
|
|
12
|
+
return splitFactory.client();
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
return options.client();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return options.splitClient;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.getSplitClient = getSplitClient;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.transformContext = void 0;
|
|
4
|
+
const types_1 = require("./types");
|
|
5
|
+
/**
|
|
6
|
+
* Transforms OpenFeature evaluation context into the consumer shape used by the Split API:
|
|
7
|
+
* targeting key, traffic type (with default), and remaining attributes.
|
|
8
|
+
*/
|
|
9
|
+
function transformContext(context, defaultTrafficType = types_1.DEFAULT_TRAFFIC_TYPE) {
|
|
10
|
+
const { targetingKey, trafficType: ttVal, ...attributes } = context;
|
|
11
|
+
const trafficType = ttVal != null && typeof ttVal === 'string' && ttVal.trim() !== ''
|
|
12
|
+
? ttVal
|
|
13
|
+
: defaultTrafficType;
|
|
14
|
+
return {
|
|
15
|
+
targetingKey,
|
|
16
|
+
trafficType,
|
|
17
|
+
attributes: JSON.parse(JSON.stringify(attributes)),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
exports.transformContext = transformContext;
|
|
@@ -2,49 +2,45 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OpenFeatureSplitProvider = void 0;
|
|
4
4
|
const server_sdk_1 = require("@openfeature/server-sdk");
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
5
|
+
const client_resolver_1 = require("./client-resolver");
|
|
6
|
+
const context_1 = require("./context");
|
|
7
|
+
const parsers_1 = require("./parsers");
|
|
8
|
+
const readiness_1 = require("./readiness");
|
|
9
|
+
const types_1 = require("./types");
|
|
8
10
|
class OpenFeatureSplitProvider {
|
|
9
|
-
getSplitClient(options) {
|
|
10
|
-
if (typeof (options) === 'string') {
|
|
11
|
-
const splitFactory = (0, splitio_1.SplitFactory)({ core: { authorizationKey: options } });
|
|
12
|
-
return splitFactory.client();
|
|
13
|
-
}
|
|
14
|
-
let splitClient;
|
|
15
|
-
try {
|
|
16
|
-
splitClient = options.client();
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
splitClient = options.splitClient;
|
|
20
|
-
}
|
|
21
|
-
return splitClient;
|
|
22
|
-
}
|
|
23
11
|
constructor(options) {
|
|
24
12
|
this.metadata = {
|
|
25
|
-
name:
|
|
13
|
+
name: types_1.PROVIDER_NAME,
|
|
26
14
|
};
|
|
15
|
+
this.runsOn = 'server';
|
|
27
16
|
this.events = new server_sdk_1.OpenFeatureEventEmitter();
|
|
28
|
-
this.
|
|
29
|
-
this.client
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
});
|
|
43
|
-
}
|
|
17
|
+
this.trafficType = types_1.DEFAULT_TRAFFIC_TYPE;
|
|
18
|
+
this.client = (0, client_resolver_1.getSplitClient)(options);
|
|
19
|
+
(0, readiness_1.attachReadyEventHandlers)(this.client, this.events, this.metadata.name);
|
|
20
|
+
this.client.on(this.client.Event.SDK_UPDATE, (updateMetadata) => {
|
|
21
|
+
const eventDetails = {
|
|
22
|
+
providerName: this.metadata.name,
|
|
23
|
+
...(updateMetadata
|
|
24
|
+
? {
|
|
25
|
+
metadata: { type: updateMetadata.type },
|
|
26
|
+
flagsChanged: updateMetadata.names || [],
|
|
27
|
+
}
|
|
28
|
+
: {}),
|
|
29
|
+
};
|
|
30
|
+
this.events.emit(server_sdk_1.ProviderEvents.ConfigurationChanged, eventDetails);
|
|
44
31
|
});
|
|
45
32
|
}
|
|
46
|
-
|
|
47
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Called by the SDK after the provider is set. Waits for the Split client to be ready.
|
|
35
|
+
* When this promise resolves, the SDK emits ProviderEvents.Ready.
|
|
36
|
+
*/
|
|
37
|
+
async initialize(_context) {
|
|
38
|
+
void _context;
|
|
39
|
+
await (0, readiness_1.waitUntilReady)(this.client, this.events, this.metadata.name);
|
|
40
|
+
}
|
|
41
|
+
async resolveBooleanEvaluation(flagKey, _, context, logger) {
|
|
42
|
+
void logger;
|
|
43
|
+
const details = await this.evaluateTreatment(flagKey, (0, context_1.transformContext)(context, this.trafficType));
|
|
48
44
|
const treatment = details.value.toLowerCase();
|
|
49
45
|
if (treatment === 'on' || treatment === 'true') {
|
|
50
46
|
return { ...details, value: true };
|
|
@@ -54,54 +50,48 @@ class OpenFeatureSplitProvider {
|
|
|
54
50
|
}
|
|
55
51
|
throw new server_sdk_1.ParseError(`Invalid boolean value for ${treatment}`);
|
|
56
52
|
}
|
|
57
|
-
async resolveStringEvaluation(flagKey, _, context) {
|
|
58
|
-
|
|
59
|
-
return
|
|
53
|
+
async resolveStringEvaluation(flagKey, _, context, logger) {
|
|
54
|
+
void logger;
|
|
55
|
+
return this.evaluateTreatment(flagKey, (0, context_1.transformContext)(context, this.trafficType));
|
|
60
56
|
}
|
|
61
|
-
async resolveNumberEvaluation(flagKey, _, context) {
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
async resolveNumberEvaluation(flagKey, _, context, logger) {
|
|
58
|
+
void logger;
|
|
59
|
+
const details = await this.evaluateTreatment(flagKey, (0, context_1.transformContext)(context, this.trafficType));
|
|
60
|
+
return { ...details, value: (0, parsers_1.parseValidNumber)(details.value) };
|
|
64
61
|
}
|
|
65
|
-
async resolveObjectEvaluation(flagKey, _, context) {
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
async resolveObjectEvaluation(flagKey, _, context, logger) {
|
|
63
|
+
void logger;
|
|
64
|
+
const details = await this.evaluateTreatment(flagKey, (0, context_1.transformContext)(context, this.trafficType));
|
|
65
|
+
return { ...details, value: (0, parsers_1.parseValidJsonObject)(details.value) };
|
|
68
66
|
}
|
|
69
67
|
async evaluateTreatment(flagKey, consumer) {
|
|
70
|
-
if (!consumer.
|
|
68
|
+
if (!consumer.targetingKey) {
|
|
71
69
|
throw new server_sdk_1.TargetingKeyMissingError('The Split provider requires a targeting key.');
|
|
72
70
|
}
|
|
73
71
|
if (flagKey == null || flagKey === '') {
|
|
74
72
|
throw new server_sdk_1.FlagNotFoundError('flagKey must be a non-empty string');
|
|
75
73
|
}
|
|
76
|
-
await this.
|
|
77
|
-
const { treatment: value, config } = await this.client.getTreatmentWithConfig(consumer.
|
|
78
|
-
if (value === CONTROL_TREATMENT) {
|
|
79
|
-
throw new server_sdk_1.FlagNotFoundError(CONTROL_VALUE_ERROR_MESSAGE);
|
|
74
|
+
await (0, readiness_1.waitUntilReady)(this.client, this.events, this.metadata.name);
|
|
75
|
+
const { treatment: value, config } = await this.client.getTreatmentWithConfig(consumer.targetingKey, flagKey, consumer.attributes);
|
|
76
|
+
if (value === types_1.CONTROL_TREATMENT) {
|
|
77
|
+
throw new server_sdk_1.FlagNotFoundError(types_1.CONTROL_VALUE_ERROR_MESSAGE);
|
|
80
78
|
}
|
|
81
|
-
const flagMetadata = { config: config
|
|
82
|
-
|
|
83
|
-
value
|
|
79
|
+
const flagMetadata = Object.freeze({ config: config ?? '' });
|
|
80
|
+
return {
|
|
81
|
+
value,
|
|
84
82
|
variant: value,
|
|
85
|
-
flagMetadata
|
|
83
|
+
flagMetadata,
|
|
86
84
|
reason: server_sdk_1.StandardResolutionReasons.TARGETING_MATCH,
|
|
87
85
|
};
|
|
88
|
-
return details;
|
|
89
86
|
}
|
|
90
87
|
async track(trackingEventName, context, details) {
|
|
91
|
-
|
|
92
|
-
const { targetingKey } = context;
|
|
93
|
-
if (targetingKey == null || targetingKey === '')
|
|
94
|
-
throw new server_sdk_1.TargetingKeyMissingError('Missing targetingKey, required to track');
|
|
95
|
-
// eventName is always required
|
|
96
|
-
if (trackingEventName == null || trackingEventName === '')
|
|
88
|
+
if (trackingEventName == null || trackingEventName === '') {
|
|
97
89
|
throw new server_sdk_1.ParseError('Missing eventName, required to track');
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (trafficType == null || trafficType === '')
|
|
104
|
-
throw new server_sdk_1.InvalidContextError('Missing trafficType variable, required to track');
|
|
90
|
+
}
|
|
91
|
+
const { targetingKey, trafficType } = (0, context_1.transformContext)(context, this.trafficType);
|
|
92
|
+
if (targetingKey == null || targetingKey === '') {
|
|
93
|
+
throw new server_sdk_1.TargetingKeyMissingError('Missing targetingKey, required to track');
|
|
94
|
+
}
|
|
105
95
|
let value;
|
|
106
96
|
let properties = {};
|
|
107
97
|
if (details != null) {
|
|
@@ -117,40 +107,5 @@ class OpenFeatureSplitProvider {
|
|
|
117
107
|
async onClose() {
|
|
118
108
|
return this.client.destroy();
|
|
119
109
|
}
|
|
120
|
-
//Transform the context into an object useful for the Split API, an key string with arbitrary Split 'Attributes'.
|
|
121
|
-
transformContext(context) {
|
|
122
|
-
const { targetingKey, ...attributes } = context;
|
|
123
|
-
return {
|
|
124
|
-
key: targetingKey,
|
|
125
|
-
// Stringify context objects include date.
|
|
126
|
-
attributes: JSON.parse(JSON.stringify(attributes)),
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
parseValidNumber(stringValue) {
|
|
130
|
-
if (stringValue === undefined) {
|
|
131
|
-
throw new server_sdk_1.ParseError(`Invalid 'undefined' value.`);
|
|
132
|
-
}
|
|
133
|
-
const result = Number.parseFloat(stringValue);
|
|
134
|
-
if (Number.isNaN(result)) {
|
|
135
|
-
throw new server_sdk_1.ParseError(`Invalid numeric value ${stringValue}`);
|
|
136
|
-
}
|
|
137
|
-
return result;
|
|
138
|
-
}
|
|
139
|
-
parseValidJsonObject(stringValue) {
|
|
140
|
-
if (stringValue === undefined) {
|
|
141
|
-
throw new server_sdk_1.ParseError(`Invalid 'undefined' JSON value.`);
|
|
142
|
-
}
|
|
143
|
-
// we may want to allow the parsing to be customized.
|
|
144
|
-
try {
|
|
145
|
-
const value = JSON.parse(stringValue);
|
|
146
|
-
if (typeof value !== 'object') {
|
|
147
|
-
throw new server_sdk_1.ParseError(`Flag value ${stringValue} had unexpected type ${typeof value}, expected "object"`);
|
|
148
|
-
}
|
|
149
|
-
return value;
|
|
150
|
-
}
|
|
151
|
-
catch (err) {
|
|
152
|
-
throw new server_sdk_1.ParseError(`Error parsing ${stringValue} as JSON, ${err}`);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
110
|
}
|
|
156
111
|
exports.OpenFeatureSplitProvider = OpenFeatureSplitProvider;
|