Local Evaluation of Salesforce RCA - CML
“When every configurator click pays a server round-trip, move evaluation to the client - but do it with a real language runtime, not duplicated if-statements.”
An independent compiler experiment: parsing a declarative constraint language into a typed AST and running it in the browser - in LWC and in React - so configurators stop paying a server round-trip per click.
CONSTRAINT COMPILERTYPESCRIPT ASTHEADLESS COMMERCEREACT & LWCCLIENT EVALUATION
Interactive evaluation moved from per-click server latency to in-memory execution (low single-digit ms across my differential harness models)
What Made the Problem Difficult
This is independent work - no employer code, no client config, no proprietary schemas. I kept seeing the same shape in enterprise quoting: a product model with hundreds of interdependent rules (eligibility, co-requisites, pricing attributes), one engine on the server, and two or three hand-maintained copies of the logic leaking into storefront JavaScript. I wanted to know whether a single, honest compiler core could serve both a CRM-embedded UI and a headless React surface without drift. So I built it as a weekend project, against a rule grammar I defined myself, informed only by public documentation patterns.
Core Platform Constraint
Systemic FrictionThe failure mode I kept observing in Salesforce Revenue Cloud (RCA) configurators:
Every toggle or attribute edit goes remote. At 300–800ms a click, fast configurators feel broken, and overlapping responses overwrite newer state.
The storefront either polls the server per keystroke (slow, rate-limit hungry) or re-implements rules by hand.
Two implementations of the same pricing logic always diverge. The bug reports arrive months later, as misquotes.
In LWC, eval() and new Function() are off the table under Lightning Web Security, so 'just compile to JS strings' is not an option.
- !No server calls during interaction: once rules are loaded, option and attribute changes evaluate locally
- !One rule implementation: the LWC path and the React path execute the same AST, not two ports
- !Sandbox-safe: no eval(), no new Function(), strict CSP-compatible
- !Fast enough to disappear: multi-rule evaluation in low single-digit milliseconds on commodity hardware
- !Small and shippable: zero-dependency bundle (~30KB class) via static resources and NPM
What System Was Designed
Four stages, all in plain TypeScript, no dependencies: 1. Scanner & parser: a hand-written lexer and recursive-descent parser turns rule text into a typed AST - precedence, negation, nested booleans, function calls. Nothing clever, just careful. 2. Scope check & dependency graph: variable bindings resolve against a catalog schema I define per demo model, producing a DAG so cascading attributes (A changes B changes C) evaluate in topological order with cycle detection. 3. Tree-walking evaluator: a small interpreter with deterministic coercion and memoised subtrees. Boring on purpose - the same input always produces the same output, on either runtime. 4. Two packages, one core: ESM for React/Next.js, IIFE/UMD for LWC via platformResourceLoader. A differential harness runs thousands of generated cases through both and diffs the results.
Pivotal Architectural Choices
Key Architectural Decisions
Client-side evaluation core vs. per-click server calls
The default I saw everywhere was remote evaluation per interaction - correct, but slow and racy.
- Server-side evaluators per keystroke
- Remote evaluation REST API
- Regex-based client parsing
The network was the budget, not the CPU. Moving evaluation client-side removed the round-trip entirely; keeping it a proper AST (not string hacks) kept semantics identical across runtimes.
Tree-walking interpreter vs. generating JS functions
new Function() would be faster on paper, but LWS and enterprise CSP forbid it - and rightly so.
- Dynamic eval() code generation
- Server-compiled bytecode cache
Compliance first, then speed. Short-circuit booleans and memoised subtrees kept my test models in the low single-digit milliseconds, which is fast enough that nobody notices the interpreter.
One shared library vs. Apex-plus-JavaScript dual maintenance
Every team I watched tried the dual implementation at least once. Every one of them paid for it in misquotes.
- Hand-maintained storefront re-implementation
- Iframing the CRM UI
- Proxying storefront clicks through a gateway
One grammar, one parser, one evaluator. Drift becomes structurally impossible rather than process-prevented.
Topological DAG resolution vs. multi-pass dirty-checking
Cascading attributes loop forever under naive re-evaluation.
- Bounded multi-pass loops
- Chained event listeners
Single pass, deterministic order, and circular rules fail loudly at authoring time instead of freezing a UI thread.
What Was Actually Built
Hand-wrote the scanner and parser - no generator dependencies - covering arithmetic, comparisons, nested AND/OR/NOT, and domain functions, with error messages that point at the offending token.
Built the dependency graph as an adjacency list over attribute references; Kahn's algorithm gives the evaluation order and rejects cycles before anything renders.
Kept the evaluator allocation-light: pre-sized scopes, memoised pure subtrees, and no code generation of any kind, so it loads cleanly under LWS.
Shipped two artifacts from one source: ESM for bundlers and a minified IIFE for LWC static resources; the LWC wrapper is a thin loader, not a fork.
Wrote a differential harness that generates thousands of rule/input combinations and asserts byte-identical outcomes between the LWC bundle and the React import - that harness is the real parity guarantee.
Added a build-time rule linter that flags unknown attributes, circular references, and dead constraints before deployment, because authoring errors are cheaper than runtime ones.
Deliberate Architectural Compromises
Trade-offs & Mitigations
⚖Bundle weight vs. network chatter
Interaction evaluation happens in memory with no network at all.
The client downloads and parses a small compiler library up front.
Kept it dependency-free and async-loadable; on my test models it parses in well under a frame.
⚖Bounded grammar vs. full macro language
Evaluation time stays predictable - no runaway recursion in the UI thread.
No unbounded recursive macros in the rule language.
Everything I actually needed in configurators fits declarative constraints; macros were never worth the risk.
⚖Interpreter overhead vs. JIT-compiled functions
Fully sandbox- and CSP-safe with no dynamic code.
Tree-walking is slower than native JIT per node.
Memoisation plus short-circuiting keeps realistic models in the low milliseconds - the network saving dwarfs it.
Verified Outcomes
Interactive evaluation moved from per-click server latency to in-memory execution (low single-digit ms across my differential harness models)
One rule implementation serves both LWC and React paths - verified by differential tests, not by convention
Authoring errors surface at build time (unknown attributes, cycles, dead rules) instead of as production misquotes
Sandbox-safe by construction: zero dynamic code generation, loads under LWS and strict CSP
Reusable across demo models without modification - the engine knows nothing about any employer's catalog
System Schematic & Data Flow
Declarative rules compile to a typed AST once, then evaluate in memory on either runtime - no per-click server calls.
Text alternative for screen readers: Architecture flow: Rule Source (demo grammar) to TS Lexer & Parser via Recursive-Descent AST; TS Lexer & Parser to Scope Check & DAG via Bindings & Order; Scope Check & DAG to Deterministic Evaluator via In-Memory, Single-Digit ms; Deterministic Evaluator to LWC & React Targets via One Core, Two Bundles
Architectural Conclusion
“Most configurator slowness is not a UI problem - it is an evaluation-placement problem. Put a real language runtime where the clicks happen and the whole architecture simplifies.”
- •When the platform boundary is the bottleneck, owning a small compiler beats adding another cache layer.
- •A differential test harness across runtimes is worth more than any parity claim in prose.
- •Interpreters earn their keep in sandboxed environments: predictable, auditable, and fast enough.