@featurevisor/sdk 0.12.1 → 0.14.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/CHANGELOG.md +22 -0
- package/README.md +86 -12
- package/dist/index.js +1 -1
- package/dist/index.js.gz +0 -0
- package/dist/index.js.map +1 -1
- package/jest.config.js +4 -0
- package/lib/client.d.ts +5 -0
- package/lib/client.js +20 -13
- package/lib/client.js.map +1 -1
- package/lib/createInstance.d.ts +8 -0
- package/lib/createInstance.js +51 -23
- package/lib/createInstance.js.map +1 -1
- package/lib/emitter.d.ts +12 -0
- package/lib/emitter.js +46 -0
- package/lib/emitter.js.map +1 -0
- package/package.json +48 -48
- package/src/client.ts +34 -13
- package/src/createInstance.spec.ts +87 -9
- package/src/createInstance.ts +79 -23
- package/src/emitter.ts +53 -0
package/src/createInstance.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DatafileContent, Attributes } from "@featurevisor/types";
|
|
2
2
|
import { FeaturevisorSDK, ConfigureBucketValue, ActivationCallback } from "./client";
|
|
3
3
|
import { createLogger, Logger } from "./logger";
|
|
4
|
+
import { Emitter } from "./emitter";
|
|
4
5
|
|
|
5
6
|
export type ReadyCallback = () => void;
|
|
6
7
|
|
|
@@ -21,6 +22,8 @@ export interface InstanceOptions {
|
|
|
21
22
|
onUpdate?: () => void;
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
export type Event = "ready" | "refresh" | "update" | "activation";
|
|
26
|
+
|
|
24
27
|
// @TODO: consider renaming it to FeaturevisorSDK in next breaking semver
|
|
25
28
|
export interface FeaturevisorInstance {
|
|
26
29
|
/**
|
|
@@ -54,9 +57,18 @@ export interface FeaturevisorInstance {
|
|
|
54
57
|
* Additions
|
|
55
58
|
*/
|
|
56
59
|
setLogLevels: Logger["setLevels"];
|
|
60
|
+
|
|
57
61
|
refresh: () => void;
|
|
58
62
|
startRefreshing: () => void;
|
|
59
63
|
stopRefreshing: () => void;
|
|
64
|
+
|
|
65
|
+
addListener: Emitter["addListener"];
|
|
66
|
+
on: Emitter["addListener"];
|
|
67
|
+
removeListener: Emitter["removeListener"];
|
|
68
|
+
off: Emitter["removeListener"];
|
|
69
|
+
removeAllListeners: Emitter["removeAllListeners"];
|
|
70
|
+
|
|
71
|
+
isReady: () => boolean;
|
|
60
72
|
}
|
|
61
73
|
|
|
62
74
|
function fetchDatafileContent(datafileUrl, options: InstanceOptions): Promise<DatafileContent> {
|
|
@@ -67,13 +79,26 @@ function fetchDatafileContent(datafileUrl, options: InstanceOptions): Promise<Da
|
|
|
67
79
|
return fetch(datafileUrl).then((res) => res.json());
|
|
68
80
|
}
|
|
69
81
|
|
|
82
|
+
interface Listeners {
|
|
83
|
+
[key: string]: Function[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface Statuses {
|
|
87
|
+
ready: boolean;
|
|
88
|
+
refreshInProgress: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
70
91
|
function getInstanceFromSdk(
|
|
71
92
|
sdk: FeaturevisorSDK,
|
|
72
93
|
options: InstanceOptions,
|
|
73
94
|
logger: Logger,
|
|
95
|
+
emitter: Emitter,
|
|
96
|
+
statuses: Statuses,
|
|
74
97
|
): FeaturevisorInstance {
|
|
75
98
|
let intervalId;
|
|
76
|
-
|
|
99
|
+
|
|
100
|
+
const on = emitter.addListener.bind(emitter);
|
|
101
|
+
const off = emitter.removeListener.bind(emitter);
|
|
77
102
|
|
|
78
103
|
const instance: FeaturevisorInstance = {
|
|
79
104
|
// variation
|
|
@@ -84,7 +109,7 @@ function getInstanceFromSdk(
|
|
|
84
109
|
getVariationString: sdk.getVariationString.bind(sdk),
|
|
85
110
|
|
|
86
111
|
// activate
|
|
87
|
-
activate: sdk.activate,
|
|
112
|
+
activate: sdk.activate.bind(sdk),
|
|
88
113
|
activateBoolean: sdk.activateBoolean.bind(sdk),
|
|
89
114
|
activateInteger: sdk.activateInteger.bind(sdk),
|
|
90
115
|
activateDouble: sdk.activateDouble.bind(sdk),
|
|
@@ -102,10 +127,18 @@ function getInstanceFromSdk(
|
|
|
102
127
|
// additions
|
|
103
128
|
setLogLevels: logger.setLevels.bind(logger),
|
|
104
129
|
|
|
130
|
+
// emitter
|
|
131
|
+
on: on,
|
|
132
|
+
addListener: on,
|
|
133
|
+
off: off,
|
|
134
|
+
removeListener: off,
|
|
135
|
+
removeAllListeners: emitter.removeAllListeners.bind(emitter),
|
|
136
|
+
|
|
137
|
+
// refresh
|
|
105
138
|
refresh() {
|
|
106
139
|
logger.debug("refreshing datafile");
|
|
107
140
|
|
|
108
|
-
if (refreshInProgress) {
|
|
141
|
+
if (statuses.refreshInProgress) {
|
|
109
142
|
return logger.warn("refresh in progress, skipping");
|
|
110
143
|
}
|
|
111
144
|
|
|
@@ -113,7 +146,7 @@ function getInstanceFromSdk(
|
|
|
113
146
|
return logger.error("cannot refresh since `datafileUrl` is not provided");
|
|
114
147
|
}
|
|
115
148
|
|
|
116
|
-
refreshInProgress = true;
|
|
149
|
+
statuses.refreshInProgress = true;
|
|
117
150
|
|
|
118
151
|
fetchDatafileContent(options.datafileUrl, options)
|
|
119
152
|
.then((datafile) => {
|
|
@@ -124,19 +157,17 @@ function getInstanceFromSdk(
|
|
|
124
157
|
sdk.setDatafile(datafile);
|
|
125
158
|
logger.info("refreshed datafile");
|
|
126
159
|
|
|
127
|
-
|
|
128
|
-
options.onRefresh();
|
|
129
|
-
}
|
|
160
|
+
emitter.emit("refresh");
|
|
130
161
|
|
|
131
|
-
if (isNotSameRevision
|
|
132
|
-
|
|
162
|
+
if (isNotSameRevision) {
|
|
163
|
+
emitter.emit("update");
|
|
133
164
|
}
|
|
134
165
|
|
|
135
|
-
refreshInProgress = false;
|
|
166
|
+
statuses.refreshInProgress = false;
|
|
136
167
|
})
|
|
137
168
|
.catch((e) => {
|
|
138
169
|
logger.error("failed to refresh datafile", { error: e });
|
|
139
|
-
refreshInProgress = false;
|
|
170
|
+
statuses.refreshInProgress = false;
|
|
140
171
|
});
|
|
141
172
|
},
|
|
142
173
|
|
|
@@ -165,6 +196,10 @@ function getInstanceFromSdk(
|
|
|
165
196
|
|
|
166
197
|
clearInterval(intervalId);
|
|
167
198
|
},
|
|
199
|
+
|
|
200
|
+
isReady() {
|
|
201
|
+
return statuses.ready;
|
|
202
|
+
},
|
|
168
203
|
};
|
|
169
204
|
|
|
170
205
|
if (options.datafileUrl && options.refreshInterval) {
|
|
@@ -190,11 +225,32 @@ export function createInstance(options: InstanceOptions) {
|
|
|
190
225
|
}
|
|
191
226
|
|
|
192
227
|
const logger = options.logger || createLogger();
|
|
228
|
+
const emitter = new Emitter();
|
|
229
|
+
const statuses: Statuses = {
|
|
230
|
+
ready: false,
|
|
231
|
+
refreshInProgress: false,
|
|
232
|
+
};
|
|
193
233
|
|
|
194
234
|
if (!options.datafileUrl && options.refreshInterval) {
|
|
195
235
|
logger.warn("refreshing datafile requires `datafileUrl` option");
|
|
196
236
|
}
|
|
197
237
|
|
|
238
|
+
if (options.onReady) {
|
|
239
|
+
emitter.addListener("ready", options.onReady);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (options.onActivation) {
|
|
243
|
+
emitter.addListener("activation", options.onActivation);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (options.onRefresh) {
|
|
247
|
+
emitter.addListener("refresh", options.onRefresh);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (options.onUpdate) {
|
|
251
|
+
emitter.addListener("update", options.onUpdate);
|
|
252
|
+
}
|
|
253
|
+
|
|
198
254
|
// datafile content is already provided
|
|
199
255
|
if (options.datafile) {
|
|
200
256
|
const sdk = new FeaturevisorSDK({
|
|
@@ -202,18 +258,17 @@ export function createInstance(options: InstanceOptions) {
|
|
|
202
258
|
onActivation: options.onActivation,
|
|
203
259
|
configureBucketValue: options.configureBucketValue,
|
|
204
260
|
logger,
|
|
261
|
+
emitter,
|
|
205
262
|
interceptAttributes: options.interceptAttributes,
|
|
263
|
+
fromInstance: true,
|
|
206
264
|
});
|
|
207
265
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
onReady();
|
|
213
|
-
}, 0);
|
|
214
|
-
}
|
|
266
|
+
statuses.ready = true;
|
|
267
|
+
setTimeout(function() {
|
|
268
|
+
emitter.emit("ready");
|
|
269
|
+
}, 0);
|
|
215
270
|
|
|
216
|
-
return getInstanceFromSdk(sdk, options, logger);
|
|
271
|
+
return getInstanceFromSdk(sdk, options, logger, emitter, statuses);
|
|
217
272
|
}
|
|
218
273
|
|
|
219
274
|
// datafile has to be fetched
|
|
@@ -222,7 +277,9 @@ export function createInstance(options: InstanceOptions) {
|
|
|
222
277
|
onActivation: options.onActivation,
|
|
223
278
|
configureBucketValue: options.configureBucketValue,
|
|
224
279
|
logger,
|
|
280
|
+
emitter,
|
|
225
281
|
interceptAttributes: options.interceptAttributes,
|
|
282
|
+
fromInstance: true,
|
|
226
283
|
});
|
|
227
284
|
|
|
228
285
|
if (options.datafileUrl) {
|
|
@@ -230,9 +287,8 @@ export function createInstance(options: InstanceOptions) {
|
|
|
230
287
|
.then((datafile) => {
|
|
231
288
|
sdk.setDatafile(datafile);
|
|
232
289
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
290
|
+
statuses.ready = true;
|
|
291
|
+
emitter.emit("ready");
|
|
236
292
|
})
|
|
237
293
|
.catch((e) => {
|
|
238
294
|
logger.error("failed to fetch datafile:");
|
|
@@ -240,5 +296,5 @@ export function createInstance(options: InstanceOptions) {
|
|
|
240
296
|
});
|
|
241
297
|
}
|
|
242
298
|
|
|
243
|
-
return getInstanceFromSdk(sdk, options, logger);
|
|
299
|
+
return getInstanceFromSdk(sdk, options, logger, emitter, statuses);
|
|
244
300
|
}
|
package/src/emitter.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type EventName = "ready" | "refresh" | "update" | "activation";
|
|
2
|
+
|
|
3
|
+
export interface Listeners {
|
|
4
|
+
[key: string]: Function[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class Emitter {
|
|
8
|
+
private _listeners: Listeners;
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
this._listeners = {};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public addListener(eventName: EventName, fn: Function): void {
|
|
15
|
+
if (typeof this._listeners[eventName] === "undefined") {
|
|
16
|
+
this._listeners[eventName] = [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
this._listeners[eventName].push(fn);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public removeListener(eventName: EventName, fn: Function): void {
|
|
23
|
+
if (typeof this._listeners[eventName] === "undefined") {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const index = this._listeners[eventName].indexOf(fn);
|
|
28
|
+
|
|
29
|
+
if (index !== -1) {
|
|
30
|
+
this._listeners[eventName].splice(index, 1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public removeAllListeners(eventName?: EventName): void {
|
|
35
|
+
if (eventName) {
|
|
36
|
+
this._listeners[eventName] = [];
|
|
37
|
+
} else {
|
|
38
|
+
Object.keys(this._listeners).forEach((key) => {
|
|
39
|
+
this._listeners[key] = [];
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public emit(eventName: EventName, ...args: any[]): void {
|
|
45
|
+
if (typeof this._listeners[eventName] === "undefined") {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this._listeners[eventName].forEach((fn) => {
|
|
50
|
+
fn(...args);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|