@featurevisor/sdk 1.35.3 → 2.0.1

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.
Files changed (86) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +2 -381
  3. package/coverage/clover.xml +707 -645
  4. package/coverage/coverage-final.json +11 -9
  5. package/coverage/lcov-report/{segments.ts.html → bucketer.ts.html} +155 -77
  6. package/coverage/lcov-report/child.ts.html +940 -0
  7. package/coverage/lcov-report/conditions.ts.html +107 -158
  8. package/coverage/lcov-report/datafileReader.ts.html +763 -103
  9. package/coverage/lcov-report/emitter.ts.html +77 -59
  10. package/coverage/lcov-report/evaluate.ts.html +689 -416
  11. package/coverage/lcov-report/events.ts.html +334 -0
  12. package/coverage/lcov-report/helpers.ts.html +184 -0
  13. package/coverage/lcov-report/{bucket.ts.html → hooks.ts.html} +86 -239
  14. package/coverage/lcov-report/index.html +119 -89
  15. package/coverage/lcov-report/instance.ts.html +341 -773
  16. package/coverage/lcov-report/logger.ts.html +64 -64
  17. package/coverage/lcov.info +1433 -1226
  18. package/dist/bucketer.d.ts +11 -0
  19. package/dist/child.d.ts +26 -0
  20. package/dist/compareVersions.d.ts +4 -0
  21. package/dist/conditions.d.ts +4 -4
  22. package/dist/datafileReader.d.ts +26 -6
  23. package/dist/emitter.d.ts +8 -9
  24. package/dist/evaluate.d.ts +31 -29
  25. package/dist/events.d.ts +5 -0
  26. package/dist/helpers.d.ts +5 -0
  27. package/dist/hooks.d.ts +45 -0
  28. package/dist/index.d.ts +3 -2
  29. package/dist/index.js +1 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/index.mjs +1 -1
  32. package/dist/index.mjs.gz +0 -0
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/instance.d.ts +40 -72
  35. package/dist/logger.d.ts +6 -5
  36. package/dist/murmurhash.d.ts +1 -0
  37. package/jest.config.js +2 -0
  38. package/lib/bucketer.d.ts +11 -0
  39. package/lib/child.d.ts +26 -0
  40. package/lib/compareVersions.d.ts +4 -0
  41. package/lib/conditions.d.ts +4 -4
  42. package/lib/datafileReader.d.ts +26 -6
  43. package/lib/emitter.d.ts +8 -9
  44. package/lib/evaluate.d.ts +31 -29
  45. package/lib/events.d.ts +5 -0
  46. package/lib/helpers.d.ts +5 -0
  47. package/lib/hooks.d.ts +45 -0
  48. package/lib/index.d.ts +3 -2
  49. package/lib/instance.d.ts +40 -72
  50. package/lib/logger.d.ts +6 -5
  51. package/lib/murmurhash.d.ts +1 -0
  52. package/package.json +3 -5
  53. package/src/bucketer.spec.ts +165 -0
  54. package/src/bucketer.ts +84 -0
  55. package/src/child.spec.ts +267 -0
  56. package/src/child.ts +285 -0
  57. package/src/compareVersions.ts +93 -0
  58. package/src/conditions.spec.ts +563 -353
  59. package/src/conditions.ts +46 -63
  60. package/src/datafileReader.spec.ts +396 -84
  61. package/src/datafileReader.ts +280 -60
  62. package/src/emitter.spec.ts +27 -86
  63. package/src/emitter.ts +38 -32
  64. package/src/evaluate.ts +349 -258
  65. package/src/events.spec.ts +154 -0
  66. package/src/events.ts +83 -0
  67. package/src/helpers.ts +33 -0
  68. package/src/hooks.ts +88 -0
  69. package/src/index.ts +3 -2
  70. package/src/instance.spec.ts +305 -489
  71. package/src/instance.ts +247 -391
  72. package/src/logger.spec.ts +212 -134
  73. package/src/logger.ts +36 -36
  74. package/src/murmurhash.ts +71 -0
  75. package/coverage/lcov-report/feature.ts.html +0 -508
  76. package/dist/bucket.d.ts +0 -30
  77. package/dist/feature.d.ts +0 -16
  78. package/dist/segments.d.ts +0 -5
  79. package/lib/bucket.d.ts +0 -30
  80. package/lib/feature.d.ts +0 -16
  81. package/lib/segments.d.ts +0 -5
  82. package/src/bucket.spec.ts +0 -37
  83. package/src/bucket.ts +0 -139
  84. package/src/feature.ts +0 -141
  85. package/src/segments.spec.ts +0 -468
  86. package/src/segments.ts +0 -58
@@ -1,71 +1,58 @@
1
- import {
1
+ import type {
2
2
  Feature,
3
3
  Segment,
4
- DatafileContentV1,
5
- DatafileContentV2,
6
- Attribute,
7
- AttributeKey,
4
+ DatafileContent,
8
5
  SegmentKey,
9
6
  FeatureKey,
7
+ Context,
8
+ Traffic,
9
+ Allocation,
10
+ GroupSegment,
11
+ Condition,
12
+ Force,
10
13
  } from "@featurevisor/types";
11
14
 
12
- export function parseJsonConditionsIfStringified<T>(record: T, key: string): T {
13
- if (typeof record[key] === "string" && record[key] !== "*") {
14
- try {
15
- record[key] = JSON.parse(record[key]);
16
- } catch (e) {
17
- console.error("Error parsing JSON", e);
18
- }
19
- }
15
+ import { conditionIsMatched } from "./conditions";
16
+ import { Logger } from "./logger";
17
+
18
+ export type GetRegex = (regexString: string, regexFlags: string) => RegExp;
19
+
20
+ export interface DatafileReaderOptions {
21
+ datafile: DatafileContent;
22
+ logger: Logger;
23
+ }
20
24
 
21
- return record;
25
+ export interface ForceResult {
26
+ force?: Force;
27
+ forceIndex?: number;
22
28
  }
23
29
 
24
30
  export class DatafileReader {
25
31
  private schemaVersion: string;
26
32
  private revision: string;
27
33
 
28
- private attributes: Record<AttributeKey, Attribute>;
29
34
  private segments: Record<SegmentKey, Segment>;
30
35
  private features: Record<FeatureKey, Feature>;
31
36
 
32
- constructor(datafileJson: DatafileContentV1 | DatafileContentV2) {
33
- this.schemaVersion = datafileJson.schemaVersion;
34
- this.revision = datafileJson.revision;
37
+ private logger: Logger;
35
38
 
36
- if (this.schemaVersion === "2") {
37
- // v2
38
- const datafileJsonV2 = datafileJson as DatafileContentV2;
39
+ // done to avoid creating new RegExp objects for the same regex string and flags.
40
+ // kept here to avoid memory leaks.
41
+ // if datafile is reset, this cache will be cleared.
42
+ private regexCache: Record<string, RegExp>;
39
43
 
40
- this.attributes = datafileJsonV2.attributes;
41
- this.segments = datafileJsonV2.segments;
42
- this.features = datafileJsonV2.features;
43
- } else {
44
- // v1
45
- const datafileJsonV1 = datafileJson as DatafileContentV1;
44
+ constructor(options: DatafileReaderOptions) {
45
+ const { datafile, logger } = options;
46
46
 
47
- this.attributes = {};
48
- datafileJsonV1.attributes.forEach((a) => {
49
- this.attributes[a.key] = a;
50
- });
47
+ this.logger = logger;
51
48
 
52
- this.segments = {};
53
- datafileJsonV1.segments.forEach((s) => {
54
- this.segments[s.key] = s;
55
- });
49
+ this.schemaVersion = datafile.schemaVersion;
50
+ this.revision = datafile.revision;
56
51
 
57
- this.features = {};
58
- datafileJsonV1.features.forEach((f) => {
59
- if (Array.isArray(f.variablesSchema)) {
60
- f.variablesSchema = f.variablesSchema.reduce((acc, variable) => {
61
- acc[variable.key] = variable;
62
- return acc;
63
- }, {});
64
- }
52
+ this.segments = datafile.segments;
53
+ this.features = datafile.features;
65
54
 
66
- this.features[f.key] = f;
67
- });
68
- }
55
+ this.regexCache = {};
69
56
  }
70
57
 
71
58
  getRevision(): string {
@@ -76,31 +63,264 @@ export class DatafileReader {
76
63
  return this.schemaVersion;
77
64
  }
78
65
 
79
- getAllAttributes(): Attribute[] {
80
- const result: Attribute[] = [];
66
+ getSegment(segmentKey: SegmentKey): Segment | undefined {
67
+ const segment = this.segments[segmentKey];
81
68
 
82
- Object.keys(this.attributes).forEach((key) => {
83
- result.push(this.attributes[key]);
84
- });
69
+ if (!segment) {
70
+ return undefined;
71
+ }
85
72
 
86
- return result;
73
+ segment.conditions = this.parseConditionsIfStringified(segment.conditions);
74
+
75
+ return segment;
87
76
  }
88
77
 
89
- getAttribute(attributeKey: AttributeKey): Attribute | undefined {
90
- return this.attributes[attributeKey];
78
+ getFeatureKeys(): string[] {
79
+ return Object.keys(this.features);
91
80
  }
92
81
 
93
- getSegment(segmentKey: SegmentKey): Segment | undefined {
94
- const segment = this.segments[segmentKey];
82
+ getFeature(featureKey: FeatureKey): Feature | undefined {
83
+ return this.features[featureKey];
84
+ }
95
85
 
96
- if (!segment) {
86
+ getVariableKeys(featureKey: FeatureKey): string[] {
87
+ const feature = this.getFeature(featureKey);
88
+
89
+ if (!feature) {
90
+ return [];
91
+ }
92
+
93
+ return Object.keys(feature.variablesSchema || {});
94
+ }
95
+
96
+ hasVariations(featureKey: FeatureKey): boolean {
97
+ const feature = this.getFeature(featureKey);
98
+
99
+ if (!feature) {
100
+ return false;
101
+ }
102
+
103
+ return Array.isArray(feature.variations) && feature.variations.length > 0;
104
+ }
105
+
106
+ getRegex(regexString: string, regexFlags?: string): RegExp {
107
+ const flags = regexFlags || "";
108
+ const cacheKey = `${regexString}-${flags}`;
109
+
110
+ if (this.regexCache[cacheKey]) {
111
+ return this.regexCache[cacheKey];
112
+ }
113
+
114
+ const regex = new RegExp(regexString, flags);
115
+ this.regexCache[cacheKey] = regex;
116
+
117
+ return regex;
118
+ }
119
+
120
+ allConditionsAreMatched(conditions: Condition[] | Condition, context: Context): boolean {
121
+ if (typeof conditions === "string") {
122
+ if (conditions === "*") {
123
+ return true;
124
+ }
125
+
126
+ return false;
127
+ }
128
+
129
+ const getRegex = (regexString: string, regexFlags: string) =>
130
+ this.getRegex(regexString, regexFlags);
131
+
132
+ if ("attribute" in conditions) {
133
+ try {
134
+ return conditionIsMatched(conditions, context, getRegex);
135
+ } catch (e) {
136
+ this.logger.warn(e.message, {
137
+ error: e,
138
+ details: {
139
+ condition: conditions,
140
+ context,
141
+ },
142
+ });
143
+
144
+ return false;
145
+ }
146
+ }
147
+
148
+ if ("and" in conditions && Array.isArray(conditions.and)) {
149
+ return conditions.and.every((c) => this.allConditionsAreMatched(c, context));
150
+ }
151
+
152
+ if ("or" in conditions && Array.isArray(conditions.or)) {
153
+ return conditions.or.some((c) => this.allConditionsAreMatched(c, context));
154
+ }
155
+
156
+ if ("not" in conditions && Array.isArray(conditions.not)) {
157
+ return conditions.not.every(
158
+ () =>
159
+ this.allConditionsAreMatched(
160
+ {
161
+ and: conditions.not,
162
+ },
163
+ context,
164
+ ) === false,
165
+ );
166
+ }
167
+
168
+ if (Array.isArray(conditions)) {
169
+ return conditions.every((c) => this.allConditionsAreMatched(c, context));
170
+ }
171
+
172
+ return false;
173
+ }
174
+
175
+ segmentIsMatched(segment: Segment, context: Context): boolean {
176
+ return this.allConditionsAreMatched(segment.conditions as Condition | Condition[], context);
177
+ }
178
+
179
+ allSegmentsAreMatched(
180
+ groupSegments: GroupSegment | GroupSegment[] | "*",
181
+ context: Context,
182
+ ): boolean {
183
+ if (groupSegments === "*") {
184
+ return true;
185
+ }
186
+
187
+ if (typeof groupSegments === "string") {
188
+ const segment = this.getSegment(groupSegments);
189
+
190
+ if (segment) {
191
+ return this.segmentIsMatched(segment, context);
192
+ }
193
+
194
+ return false;
195
+ }
196
+
197
+ if (typeof groupSegments === "object") {
198
+ if ("and" in groupSegments && Array.isArray(groupSegments.and)) {
199
+ return groupSegments.and.every((groupSegment) =>
200
+ this.allSegmentsAreMatched(groupSegment, context),
201
+ );
202
+ }
203
+
204
+ if ("or" in groupSegments && Array.isArray(groupSegments.or)) {
205
+ return groupSegments.or.some((groupSegment) =>
206
+ this.allSegmentsAreMatched(groupSegment, context),
207
+ );
208
+ }
209
+
210
+ if ("not" in groupSegments && Array.isArray(groupSegments.not)) {
211
+ return groupSegments.not.every(
212
+ (groupSegment) => this.allSegmentsAreMatched(groupSegment, context) === false,
213
+ );
214
+ }
215
+ }
216
+
217
+ if (Array.isArray(groupSegments)) {
218
+ return groupSegments.every((groupSegment) =>
219
+ this.allSegmentsAreMatched(groupSegment, context),
220
+ );
221
+ }
222
+
223
+ return false;
224
+ }
225
+
226
+ getMatchedTraffic(traffic: Traffic[], context: Context): Traffic | undefined {
227
+ return traffic.find((t) => {
228
+ if (!this.allSegmentsAreMatched(this.parseSegmentsIfStringified(t.segments), context)) {
229
+ return false;
230
+ }
231
+
232
+ return true;
233
+ });
234
+ }
235
+
236
+ getMatchedAllocation(traffic: Traffic, bucketValue: number): Allocation | undefined {
237
+ if (!traffic.allocation) {
97
238
  return undefined;
98
239
  }
99
240
 
100
- return parseJsonConditionsIfStringified(segment, "conditions");
241
+ for (const allocation of traffic.allocation) {
242
+ const [start, end] = allocation.range;
243
+
244
+ if (allocation.range && start <= bucketValue && end >= bucketValue) {
245
+ return allocation;
246
+ }
247
+ }
248
+
249
+ return undefined;
101
250
  }
102
251
 
103
- getFeature(featureKey: FeatureKey): Feature | undefined {
104
- return this.features[featureKey];
252
+ getMatchedForce(featureKey: FeatureKey | Feature, context: Context): ForceResult {
253
+ const result: ForceResult = {
254
+ force: undefined,
255
+ forceIndex: undefined,
256
+ };
257
+
258
+ const feature = typeof featureKey === "string" ? this.getFeature(featureKey) : featureKey;
259
+
260
+ if (!feature || !feature.force) {
261
+ return result;
262
+ }
263
+
264
+ for (let i = 0; i < feature.force.length; i++) {
265
+ const currentForce = feature.force[i];
266
+
267
+ if (
268
+ currentForce.conditions &&
269
+ this.allConditionsAreMatched(
270
+ this.parseConditionsIfStringified(currentForce.conditions),
271
+ context,
272
+ )
273
+ ) {
274
+ result.force = currentForce;
275
+ result.forceIndex = i;
276
+ break;
277
+ }
278
+
279
+ if (
280
+ currentForce.segments &&
281
+ this.allSegmentsAreMatched(this.parseSegmentsIfStringified(currentForce.segments), context)
282
+ ) {
283
+ result.force = currentForce;
284
+ result.forceIndex = i;
285
+ break;
286
+ }
287
+ }
288
+
289
+ return result;
290
+ }
291
+
292
+ parseConditionsIfStringified(conditions: Condition | Condition[]): Condition | Condition[] {
293
+ if (typeof conditions !== "string") {
294
+ // already parsed
295
+ return conditions;
296
+ }
297
+
298
+ if (conditions === "*") {
299
+ // everyone
300
+ return conditions;
301
+ }
302
+
303
+ try {
304
+ return JSON.parse(conditions);
305
+ } catch (e) {
306
+ this.logger.error("Error parsing conditions", {
307
+ error: e,
308
+ details: {
309
+ conditions,
310
+ },
311
+ });
312
+
313
+ return conditions;
314
+ }
315
+ }
316
+
317
+ parseSegmentsIfStringified(
318
+ segments: GroupSegment | GroupSegment[],
319
+ ): GroupSegment | GroupSegment[] {
320
+ if (typeof segments === "string" && (segments.startsWith("{") || segments.startsWith("["))) {
321
+ return JSON.parse(segments);
322
+ }
323
+
324
+ return segments;
105
325
  }
106
326
  }
@@ -1,100 +1,41 @@
1
- import { Emitter } from "./emitter";
1
+ import { Emitter, EventDetails } from "./emitter";
2
2
 
3
- describe("sdk :: Emitter", () => {
3
+ describe("Emitter", () => {
4
4
  let emitter: Emitter;
5
+ let handledDetails: EventDetails[] = [];
6
+
7
+ function handleDetails(details: EventDetails) {
8
+ handledDetails.push(details);
9
+ }
5
10
 
6
11
  beforeEach(() => {
7
12
  emitter = new Emitter();
13
+ handledDetails = [];
8
14
  });
9
15
 
10
- describe("addListener", () => {
11
- it("should add a listener", () => {
12
- const fn = jest.fn();
13
-
14
- emitter.addListener("ready", fn);
15
-
16
- expect(emitter["_listeners"]["ready"]).toEqual([fn]);
17
- });
18
-
19
- it("should add multiple listeners", () => {
20
- const fn1 = jest.fn();
21
- const fn2 = jest.fn();
22
-
23
- emitter.addListener("ready", fn1);
24
- emitter.addListener("ready", fn2);
25
-
26
- expect(emitter["_listeners"]["ready"]).toEqual([fn1, fn2]);
27
- });
28
-
29
- it("should add multiple listeners for different events", () => {
30
- const fn1 = jest.fn();
31
- const fn2 = jest.fn();
32
-
33
- emitter.addListener("ready", fn1);
34
- emitter.addListener("update", fn2);
35
-
36
- expect(emitter["_listeners"]["ready"]).toEqual([fn1]);
37
- expect(emitter["_listeners"]["update"]).toEqual([fn2]);
38
- });
39
- });
40
-
41
- describe("removeListener", () => {
42
- it("should remove a listener", () => {
43
- const fn = jest.fn();
44
-
45
- emitter.addListener("ready", fn);
46
- emitter.removeListener("ready", fn);
47
- emitter.removeListener("update", fn);
48
-
49
- expect(emitter["_listeners"]["ready"]).toEqual([]);
50
- });
51
-
52
- it("should remove multiple listeners", () => {
53
- const fn1 = jest.fn();
54
- const fn2 = jest.fn();
55
-
56
- emitter.addListener("ready", fn1);
57
- emitter.addListener("ready", fn2);
58
- emitter.removeListener("ready", fn1);
59
- emitter.removeListener("ready", fn2);
60
-
61
- expect(emitter["_listeners"]["ready"]).toEqual([]);
62
- });
63
-
64
- it("should remove multiple listeners for different events", () => {
65
- const fn1 = jest.fn();
66
- const fn2 = jest.fn();
67
-
68
- emitter.addListener("ready", fn1);
69
- emitter.addListener("update", fn2);
70
- emitter.removeListener("ready", fn1);
71
- emitter.removeListener("update", fn2);
72
-
73
- expect(emitter["_listeners"]["ready"]).toEqual([]);
74
- expect(emitter["_listeners"]["update"]).toEqual([]);
75
- });
76
- });
16
+ it("should add a listener for an event", function () {
17
+ const unsubscribe = emitter.on("datafile_set", handleDetails);
77
18
 
78
- describe("removeAllListeners", () => {
79
- it("should remove all listeners", () => {
80
- const fn1 = jest.fn();
81
- const fn2 = jest.fn();
19
+ expect(emitter.listeners["datafile_set"]).toContain(handleDetails);
20
+ expect(emitter.listeners["datafile_changed"]).toBeUndefined();
21
+ expect(emitter.listeners["context_set"]).toBeUndefined();
22
+ expect(emitter.listeners["datafile_set"].length).toBe(1);
82
23
 
83
- emitter.addListener("ready", fn1);
84
- emitter.addListener("update", fn2);
85
- emitter.removeAllListeners();
86
- });
24
+ // trigger already subscribed event
25
+ emitter.trigger("datafile_set", { key: "value" });
26
+ expect(handledDetails.length).toBe(1);
27
+ expect(handledDetails[0]).toEqual({ key: "value" });
87
28
 
88
- it("should remove all listeners for a specific event", () => {
89
- const fn1 = jest.fn();
90
- const fn2 = jest.fn();
29
+ // trigger unsubscribed event
30
+ emitter.trigger("sticky_set", { key: "value2" });
31
+ expect(handledDetails.length).toBe(1);
91
32
 
92
- emitter.addListener("ready", fn1);
93
- emitter.addListener("update", fn2);
94
- emitter.removeAllListeners("ready");
33
+ // unsubscribe
34
+ unsubscribe();
35
+ expect(emitter.listeners["datafile_set"].length).toBe(0);
95
36
 
96
- expect(emitter["_listeners"]["ready"]).toEqual([]);
97
- expect(emitter["_listeners"]["update"]).toEqual([fn2]);
98
- });
37
+ // clear all
38
+ emitter.clearAll();
39
+ expect(emitter.listeners).toEqual({});
99
40
  });
100
41
  });
package/src/emitter.ts CHANGED
@@ -1,53 +1,59 @@
1
- export type EventName = "ready" | "refresh" | "update" | "activation";
1
+ export type EventName = "datafile_set" | "context_set" | "sticky_set";
2
2
 
3
- export interface Listeners {
4
- [key: string]: Function[];
5
- }
3
+ export type EventDetails = Record<string, unknown>;
4
+
5
+ export type EventCallback = (details: EventDetails) => void;
6
+
7
+ export type Listeners = Record<EventName, EventCallback[]> | {}; // eslint-disable-line
6
8
 
7
9
  export class Emitter {
8
- private _listeners: Listeners;
10
+ listeners: Listeners;
9
11
 
10
12
  constructor() {
11
- this._listeners = {};
13
+ this.listeners = {};
12
14
  }
13
15
 
14
- public addListener(eventName: EventName, fn: Function): void {
15
- if (typeof this._listeners[eventName] === "undefined") {
16
- this._listeners[eventName] = [];
16
+ on(eventName: EventName, callback: EventCallback) {
17
+ if (!this.listeners[eventName]) {
18
+ this.listeners[eventName] = [];
17
19
  }
18
20
 
19
- this._listeners[eventName].push(fn);
20
- }
21
+ const listeners = this.listeners[eventName];
22
+ listeners.push(callback);
21
23
 
22
- public removeListener(eventName: EventName, fn: Function): void {
23
- if (typeof this._listeners[eventName] === "undefined") {
24
- return;
25
- }
24
+ let isActive = true;
26
25
 
27
- const index = this._listeners[eventName].indexOf(fn);
26
+ return function unsubscribe() {
27
+ if (!isActive) {
28
+ return;
29
+ }
28
30
 
29
- if (index !== -1) {
30
- this._listeners[eventName].splice(index, 1);
31
- }
32
- }
31
+ isActive = false;
33
32
 
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
- }
33
+ const index = listeners.indexOf(callback);
34
+ if (index !== -1) {
35
+ listeners.splice(index, 1);
36
+ }
37
+ };
42
38
  }
43
39
 
44
- public emit(eventName: EventName, ...args: any[]): void {
45
- if (typeof this._listeners[eventName] === "undefined") {
40
+ trigger(eventName: EventName, details: EventDetails = {}) {
41
+ const listeners = this.listeners[eventName];
42
+
43
+ if (!listeners) {
46
44
  return;
47
45
  }
48
46
 
49
- this._listeners[eventName].forEach((fn) => {
50
- fn(...args);
47
+ listeners.forEach(function (listener) {
48
+ try {
49
+ listener(details);
50
+ } catch (err) {
51
+ console.error(err);
52
+ }
51
53
  });
52
54
  }
55
+
56
+ clearAll() {
57
+ this.listeners = {};
58
+ }
53
59
  }