@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +168 -0
  2. package/LICENSE.md +16 -4
  3. package/README.md +106 -23
  4. package/__tests__/builder.test.ts +363 -0
  5. package/__tests__/cache.test.ts +130 -0
  6. package/__tests__/config.test.ts +35 -0
  7. package/__tests__/engine.test.ts +1213 -0
  8. package/__tests__/evaluate.test.ts +339 -0
  9. package/__tests__/exports.test.ts +30 -0
  10. package/__tests__/filter-sort.test.ts +303 -0
  11. package/__tests__/integration.test.ts +419 -0
  12. package/__tests__/money-integration.test.ts +149 -0
  13. package/__tests__/validation.test.ts +862 -0
  14. package/biome.json +39 -0
  15. package/docs/MIGRATION-v3.md +118 -0
  16. package/fot.config.ts +5 -0
  17. package/package.json +31 -67
  18. package/src/analyzer/analysis.ts +401 -0
  19. package/src/builder/conditions.ts +321 -0
  20. package/src/builder/rule.ts +192 -0
  21. package/src/cache/cache.ts +135 -0
  22. package/src/cache/noop.ts +20 -0
  23. package/src/core/evaluate.ts +185 -0
  24. package/src/core/filter.ts +85 -0
  25. package/src/core/group.ts +103 -0
  26. package/src/core/sort.ts +90 -0
  27. package/src/engine/engine.ts +462 -0
  28. package/src/engine/hooks.ts +235 -0
  29. package/src/engine/state.ts +322 -0
  30. package/src/index.ts +303 -0
  31. package/src/optimizer/index-builder.ts +381 -0
  32. package/src/serialization/serializer.ts +408 -0
  33. package/src/simulation/simulator.ts +359 -0
  34. package/src/types/config.ts +184 -0
  35. package/src/types/consequence.ts +38 -0
  36. package/src/types/evaluation.ts +87 -0
  37. package/src/types/rule.ts +112 -0
  38. package/src/types/state.ts +116 -0
  39. package/src/utils/conditions.ts +108 -0
  40. package/src/utils/hash.ts +30 -0
  41. package/src/utils/id.ts +6 -0
  42. package/src/utils/time.ts +42 -0
  43. package/src/validation/conflicts.ts +440 -0
  44. package/src/validation/integrity.ts +473 -0
  45. package/src/validation/schema.ts +386 -0
  46. package/src/versioning/version-store.ts +337 -0
  47. package/tsconfig.json +29 -0
  48. package/dist/index.cjs +0 -3088
  49. package/dist/index.d.cts +0 -1173
  50. package/dist/index.d.ts +0 -1173
  51. 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
+ };