A simple, async rule engine for TypeScript/JavaScript.
The engine takes a context (immutable, deep-frozen input), a list of rules, and an optional initial result. On each iteration, it finds the first rule whose evaluate function returns true and runs its action, which returns partial updates merged into the result. This repeats until no rule evaluates to true.
npm install @arvarus/basic-rules-engineconst ruleEngine = new Engine(context, rules, initialResult);
await ruleEngine.run();
console.log(ruleEngine.getResult());import Engine from '@arvarus/basic-rules-engine';
const context = { startValue: 0, endValue: 3 };
const initialResult = {};
const rules = [
{
name: 'Init result',
evaluate: async (context, result) => result.count == undefined,
action: async (context) =>
Promise.resolve({ count: context.startValue, flag: false }),
},
{
name: 'Increment count when less than 3',
evaluate: async (context, result) =>
Promise.resolve(result.flag !== true && (result.count ?? 0) < context.endValue),
action: async (context, result) =>
Promise.resolve({ count: (result.count ?? 0) + 1 }),
},
{
name: 'Set flag when count equals 3',
evaluate: async (context, result) =>
Promise.resolve(result.count === context.endValue && !result.flag),
action: async () => Promise.resolve({ flag: true }),
},
];
const ruleEngine = new Engine(context, rules, initialResult);
await ruleEngine.run();
console.log(ruleEngine.getResult());
// { count: 3, flag: true }More runnable examples live in the examples/ folder (not included in the npm package):
- 01-counter.js — the basics: context, rules, result merging
- 02-shopping-cart.js — ordered business rules (pricing and discounts)
- 03-approval-workflow.js —
swapBufferandmaxIterations
| Parameter | Type | Description |
|---|---|---|
context |
object |
Immutable input data (deep-frozen) |
rules |
Rule[] |
Ordered list of rules (default: []) |
initialResult |
object |
Starting result state (default: {}) |
{
name?: string;
swapBuffer?: object; // mutable storage local to the rule, shared between evaluate and action
evaluate: (context, result) => Promise<boolean>;
action: (context, result) => Promise<Partial<Result>>;
}| Method | Description |
|---|---|
setRules(rules) |
Replace the rule list (chainable) |
setInitialResult(result) |
Replace the current result (partial result accepted, chainable) |
getResult() |
Return the current result |
run(options?) |
Execute rules; resolves with the final result |
| Option | Type | Default | Description |
|---|---|---|---|
maxIterations |
number |
1000 |
Maximum rule evaluations; throws if exceeded (prevents infinite loops) |
- The
evaluatefunction must return a boolean - The
evaluatefunction must not change the context or the result - The
actionfunction must not change the context or the result directly — return a partial result instead - Rules are evaluated in order; the first matching rule runs, then evaluation restarts from the top
npm run compile # compile TypeScript (bin/, tests excluded)
npm test # run tests with coverage (100% enforced)
npm run lint # lint
npm run format # format with PrettierThe package has no runtime dependencies. Tests enforce 100% code coverage (coverageThreshold in jest.config.ts).
- 3.0.x:
- simplified build (no more
copyfiles) setInitialResultaccepts a partial result- runnable
examples/folder - 100% coverage enforced,
exportsfield (internal modules likedeep-freezeare no longer deep-importable)
- simplified build (no more
- 2.2.x: removed external
deep-freeze-strictdependency (built-in implementation) - 2.1.x:
runnow acceptsRunOptions(maxIterations) - 2.0.x: (Breaking change)
runnow returns the result - 1.0.x: initial version
GPL-3.0-only