@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.
- package/CHANGELOG.md +8 -0
- package/README.md +2 -381
- package/coverage/clover.xml +707 -645
- package/coverage/coverage-final.json +11 -9
- package/coverage/lcov-report/{segments.ts.html → bucketer.ts.html} +155 -77
- package/coverage/lcov-report/child.ts.html +940 -0
- package/coverage/lcov-report/conditions.ts.html +107 -158
- package/coverage/lcov-report/datafileReader.ts.html +763 -103
- package/coverage/lcov-report/emitter.ts.html +77 -59
- package/coverage/lcov-report/evaluate.ts.html +689 -416
- package/coverage/lcov-report/events.ts.html +334 -0
- package/coverage/lcov-report/helpers.ts.html +184 -0
- package/coverage/lcov-report/{bucket.ts.html → hooks.ts.html} +86 -239
- package/coverage/lcov-report/index.html +119 -89
- package/coverage/lcov-report/instance.ts.html +341 -773
- package/coverage/lcov-report/logger.ts.html +64 -64
- package/coverage/lcov.info +1433 -1226
- package/dist/bucketer.d.ts +11 -0
- package/dist/child.d.ts +26 -0
- package/dist/compareVersions.d.ts +4 -0
- package/dist/conditions.d.ts +4 -4
- package/dist/datafileReader.d.ts +26 -6
- package/dist/emitter.d.ts +8 -9
- package/dist/evaluate.d.ts +31 -29
- package/dist/events.d.ts +5 -0
- package/dist/helpers.d.ts +5 -0
- package/dist/hooks.d.ts +45 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.gz +0 -0
- package/dist/index.mjs.map +1 -1
- package/dist/instance.d.ts +40 -72
- package/dist/logger.d.ts +6 -5
- package/dist/murmurhash.d.ts +1 -0
- package/jest.config.js +2 -0
- package/lib/bucketer.d.ts +11 -0
- package/lib/child.d.ts +26 -0
- package/lib/compareVersions.d.ts +4 -0
- package/lib/conditions.d.ts +4 -4
- package/lib/datafileReader.d.ts +26 -6
- package/lib/emitter.d.ts +8 -9
- package/lib/evaluate.d.ts +31 -29
- package/lib/events.d.ts +5 -0
- package/lib/helpers.d.ts +5 -0
- package/lib/hooks.d.ts +45 -0
- package/lib/index.d.ts +3 -2
- package/lib/instance.d.ts +40 -72
- package/lib/logger.d.ts +6 -5
- package/lib/murmurhash.d.ts +1 -0
- package/package.json +3 -5
- package/src/bucketer.spec.ts +165 -0
- package/src/bucketer.ts +84 -0
- package/src/child.spec.ts +267 -0
- package/src/child.ts +285 -0
- package/src/compareVersions.ts +93 -0
- package/src/conditions.spec.ts +563 -353
- package/src/conditions.ts +46 -63
- package/src/datafileReader.spec.ts +396 -84
- package/src/datafileReader.ts +280 -60
- package/src/emitter.spec.ts +27 -86
- package/src/emitter.ts +38 -32
- package/src/evaluate.ts +349 -258
- package/src/events.spec.ts +154 -0
- package/src/events.ts +83 -0
- package/src/helpers.ts +33 -0
- package/src/hooks.ts +88 -0
- package/src/index.ts +3 -2
- package/src/instance.spec.ts +305 -489
- package/src/instance.ts +247 -391
- package/src/logger.spec.ts +212 -134
- package/src/logger.ts +36 -36
- package/src/murmurhash.ts +71 -0
- package/coverage/lcov-report/feature.ts.html +0 -508
- package/dist/bucket.d.ts +0 -30
- package/dist/feature.d.ts +0 -16
- package/dist/segments.d.ts +0 -5
- package/lib/bucket.d.ts +0 -30
- package/lib/feature.d.ts +0 -16
- package/lib/segments.d.ts +0 -5
- package/src/bucket.spec.ts +0 -37
- package/src/bucket.ts +0 -139
- package/src/feature.ts +0 -141
- package/src/segments.spec.ts +0 -468
- package/src/segments.ts +0 -58
package/src/datafileReader.ts
CHANGED
|
@@ -1,71 +1,58 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type {
|
|
2
2
|
Feature,
|
|
3
3
|
Segment,
|
|
4
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
this.schemaVersion = datafileJson.schemaVersion;
|
|
34
|
-
this.revision = datafileJson.revision;
|
|
37
|
+
private logger: Logger;
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
48
|
-
datafileJsonV1.attributes.forEach((a) => {
|
|
49
|
-
this.attributes[a.key] = a;
|
|
50
|
-
});
|
|
47
|
+
this.logger = logger;
|
|
51
48
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
this.segments[s.key] = s;
|
|
55
|
-
});
|
|
49
|
+
this.schemaVersion = datafile.schemaVersion;
|
|
50
|
+
this.revision = datafile.revision;
|
|
56
51
|
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
const
|
|
66
|
+
getSegment(segmentKey: SegmentKey): Segment | undefined {
|
|
67
|
+
const segment = this.segments[segmentKey];
|
|
81
68
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
69
|
+
if (!segment) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
85
72
|
|
|
86
|
-
|
|
73
|
+
segment.conditions = this.parseConditionsIfStringified(segment.conditions);
|
|
74
|
+
|
|
75
|
+
return segment;
|
|
87
76
|
}
|
|
88
77
|
|
|
89
|
-
|
|
90
|
-
return this.
|
|
78
|
+
getFeatureKeys(): string[] {
|
|
79
|
+
return Object.keys(this.features);
|
|
91
80
|
}
|
|
92
81
|
|
|
93
|
-
|
|
94
|
-
|
|
82
|
+
getFeature(featureKey: FeatureKey): Feature | undefined {
|
|
83
|
+
return this.features[featureKey];
|
|
84
|
+
}
|
|
95
85
|
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
}
|
package/src/emitter.spec.ts
CHANGED
|
@@ -1,100 +1,41 @@
|
|
|
1
|
-
import { Emitter } from "./emitter";
|
|
1
|
+
import { Emitter, EventDetails } from "./emitter";
|
|
2
2
|
|
|
3
|
-
describe("
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
29
|
+
// trigger unsubscribed event
|
|
30
|
+
emitter.trigger("sticky_set", { key: "value2" });
|
|
31
|
+
expect(handledDetails.length).toBe(1);
|
|
91
32
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
33
|
+
// unsubscribe
|
|
34
|
+
unsubscribe();
|
|
35
|
+
expect(emitter.listeners["datafile_set"].length).toBe(0);
|
|
95
36
|
|
|
96
|
-
|
|
97
|
-
|
|
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 = "
|
|
1
|
+
export type EventName = "datafile_set" | "context_set" | "sticky_set";
|
|
2
2
|
|
|
3
|
-
export
|
|
4
|
-
|
|
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
|
-
|
|
10
|
+
listeners: Listeners;
|
|
9
11
|
|
|
10
12
|
constructor() {
|
|
11
|
-
this.
|
|
13
|
+
this.listeners = {};
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
if (
|
|
16
|
-
this.
|
|
16
|
+
on(eventName: EventName, callback: EventCallback) {
|
|
17
|
+
if (!this.listeners[eventName]) {
|
|
18
|
+
this.listeners[eventName] = [];
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
this.
|
|
20
|
-
|
|
21
|
+
const listeners = this.listeners[eventName];
|
|
22
|
+
listeners.push(callback);
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
if (typeof this._listeners[eventName] === "undefined") {
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
24
|
+
let isActive = true;
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
return function unsubscribe() {
|
|
27
|
+
if (!isActive) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
this._listeners[eventName].splice(index, 1);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
31
|
+
isActive = false;
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
45
|
-
|
|
40
|
+
trigger(eventName: EventName, details: EventDetails = {}) {
|
|
41
|
+
const listeners = this.listeners[eventName];
|
|
42
|
+
|
|
43
|
+
if (!listeners) {
|
|
46
44
|
return;
|
|
47
45
|
}
|
|
48
46
|
|
|
49
|
-
|
|
50
|
-
|
|
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
|
}
|