Skip to content

Commit ecd76dd

Browse files
Merge pull request #23 from drone/FFM-1500
[FFM-1500] ability to listen on ff events
2 parents a618c69 + 531205c commit ecd76dd

File tree

14 files changed

+262
-26
lines changed

14 files changed

+262
-26
lines changed

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,46 @@ client
158158
});
159159
```
160160

161+
## Listening on events
162+
163+
You can listen on these events:
164+
165+
- `Event.READY` - SDK successfully initialized
166+
- `Event.FAILED` - SDK throws an error
167+
- `Event.CHANGED` - any new version of flag or segment triggers this event, if segment is changed then it will find all flags with segment match operator
168+
169+
Methods:
170+
171+
```typescript
172+
on(Event.READY, () => {
173+
console.log('READY');
174+
});
175+
176+
on(Event.FAILED, () => {
177+
console.log('FAILED');
178+
});
179+
180+
on(Event.CHANGED, (identifier) => {
181+
console.log('Changed', identifier);
182+
});
183+
```
184+
185+
and if you want to remove the `functionReference` listener for `Event.READY`:
186+
187+
```
188+
off(Event.READY, functionReference);
189+
```
190+
191+
or if you want to remove all listeners on `Event.READY`:
192+
193+
```
194+
off(Event.READY);
195+
```
196+
197+
or if you call `off()` without params it will close the client.
198+
199+
> All events are applicable to off() function.
200+
161201
## License
162202

163203
Licensed under the APLv2.

example/cf_reactive.mjs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import CfClient, { Event } from '@harnessio/ff-nodejs-server-sdk';
2+
3+
CfClient.init('1c100d25-4c3f-487b-b198-3b3d01df5794');
4+
5+
CfClient.on(Event.READY, () => {
6+
console.log('READY');
7+
});
8+
9+
CfClient.on(Event.FAILED, (error) => {
10+
console.log('FAILED with err:', error);
11+
});
12+
13+
CfClient.on(Event.CHANGED, (identifier) => {
14+
console.log('Changed', identifier);
15+
});
16+
17+
console.log('Starting application');
18+
19+
setInterval(async () => {
20+
const value = await CfClient.boolVariation('test', null, false);
21+
console.log('Evaluation for flag test and target none: ', value);
22+
}, 10000);
23+
24+
console.log('Application started');

example/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

example/package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44
"description": "NodeJS Server SDK sample",
55
"main": "index.js",
66
"scripts": {
7-
"cf": "node index_cf.mjs",
8-
"client": "node index.mjs",
9-
"start": "node index.js",
7+
"cf": "node --enable-source-maps index_cf.mjs",
8+
"cf_reactive": "node --enable-source-maps cf_reactive.mjs",
9+
"reactive": "node --enable-source-maps reactive.js",
10+
"client": "node --enable-source-maps index.mjs",
11+
"wait": "node --enable-source-maps wait.js",
12+
"start": "node --enable-source-maps index.js",
1013
"test": "echo \"Error: no test specified\" && exit 1"
1114
},
1215
"author": "[email protected]",

example/reactive.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const { Client, Event } = require('@harnessio/ff-nodejs-server-sdk');
2+
3+
const client = new Client('1c100d25-4c3f-487b-b198-3b3d01df5794', {
4+
enableStream: false,
5+
});
6+
7+
client.on(Event.READY, () => {
8+
console.log('READY');
9+
});
10+
11+
client.on(Event.FAILED, () => {
12+
console.log('FAILED');
13+
});
14+
15+
client.on(Event.CHANGED, (identifier) => {
16+
if (identifier === 'test') {
17+
console.log('test flag changed');
18+
}
19+
});
20+
21+
console.log('Starting application');
22+
23+
setInterval(async () => {
24+
const target = {
25+
identifier: 'harness',
26+
};
27+
const value = await client.boolVariation('test', target, false);
28+
console.log('Evaluation for flag test and target: ', value, target);
29+
}, 10000);
30+
31+
console.log('Application started');

example/wait.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const { Client } = require('@harnessio/ff-nodejs-server-sdk');
2+
3+
console.log('Starting application');
4+
const client = new Client('1c100d25-4c3f-487b-b198-3b3d01df5794');
5+
client
6+
.waitForInitialization()
7+
.then(() => {
8+
setInterval(async () => {
9+
const target = {
10+
identifier: 'harness',
11+
};
12+
const value = await client.boolVariation('test', target, false);
13+
console.log('Evaluation for flag test and target: ', value, target);
14+
}, 10000);
15+
16+
console.log('Application started');
17+
})
18+
.catch((error) => {
19+
console.log('Error', error);
20+
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@harnessio/ff-nodejs-server-sdk",
3-
"version": "1.1.1",
3+
"version": "1.2.0",
44
"description": "Feature flags SDK for NodeJS environments",
55
"main": "dist/cjs/index.js",
66
"module": "dist/esm/index.js",

src/cache.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,16 @@ export class SimpleCache implements KeyValueStore {
66
set(key: string, value: unknown): void {
77
this.cache[key] = value;
88
}
9+
910
get(key: string): unknown {
1011
return this.cache[key];
1112
}
13+
1214
del(key: string): void {
1315
delete this.cache[key];
1416
}
17+
18+
keys(): string[] {
19+
return Object.keys(this.cache);
20+
}
1521
}

src/client.ts

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as events from 'events';
1+
import EventEmitter from 'events';
22
import jwt_decode from 'jwt-decode';
33
import axios from 'axios';
44
import axiosRetry from 'axios-retry';
@@ -9,7 +9,7 @@ import { PollerEvent, PollingProcessor } from './polling';
99
import { StreamProcessor } from './streaming';
1010
import { Evaluator } from './evaluator';
1111
import { defaultOptions } from './constants';
12-
import { Repository, StorageRepository } from './repository';
12+
import { Repository, RepositoryEvent, StorageRepository } from './repository';
1313
import {
1414
MetricEvent,
1515
MetricsProcessor,
@@ -26,7 +26,7 @@ enum Processor {
2626
METRICS,
2727
}
2828

29-
export enum ClientEvent {
29+
export enum Event {
3030
READY = 'ready',
3131
FAILED = 'failed',
3232
CHANGED = 'changed',
@@ -41,7 +41,7 @@ export default class Client {
4141
private configuration: Configuration;
4242
private options: Options;
4343
private cluster = '1';
44-
private eventBus = new events.EventEmitter();
44+
private eventBus = new EventEmitter();
4545
private pollProcessor: PollingProcessor;
4646
private streamProcessor: StreamProcessor;
4747
private metricsProcessor: MetricsProcessorInterface;
@@ -72,6 +72,7 @@ export default class Client {
7272
this.repository = new StorageRepository(
7373
this.options.cache,
7474
this.options.store,
75+
this.eventBus,
7576
);
7677
this.evaluator = new Evaluator(this.repository);
7778
this.api = new ClientApi(this.configuration);
@@ -86,7 +87,7 @@ export default class Client {
8687

8788
this.eventBus.on(PollerEvent.ERROR, () => {
8889
this.failure = true;
89-
this.eventBus.emit(ClientEvent.FAILED);
90+
this.eventBus.emit(Event.FAILED);
9091
});
9192

9293
this.eventBus.on(StreamEvent.READY, () => {
@@ -95,7 +96,7 @@ export default class Client {
9596

9697
this.eventBus.on(StreamEvent.ERROR, () => {
9798
this.failure = true;
98-
this.eventBus.emit(ClientEvent.FAILED);
99+
this.eventBus.emit(Event.FAILED);
99100
});
100101

101102
this.eventBus.on(MetricEvent.READY, () => {
@@ -104,7 +105,7 @@ export default class Client {
104105

105106
this.eventBus.on(MetricEvent.ERROR, () => {
106107
this.failure = true;
107-
this.eventBus.emit(ClientEvent.FAILED);
108+
this.eventBus.emit(Event.FAILED);
108109
});
109110

110111
this.eventBus.on(StreamEvent.CONNECTED, () => {
@@ -114,6 +115,47 @@ export default class Client {
114115
this.eventBus.on(StreamEvent.DISCONNECTED, () => {
115116
this.pollProcessor.start();
116117
});
118+
119+
for (const event of Object.values(RepositoryEvent)) {
120+
this.eventBus.on(event, (identifier) => {
121+
switch (event) {
122+
case RepositoryEvent.FLAG_STORED:
123+
case RepositoryEvent.FLAG_DELETED:
124+
this.eventBus.emit(Event.CHANGED, identifier);
125+
break;
126+
case RepositoryEvent.SEGMENT_STORED:
127+
case RepositoryEvent.SEGMENT_DELETED:
128+
// find all flags where segment match and emit the event
129+
this.repository
130+
.findFlagsBySegment(identifier)
131+
.then((values: string[]) => {
132+
values.forEach((value) =>
133+
this.eventBus.emit(Event.CHANGED, value),
134+
);
135+
});
136+
break;
137+
}
138+
});
139+
}
140+
}
141+
142+
on(event: Event, callback: (...args: unknown[]) => void): void {
143+
const arrayObjects = [];
144+
145+
for (const value of Object.values(Event)) {
146+
arrayObjects.push(value);
147+
}
148+
if (arrayObjects.includes(event)) {
149+
this.eventBus.on(event, callback);
150+
}
151+
}
152+
153+
off(event?: string, callback?: () => void): void {
154+
if (event) {
155+
this.eventBus.off(event, callback);
156+
} else {
157+
this.close();
158+
}
117159
}
118160

119161
private async authenticate(): Promise<void> {
@@ -145,10 +187,10 @@ export default class Client {
145187
this.waitForInitialize = Promise.reject(this.failure);
146188
} else {
147189
this.waitForInitialize = new Promise((resolve, reject) => {
148-
this.eventBus.once(ClientEvent.READY, () => {
190+
this.eventBus.once(Event.READY, () => {
149191
resolve(this);
150192
});
151-
this.eventBus.once(ClientEvent.FAILED, reject);
193+
this.eventBus.once(Event.FAILED, reject);
152194
});
153195
}
154196
return this.waitForInitialize;
@@ -183,7 +225,7 @@ export default class Client {
183225
}
184226

185227
this.initialized = true;
186-
this.eventBus.emit(ClientEvent.READY);
228+
this.eventBus.emit(Event.READY);
187229
}
188230

189231
private async run(): Promise<void> {
@@ -304,5 +346,6 @@ export default class Client {
304346
if (this.metricsProcessor) {
305347
this.metricsProcessor.close();
306348
}
349+
this.eventBus.removeAllListeners();
307350
}
308351
}

src/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import Client, { ClientEvent } from './client';
1+
import Client, { Event } from './client';
22
import LRU from 'lru-cache';
33
import { Options, Target } from './types';
44
import { Logger } from './log';
@@ -7,7 +7,7 @@ import { FileStore } from './store';
77

88
export {
99
Client,
10-
ClientEvent,
10+
Event,
1111
Options,
1212
Target,
1313
AsyncKeyValueStore,
@@ -54,6 +54,12 @@ export default {
5454
): Promise<Record<string, unknown>> {
5555
return this.instance.jsonVariation(identifier, target, defaultValue);
5656
},
57+
on: function (event: Event, callback: (...args: unknown[]) => void): void {
58+
this.instance.on(event, callback);
59+
},
60+
off: function (event?: Event, callback?: () => void): void {
61+
this.instance.off(event, callback);
62+
},
5763
close: function (): void {
5864
return this.instance.close();
5965
},

0 commit comments

Comments
 (0)