4.9 KiB
Optimizer — @suaveplan/rules/optimizer
(no submodules)
Purpose
Pure functional optimiser for the Rule tree. Applies four independent passes — flatten (collapse nested AND-of-AND / OR-of-OR), dedupe (drop structurally-equal siblings via hashRule), simplify (unwrap single-child AND/OR), sort (rearrange AND/OR children by ascending estimated cost for short-circuit benefit) — each toggleable via OptimizeOptions. Companion analytics — analyzeRule, estimateCost, findFields, hasAsyncComponents, countRules, isOptimisationStable — expose the shapes the optimiser uses internally so callers can introspect rules without re-walking.
The optimiser never changes rule semantics — both passes either reduce work or reorder commutative siblings. NOT is preserved unchanged (single-child by design); group operators are sorted only when explicitly and / or.
Features
- Four toggleable passes; every pass defaults to
true. - Structural hashing via
serializer.hashRulefor dedupe equality. RuleStatsanalytics:totalRules,maxDepth,fields,hasAsync,estimatedCost.OptimizeResultreportsbeforeandafterstats so callers can measure savings.- Per-operator cost estimates favour short-circuiting cheap conditions first.
Usage: Basic Example
import { and, condition } from "@suaveplan/rules/builder";
import { optimizeRule } from "@suaveplan/rules/optimizer";
const r = and(
and(condition("a", "equals", 1), condition("b", "equals", 2)),
condition("c", "equals", 3),
);
const { rule: optimised, before, after } = optimizeRule(r);
// optimised is the flat 3-child AND; before.totalRules > after.totalRules
Usage: Advanced Example
import { and, condition } from "@suaveplan/rules/builder";
import {
analyzeRule,
findFields,
optimizeRule,
} from "@suaveplan/rules/optimizer";
const policy = and(
condition("region", "matches", "^US|CA$"), // cost 10
condition("age", "greaterThanOrEqual", 18), // cost 1
condition("verified", "equals", true), // cost 1
);
// Only run the sort pass — keep nested structure intact
const { rule } = optimizeRule(policy, {
flatten: false,
dedupe: false,
simplify: false,
sort: true,
});
const stats = analyzeRule(rule);
console.log(stats); // { totalRules: 4, maxDepth: 1, fields: [...], hasAsync: false, estimatedCost: 12 }
console.log(findFields(rule)); // ["region", "age", "verified"]
API Reference
optimizeRule(rule, options?) → OptimizeResult
Runs the configured passes. Returns { rule, before, after } where stats are from analyzeRule.
interface OptimizeOptions
| Option | Default | Effect |
|---|---|---|
flatten |
true |
Collapse same-operator nested logicals (AND-of-AND, OR-of-OR). |
dedupe |
true |
Drop structurally-equal siblings (uses hashRule). |
simplify |
true |
Unwrap single-child AND/OR (NOT is preserved). |
sort |
true |
Reorder AND/OR children by ascending estimated cost. |
Analytics
analyzeRule(rule)→RuleStats.estimateCost(rule)→ number — sum of per-node costs.findFields(rule)→ list of distinct field paths touched.hasAsyncComponents(rule)→truewhen any node is aCustomRuleorComputedRule.countRules(rule)→ total node count.isOptimisationStable(rule)→truewhenoptimizeRule(rule).rulehashes the same as the input.
Cost model
| Node | Cost |
|---|---|
equals / notEquals / strict* / gt / gte / lt / lte / isNull / isNotNull / exists / notExists |
1 |
matches / notMatches |
10 |
containsAll / containsAny / overlaps |
5 |
| every other condition operator | 2 |
custom / computed |
20 |
logical / group |
0 (cost lives on children) |
Per-node costs are constants; the model assumes no live profiling of operator throughput. Callers that have measured workloads can build their own cost model and reorder explicitly via the builder.
Implementation Notes
- The dedupe pass canonicalises children by
hashRuleso commutative-order-different but structurally-identical children collapse together. The first occurrence wins. - Sort is stable per the JS spec; equal-cost children preserve input order.
- The
simplifypass intentionally does NOT unwrapnotbecause a single-child NOT is the only valid form — collapsing it would lose the inversion. Similarly, single-child groups are not unwrapped because theirnameis a contract for callers. isOptimisationStableis intended for build-time CI checks: rules that hash differently before vs after optimisation indicate either dead code in the rule tree or an opportunity to commit the optimised form back into source.- Field discovery (
findFields) walks bothConditionRule.fieldandComputedRule.field. Groupnameis NOT considered a field.