Skip to content

Commit e552b8f

Browse files
Merge pull request #14 from splitio/fme-10028
[FME-10028] support async clients and add events
2 parents 518e36d + a3f14d7 commit e552b8f

File tree

10 files changed

+221
-23
lines changed

10 files changed

+221
-23
lines changed

.github/workflows/test.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ jobs:
1515
steps:
1616
- name: Checkout code
1717
uses: actions/checkout@v5
18+
19+
- name: Install Redis
20+
run: |
21+
sudo add-apt-repository ppa:redislabs/redis
22+
sudo apt-get install -y redis-tools redis-server
23+
24+
- name: Check Redis
25+
run: redis-cli ping
1826

1927
- name: Setup Node.js
2028
uses: actions/setup-node@v4

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ npm-debug.log*
55
yarn-debug.log*
66
yarn-error.log*
77
lerna-debug.log*
8+
dump.rdb
89

910
# Diagnostic reports (https://nodejs.org/api/report.html)
1011
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

package-lock.json

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"globals": "^16.3.0",
5252
"jest": "^29.7.0",
5353
"jiti": "^2.5.1",
54+
"redis-server": "^1.2.2",
5455
"replace": "^1.2.1",
5556
"rimraf": "^3.0.2",
5657
"ts-jest": "^29.4.1",
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FLUSHDB
2+
DEL 'REDIS_NODE_UT.SPLITIO.segment.UT_SEGMENT'
3+
SADD 'REDIS_NODE_UT.SPLITIO.segment.UT_SEGMENT' UT_Segment_member
4+
SET 'REDIS_NODE_UT.SPLITIO.segment.UT_SEGMENT.till' 1492721958710
5+
SET 'REDIS_NODE_UT.SPLITIO.split.UT_IN_SEGMENT' '{"changeNumber":1492722104980,"trafficTypeName":"machine","name":"UT_IN_SEGMENT","seed":-202209840,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"","attribute":""},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"UT_SEGMENT"},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100}],"label":"whitelisted segment"},{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"machine","attribute":""},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":100}],"label":"in segment all"}]}'
6+
SET 'REDIS_NODE_UT.SPLITIO.split.UT_NOT_IN_SEGMENT' '{"changeNumber":1492722747908,"trafficTypeName":"machine","name":"UT_NOT_IN_SEGMENT","seed":-56653132,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"machine","attribute":""},"matcherType":"IN_SEGMENT","negate":true,"userDefinedSegmentMatcherData":{"segmentName":"UT_SEGMENT"},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"not in segment UT_SEGMENT"}]}'
7+
SET 'REDIS_NODE_UT.SPLITIO.split.UT_NOT_SET_MATCHER' '{"changeNumber":1492723024413,"trafficTypeName":"machine","name":"UT_NOT_SET_MATCHER","seed":-93553840,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"machine","attribute":"permissions"},"matcherType":"CONTAINS_ANY_OF_SET","negate":true,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":["create","delete","update"]},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"permissions does not contain any of [create, delete, ...]"}]}'
8+
SET 'REDIS_NODE_UT.SPLITIO.split.UT_SET_MATCHER' '{"changeNumber":1492722926004,"trafficTypeName":"machine","name":"UT_SET_MATCHER","seed":-1995997836,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"machine","attribute":"permissions"},"matcherType":"CONTAINS_ANY_OF_SET","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":["admin","premium","idol"]},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"permissions contains any of [admin, premium, ...]"}]}'
9+
SET 'REDIS_NODE_UT.SPLITIO.split.always-on' '{"changeNumber":1487277320548,"trafficTypeName":"user","name":"always-on","seed":1684183541,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":""},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"in segment all"}]}'
10+
SET 'REDIS_NODE_UT.SPLITIO.split.always-o.n-with-config' '{"changeNumber":1487277320548,"trafficTypeName":"user","name":"always-o.n-with-config","seed":1684183541,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":""},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"o.n","size":100},{"treatment":"off","size":0}],"label":"in segment all"}],"configurations":{"o.n":"{\"color\":\"brown\"}"}}'
11+
SET 'REDIS_NODE_UT.SPLITIO.splits.till' 1492723024413

src/__tests__/nodeSuites/client.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable jest/no-conditional-expect */
22
import { OpenFeatureSplitProvider } from '../../lib/js-split-provider';
3-
import { getSplitClient } from '../testUtils';
3+
import { getLocalHostSplitClient } from '../testUtils';
44

55
import { OpenFeature } from '@openfeature/server-sdk';
66

@@ -11,7 +11,7 @@ describe('client tests', () => {
1111
let provider;
1212

1313
beforeEach(() => {
14-
splitClient = getSplitClient();
14+
splitClient = getLocalHostSplitClient();
1515
provider = new OpenFeatureSplitProvider({ splitClient });
1616

1717
OpenFeature.setProvider(provider);
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import RedisServer from 'redis-server';
2+
import { exec } from 'child_process';
3+
import { OpenFeature } from '@openfeature/server-sdk';
4+
5+
import { getRedisSplitClient } from '../testUtils';
6+
import { OpenFeatureSplitProvider } from '../../lib/js-split-provider';
7+
8+
const redisPort = '6385';
9+
10+
/**
11+
* Initialize redis server and run a cli bash command to load redis with data to do the proper tests
12+
*/
13+
const startRedis = () => {
14+
// Simply pass the port that you want a Redis server to listen on.
15+
const server = new RedisServer(redisPort);
16+
17+
const promise = new Promise((resolve, reject) => {
18+
server
19+
.open()
20+
.then(() => {
21+
exec(`cat ./src/__tests__/mocks/redis-commands.txt | redis-cli -p ${redisPort}`, err => {
22+
if (err) {
23+
reject(server);
24+
// Node.js couldn't execute the command
25+
return;
26+
}
27+
resolve(server);
28+
});
29+
});
30+
});
31+
32+
return promise;
33+
};
34+
35+
let redisServer
36+
let splitClient
37+
38+
beforeAll(async () => {
39+
redisServer = await startRedis();
40+
}, 30000);
41+
42+
afterAll(async () => {
43+
await redisServer.close();
44+
await splitClient.destroy();
45+
});
46+
47+
describe('Regular usage - DEBUG strategy', () => {
48+
splitClient = getRedisSplitClient(redisPort);
49+
const provider = new OpenFeatureSplitProvider({ splitClient });
50+
51+
OpenFeature.setProviderAndWait(provider);
52+
const client = OpenFeature.getClient();
53+
54+
test('Evaluate always on flag', async () => {
55+
await client.getBooleanValue('always-on', false, {targetingKey: 'emma-ss'}).then(result => {
56+
expect(result).toBe(true);
57+
});
58+
});
59+
60+
test('Evaluate user in segment', async () => {
61+
await client.getBooleanValue('UT_IN_SEGMENT', false, {targetingKey: 'UT_Segment_member', properties: { /* empty properties are ignored */ }}).then(result => {
62+
expect(result).toBe(true);
63+
});
64+
65+
await client.getBooleanValue('UT_IN_SEGMENT', false, {targetingKey: 'UT_Segment_member', properties: { some: 'value1' } }).then(result => {
66+
expect(result).toBe(true);
67+
});
68+
69+
await client.getBooleanValue('UT_IN_SEGMENT', false, {targetingKey: 'other' }).then(result => {
70+
expect(result).toBe(false);
71+
});
72+
73+
await client.getBooleanValue('UT_NOT_IN_SEGMENT', true, {targetingKey: 'UT_Segment_member' }).then(result => {
74+
expect(result).toBe(false);
75+
});
76+
77+
await client.getBooleanValue('UT_NOT_IN_SEGMENT', true, {targetingKey: 'other' }).then(result => {
78+
expect(result).toBe(true);
79+
});
80+
81+
await client.getBooleanValue('UT_NOT_IN_SEGMENT', true, {targetingKey: 'other' }).then(result => {
82+
expect(result).toBe(true);
83+
});
84+
});
85+
86+
test('Evaluate with attributes set matcher', async () => {
87+
await client.getBooleanValue('UT_SET_MATCHER', false, {targetingKey: 'UT_Segment_member', permissions: ['admin'] }).then(result => {
88+
expect(result).toBe(true);
89+
});
90+
91+
await client.getBooleanValue('UT_SET_MATCHER', false, {targetingKey: 'UT_Segment_member', permissions: ['not_matching'] }).then(result => {
92+
expect(result).toBe(false);
93+
});
94+
95+
await client.getBooleanValue('UT_NOT_SET_MATCHER', true, {targetingKey: 'UT_Segment_member', permissions: ['create'] }).then(result => {
96+
expect(result).toBe(false);
97+
});
98+
99+
await client.getBooleanValue('UT_NOT_SET_MATCHER', true, {targetingKey: 'UT_Segment_member', permissions: ['not_matching'] }).then(result => {
100+
expect(result).toBe(true);
101+
});
102+
})
103+
104+
test('Evaluate with dynamic config', async () => {
105+
await client.getBooleanDetails('UT_NOT_SET_MATCHER', true, {targetingKey: 'UT_Segment_member', permissions: ['not_matching'] }).then(result => {
106+
expect(result.value).toBe(true);
107+
expect(result.flagMetadata).toEqual({'config': ''});
108+
});
109+
110+
await client.getStringDetails('always-o.n-with-config', 'control', {targetingKey: 'other'}).then(result => {
111+
expect(result.value).toBe('o.n');
112+
expect(result.flagMetadata).toEqual({config: '{"color":"brown"}'});
113+
});
114+
})
115+
});

src/__tests__/nodeSuites/provider.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable jest/no-conditional-expect */
2-
import { getSplitClient } from '../testUtils';
2+
import { getLocalHostSplitClient } from '../testUtils';
33
import { OpenFeatureSplitProvider } from '../../lib/js-split-provider';
44

55
describe('provider tests', () => {
@@ -8,7 +8,7 @@ describe('provider tests', () => {
88
let provider;
99

1010
beforeEach(() => {
11-
splitClient = getSplitClient();
11+
splitClient = getLocalHostSplitClient();
1212
provider = new OpenFeatureSplitProvider({ splitClient });
1313
});
1414

src/__tests__/testUtils/index.js

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,16 +70,40 @@ export function url(settings, target) {
7070
return `${settings.urls.sdk}${target}`;
7171
}
7272

73+
const getRedisConfig = (redisPort) => ({
74+
core: {
75+
authorizationKey: 'SOME SDK KEY' // in consumer mode, SDK key is only used to track and log warning regarding duplicated SDK instances
76+
},
77+
mode: 'consumer',
78+
storage: {
79+
type: 'REDIS',
80+
prefix: 'REDIS_NODE_UT',
81+
options: {
82+
url: `redis://localhost:${redisPort}/0`
83+
}
84+
},
85+
sync: {
86+
impressionsMode: 'DEBUG'
87+
},
88+
startup: {
89+
readyTimeout: 36000 // 10hs
90+
}
91+
});
7392

74-
/**
75-
* get a Split client in localhost mode for testing purposes
76-
*/
77-
export function getSplitClient(apiKey = 'localhost') {
78-
return SplitFactory({
93+
const config = {
7994
core: {
80-
authorizationKey: apiKey
95+
authorizationKey: 'localhost'
8196
},
8297
features: './split.yaml',
8398
debug: 'DEBUG'
84-
}).client();
99+
}
100+
/**
101+
* get a Split client in localhost mode for testing purposes
102+
*/
103+
export function getLocalHostSplitClient() {
104+
return SplitFactory(config).client();
85105
}
106+
107+
export function getRedisSplitClient(redisPort) {
108+
return SplitFactory(getRedisConfig(redisPort)).client();
109+
}

src/lib/js-split-provider.ts

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
import {
22
EvaluationContext,
3-
Provider,
4-
ResolutionDetails,
5-
ParseError,
63
FlagNotFoundError,
4+
InvalidContextError,
75
JsonValue,
8-
TargetingKeyMissingError,
6+
OpenFeatureEventEmitter,
7+
ParseError,
8+
Provider,
9+
ProviderEvents,
10+
ResolutionDetails,
911
StandardResolutionReasons,
10-
TrackingEventDetails,
11-
InvalidContextError,
12+
TargetingKeyMissingError,
13+
TrackingEventDetails
1214
} from '@openfeature/server-sdk';
15+
import { SplitFactory } from '@splitsoftware/splitio';
1316
import type SplitIO from '@splitsoftware/splitio/types/splitio';
1417

15-
export interface SplitProviderOptions {
16-
splitClient: SplitIO.IClient;
18+
type SplitProviderOptions = {
19+
splitClient: SplitIO.IClient | SplitIO.IAsyncClient;
1720
}
1821

1922
type Consumer = {
@@ -29,10 +32,21 @@ export class OpenFeatureSplitProvider implements Provider {
2932
name: 'split',
3033
};
3134
private initialized: Promise<void>;
32-
private client: SplitIO.IClient;
35+
private client: SplitIO.IClient | SplitIO.IAsyncClient;
36+
37+
public readonly events = new OpenFeatureEventEmitter();
3338

34-
constructor(options: SplitProviderOptions) {
35-
this.client = options.splitClient;
39+
constructor(options: SplitProviderOptions | string) {
40+
41+
if (typeof(options) === 'string') {
42+
const splitFactory = SplitFactory({core: { authorizationKey: options } });
43+
this.client = splitFactory.client();
44+
} else {
45+
this.client = options.splitClient;
46+
}
47+
this.client.on(this.client.Event.SDK_UPDATE, (payload) => {
48+
this.events.emit(ProviderEvents.ConfigurationChanged, payload)
49+
});
3650
this.initialized = new Promise((resolve) => {
3751
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3852
if ((this.client as any).__getStatus().isReady) {
@@ -121,7 +135,7 @@ export class OpenFeatureSplitProvider implements Provider {
121135
}
122136

123137
await this.initialized;
124-
const { treatment: value, config }: SplitIO.TreatmentWithConfig = this.client.getTreatmentWithConfig(
138+
const { treatment: value, config }: SplitIO.TreatmentWithConfig = await this.client.getTreatmentWithConfig(
125139
consumer.key,
126140
flagKey,
127141
consumer.attributes

0 commit comments

Comments
 (0)