@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
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { EvaluationContext } from '@openfeature/server-sdk';
|
|
2
|
+
import type { Consumer } from './types';
|
|
3
|
+
import { DEFAULT_TRAFFIC_TYPE } from './types';
|
|
4
|
+
|
|
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
|
+
export function transformContext(
|
|
10
|
+
context: EvaluationContext,
|
|
11
|
+
defaultTrafficType: string = DEFAULT_TRAFFIC_TYPE
|
|
12
|
+
): Consumer {
|
|
13
|
+
const { targetingKey, trafficType: ttVal, ...attributes } = context;
|
|
14
|
+
const trafficType =
|
|
15
|
+
ttVal != null && typeof ttVal === 'string' && ttVal.trim() !== ''
|
|
16
|
+
? ttVal
|
|
17
|
+
: defaultTrafficType;
|
|
18
|
+
return {
|
|
19
|
+
targetingKey,
|
|
20
|
+
trafficType,
|
|
21
|
+
attributes: JSON.parse(JSON.stringify(attributes)),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
EvaluationContext,
|
|
3
|
+
EventDetails,
|
|
3
4
|
FlagNotFoundError,
|
|
4
|
-
InvalidContextError,
|
|
5
5
|
JsonValue,
|
|
6
|
+
Logger,
|
|
6
7
|
OpenFeatureEventEmitter,
|
|
7
8
|
ParseError,
|
|
8
9
|
Provider,
|
|
@@ -12,157 +13,159 @@ import {
|
|
|
12
13
|
TargetingKeyMissingError,
|
|
13
14
|
TrackingEventDetails
|
|
14
15
|
} from '@openfeature/server-sdk';
|
|
15
|
-
import { SplitFactory } from '@splitsoftware/splitio';
|
|
16
16
|
import type SplitIO from '@splitsoftware/splitio/types/splitio';
|
|
17
|
+
import { getSplitClient } from './client-resolver';
|
|
18
|
+
import { transformContext } from './context';
|
|
19
|
+
import { parseValidJsonObject, parseValidNumber } from './parsers';
|
|
20
|
+
import { attachReadyEventHandlers, waitUntilReady } from './readiness';
|
|
21
|
+
import {
|
|
22
|
+
CONTROL_TREATMENT,
|
|
23
|
+
CONTROL_VALUE_ERROR_MESSAGE,
|
|
24
|
+
DEFAULT_TRAFFIC_TYPE,
|
|
25
|
+
PROVIDER_NAME,
|
|
26
|
+
SplitProviderConstructorOptions,
|
|
27
|
+
type Consumer
|
|
28
|
+
} from './types';
|
|
17
29
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
type Consumer = {
|
|
23
|
-
key: string | undefined;
|
|
24
|
-
attributes: SplitIO.Attributes;
|
|
25
|
-
};
|
|
30
|
+
export class OpenFeatureSplitProvider implements Provider {
|
|
31
|
+
readonly metadata = {
|
|
32
|
+
name: PROVIDER_NAME,
|
|
33
|
+
} as const;
|
|
26
34
|
|
|
27
|
-
|
|
28
|
-
const CONTROL_TREATMENT = 'control';
|
|
35
|
+
readonly runsOn = 'server' as const;
|
|
29
36
|
|
|
30
|
-
export class OpenFeatureSplitProvider implements Provider {
|
|
31
|
-
metadata = {
|
|
32
|
-
name: 'split',
|
|
33
|
-
};
|
|
34
|
-
private initialized: Promise<void>;
|
|
35
37
|
private client: SplitIO.IClient | SplitIO.IAsyncClient;
|
|
36
|
-
|
|
38
|
+
private trafficType: string;
|
|
37
39
|
public readonly events = new OpenFeatureEventEmitter();
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
this.events.emit(ProviderEvents.ConfigurationChanged)
|
|
61
|
-
});
|
|
62
|
-
this.initialized = new Promise((resolve) => {
|
|
63
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
-
if ((this.client as any).__getStatus().isReady) {
|
|
65
|
-
console.log(`${this.metadata.name} provider initialized`);
|
|
66
|
-
resolve();
|
|
67
|
-
} else {
|
|
68
|
-
this.client.on(this.client.Event.SDK_READY, () => {
|
|
69
|
-
console.log(`${this.metadata.name} provider initialized`);
|
|
70
|
-
resolve();
|
|
71
|
-
});
|
|
41
|
+
constructor(
|
|
42
|
+
options: SplitProviderConstructorOptions
|
|
43
|
+
) {
|
|
44
|
+
this.trafficType = DEFAULT_TRAFFIC_TYPE;
|
|
45
|
+
this.client = getSplitClient(options);
|
|
46
|
+
|
|
47
|
+
attachReadyEventHandlers(this.client, this.events, this.metadata.name);
|
|
48
|
+
|
|
49
|
+
this.client.on(
|
|
50
|
+
this.client.Event.SDK_UPDATE,
|
|
51
|
+
(updateMetadata: SplitIO.SdkUpdateMetadata) => {
|
|
52
|
+
const eventDetails: EventDetails = {
|
|
53
|
+
providerName: this.metadata.name,
|
|
54
|
+
...(updateMetadata
|
|
55
|
+
? {
|
|
56
|
+
metadata: { type: updateMetadata.type },
|
|
57
|
+
flagsChanged: updateMetadata.names || [],
|
|
58
|
+
}
|
|
59
|
+
: {}),
|
|
60
|
+
};
|
|
61
|
+
this.events.emit(ProviderEvents.ConfigurationChanged, eventDetails);
|
|
72
62
|
}
|
|
73
|
-
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Called by the SDK after the provider is set. Waits for the Split client to be ready.
|
|
68
|
+
* When this promise resolves, the SDK emits ProviderEvents.Ready.
|
|
69
|
+
*/
|
|
70
|
+
async initialize(_context?: EvaluationContext): Promise<void> {
|
|
71
|
+
void _context;
|
|
72
|
+
await waitUntilReady(this.client, this.events, this.metadata.name);
|
|
74
73
|
}
|
|
75
74
|
|
|
76
|
-
async resolveBooleanEvaluation(
|
|
75
|
+
public async resolveBooleanEvaluation(
|
|
77
76
|
flagKey: string,
|
|
78
77
|
_: boolean,
|
|
79
|
-
context: EvaluationContext
|
|
78
|
+
context: EvaluationContext,
|
|
79
|
+
logger: Logger
|
|
80
80
|
): Promise<ResolutionDetails<boolean>> {
|
|
81
|
+
void logger;
|
|
81
82
|
const details = await this.evaluateTreatment(
|
|
82
83
|
flagKey,
|
|
83
|
-
|
|
84
|
+
transformContext(context, this.trafficType)
|
|
84
85
|
);
|
|
85
86
|
const treatment = details.value.toLowerCase();
|
|
86
87
|
|
|
87
|
-
if (
|
|
88
|
+
if (treatment === 'on' || treatment === 'true') {
|
|
88
89
|
return { ...details, value: true };
|
|
89
90
|
}
|
|
90
|
-
|
|
91
|
-
if ( treatment === 'off' || treatment === 'false' ) {
|
|
91
|
+
if (treatment === 'off' || treatment === 'false') {
|
|
92
92
|
return { ...details, value: false };
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
throw new ParseError(`Invalid boolean value for ${treatment}`);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
async resolveStringEvaluation(
|
|
98
|
+
public async resolveStringEvaluation(
|
|
99
99
|
flagKey: string,
|
|
100
100
|
_: string,
|
|
101
|
-
context: EvaluationContext
|
|
101
|
+
context: EvaluationContext,
|
|
102
|
+
logger: Logger
|
|
102
103
|
): Promise<ResolutionDetails<string>> {
|
|
103
|
-
|
|
104
|
+
void logger;
|
|
105
|
+
return this.evaluateTreatment(
|
|
104
106
|
flagKey,
|
|
105
|
-
|
|
107
|
+
transformContext(context, this.trafficType)
|
|
106
108
|
);
|
|
107
|
-
return details;
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
async resolveNumberEvaluation(
|
|
111
|
+
public async resolveNumberEvaluation(
|
|
111
112
|
flagKey: string,
|
|
112
113
|
_: number,
|
|
113
|
-
context: EvaluationContext
|
|
114
|
+
context: EvaluationContext,
|
|
115
|
+
logger: Logger
|
|
114
116
|
): Promise<ResolutionDetails<number>> {
|
|
117
|
+
void logger;
|
|
115
118
|
const details = await this.evaluateTreatment(
|
|
116
119
|
flagKey,
|
|
117
|
-
|
|
120
|
+
transformContext(context, this.trafficType)
|
|
118
121
|
);
|
|
119
|
-
return { ...details, value:
|
|
122
|
+
return { ...details, value: parseValidNumber(details.value) };
|
|
120
123
|
}
|
|
121
124
|
|
|
122
|
-
async resolveObjectEvaluation<U extends JsonValue>(
|
|
125
|
+
public async resolveObjectEvaluation<U extends JsonValue>(
|
|
123
126
|
flagKey: string,
|
|
124
127
|
_: U,
|
|
125
|
-
context: EvaluationContext
|
|
128
|
+
context: EvaluationContext,
|
|
129
|
+
logger: Logger
|
|
126
130
|
): Promise<ResolutionDetails<U>> {
|
|
131
|
+
void logger;
|
|
127
132
|
const details = await this.evaluateTreatment(
|
|
128
133
|
flagKey,
|
|
129
|
-
|
|
134
|
+
transformContext(context, this.trafficType)
|
|
130
135
|
);
|
|
131
|
-
return { ...details, value:
|
|
136
|
+
return { ...details, value: parseValidJsonObject<U>(details.value) };
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
private async evaluateTreatment(
|
|
135
140
|
flagKey: string,
|
|
136
141
|
consumer: Consumer
|
|
137
142
|
): Promise<ResolutionDetails<string>> {
|
|
138
|
-
if (!consumer.
|
|
143
|
+
if (!consumer.targetingKey) {
|
|
139
144
|
throw new TargetingKeyMissingError(
|
|
140
145
|
'The Split provider requires a targeting key.'
|
|
141
146
|
);
|
|
142
147
|
}
|
|
143
148
|
if (flagKey == null || flagKey === '') {
|
|
144
|
-
throw new FlagNotFoundError(
|
|
145
|
-
'flagKey must be a non-empty string'
|
|
146
|
-
);
|
|
149
|
+
throw new FlagNotFoundError('flagKey must be a non-empty string');
|
|
147
150
|
}
|
|
148
151
|
|
|
149
|
-
await this.
|
|
150
|
-
const { treatment: value, config }: SplitIO.TreatmentWithConfig =
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
await waitUntilReady(this.client, this.events, this.metadata.name);
|
|
153
|
+
const { treatment: value, config }: SplitIO.TreatmentWithConfig =
|
|
154
|
+
await this.client.getTreatmentWithConfig(
|
|
155
|
+
consumer.targetingKey,
|
|
156
|
+
flagKey,
|
|
157
|
+
consumer.attributes
|
|
158
|
+
);
|
|
155
159
|
if (value === CONTROL_TREATMENT) {
|
|
156
160
|
throw new FlagNotFoundError(CONTROL_VALUE_ERROR_MESSAGE);
|
|
157
161
|
}
|
|
158
|
-
const flagMetadata = { config: config
|
|
159
|
-
|
|
160
|
-
value
|
|
162
|
+
const flagMetadata = Object.freeze({ config: config ?? '' });
|
|
163
|
+
return {
|
|
164
|
+
value,
|
|
161
165
|
variant: value,
|
|
162
|
-
flagMetadata
|
|
166
|
+
flagMetadata,
|
|
163
167
|
reason: StandardResolutionReasons.TARGETING_MATCH,
|
|
164
168
|
};
|
|
165
|
-
return details;
|
|
166
169
|
}
|
|
167
170
|
|
|
168
171
|
async track(
|
|
@@ -170,26 +173,21 @@ export class OpenFeatureSplitProvider implements Provider {
|
|
|
170
173
|
context: EvaluationContext,
|
|
171
174
|
details: TrackingEventDetails
|
|
172
175
|
): Promise<void> {
|
|
173
|
-
|
|
174
|
-
// targetingKey is always required
|
|
175
|
-
const { targetingKey } = context;
|
|
176
|
-
if (targetingKey == null || targetingKey === '')
|
|
177
|
-
throw new TargetingKeyMissingError('Missing targetingKey, required to track');
|
|
178
|
-
|
|
179
|
-
// eventName is always required
|
|
180
|
-
if (trackingEventName == null || trackingEventName === '')
|
|
176
|
+
if (trackingEventName == null || trackingEventName === '') {
|
|
181
177
|
throw new ParseError('Missing eventName, required to track');
|
|
178
|
+
}
|
|
182
179
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
180
|
+
const { targetingKey, trafficType } = transformContext(
|
|
181
|
+
context,
|
|
182
|
+
this.trafficType
|
|
183
|
+
);
|
|
184
|
+
if (targetingKey == null || targetingKey === '') {
|
|
185
|
+
throw new TargetingKeyMissingError(
|
|
186
|
+
'Missing targetingKey, required to track'
|
|
187
|
+
);
|
|
188
|
+
}
|
|
191
189
|
|
|
192
|
-
let value;
|
|
190
|
+
let value: number | undefined;
|
|
193
191
|
let properties: SplitIO.Properties = {};
|
|
194
192
|
if (details != null) {
|
|
195
193
|
if (details.value != null) {
|
|
@@ -198,53 +196,18 @@ export class OpenFeatureSplitProvider implements Provider {
|
|
|
198
196
|
if (details.properties != null) {
|
|
199
197
|
properties = details.properties as SplitIO.Properties;
|
|
200
198
|
}
|
|
201
|
-
}
|
|
199
|
+
}
|
|
202
200
|
|
|
203
|
-
this.client.track(
|
|
201
|
+
this.client.track(
|
|
202
|
+
targetingKey,
|
|
203
|
+
trafficType,
|
|
204
|
+
trackingEventName,
|
|
205
|
+
value,
|
|
206
|
+
properties
|
|
207
|
+
);
|
|
204
208
|
}
|
|
205
209
|
|
|
206
|
-
async onClose?(): Promise<void> {
|
|
210
|
+
public async onClose?(): Promise<void> {
|
|
207
211
|
return this.client.destroy();
|
|
208
212
|
}
|
|
209
|
-
|
|
210
|
-
//Transform the context into an object useful for the Split API, an key string with arbitrary Split 'Attributes'.
|
|
211
|
-
private transformContext(context: EvaluationContext): Consumer {
|
|
212
|
-
const { targetingKey, ...attributes } = context;
|
|
213
|
-
return {
|
|
214
|
-
key: targetingKey,
|
|
215
|
-
// Stringify context objects include date.
|
|
216
|
-
attributes: JSON.parse(JSON.stringify(attributes)),
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
private parseValidNumber(stringValue: string | undefined) {
|
|
221
|
-
if (stringValue === undefined) {
|
|
222
|
-
throw new ParseError(`Invalid 'undefined' value.`);
|
|
223
|
-
}
|
|
224
|
-
const result = Number.parseFloat(stringValue);
|
|
225
|
-
if (Number.isNaN(result)) {
|
|
226
|
-
throw new ParseError(`Invalid numeric value ${stringValue}`);
|
|
227
|
-
}
|
|
228
|
-
return result;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
private parseValidJsonObject<T extends JsonValue>(
|
|
232
|
-
stringValue: string | undefined
|
|
233
|
-
): T {
|
|
234
|
-
if (stringValue === undefined) {
|
|
235
|
-
throw new ParseError(`Invalid 'undefined' JSON value.`);
|
|
236
|
-
}
|
|
237
|
-
// we may want to allow the parsing to be customized.
|
|
238
|
-
try {
|
|
239
|
-
const value = JSON.parse(stringValue);
|
|
240
|
-
if (typeof value !== 'object') {
|
|
241
|
-
throw new ParseError(
|
|
242
|
-
`Flag value ${stringValue} had unexpected type ${typeof value}, expected "object"`
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
return value;
|
|
246
|
-
} catch (err) {
|
|
247
|
-
throw new ParseError(`Error parsing ${stringValue} as JSON, ${err}`);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ParseError } from '@openfeature/server-sdk';
|
|
2
|
+
import type { JsonValue } from '@openfeature/server-sdk';
|
|
3
|
+
|
|
4
|
+
export function parseValidNumber(stringValue: string | undefined): number {
|
|
5
|
+
if (stringValue === undefined) {
|
|
6
|
+
throw new ParseError(`Invalid 'undefined' value.`);
|
|
7
|
+
}
|
|
8
|
+
const result = Number.parseFloat(stringValue);
|
|
9
|
+
if (Number.isNaN(result)) {
|
|
10
|
+
throw new ParseError(`Invalid numeric value ${stringValue}`);
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function parseValidJsonObject<T extends JsonValue>(
|
|
16
|
+
stringValue: string | undefined
|
|
17
|
+
): T {
|
|
18
|
+
if (stringValue === undefined) {
|
|
19
|
+
throw new ParseError(`Invalid 'undefined' JSON value.`);
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const value = JSON.parse(stringValue);
|
|
23
|
+
if (typeof value !== 'object') {
|
|
24
|
+
throw new ParseError(
|
|
25
|
+
`Flag value ${stringValue} had unexpected type ${typeof value}, expected "object"`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
} catch (err) {
|
|
30
|
+
throw new ParseError(`Error parsing ${stringValue} as JSON, ${err}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { OpenFeatureEventEmitter } from '@openfeature/server-sdk';
|
|
2
|
+
import { ProviderEvents } from '@openfeature/server-sdk';
|
|
3
|
+
import type SplitIO from '@splitsoftware/splitio/types/splitio';
|
|
4
|
+
import { PROVIDER_NAME } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Builds OpenFeature Ready event details including Split SDK ready metadata.
|
|
8
|
+
* Handles Split SDK not passing metadata (e.g. in consumer/Redis mode).
|
|
9
|
+
*/
|
|
10
|
+
function buildReadyEventDetails(
|
|
11
|
+
providerName: string,
|
|
12
|
+
splitMetadata: SplitIO.SdkReadyMetadata | undefined,
|
|
13
|
+
readyFromCache: boolean
|
|
14
|
+
) {
|
|
15
|
+
const metadata: Record<string, string | boolean | number> = {
|
|
16
|
+
readyFromCache,
|
|
17
|
+
initialCacheLoad: splitMetadata?.initialCacheLoad ?? false,
|
|
18
|
+
};
|
|
19
|
+
if (splitMetadata?.lastUpdateTimestamp != null) {
|
|
20
|
+
metadata.lastUpdateTimestamp = splitMetadata.lastUpdateTimestamp;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
providerName: providerName || PROVIDER_NAME,
|
|
24
|
+
metadata,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Registers Split SDK_READY and SDK_READY_FROM_CACHE listeners and forwards them
|
|
30
|
+
* as OpenFeature ProviderEvents.Ready with event metadata (initialCacheLoad, lastUpdateTimestamp, readyFromCache).
|
|
31
|
+
* If the client is already ready when attaching (e.g. localhost or reused client), emits Ready once
|
|
32
|
+
* with best-effort metadata so handlers always receive at least one Ready when the client is ready.
|
|
33
|
+
*/
|
|
34
|
+
export function attachReadyEventHandlers(
|
|
35
|
+
client: SplitIO.IClient | SplitIO.IAsyncClient,
|
|
36
|
+
events: OpenFeatureEventEmitter,
|
|
37
|
+
providerName: string = PROVIDER_NAME
|
|
38
|
+
): void {
|
|
39
|
+
client.on(
|
|
40
|
+
client.Event.SDK_READY_FROM_CACHE,
|
|
41
|
+
(splitMetadata: SplitIO.SdkReadyMetadata) => {
|
|
42
|
+
events.emit(
|
|
43
|
+
ProviderEvents.Ready,
|
|
44
|
+
buildReadyEventDetails(providerName, splitMetadata, true)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
client.on(client.Event.SDK_READY, (splitMetadata: SplitIO.SdkReadyMetadata) => {
|
|
49
|
+
events.emit(
|
|
50
|
+
ProviderEvents.Ready,
|
|
51
|
+
buildReadyEventDetails(providerName, splitMetadata, false)
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const status = client.getStatus();
|
|
56
|
+
if (status.isReady) {
|
|
57
|
+
events.emit(
|
|
58
|
+
ProviderEvents.Ready,
|
|
59
|
+
buildReadyEventDetails(providerName, undefined, false)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Returns a promise that resolves when the Split client is ready (SDK_READY),
|
|
66
|
+
* or rejects if the client has timed out (SDK_READY_TIMED_OUT).
|
|
67
|
+
* Used to gate evaluations until the SDK has synchronized with the backend.
|
|
68
|
+
*/
|
|
69
|
+
export function waitUntilReady(
|
|
70
|
+
client: SplitIO.IClient | SplitIO.IAsyncClient,
|
|
71
|
+
events: OpenFeatureEventEmitter,
|
|
72
|
+
providerName: string = PROVIDER_NAME
|
|
73
|
+
): Promise<void> {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const status = client.getStatus();
|
|
76
|
+
if (status.isReady) {
|
|
77
|
+
emitReadyEvent(client, events, providerName);
|
|
78
|
+
resolve();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (status.hasTimedout) {
|
|
82
|
+
reject();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
client.on(client.Event.SDK_READY_TIMED_OUT, reject);
|
|
86
|
+
client.on(client.Event.SDK_READY, () => {
|
|
87
|
+
emitReadyEvent(client, events, providerName);
|
|
88
|
+
resolve();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function emitReadyEvent(
|
|
94
|
+
client: SplitIO.IClient | SplitIO.IAsyncClient,
|
|
95
|
+
events: OpenFeatureEventEmitter,
|
|
96
|
+
providerName: string = PROVIDER_NAME
|
|
97
|
+
): void {
|
|
98
|
+
events.emit(
|
|
99
|
+
ProviderEvents.Ready,
|
|
100
|
+
buildReadyEventDetails(providerName, undefined, false)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
package/src/lib/types.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type SplitIO from '@splitsoftware/splitio/types/splitio';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Options when providing an existing Split client to the provider.
|
|
5
|
+
*/
|
|
6
|
+
export type SplitProviderOptions = {
|
|
7
|
+
splitClient: SplitIO.IClient | SplitIO.IAsyncClient;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Consumer representation used for Split API calls:
|
|
12
|
+
* targeting key, traffic type, and attributes.
|
|
13
|
+
*/
|
|
14
|
+
export type Consumer = {
|
|
15
|
+
targetingKey: string | undefined;
|
|
16
|
+
trafficType: string;
|
|
17
|
+
attributes: SplitIO.Attributes;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Union of all constructor argument types for the Split OpenFeature provider.
|
|
22
|
+
*/
|
|
23
|
+
export type SplitProviderConstructorOptions =
|
|
24
|
+
| SplitProviderOptions
|
|
25
|
+
| string
|
|
26
|
+
| SplitIO.ISDK
|
|
27
|
+
| SplitIO.IAsyncSDK;
|
|
28
|
+
|
|
29
|
+
export const CONTROL_TREATMENT = 'control';
|
|
30
|
+
export const CONTROL_VALUE_ERROR_MESSAGE = 'Received the "control" value from Split.';
|
|
31
|
+
export const DEFAULT_TRAFFIC_TYPE = 'user';
|
|
32
|
+
export const PROVIDER_NAME = 'split';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type SplitIO from '@splitsoftware/splitio/types/splitio';
|
|
2
|
+
import type { SplitProviderConstructorOptions } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the Split client from the various supported constructor option shapes.
|
|
5
|
+
* Supports: API key (string), Split SDK/AsyncSDK (factory), or pre-built splitClient.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getSplitClient(options: SplitProviderConstructorOptions): SplitIO.IClient | SplitIO.IAsyncClient;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { EvaluationContext } from '@openfeature/server-sdk';
|
|
2
|
+
import type { Consumer } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Transforms OpenFeature evaluation context into the consumer shape used by the Split API:
|
|
5
|
+
* targeting key, traffic type (with default), and remaining attributes.
|
|
6
|
+
*/
|
|
7
|
+
export declare function transformContext(context: EvaluationContext, defaultTrafficType?: string): Consumer;
|
|
@@ -1,26 +1,24 @@
|
|
|
1
|
-
import { EvaluationContext, JsonValue, OpenFeatureEventEmitter, Provider, ResolutionDetails, TrackingEventDetails } from '@openfeature/server-sdk';
|
|
2
|
-
import
|
|
3
|
-
type SplitProviderOptions = {
|
|
4
|
-
splitClient: SplitIO.IClient | SplitIO.IAsyncClient;
|
|
5
|
-
};
|
|
1
|
+
import { EvaluationContext, JsonValue, Logger, OpenFeatureEventEmitter, Provider, ResolutionDetails, TrackingEventDetails } from '@openfeature/server-sdk';
|
|
2
|
+
import { SplitProviderConstructorOptions } from './types';
|
|
6
3
|
export declare class OpenFeatureSplitProvider implements Provider {
|
|
7
|
-
metadata: {
|
|
8
|
-
name:
|
|
4
|
+
readonly metadata: {
|
|
5
|
+
readonly name: "split";
|
|
9
6
|
};
|
|
10
|
-
|
|
7
|
+
readonly runsOn: "server";
|
|
11
8
|
private client;
|
|
9
|
+
private trafficType;
|
|
12
10
|
readonly events: OpenFeatureEventEmitter;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
constructor(options: SplitProviderConstructorOptions);
|
|
12
|
+
/**
|
|
13
|
+
* Called by the SDK after the provider is set. Waits for the Split client to be ready.
|
|
14
|
+
* When this promise resolves, the SDK emits ProviderEvents.Ready.
|
|
15
|
+
*/
|
|
16
|
+
initialize(_context?: EvaluationContext): Promise<void>;
|
|
17
|
+
resolveBooleanEvaluation(flagKey: string, _: boolean, context: EvaluationContext, logger: Logger): Promise<ResolutionDetails<boolean>>;
|
|
18
|
+
resolveStringEvaluation(flagKey: string, _: string, context: EvaluationContext, logger: Logger): Promise<ResolutionDetails<string>>;
|
|
19
|
+
resolveNumberEvaluation(flagKey: string, _: number, context: EvaluationContext, logger: Logger): Promise<ResolutionDetails<number>>;
|
|
20
|
+
resolveObjectEvaluation<U extends JsonValue>(flagKey: string, _: U, context: EvaluationContext, logger: Logger): Promise<ResolutionDetails<U>>;
|
|
19
21
|
private evaluateTreatment;
|
|
20
22
|
track(trackingEventName: string, context: EvaluationContext, details: TrackingEventDetails): Promise<void>;
|
|
21
23
|
onClose?(): Promise<void>;
|
|
22
|
-
private transformContext;
|
|
23
|
-
private parseValidNumber;
|
|
24
|
-
private parseValidJsonObject;
|
|
25
24
|
}
|
|
26
|
-
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { OpenFeatureEventEmitter } from '@openfeature/server-sdk';
|
|
2
|
+
import type SplitIO from '@splitsoftware/splitio/types/splitio';
|
|
3
|
+
/**
|
|
4
|
+
* Registers Split SDK_READY and SDK_READY_FROM_CACHE listeners and forwards them
|
|
5
|
+
* as OpenFeature ProviderEvents.Ready with event metadata (initialCacheLoad, lastUpdateTimestamp, readyFromCache).
|
|
6
|
+
* If the client is already ready when attaching (e.g. localhost or reused client), emits Ready once
|
|
7
|
+
* with best-effort metadata so handlers always receive at least one Ready when the client is ready.
|
|
8
|
+
*/
|
|
9
|
+
export declare function attachReadyEventHandlers(client: SplitIO.IClient | SplitIO.IAsyncClient, events: OpenFeatureEventEmitter, providerName?: string): void;
|
|
10
|
+
/**
|
|
11
|
+
* Returns a promise that resolves when the Split client is ready (SDK_READY),
|
|
12
|
+
* or rejects if the client has timed out (SDK_READY_TIMED_OUT).
|
|
13
|
+
* Used to gate evaluations until the SDK has synchronized with the backend.
|
|
14
|
+
*/
|
|
15
|
+
export declare function waitUntilReady(client: SplitIO.IClient | SplitIO.IAsyncClient, events: OpenFeatureEventEmitter, providerName?: string): Promise<void>;
|
|
16
|
+
export declare function emitReadyEvent(client: SplitIO.IClient | SplitIO.IAsyncClient, events: OpenFeatureEventEmitter, providerName?: string): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type SplitIO from '@splitsoftware/splitio/types/splitio';
|
|
2
|
+
/**
|
|
3
|
+
* Options when providing an existing Split client to the provider.
|
|
4
|
+
*/
|
|
5
|
+
export type SplitProviderOptions = {
|
|
6
|
+
splitClient: SplitIO.IClient | SplitIO.IAsyncClient;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Consumer representation used for Split API calls:
|
|
10
|
+
* targeting key, traffic type, and attributes.
|
|
11
|
+
*/
|
|
12
|
+
export type Consumer = {
|
|
13
|
+
targetingKey: string | undefined;
|
|
14
|
+
trafficType: string;
|
|
15
|
+
attributes: SplitIO.Attributes;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Union of all constructor argument types for the Split OpenFeature provider.
|
|
19
|
+
*/
|
|
20
|
+
export type SplitProviderConstructorOptions = SplitProviderOptions | string | SplitIO.ISDK | SplitIO.IAsyncSDK;
|
|
21
|
+
export declare const CONTROL_TREATMENT = "control";
|
|
22
|
+
export declare const CONTROL_VALUE_ERROR_MESSAGE = "Received the \"control\" value from Split.";
|
|
23
|
+
export declare const DEFAULT_TRAFFIC_TYPE = "user";
|
|
24
|
+
export declare const PROVIDER_NAME = "split";
|