@eventvisor/sdk 0.0.2
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/LICENSE +21 -0
- package/README.md +9 -0
- package/dist/attributesManager.d.ts +36 -0
- package/dist/bucketer.d.ts +30 -0
- package/dist/compareVersions.d.ts +4 -0
- package/dist/conditions.d.ts +20 -0
- package/dist/datafileReader.d.ts +29 -0
- package/dist/effectsManager.d.ts +33 -0
- package/dist/emitter.d.ts +11 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.gz +0 -0
- package/dist/index.mjs.map +1 -0
- package/dist/instance.d.ts +67 -0
- package/dist/logger.d.ts +26 -0
- package/dist/modulesManager.d.ts +67 -0
- package/dist/murmurhash.d.ts +1 -0
- package/dist/persister.d.ts +40 -0
- package/dist/sourceResolver.d.ts +31 -0
- package/dist/transformer.d.ts +21 -0
- package/dist/validator.d.ts +28 -0
- package/jest.config.js +6 -0
- package/lib/attributesManager.d.ts +36 -0
- package/lib/bucketer.d.ts +30 -0
- package/lib/compareVersions.d.ts +4 -0
- package/lib/conditions.d.ts +20 -0
- package/lib/datafileReader.d.ts +29 -0
- package/lib/effectsManager.d.ts +33 -0
- package/lib/emitter.d.ts +11 -0
- package/lib/index.d.ts +12 -0
- package/lib/instance.d.ts +67 -0
- package/lib/logger.d.ts +26 -0
- package/lib/modulesManager.d.ts +67 -0
- package/lib/murmurhash.d.ts +1 -0
- package/lib/persister.d.ts +40 -0
- package/lib/sourceResolver.d.ts +31 -0
- package/lib/transformer.d.ts +21 -0
- package/lib/validator.d.ts +28 -0
- package/package.json +45 -0
- package/src/attributesManager.ts +181 -0
- package/src/bucketer.spec.ts +156 -0
- package/src/bucketer.ts +152 -0
- package/src/compareVersions.ts +93 -0
- package/src/conditions.ts +224 -0
- package/src/datafileReader.ts +133 -0
- package/src/effectsManager.ts +214 -0
- package/src/emitter.ts +64 -0
- package/src/index.spec.ts +5 -0
- package/src/index.ts +14 -0
- package/src/instance.spec.ts +184 -0
- package/src/instance.ts +608 -0
- package/src/logger.ts +90 -0
- package/src/modulesManager.ts +276 -0
- package/src/murmurhash.ts +71 -0
- package/src/persister.ts +162 -0
- package/src/sourceResolver.spec.ts +253 -0
- package/src/sourceResolver.ts +213 -0
- package/src/transformer.ts +316 -0
- package/src/transformer_static.spec.ts +377 -0
- package/src/transformer_types.spec.ts +820 -0
- package/src/validator.spec.ts +579 -0
- package/src/validator.ts +366 -0
- package/tsconfig.cjs.json +8 -0
- package/tsconfig.esm.json +8 -0
- package/webpack.config.js +80 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { PlainCondition, Condition, Inputs } from "@eventvisor/types";
|
|
2
|
+
|
|
3
|
+
import type { GetRegex } from "./datafileReader";
|
|
4
|
+
import type { SourceResolver } from "./sourceResolver";
|
|
5
|
+
|
|
6
|
+
import { compareVersions } from "./compareVersions";
|
|
7
|
+
import { Logger } from "./logger";
|
|
8
|
+
|
|
9
|
+
export type GetConditionsChecker = () => ConditionsChecker;
|
|
10
|
+
|
|
11
|
+
export interface ConditionsCheckerOptions {
|
|
12
|
+
getRegex: GetRegex;
|
|
13
|
+
sourceResolver: SourceResolver;
|
|
14
|
+
logger: Logger;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class ConditionsChecker {
|
|
18
|
+
private getRegex: GetRegex;
|
|
19
|
+
private sourceResolver: SourceResolver;
|
|
20
|
+
private logger: Logger;
|
|
21
|
+
|
|
22
|
+
constructor(options: ConditionsCheckerOptions) {
|
|
23
|
+
this.getRegex = options.getRegex;
|
|
24
|
+
this.sourceResolver = options.sourceResolver;
|
|
25
|
+
this.logger = options.logger;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async isMatched(condition: PlainCondition, inputs: Inputs): Promise<boolean> {
|
|
29
|
+
const { operator, value, regexFlags } = condition;
|
|
30
|
+
|
|
31
|
+
const sourceValue = await this.sourceResolver.resolve(condition, inputs);
|
|
32
|
+
|
|
33
|
+
if (operator === "equals") {
|
|
34
|
+
return sourceValue === value;
|
|
35
|
+
} else if (operator === "notEquals") {
|
|
36
|
+
return sourceValue !== value;
|
|
37
|
+
} else if (operator === "before" || operator === "after") {
|
|
38
|
+
// date comparisons
|
|
39
|
+
const valueInContext = sourceValue as string | Date;
|
|
40
|
+
|
|
41
|
+
const dateInContext =
|
|
42
|
+
valueInContext instanceof Date ? valueInContext : new Date(valueInContext);
|
|
43
|
+
const dateInCondition = value instanceof Date ? value : new Date(value as string);
|
|
44
|
+
|
|
45
|
+
return operator === "before"
|
|
46
|
+
? dateInContext < dateInCondition
|
|
47
|
+
: dateInContext > dateInCondition;
|
|
48
|
+
} else if (
|
|
49
|
+
Array.isArray(value) &&
|
|
50
|
+
(["string", "number"].indexOf(typeof sourceValue) !== -1 || sourceValue === null)
|
|
51
|
+
) {
|
|
52
|
+
// in / notIn (where condition value is an array)
|
|
53
|
+
const valueInContext = sourceValue as string;
|
|
54
|
+
|
|
55
|
+
if (operator === "in") {
|
|
56
|
+
return value.indexOf(valueInContext) !== -1;
|
|
57
|
+
} else if (operator === "notIn") {
|
|
58
|
+
return value.indexOf(valueInContext) === -1;
|
|
59
|
+
}
|
|
60
|
+
} else if (typeof sourceValue === "string" && typeof value === "string") {
|
|
61
|
+
// string
|
|
62
|
+
const valueInContext = sourceValue as string;
|
|
63
|
+
|
|
64
|
+
if (operator === "contains") {
|
|
65
|
+
return valueInContext.indexOf(value) !== -1;
|
|
66
|
+
} else if (operator === "notContains") {
|
|
67
|
+
return valueInContext.indexOf(value) === -1;
|
|
68
|
+
} else if (operator === "startsWith") {
|
|
69
|
+
return valueInContext.startsWith(value);
|
|
70
|
+
} else if (operator === "endsWith") {
|
|
71
|
+
return valueInContext.endsWith(value);
|
|
72
|
+
} else if (operator === "semverEquals") {
|
|
73
|
+
return compareVersions(valueInContext, value) === 0;
|
|
74
|
+
} else if (operator === "semverNotEquals") {
|
|
75
|
+
return compareVersions(valueInContext, value) !== 0;
|
|
76
|
+
} else if (operator === "semverGreaterThan") {
|
|
77
|
+
return compareVersions(valueInContext, value) === 1;
|
|
78
|
+
} else if (operator === "semverGreaterThanOrEquals") {
|
|
79
|
+
return compareVersions(valueInContext, value) >= 0;
|
|
80
|
+
} else if (operator === "semverLessThan") {
|
|
81
|
+
return compareVersions(valueInContext, value) === -1;
|
|
82
|
+
} else if (operator === "semverLessThanOrEquals") {
|
|
83
|
+
return compareVersions(valueInContext, value) <= 0;
|
|
84
|
+
} else if (operator === "matches") {
|
|
85
|
+
const regex = this.getRegex(value, regexFlags || "");
|
|
86
|
+
return regex.test(valueInContext);
|
|
87
|
+
} else if (operator === "notMatches") {
|
|
88
|
+
const regex = this.getRegex(value, regexFlags || "");
|
|
89
|
+
return !regex.test(valueInContext);
|
|
90
|
+
}
|
|
91
|
+
} else if (typeof sourceValue === "number" && typeof value === "number") {
|
|
92
|
+
// numeric
|
|
93
|
+
const valueInContext = sourceValue as number;
|
|
94
|
+
|
|
95
|
+
if (operator === "greaterThan") {
|
|
96
|
+
return valueInContext > value;
|
|
97
|
+
} else if (operator === "greaterThanOrEquals") {
|
|
98
|
+
return valueInContext >= value;
|
|
99
|
+
} else if (operator === "lessThan") {
|
|
100
|
+
return valueInContext < value;
|
|
101
|
+
} else if (operator === "lessThanOrEquals") {
|
|
102
|
+
return valueInContext <= value;
|
|
103
|
+
}
|
|
104
|
+
} else if (operator === "exists") {
|
|
105
|
+
// @TODO: may require extra care for null values
|
|
106
|
+
return typeof sourceValue !== "undefined";
|
|
107
|
+
} else if (operator === "notExists") {
|
|
108
|
+
return typeof sourceValue === "undefined";
|
|
109
|
+
} else if (Array.isArray(sourceValue) && typeof value === "string") {
|
|
110
|
+
// includes / notIncludes (where context value is an array)
|
|
111
|
+
const valueInContext = sourceValue as string[];
|
|
112
|
+
|
|
113
|
+
if (operator === "includes") {
|
|
114
|
+
return valueInContext.indexOf(value) > -1;
|
|
115
|
+
} else if (operator === "notIncludes") {
|
|
116
|
+
return valueInContext.indexOf(value) === -1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async _allAreMatched(
|
|
124
|
+
conditions: Condition[] | Condition,
|
|
125
|
+
inputs: Inputs,
|
|
126
|
+
): Promise<boolean> {
|
|
127
|
+
if (typeof conditions === "string") {
|
|
128
|
+
if (conditions === "*") {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if ("operator" in conditions) {
|
|
136
|
+
try {
|
|
137
|
+
return this.isMatched(conditions, inputs);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
this.logger.warn(e.message, {
|
|
140
|
+
error: e,
|
|
141
|
+
details: {
|
|
142
|
+
condition: conditions,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if ("and" in conditions && Array.isArray(conditions.and)) {
|
|
151
|
+
for (const c of conditions.and) {
|
|
152
|
+
if (!(await this._allAreMatched(c, inputs))) {
|
|
153
|
+
return false; // If any condition fails, return false
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return true; // All conditions passed
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if ("or" in conditions && Array.isArray(conditions.or)) {
|
|
160
|
+
for (const c of conditions.or) {
|
|
161
|
+
if (await this._allAreMatched(c, inputs)) {
|
|
162
|
+
return true; // If any condition passes, return true
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return false; // No conditions passed
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if ("not" in conditions && Array.isArray(conditions.not)) {
|
|
169
|
+
for (const c of conditions.not) {
|
|
170
|
+
if (await this._allAreMatched(c, inputs)) {
|
|
171
|
+
return false; // If any condition passes, return false (since this is NOT)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return true; // No conditions passed, which is what we want for NOT
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (Array.isArray(conditions)) {
|
|
178
|
+
let result = true;
|
|
179
|
+
for (const c of conditions) {
|
|
180
|
+
if (!(await this._allAreMatched(c, inputs))) {
|
|
181
|
+
result = false;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async allAreMatched(conditions: Condition[] | Condition, inputs: Inputs): Promise<boolean> {
|
|
193
|
+
const parsedConditions = this.parseIfStringified(conditions);
|
|
194
|
+
|
|
195
|
+
const result = this._allAreMatched(parsedConditions, inputs);
|
|
196
|
+
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
parseIfStringified(conditions: Condition | Condition[]): Condition | Condition[] {
|
|
201
|
+
if (typeof conditions !== "string") {
|
|
202
|
+
// already parsed
|
|
203
|
+
return conditions;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (conditions === "*") {
|
|
207
|
+
// everyone
|
|
208
|
+
return conditions;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
return JSON.parse(conditions);
|
|
213
|
+
} catch (e) {
|
|
214
|
+
this.logger.error("Error parsing conditions", {
|
|
215
|
+
error: e,
|
|
216
|
+
details: {
|
|
217
|
+
conditions,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return conditions;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AttributeName,
|
|
3
|
+
DatafileContent,
|
|
4
|
+
DestinationName,
|
|
5
|
+
EffectName,
|
|
6
|
+
EventName,
|
|
7
|
+
Persist,
|
|
8
|
+
ComplexPersist,
|
|
9
|
+
Attribute,
|
|
10
|
+
Effect,
|
|
11
|
+
Event,
|
|
12
|
+
Destination,
|
|
13
|
+
} from "@eventvisor/types";
|
|
14
|
+
|
|
15
|
+
import { Logger } from "./logger";
|
|
16
|
+
|
|
17
|
+
export interface DatafileReaderOptions {
|
|
18
|
+
datafile: DatafileContent;
|
|
19
|
+
logger: Logger;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type GetDatafileReader = () => DatafileReader;
|
|
23
|
+
|
|
24
|
+
export type GetRegex = (regexString: string, regexFlags?: string) => RegExp;
|
|
25
|
+
|
|
26
|
+
export const emptyDatafile: DatafileContent = {
|
|
27
|
+
schemaVersion: "1",
|
|
28
|
+
revision: "0",
|
|
29
|
+
|
|
30
|
+
attributes: {},
|
|
31
|
+
events: {},
|
|
32
|
+
destinations: {},
|
|
33
|
+
effects: {},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function getComplexPersists(persist: Persist): ComplexPersist[] {
|
|
37
|
+
let result: ComplexPersist[] = [];
|
|
38
|
+
|
|
39
|
+
if (typeof persist === "string") {
|
|
40
|
+
result.push({ storage: persist });
|
|
41
|
+
} else if (Array.isArray(persist)) {
|
|
42
|
+
for (const p of persist) {
|
|
43
|
+
const r = getComplexPersists(p);
|
|
44
|
+
|
|
45
|
+
if (r) {
|
|
46
|
+
result = result.concat(r);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} else if (typeof persist === "object") {
|
|
50
|
+
result.push(persist);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class DatafileReader {
|
|
57
|
+
private schemaVersion: string;
|
|
58
|
+
private revision: string;
|
|
59
|
+
|
|
60
|
+
private datafile: DatafileContent;
|
|
61
|
+
private logger: Logger;
|
|
62
|
+
private regexCache: Record<string, RegExp>;
|
|
63
|
+
|
|
64
|
+
constructor(options: DatafileReaderOptions) {
|
|
65
|
+
const { datafile, logger } = options;
|
|
66
|
+
|
|
67
|
+
this.datafile = datafile;
|
|
68
|
+
this.schemaVersion = datafile.schemaVersion;
|
|
69
|
+
this.revision = datafile.revision;
|
|
70
|
+
|
|
71
|
+
this.regexCache = {};
|
|
72
|
+
|
|
73
|
+
this.logger = logger;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getSchemaVersion(): string {
|
|
77
|
+
return this.schemaVersion;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getRevision(): string {
|
|
81
|
+
return this.revision;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
getAttribute(attributeName: AttributeName): Attribute | undefined {
|
|
85
|
+
return this.datafile.attributes[attributeName];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getAttributeNames(): AttributeName[] {
|
|
89
|
+
return Object.keys(this.datafile.attributes);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getEvent(eventName: EventName): Event | undefined {
|
|
93
|
+
return this.datafile.events[eventName];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getDestination(destinationName: DestinationName): Destination | undefined {
|
|
97
|
+
return this.datafile.destinations[destinationName];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getDestinationNames(): DestinationName[] {
|
|
101
|
+
return Object.keys(this.datafile.destinations);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
getEffect(effectName: EffectName): Effect | undefined {
|
|
105
|
+
return this.datafile.effects[effectName];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
getEffectNames(): EffectName[] {
|
|
109
|
+
return Object.keys(this.datafile.effects);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
getRegex(regexString: string, regexFlags?: string): RegExp {
|
|
113
|
+
const flags = regexFlags || "";
|
|
114
|
+
const cacheKey = `${regexString}-${flags}`;
|
|
115
|
+
|
|
116
|
+
if (this.regexCache[cacheKey]) {
|
|
117
|
+
return this.regexCache[cacheKey];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const regex = new RegExp(regexString, flags);
|
|
121
|
+
this.regexCache[cacheKey] = regex;
|
|
122
|
+
|
|
123
|
+
return regex;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getPersists(schema: Attribute | Effect): ComplexPersist[] | null {
|
|
127
|
+
if (!schema || !schema.persist) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return getComplexPersists(schema.persist);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { EventName, AttributeName, EffectName, Value, EffectOnType } from "@eventvisor/types";
|
|
2
|
+
|
|
3
|
+
import type { GetDatafileReader } from "./datafileReader";
|
|
4
|
+
import type { Logger } from "./logger";
|
|
5
|
+
import type { GetTransformer } from "./transformer";
|
|
6
|
+
import type { GetConditionsChecker } from "./conditions";
|
|
7
|
+
import type { ModulesManager } from "./modulesManager";
|
|
8
|
+
import { initializeFromStorage, persistEntity } from "./persister";
|
|
9
|
+
|
|
10
|
+
export type StatesByEffect = Record<EffectName, Value>;
|
|
11
|
+
|
|
12
|
+
export interface DispatchOptions {
|
|
13
|
+
eventType: EffectOnType;
|
|
14
|
+
name: EventName | AttributeName;
|
|
15
|
+
value: Value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface EffectsManagerOptions {
|
|
19
|
+
logger: Logger;
|
|
20
|
+
getDatafileReader: GetDatafileReader;
|
|
21
|
+
getTransformer: GetTransformer;
|
|
22
|
+
getConditionsChecker: GetConditionsChecker;
|
|
23
|
+
modulesManager: ModulesManager;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class EffectsManager {
|
|
27
|
+
private logger: Logger;
|
|
28
|
+
private getDatafileReader: GetDatafileReader;
|
|
29
|
+
private getTransformer: GetTransformer;
|
|
30
|
+
private getConditionsChecker: GetConditionsChecker;
|
|
31
|
+
private modulesManager: ModulesManager;
|
|
32
|
+
|
|
33
|
+
private statesByEffect: StatesByEffect = {};
|
|
34
|
+
|
|
35
|
+
constructor(options: EffectsManagerOptions) {
|
|
36
|
+
this.logger = options.logger;
|
|
37
|
+
this.getDatafileReader = options.getDatafileReader;
|
|
38
|
+
this.getTransformer = options.getTransformer;
|
|
39
|
+
this.getConditionsChecker = options.getConditionsChecker;
|
|
40
|
+
this.modulesManager = options.modulesManager;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async initialize(): Promise<void> {
|
|
44
|
+
const datafileReader = this.getDatafileReader();
|
|
45
|
+
const effects = datafileReader.getEffectNames();
|
|
46
|
+
|
|
47
|
+
const persistedResult = await initializeFromStorage({
|
|
48
|
+
datafileReader,
|
|
49
|
+
conditionsChecker: this.getConditionsChecker(),
|
|
50
|
+
modulesManager: this.modulesManager,
|
|
51
|
+
storageKeyPrefix: "effects_",
|
|
52
|
+
getEntityNames: () => datafileReader.getEffectNames(),
|
|
53
|
+
getEntity: (entityName: string) => datafileReader.getEffect(entityName),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
for (const effectName of effects) {
|
|
57
|
+
const effect = datafileReader.getEffect(effectName);
|
|
58
|
+
|
|
59
|
+
if (!effect) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (typeof this.statesByEffect[effectName] !== "undefined") {
|
|
64
|
+
// possibly called via refresh() method after initialization
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (typeof persistedResult[effectName] !== "undefined") {
|
|
69
|
+
// from storage
|
|
70
|
+
this.statesByEffect[effectName] = persistedResult[effectName];
|
|
71
|
+
} else {
|
|
72
|
+
// from initial state of effect
|
|
73
|
+
if (typeof effect.state !== "undefined") {
|
|
74
|
+
if (typeof this.statesByEffect[effectName] === "undefined") {
|
|
75
|
+
this.statesByEffect[effectName] = effect.state;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async dispatch(dispatchOptions: DispatchOptions) {
|
|
83
|
+
// @TODO: rename to actionType
|
|
84
|
+
const { eventType, name, value } = dispatchOptions;
|
|
85
|
+
|
|
86
|
+
const datafileReader = this.getDatafileReader();
|
|
87
|
+
const conditionsChecker = this.getConditionsChecker();
|
|
88
|
+
const transformer = this.getTransformer();
|
|
89
|
+
|
|
90
|
+
const allEffects = datafileReader.getEffectNames();
|
|
91
|
+
|
|
92
|
+
for (const effectName of allEffects) {
|
|
93
|
+
const effect = datafileReader.getEffect(effectName);
|
|
94
|
+
|
|
95
|
+
if (!effect) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (eventType === "event_tracked") {
|
|
100
|
+
if (Array.isArray(effect.on) && !effect.on.includes("event_tracked")) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof effect.on === "object" && !effect.on["event_tracked"]?.includes(name)) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (eventType === "attribute_set") {
|
|
110
|
+
if (Array.isArray(effect.on) && !effect.on.includes("attribute_set")) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (typeof effect.on === "object" && !effect.on["attribute_set"]?.includes(name)) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// conditions
|
|
120
|
+
if (effect.conditions) {
|
|
121
|
+
const isMatched = await conditionsChecker.allAreMatched(effect.conditions, {
|
|
122
|
+
payload: value,
|
|
123
|
+
eventName: eventType === "event_tracked" ? name : undefined,
|
|
124
|
+
attributeName: eventType === "attribute_set" ? name : undefined,
|
|
125
|
+
state: this.statesByEffect[effectName],
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (!isMatched) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// steps
|
|
134
|
+
if (effect.steps) {
|
|
135
|
+
for (const step of effect.steps) {
|
|
136
|
+
let stepPassed = true;
|
|
137
|
+
|
|
138
|
+
// conditions
|
|
139
|
+
if (step.conditions) {
|
|
140
|
+
const conditionsChecker = this.getConditionsChecker();
|
|
141
|
+
const isMatched = await conditionsChecker.allAreMatched(step.conditions, {
|
|
142
|
+
payload: value,
|
|
143
|
+
eventName: eventType === "event_tracked" ? name : undefined,
|
|
144
|
+
attributeName: eventType === "attribute_set" ? name : undefined,
|
|
145
|
+
state: this.statesByEffect[effectName],
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (!isMatched) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// handler
|
|
154
|
+
if (step.handler) {
|
|
155
|
+
try {
|
|
156
|
+
await this.modulesManager.handle(step.handler, effectName, effect, step, value);
|
|
157
|
+
} catch (handlerError) {
|
|
158
|
+
this.logger.error(`Effect handler error`, {
|
|
159
|
+
effectName,
|
|
160
|
+
step,
|
|
161
|
+
error: handlerError,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
stepPassed = false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// continueOnError
|
|
169
|
+
if (!stepPassed && typeof step.continueOnError === "boolean" && !step.continueOnError) {
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// transforms
|
|
174
|
+
if (step.transforms) {
|
|
175
|
+
this.statesByEffect[effectName] = await transformer.applyAll(
|
|
176
|
+
this.statesByEffect[effectName],
|
|
177
|
+
step.transforms,
|
|
178
|
+
{
|
|
179
|
+
eventName: eventType === "event_tracked" ? name : undefined,
|
|
180
|
+
attributeName: eventType === "attribute_set" ? name : undefined,
|
|
181
|
+
state: this.statesByEffect[effectName],
|
|
182
|
+
},
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// persist
|
|
189
|
+
await persistEntity({
|
|
190
|
+
datafileReader,
|
|
191
|
+
conditionsChecker,
|
|
192
|
+
modulesManager: this.modulesManager,
|
|
193
|
+
storageKeyPrefix: "effects_",
|
|
194
|
+
entityName: effectName,
|
|
195
|
+
entity: effect,
|
|
196
|
+
value: this.statesByEffect[effectName],
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// called after datafile refresh
|
|
202
|
+
refresh() {
|
|
203
|
+
// @TODO: think
|
|
204
|
+
this.initialize();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
getAllStates() {
|
|
208
|
+
return this.statesByEffect;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
getStateValue(name: EffectName) {
|
|
212
|
+
return this.statesByEffect[name];
|
|
213
|
+
}
|
|
214
|
+
}
|
package/src/emitter.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export type EmitType =
|
|
2
|
+
| "ready"
|
|
3
|
+
| "datafile_set"
|
|
4
|
+
| "attribute_set"
|
|
5
|
+
| "attribute_removed"
|
|
6
|
+
| "event_tracked";
|
|
7
|
+
|
|
8
|
+
export type EventDetails = Record<string, unknown>;
|
|
9
|
+
|
|
10
|
+
export type EventCallback = (details: EventDetails) => void;
|
|
11
|
+
|
|
12
|
+
export type Listeners = Record<EmitType, EventCallback[]> | {}; // eslint-disable-line
|
|
13
|
+
|
|
14
|
+
export class Emitter {
|
|
15
|
+
listeners: Listeners;
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
this.listeners = {};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
on(emitType: EmitType, callback: EventCallback) {
|
|
22
|
+
if (!this.listeners[emitType]) {
|
|
23
|
+
this.listeners[emitType] = [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const listeners = this.listeners[emitType];
|
|
27
|
+
listeners.push(callback);
|
|
28
|
+
|
|
29
|
+
let isActive = true;
|
|
30
|
+
|
|
31
|
+
return function unsubscribe() {
|
|
32
|
+
if (!isActive) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
isActive = false;
|
|
37
|
+
|
|
38
|
+
const index = listeners.indexOf(callback);
|
|
39
|
+
if (index !== -1) {
|
|
40
|
+
listeners.splice(index, 1);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
trigger(emitType: EmitType, details: EventDetails = {}) {
|
|
46
|
+
const listeners = this.listeners[emitType];
|
|
47
|
+
|
|
48
|
+
if (!listeners) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
listeners.forEach(function (listener) {
|
|
53
|
+
try {
|
|
54
|
+
listener(details);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error(err);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
clearAll() {
|
|
62
|
+
this.listeners = {};
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from "./instance";
|
|
2
|
+
|
|
3
|
+
export * from "./attributesManager";
|
|
4
|
+
export * from "./datafileReader";
|
|
5
|
+
export * from "./emitter";
|
|
6
|
+
export * from "./logger";
|
|
7
|
+
export * from "./transformer";
|
|
8
|
+
export * from "./bucketer";
|
|
9
|
+
export * from "./sourceResolver";
|
|
10
|
+
export * from "./modulesManager";
|
|
11
|
+
export * from "./effectsManager";
|
|
12
|
+
|
|
13
|
+
export * from "./murmurhash";
|
|
14
|
+
export * from "./compareVersions";
|