@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.
@@ -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
- let refreshInProgress = false;
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
- if (typeof options.onRefresh === "function") {
128
- options.onRefresh();
129
- }
160
+ emitter.emit("refresh");
130
161
 
131
- if (isNotSameRevision && typeof options.onUpdate === "function") {
132
- options.onUpdate();
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
- if (typeof options.onReady === "function") {
209
- const onReady = options.onReady;
210
-
211
- setTimeout(function () {
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
- if (typeof options.onReady === "function") {
234
- options.onReady();
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
+ }