@f-o-t/rules-engine 2.0.2 → 3.0.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 +168 -0
- package/LICENSE.md +16 -4
- package/README.md +106 -23
- package/__tests__/builder.test.ts +363 -0
- package/__tests__/cache.test.ts +130 -0
- package/__tests__/config.test.ts +35 -0
- package/__tests__/engine.test.ts +1213 -0
- package/__tests__/evaluate.test.ts +339 -0
- package/__tests__/exports.test.ts +30 -0
- package/__tests__/filter-sort.test.ts +303 -0
- package/__tests__/integration.test.ts +419 -0
- package/__tests__/money-integration.test.ts +149 -0
- package/__tests__/validation.test.ts +862 -0
- package/biome.json +39 -0
- package/docs/MIGRATION-v3.md +118 -0
- package/fot.config.ts +5 -0
- package/package.json +31 -67
- package/src/analyzer/analysis.ts +401 -0
- package/src/builder/conditions.ts +321 -0
- package/src/builder/rule.ts +192 -0
- package/src/cache/cache.ts +135 -0
- package/src/cache/noop.ts +20 -0
- package/src/core/evaluate.ts +185 -0
- package/src/core/filter.ts +85 -0
- package/src/core/group.ts +103 -0
- package/src/core/sort.ts +90 -0
- package/src/engine/engine.ts +462 -0
- package/src/engine/hooks.ts +235 -0
- package/src/engine/state.ts +322 -0
- package/src/index.ts +303 -0
- package/src/optimizer/index-builder.ts +381 -0
- package/src/serialization/serializer.ts +408 -0
- package/src/simulation/simulator.ts +359 -0
- package/src/types/config.ts +184 -0
- package/src/types/consequence.ts +38 -0
- package/src/types/evaluation.ts +87 -0
- package/src/types/rule.ts +112 -0
- package/src/types/state.ts +116 -0
- package/src/utils/conditions.ts +108 -0
- package/src/utils/hash.ts +30 -0
- package/src/utils/id.ts +6 -0
- package/src/utils/time.ts +42 -0
- package/src/validation/conflicts.ts +440 -0
- package/src/validation/integrity.ts +473 -0
- package/src/validation/schema.ts +386 -0
- package/src/versioning/version-store.ts +337 -0
- package/tsconfig.json +29 -0
- package/dist/index.cjs +0 -3088
- package/dist/index.d.cts +0 -1173
- package/dist/index.d.ts +0 -1173
- package/dist/index.js +0 -3072
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import { type Cache, createCache } from "../cache/cache";
|
|
2
|
+
import { createNoopCache } from "../cache/noop";
|
|
3
|
+
import { evaluateRule } from "../core/evaluate";
|
|
4
|
+
import { createEvaluator } from "@f-o-t/condition-evaluator";
|
|
5
|
+
import {
|
|
6
|
+
type EngineConfig,
|
|
7
|
+
getDefaultCacheConfig,
|
|
8
|
+
getDefaultConflictResolution,
|
|
9
|
+
getDefaultLogLevel,
|
|
10
|
+
getDefaultValidationConfig,
|
|
11
|
+
getDefaultVersioningConfig,
|
|
12
|
+
type ResolvedEngineConfig,
|
|
13
|
+
} from "../types/config";
|
|
14
|
+
import type {
|
|
15
|
+
AggregatedConsequence,
|
|
16
|
+
ConsequenceDefinitions,
|
|
17
|
+
DefaultConsequences,
|
|
18
|
+
} from "../types/consequence";
|
|
19
|
+
import type {
|
|
20
|
+
EngineExecutionResult,
|
|
21
|
+
EvaluateOptions,
|
|
22
|
+
EvaluationContext,
|
|
23
|
+
} from "../types/evaluation";
|
|
24
|
+
import type {
|
|
25
|
+
Rule,
|
|
26
|
+
RuleFilters,
|
|
27
|
+
RuleInput,
|
|
28
|
+
RuleSet,
|
|
29
|
+
RuleSetInput,
|
|
30
|
+
} from "../types/rule";
|
|
31
|
+
import type {
|
|
32
|
+
CacheStats,
|
|
33
|
+
EngineState,
|
|
34
|
+
EngineStats,
|
|
35
|
+
MutableEngineState,
|
|
36
|
+
} from "../types/state";
|
|
37
|
+
import { createInitialState } from "../types/state";
|
|
38
|
+
import { hashContext, hashRules } from "../utils/hash";
|
|
39
|
+
import { generateId } from "../utils/id";
|
|
40
|
+
import { measureTime } from "../utils/time";
|
|
41
|
+
import {
|
|
42
|
+
executeAfterEvaluation,
|
|
43
|
+
executeAfterRuleEvaluation,
|
|
44
|
+
executeBeforeEvaluation,
|
|
45
|
+
executeBeforeRuleEvaluation,
|
|
46
|
+
executeOnCacheHit,
|
|
47
|
+
executeOnCacheMiss,
|
|
48
|
+
executeOnConsequenceCollected,
|
|
49
|
+
executeOnRuleError,
|
|
50
|
+
executeOnRuleMatch,
|
|
51
|
+
executeOnRuleSkip,
|
|
52
|
+
executeOnSlowRule,
|
|
53
|
+
} from "./hooks";
|
|
54
|
+
import {
|
|
55
|
+
addRule,
|
|
56
|
+
addRuleSet,
|
|
57
|
+
addRules,
|
|
58
|
+
clearRules,
|
|
59
|
+
disableRule,
|
|
60
|
+
enableRule,
|
|
61
|
+
getRule,
|
|
62
|
+
getRuleSet,
|
|
63
|
+
getRuleSets,
|
|
64
|
+
getRules,
|
|
65
|
+
getRulesInSet,
|
|
66
|
+
removeRule,
|
|
67
|
+
removeRuleSet,
|
|
68
|
+
updateRule,
|
|
69
|
+
} from "./state";
|
|
70
|
+
|
|
71
|
+
export type Engine<
|
|
72
|
+
TContext = unknown,
|
|
73
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
74
|
+
> = {
|
|
75
|
+
readonly addRule: (
|
|
76
|
+
input: RuleInput<TContext, TConsequences>,
|
|
77
|
+
) => Rule<TContext, TConsequences>;
|
|
78
|
+
readonly addRules: (
|
|
79
|
+
inputs: RuleInput<TContext, TConsequences>[],
|
|
80
|
+
) => Rule<TContext, TConsequences>[];
|
|
81
|
+
readonly removeRule: (ruleId: string) => boolean;
|
|
82
|
+
readonly updateRule: (
|
|
83
|
+
ruleId: string,
|
|
84
|
+
updates: Partial<RuleInput<TContext, TConsequences>>,
|
|
85
|
+
) => Rule<TContext, TConsequences> | undefined;
|
|
86
|
+
readonly getRule: (
|
|
87
|
+
ruleId: string,
|
|
88
|
+
) => Rule<TContext, TConsequences> | undefined;
|
|
89
|
+
readonly getRules: (
|
|
90
|
+
filters?: RuleFilters,
|
|
91
|
+
) => ReadonlyArray<Rule<TContext, TConsequences>>;
|
|
92
|
+
readonly enableRule: (ruleId: string) => boolean;
|
|
93
|
+
readonly disableRule: (ruleId: string) => boolean;
|
|
94
|
+
readonly clearRules: () => void;
|
|
95
|
+
|
|
96
|
+
readonly addRuleSet: (input: RuleSetInput) => RuleSet;
|
|
97
|
+
readonly getRuleSet: (ruleSetId: string) => RuleSet | undefined;
|
|
98
|
+
readonly getRuleSets: () => ReadonlyArray<RuleSet>;
|
|
99
|
+
readonly removeRuleSet: (ruleSetId: string) => boolean;
|
|
100
|
+
|
|
101
|
+
readonly evaluate: (
|
|
102
|
+
context: TContext,
|
|
103
|
+
options?: EvaluateOptions,
|
|
104
|
+
) => Promise<EngineExecutionResult<TContext, TConsequences>>;
|
|
105
|
+
|
|
106
|
+
readonly clearCache: () => void;
|
|
107
|
+
readonly getCacheStats: () => CacheStats;
|
|
108
|
+
|
|
109
|
+
readonly getState: () => Readonly<EngineState<TContext, TConsequences>>;
|
|
110
|
+
readonly getStats: () => EngineStats;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const resolveConfig = <
|
|
114
|
+
TContext = unknown,
|
|
115
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
116
|
+
>(
|
|
117
|
+
config: EngineConfig<TContext, TConsequences>,
|
|
118
|
+
): ResolvedEngineConfig<TContext, TConsequences> => {
|
|
119
|
+
// Validate evaluator config
|
|
120
|
+
if (!config.evaluator && !config.operators) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
"Engine requires either 'evaluator' or 'operators' config. " +
|
|
123
|
+
"Pass { evaluator: createEvaluator() } for built-in operators only, " +
|
|
124
|
+
"or { operators: customOperators } to use custom operators."
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Create evaluator from config
|
|
129
|
+
const evaluator = config.evaluator
|
|
130
|
+
? config.evaluator
|
|
131
|
+
: createEvaluator({ operators: config.operators });
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
consequences: config.consequences,
|
|
135
|
+
conflictResolution:
|
|
136
|
+
config.conflictResolution ?? getDefaultConflictResolution(),
|
|
137
|
+
cache: {
|
|
138
|
+
...getDefaultCacheConfig(),
|
|
139
|
+
...config.cache,
|
|
140
|
+
},
|
|
141
|
+
validation: {
|
|
142
|
+
...getDefaultValidationConfig(),
|
|
143
|
+
...config.validation,
|
|
144
|
+
},
|
|
145
|
+
versioning: {
|
|
146
|
+
...getDefaultVersioningConfig(),
|
|
147
|
+
...config.versioning,
|
|
148
|
+
},
|
|
149
|
+
hooks: config.hooks ?? {},
|
|
150
|
+
logLevel: config.logLevel ?? getDefaultLogLevel(),
|
|
151
|
+
logger: config.logger ?? console,
|
|
152
|
+
continueOnError: config.continueOnError ?? true,
|
|
153
|
+
slowRuleThresholdMs: config.slowRuleThresholdMs ?? 10,
|
|
154
|
+
hookTimeoutMs: config.hookTimeoutMs,
|
|
155
|
+
evaluator, // Add to resolved config
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const createEngine = <
|
|
160
|
+
TContext = unknown,
|
|
161
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
162
|
+
>(
|
|
163
|
+
config: EngineConfig<TContext, TConsequences> = {},
|
|
164
|
+
): Engine<TContext, TConsequences> => {
|
|
165
|
+
const resolvedConfig = resolveConfig(config);
|
|
166
|
+
const state: MutableEngineState<TContext, TConsequences> =
|
|
167
|
+
createInitialState();
|
|
168
|
+
|
|
169
|
+
const cache: Cache<EngineExecutionResult<TContext, TConsequences>> =
|
|
170
|
+
resolvedConfig.cache.enabled
|
|
171
|
+
? createCache({
|
|
172
|
+
ttl: resolvedConfig.cache.ttl,
|
|
173
|
+
maxSize: resolvedConfig.cache.maxSize,
|
|
174
|
+
})
|
|
175
|
+
: createNoopCache();
|
|
176
|
+
|
|
177
|
+
let totalEvaluations = 0;
|
|
178
|
+
let totalMatches = 0;
|
|
179
|
+
let totalErrors = 0;
|
|
180
|
+
let cacheHits = 0;
|
|
181
|
+
let cacheMisses = 0;
|
|
182
|
+
let totalEvaluationTime = 0;
|
|
183
|
+
|
|
184
|
+
const evaluate = async (
|
|
185
|
+
contextData: TContext,
|
|
186
|
+
options: EvaluateOptions = {},
|
|
187
|
+
): Promise<EngineExecutionResult<TContext, TConsequences>> => {
|
|
188
|
+
const context: EvaluationContext<TContext> = {
|
|
189
|
+
data: contextData,
|
|
190
|
+
timestamp: new Date(),
|
|
191
|
+
correlationId: generateId(),
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
let rulesToEvaluate = getRules(state, {
|
|
195
|
+
enabled: options.skipDisabled !== false ? true : undefined,
|
|
196
|
+
tags: options.tags,
|
|
197
|
+
category: options.category,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (options.ruleSetId) {
|
|
201
|
+
const ruleSetRules = getRulesInSet(state, options.ruleSetId);
|
|
202
|
+
const ruleSetIds = new Set(ruleSetRules.map((r) => r.id));
|
|
203
|
+
rulesToEvaluate = rulesToEvaluate.filter((r) => ruleSetIds.has(r.id));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Rules are already sorted by priority via state.ruleOrder (sorted on add/update)
|
|
207
|
+
|
|
208
|
+
if (options.maxRules && options.maxRules > 0) {
|
|
209
|
+
rulesToEvaluate = rulesToEvaluate.slice(0, options.maxRules);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const cacheKey = !options.bypassCache
|
|
213
|
+
? `${hashContext(contextData)}:${hashRules(rulesToEvaluate.map((r) => r.id))}`
|
|
214
|
+
: null;
|
|
215
|
+
|
|
216
|
+
if (cacheKey && cache.has(cacheKey)) {
|
|
217
|
+
const cached = cache.get(cacheKey);
|
|
218
|
+
if (cached) {
|
|
219
|
+
cacheHits++;
|
|
220
|
+
await executeOnCacheHit(
|
|
221
|
+
resolvedConfig.hooks,
|
|
222
|
+
cacheKey,
|
|
223
|
+
cached,
|
|
224
|
+
resolvedConfig.hookTimeoutMs,
|
|
225
|
+
);
|
|
226
|
+
return { ...cached, cacheHit: true };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (cacheKey) {
|
|
231
|
+
cacheMisses++;
|
|
232
|
+
await executeOnCacheMiss(
|
|
233
|
+
resolvedConfig.hooks,
|
|
234
|
+
cacheKey,
|
|
235
|
+
resolvedConfig.hookTimeoutMs,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
await executeBeforeEvaluation(
|
|
240
|
+
resolvedConfig.hooks,
|
|
241
|
+
context,
|
|
242
|
+
rulesToEvaluate,
|
|
243
|
+
resolvedConfig.hookTimeoutMs,
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const { result: evaluationResult, durationMs } = measureTime(() => {
|
|
247
|
+
const results: Array<{
|
|
248
|
+
rule: Rule<TContext, TConsequences>;
|
|
249
|
+
result: ReturnType<typeof evaluateRule<TContext, TConsequences>>;
|
|
250
|
+
}> = [];
|
|
251
|
+
|
|
252
|
+
for (const rule of rulesToEvaluate) {
|
|
253
|
+
const result = evaluateRule(rule, context, resolvedConfig.evaluator, { skipDisabled: true });
|
|
254
|
+
results.push({ rule, result });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return results;
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const ruleResults: ReturnType<
|
|
261
|
+
typeof evaluateRule<TContext, TConsequences>
|
|
262
|
+
>[] = [];
|
|
263
|
+
const matchedRules: Rule<TContext, TConsequences>[] = [];
|
|
264
|
+
const consequences: AggregatedConsequence<TConsequences>[] = [];
|
|
265
|
+
let stoppedEarly = false;
|
|
266
|
+
let stoppedByRuleId: string | undefined;
|
|
267
|
+
let rulesErrored = 0;
|
|
268
|
+
|
|
269
|
+
const conflictResolution =
|
|
270
|
+
options.conflictResolution ?? resolvedConfig.conflictResolution;
|
|
271
|
+
|
|
272
|
+
for (const { rule, result } of evaluationResult) {
|
|
273
|
+
await executeBeforeRuleEvaluation(
|
|
274
|
+
resolvedConfig.hooks,
|
|
275
|
+
rule,
|
|
276
|
+
context,
|
|
277
|
+
resolvedConfig.hookTimeoutMs,
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
ruleResults.push(result);
|
|
281
|
+
|
|
282
|
+
if (result.error) {
|
|
283
|
+
rulesErrored++;
|
|
284
|
+
await executeOnRuleError(
|
|
285
|
+
resolvedConfig.hooks,
|
|
286
|
+
rule,
|
|
287
|
+
result.error,
|
|
288
|
+
resolvedConfig.hookTimeoutMs,
|
|
289
|
+
);
|
|
290
|
+
if (!resolvedConfig.continueOnError) {
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (result.skipped) {
|
|
296
|
+
await executeOnRuleSkip(
|
|
297
|
+
resolvedConfig.hooks,
|
|
298
|
+
rule,
|
|
299
|
+
result.skipReason ?? "Unknown",
|
|
300
|
+
resolvedConfig.hookTimeoutMs,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (result.evaluationTimeMs > resolvedConfig.slowRuleThresholdMs) {
|
|
305
|
+
await executeOnSlowRule(
|
|
306
|
+
resolvedConfig.hooks,
|
|
307
|
+
rule,
|
|
308
|
+
result.evaluationTimeMs,
|
|
309
|
+
resolvedConfig.slowRuleThresholdMs,
|
|
310
|
+
resolvedConfig.hookTimeoutMs,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
await executeAfterRuleEvaluation(
|
|
315
|
+
resolvedConfig.hooks,
|
|
316
|
+
rule,
|
|
317
|
+
result,
|
|
318
|
+
resolvedConfig.hookTimeoutMs,
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
if (result.matched) {
|
|
322
|
+
matchedRules.push(rule);
|
|
323
|
+
|
|
324
|
+
for (const consequence of result.consequences) {
|
|
325
|
+
consequences.push(consequence);
|
|
326
|
+
await executeOnConsequenceCollected(
|
|
327
|
+
resolvedConfig.hooks,
|
|
328
|
+
rule,
|
|
329
|
+
consequence,
|
|
330
|
+
resolvedConfig.hookTimeoutMs,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
await executeOnRuleMatch(
|
|
335
|
+
resolvedConfig.hooks,
|
|
336
|
+
rule,
|
|
337
|
+
context,
|
|
338
|
+
resolvedConfig.hookTimeoutMs,
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
if (rule.stopOnMatch) {
|
|
342
|
+
stoppedEarly = true;
|
|
343
|
+
stoppedByRuleId = rule.id;
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (conflictResolution === "first-match") {
|
|
348
|
+
stoppedEarly = true;
|
|
349
|
+
stoppedByRuleId = rule.id;
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
totalEvaluations++;
|
|
356
|
+
totalMatches += matchedRules.length;
|
|
357
|
+
totalErrors += rulesErrored;
|
|
358
|
+
totalEvaluationTime += durationMs;
|
|
359
|
+
|
|
360
|
+
const executionResult: EngineExecutionResult<TContext, TConsequences> = {
|
|
361
|
+
context,
|
|
362
|
+
results: ruleResults,
|
|
363
|
+
matchedRules,
|
|
364
|
+
consequences,
|
|
365
|
+
totalRulesEvaluated: ruleResults.length,
|
|
366
|
+
totalRulesMatched: matchedRules.length,
|
|
367
|
+
totalRulesSkipped: ruleResults.filter((r) => r.skipped).length,
|
|
368
|
+
totalRulesErrored: rulesErrored,
|
|
369
|
+
executionTimeMs: durationMs,
|
|
370
|
+
stoppedEarly,
|
|
371
|
+
stoppedByRuleId,
|
|
372
|
+
cacheHit: false,
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
if (cacheKey) {
|
|
376
|
+
cache.set(cacheKey, executionResult);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
await executeAfterEvaluation(
|
|
380
|
+
resolvedConfig.hooks,
|
|
381
|
+
executionResult,
|
|
382
|
+
resolvedConfig.hookTimeoutMs,
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
return executionResult;
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
addRule: (input) => addRule(state, input),
|
|
390
|
+
addRules: (inputs) => addRules(state, inputs),
|
|
391
|
+
removeRule: (ruleId) => {
|
|
392
|
+
const result = removeRule(state, ruleId);
|
|
393
|
+
if (result) cache.clear();
|
|
394
|
+
return result;
|
|
395
|
+
},
|
|
396
|
+
updateRule: (ruleId, updates) => {
|
|
397
|
+
const result = updateRule(state, ruleId, updates);
|
|
398
|
+
if (result) cache.clear();
|
|
399
|
+
return result;
|
|
400
|
+
},
|
|
401
|
+
getRule: (ruleId) => getRule(state, ruleId),
|
|
402
|
+
getRules: (filters) => getRules(state, filters),
|
|
403
|
+
enableRule: (ruleId) => {
|
|
404
|
+
const result = enableRule(state, ruleId);
|
|
405
|
+
if (result) cache.clear();
|
|
406
|
+
return result;
|
|
407
|
+
},
|
|
408
|
+
disableRule: (ruleId) => {
|
|
409
|
+
const result = disableRule(state, ruleId);
|
|
410
|
+
if (result) cache.clear();
|
|
411
|
+
return result;
|
|
412
|
+
},
|
|
413
|
+
clearRules: () => {
|
|
414
|
+
clearRules(state);
|
|
415
|
+
cache.clear();
|
|
416
|
+
},
|
|
417
|
+
|
|
418
|
+
addRuleSet: (input) => addRuleSet(state, input),
|
|
419
|
+
getRuleSet: (ruleSetId) => getRuleSet(state, ruleSetId),
|
|
420
|
+
getRuleSets: () => getRuleSets(state),
|
|
421
|
+
removeRuleSet: (ruleSetId) => removeRuleSet(state, ruleSetId),
|
|
422
|
+
|
|
423
|
+
evaluate,
|
|
424
|
+
|
|
425
|
+
clearCache: () => cache.clear(),
|
|
426
|
+
getCacheStats: () => cache.getStats(),
|
|
427
|
+
|
|
428
|
+
getState: () => ({
|
|
429
|
+
rules: state.rules as ReadonlyMap<
|
|
430
|
+
string,
|
|
431
|
+
Rule<TContext, TConsequences>
|
|
432
|
+
>,
|
|
433
|
+
ruleSets: state.ruleSets as ReadonlyMap<string, RuleSet>,
|
|
434
|
+
ruleOrder: state.ruleOrder,
|
|
435
|
+
}),
|
|
436
|
+
|
|
437
|
+
getStats: () => {
|
|
438
|
+
const enabledRules = Array.from(state.rules.values()).filter(
|
|
439
|
+
(r) => r.enabled,
|
|
440
|
+
).length;
|
|
441
|
+
return {
|
|
442
|
+
totalRules: state.rules.size,
|
|
443
|
+
enabledRules,
|
|
444
|
+
disabledRules: state.rules.size - enabledRules,
|
|
445
|
+
totalRuleSets: state.ruleSets.size,
|
|
446
|
+
totalEvaluations,
|
|
447
|
+
totalMatches,
|
|
448
|
+
totalErrors,
|
|
449
|
+
avgEvaluationTimeMs:
|
|
450
|
+
totalEvaluations > 0
|
|
451
|
+
? totalEvaluationTime / totalEvaluations
|
|
452
|
+
: 0,
|
|
453
|
+
cacheHits,
|
|
454
|
+
cacheMisses,
|
|
455
|
+
cacheHitRate:
|
|
456
|
+
cacheHits + cacheMisses > 0
|
|
457
|
+
? cacheHits / (cacheHits + cacheMisses)
|
|
458
|
+
: 0,
|
|
459
|
+
};
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import type { EngineHooks } from "../types/config";
|
|
2
|
+
import type {
|
|
3
|
+
AggregatedConsequence,
|
|
4
|
+
ConsequenceDefinitions,
|
|
5
|
+
DefaultConsequences,
|
|
6
|
+
} from "../types/consequence";
|
|
7
|
+
import type {
|
|
8
|
+
EngineExecutionResult,
|
|
9
|
+
EvaluationContext,
|
|
10
|
+
RuleEvaluationResult,
|
|
11
|
+
} from "../types/evaluation";
|
|
12
|
+
import type { Rule } from "../types/rule";
|
|
13
|
+
import { withTimeout } from "../utils/time";
|
|
14
|
+
|
|
15
|
+
const toError = (error: unknown): Error =>
|
|
16
|
+
error instanceof Error ? error : new Error(String(error));
|
|
17
|
+
|
|
18
|
+
const executeWithTimeout = async (
|
|
19
|
+
hookName: string,
|
|
20
|
+
hookFn: () => void | Promise<void>,
|
|
21
|
+
hooks: EngineHooks<unknown, ConsequenceDefinitions>,
|
|
22
|
+
timeoutMs?: number,
|
|
23
|
+
): Promise<void> => {
|
|
24
|
+
try {
|
|
25
|
+
const promise = Promise.resolve(hookFn());
|
|
26
|
+
if (timeoutMs !== undefined && timeoutMs > 0) {
|
|
27
|
+
await withTimeout(
|
|
28
|
+
promise,
|
|
29
|
+
timeoutMs,
|
|
30
|
+
`Hook '${hookName}' timed out after ${timeoutMs}ms`,
|
|
31
|
+
);
|
|
32
|
+
} else {
|
|
33
|
+
await promise;
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
hooks.onHookError?.(hookName, toError(error));
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const executeBeforeEvaluation = async <
|
|
41
|
+
TContext = unknown,
|
|
42
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
43
|
+
>(
|
|
44
|
+
hooks: EngineHooks<TContext, TConsequences>,
|
|
45
|
+
context: EvaluationContext<TContext>,
|
|
46
|
+
rules: ReadonlyArray<Rule<TContext, TConsequences>>,
|
|
47
|
+
timeoutMs?: number,
|
|
48
|
+
): Promise<void> => {
|
|
49
|
+
if (!hooks.beforeEvaluation) return;
|
|
50
|
+
await executeWithTimeout(
|
|
51
|
+
"beforeEvaluation",
|
|
52
|
+
() => hooks.beforeEvaluation?.(context, rules),
|
|
53
|
+
hooks as EngineHooks<unknown, ConsequenceDefinitions>,
|
|
54
|
+
timeoutMs,
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const executeAfterEvaluation = async <
|
|
59
|
+
TContext = unknown,
|
|
60
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
61
|
+
>(
|
|
62
|
+
hooks: EngineHooks<TContext, TConsequences>,
|
|
63
|
+
result: EngineExecutionResult<TContext, TConsequences>,
|
|
64
|
+
timeoutMs?: number,
|
|
65
|
+
): Promise<void> => {
|
|
66
|
+
if (!hooks.afterEvaluation) return;
|
|
67
|
+
await executeWithTimeout(
|
|
68
|
+
"afterEvaluation",
|
|
69
|
+
() => hooks.afterEvaluation?.(result),
|
|
70
|
+
hooks as EngineHooks<unknown, ConsequenceDefinitions>,
|
|
71
|
+
timeoutMs,
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const executeBeforeRuleEvaluation = async <
|
|
76
|
+
TContext = unknown,
|
|
77
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
78
|
+
>(
|
|
79
|
+
hooks: EngineHooks<TContext, TConsequences>,
|
|
80
|
+
rule: Rule<TContext, TConsequences>,
|
|
81
|
+
context: EvaluationContext<TContext>,
|
|
82
|
+
timeoutMs?: number,
|
|
83
|
+
): Promise<void> => {
|
|
84
|
+
if (!hooks.beforeRuleEvaluation) return;
|
|
85
|
+
await executeWithTimeout(
|
|
86
|
+
"beforeRuleEvaluation",
|
|
87
|
+
() => hooks.beforeRuleEvaluation?.(rule, context),
|
|
88
|
+
hooks as EngineHooks<unknown, ConsequenceDefinitions>,
|
|
89
|
+
timeoutMs,
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const executeAfterRuleEvaluation = async <
|
|
94
|
+
TContext = unknown,
|
|
95
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
96
|
+
>(
|
|
97
|
+
hooks: EngineHooks<TContext, TConsequences>,
|
|
98
|
+
rule: Rule<TContext, TConsequences>,
|
|
99
|
+
result: RuleEvaluationResult<TContext, TConsequences>,
|
|
100
|
+
timeoutMs?: number,
|
|
101
|
+
): Promise<void> => {
|
|
102
|
+
if (!hooks.afterRuleEvaluation) return;
|
|
103
|
+
await executeWithTimeout(
|
|
104
|
+
"afterRuleEvaluation",
|
|
105
|
+
() => hooks.afterRuleEvaluation?.(rule, result),
|
|
106
|
+
hooks as EngineHooks<unknown, ConsequenceDefinitions>,
|
|
107
|
+
timeoutMs,
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const executeOnRuleMatch = async <
|
|
112
|
+
TContext = unknown,
|
|
113
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
114
|
+
>(
|
|
115
|
+
hooks: EngineHooks<TContext, TConsequences>,
|
|
116
|
+
rule: Rule<TContext, TConsequences>,
|
|
117
|
+
context: EvaluationContext<TContext>,
|
|
118
|
+
timeoutMs?: number,
|
|
119
|
+
): Promise<void> => {
|
|
120
|
+
if (!hooks.onRuleMatch) return;
|
|
121
|
+
await executeWithTimeout(
|
|
122
|
+
"onRuleMatch",
|
|
123
|
+
() => hooks.onRuleMatch?.(rule, context),
|
|
124
|
+
hooks as EngineHooks<unknown, ConsequenceDefinitions>,
|
|
125
|
+
timeoutMs,
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const executeOnRuleSkip = async <
|
|
130
|
+
TContext = unknown,
|
|
131
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
132
|
+
>(
|
|
133
|
+
hooks: EngineHooks<TContext, TConsequences>,
|
|
134
|
+
rule: Rule<TContext, TConsequences>,
|
|
135
|
+
reason: string,
|
|
136
|
+
timeoutMs?: number,
|
|
137
|
+
): Promise<void> => {
|
|
138
|
+
if (!hooks.onRuleSkip) return;
|
|
139
|
+
await executeWithTimeout(
|
|
140
|
+
"onRuleSkip",
|
|
141
|
+
() => hooks.onRuleSkip?.(rule, reason),
|
|
142
|
+
hooks as EngineHooks<unknown, ConsequenceDefinitions>,
|
|
143
|
+
timeoutMs,
|
|
144
|
+
);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export const executeOnRuleError = async <
|
|
148
|
+
TContext = unknown,
|
|
149
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
150
|
+
>(
|
|
151
|
+
hooks: EngineHooks<TContext, TConsequences>,
|
|
152
|
+
rule: Rule<TContext, TConsequences>,
|
|
153
|
+
ruleError: Error,
|
|
154
|
+
timeoutMs?: number,
|
|
155
|
+
): Promise<void> => {
|
|
156
|
+
if (!hooks.onRuleError) return;
|
|
157
|
+
await executeWithTimeout(
|
|
158
|
+
"onRuleError",
|
|
159
|
+
() => hooks.onRuleError?.(rule, ruleError),
|
|
160
|
+
hooks as EngineHooks<unknown, ConsequenceDefinitions>,
|
|
161
|
+
timeoutMs,
|
|
162
|
+
);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export const executeOnConsequenceCollected = async <
|
|
166
|
+
TContext = unknown,
|
|
167
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
168
|
+
>(
|
|
169
|
+
hooks: EngineHooks<TContext, TConsequences>,
|
|
170
|
+
rule: Rule<TContext, TConsequences>,
|
|
171
|
+
consequence: AggregatedConsequence<TConsequences>,
|
|
172
|
+
timeoutMs?: number,
|
|
173
|
+
): Promise<void> => {
|
|
174
|
+
if (!hooks.onConsequenceCollected) return;
|
|
175
|
+
await executeWithTimeout(
|
|
176
|
+
"onConsequenceCollected",
|
|
177
|
+
() => hooks.onConsequenceCollected?.(rule, consequence),
|
|
178
|
+
hooks as EngineHooks<unknown, ConsequenceDefinitions>,
|
|
179
|
+
timeoutMs,
|
|
180
|
+
);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const executeOnCacheHit = async <
|
|
184
|
+
TContext = unknown,
|
|
185
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
186
|
+
>(
|
|
187
|
+
hooks: EngineHooks<TContext, TConsequences>,
|
|
188
|
+
key: string,
|
|
189
|
+
result: EngineExecutionResult<TContext, TConsequences>,
|
|
190
|
+
timeoutMs?: number,
|
|
191
|
+
): Promise<void> => {
|
|
192
|
+
if (!hooks.onCacheHit) return;
|
|
193
|
+
await executeWithTimeout(
|
|
194
|
+
"onCacheHit",
|
|
195
|
+
() => hooks.onCacheHit?.(key, result),
|
|
196
|
+
hooks as EngineHooks<unknown, ConsequenceDefinitions>,
|
|
197
|
+
timeoutMs,
|
|
198
|
+
);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export const executeOnCacheMiss = async <
|
|
202
|
+
TContext = unknown,
|
|
203
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
204
|
+
>(
|
|
205
|
+
hooks: EngineHooks<TContext, TConsequences>,
|
|
206
|
+
key: string,
|
|
207
|
+
timeoutMs?: number,
|
|
208
|
+
): Promise<void> => {
|
|
209
|
+
if (!hooks.onCacheMiss) return;
|
|
210
|
+
await executeWithTimeout(
|
|
211
|
+
"onCacheMiss",
|
|
212
|
+
() => hooks.onCacheMiss?.(key),
|
|
213
|
+
hooks as EngineHooks<unknown, ConsequenceDefinitions>,
|
|
214
|
+
timeoutMs,
|
|
215
|
+
);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export const executeOnSlowRule = async <
|
|
219
|
+
TContext = unknown,
|
|
220
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
221
|
+
>(
|
|
222
|
+
hooks: EngineHooks<TContext, TConsequences>,
|
|
223
|
+
rule: Rule<TContext, TConsequences>,
|
|
224
|
+
timeMs: number,
|
|
225
|
+
threshold: number,
|
|
226
|
+
timeoutMs?: number,
|
|
227
|
+
): Promise<void> => {
|
|
228
|
+
if (!hooks.onSlowRule) return;
|
|
229
|
+
await executeWithTimeout(
|
|
230
|
+
"onSlowRule",
|
|
231
|
+
() => hooks.onSlowRule?.(rule, timeMs, threshold),
|
|
232
|
+
hooks as EngineHooks<unknown, ConsequenceDefinitions>,
|
|
233
|
+
timeoutMs,
|
|
234
|
+
);
|
|
235
|
+
};
|