Skip to content

Repository files navigation

Flywheel

A powerful, hierarchical rule engine for dynamic field configuration using a condition-action pattern. Flywheel enables complex form logic, calculations, and field state management with automatic dependency tracking and intelligent caching.

Table of Contents

Features

  • 🎯 Condition-Action Rules: Define when conditions trigger specific actions
  • 🔄 Automatic Dependency Tracking: Intelligent evaluation order and caching
  • Performance Optimized: Smart caching with dependency-based invalidation
  • 🏗️ Modular Architecture: Separate components for field state, caching, and dependency management
  • 🚀 Field Initialization: Context-aware field initialization with init actions
  • 🧩 Extensible: Custom operators, actions, and field state properties with dependency tracking
  • 🔍 Debug-Friendly: Comprehensive validation and evaluation tracing
  • 📊 Rich Logic System: 25+ built-in operators with unlimited nesting
  • 🏗️ Type-Safe: Full TypeScript support with comprehensive type definitions

Installation

npm install flywheel-rules

Quick Start

import{RuleEngine}from'flywheel-rules';// Initialize the rule engineconstengine=newRuleEngine({onEvent: (eventType,params)=>{console.log(`Event: ${eventType}`,params);}});// Define rulesconstruleSet={field1: [{condition: {">": [{"var": ["age"]},18]},action: {set: {target: "field1.isVisible",value: true}},priority: 1}]};// Load rules and evaluateengine.loadRuleSet(ruleSet);engine.updateFieldValue({age: 25});constfieldState=engine.evaluateField("field1");console.log(fieldState);// { isVisible: true, isRequired: false, ... }

Core Concepts

Rules Structure

Rules follow a condition-action pattern with priority-based execution:

interfaceFieldRule{condition: Logic;// When to executeaction: Action;// What to execute priority: number;// Execution order (lower = first)}interfaceRuleSet{[fieldName: string]: FieldRule[];}

Field State

Each field maintains state properties that can be modified by rules:

interfaceFieldState{value?: any;// Field valueisVisible: boolean;// Field visibilityisRequired: boolean;// Field requirementcalculatedValue?: any;// Computed values// ... extensible via onFieldStateCreation}

Field Initialization

Fields can be initialized with default state and values using init actions:

// Context-based field initialization{condition: {"==": [{"var": ["userType"]},"premium"]},action: {"init": {fieldState: {isVisible: true,theme: "premium"},fieldValue: "default-premium-value"}},priority: 0// Init actions typically run first}

API Reference

RuleEngine

Constructor

constructor(options?: RuleEngineOptions)

Options:

  • onEvent?: Handler for custom events triggered by rules
  • onFieldStateCreation?: Customize default field state properties

Core Methods

// Load and validate rule setloadRuleSet(ruleSet: RuleSet): void// Update field values and trigger re-evaluationupdateFieldValue(fieldUpdates: Record<string,any>): string[]// Get current field valuegetFieldValue(fieldName: string): any
// Evaluate specific field and return complete stateevaluateField(fieldName: string): FieldState// Register reusable condition logicregisterSharedRules(sharedRules: Record<string,Logic>): void// Register lookup tables for data relationshipsregisterLookupTables(tables: LookupTable[]): void// Register custom action types with optional dependency trackingregisterActionHandler(params: {actionType: string;handler: (payload: any,context: any,helpers?: ActionHandlerOptions)=>void;dependencyVisitor?: CustomActionDependencyVisitor;}): void// Register custom logic operators with optional dependency trackingregisterCustomLogic(params: {operator: string;handler: (args: any[],context: any)=>any;dependencyVisitor?: CustomLogicDependencyVisitor;}): void// Debug utilitiesgetDependenciesOf(fieldName: string): string[]getLogicResolver(): LogicResolver

LookupTable

interfaceLookupTable{table: any[];// Array of lookup recordsprimaryKey: string;// Field to match againstname?: string;// Optional table name}

Logic Operators

Variable Access

{"var": ["fieldName"]}// Access field value{"var": ["field.nested.property"]}// Dot notation support{"var": ["$"]}// Current item in array operations

Arithmetic Operations

{"+": [1,2,3]}// Addition: 6{"-": [10,3]}// Subtraction: 7{"*": [4,5]}// Multiplication: 20{"/": [15,3]}// Division: 5{"sqrt": [16]}// Square root: 4{"floor": [3.7]}// Floor: 3{"abs": [-5]}// Absolute: 5

Comparison Operations

{">": [5,3]}// Greater than: true{"<": [2,7]}// Less than: true{">=": [5,5]}// Greater or equal: true{"<=": [3,5]}// Less or equal: true{"==": ["hello","hello"]}// Equal: true{"!=": [1,2]}// Not equal: true

Logical Operations

{"and": [true,false]}// Logical AND: false{"or": [true,false]}// Logical OR: true{"not": [true]}// Logical NOT: false

Conditional Logic

{"if": [condition,trueValue,falseValue]}// Example{"if": [{">": [{"var": ["age"]},18]},"Adult","Minor"]}

Array Operations

// Test if any element matches condition{"some": [{"var": ["items"]},{">": [{"var": ["$"]},10]}]}// Test if all elements match condition {"every": [{"var": ["scores"]},{">=": [{"var": ["$"]},60]}]}// Transform array elements{"map": [{"var": ["prices"]},{"*": [{"var": ["$"]},1.1]}]}

Field State Access

{"fieldState": ["otherField.isVisible"]}// Access other field's state{"fieldState": ["field.calculatedValue"]}// Access calculated values

Lookup Operations

// Lookup table syntax sugar{"varTable": "userId@users.name"}// Explicit lookup operation{"lookup": ["users",{"var": ["userId"]},"name"]}

Shared Rules

// Reference shared rule{"$ref": "isAdult"}// Register shared rulesengine.registerSharedRules({"isAdult": {">=": [{"var": ["age"]},18]},"hasEmail": {"!=": [{"var": ["email"]},""]}});

Action Types

Field Initialization Actions

// Initialize field state and/or value (processed before other rules){"init": {fieldState: {isVisible: true,currency: "USD"},fieldValue: "default-value"}}// Conditional initialization based on context{condition: {"==": [{"var": ["user.role"]},"premium"]},action: {"init": {fieldState: {paymentMethods: ["card","paypal","crypto"],allowSavedCards: true},fieldValue: {paymentMethod: "card"}}},priority: 0// Init rules typically use priority 0 or negative}

Field Value and State Actions

// Set any field property (value or state){"set": {target: "fieldName.value",value: "Hello World"}}{"set": {target: "fieldName.isVisible",value: true}}{"set": {target: "fieldName.customProperty",value: "custom"}}// Copy value from another field{"copy": {source: "sourceField.value",target: "targetField.value"}}

Calculation Actions

// Calculate field values{"calculate": {target: "total.value",formula: {"+": [{"var": ["price.value"]},{"var": ["tax.value"]}]}}}// Calculate field state properties {"calculate": {target: "field.calculatedValue",formula: {"+": [{"var": ["price.value"]},{"var": ["tax.value"]}]}}}

Event Actions

// Trigger custom events{"trigger": {event: "validation_failed",params: {field: "email"}}}

Batch Actions

// Execute multiple actions{"batch": [{"set": {target: "field1.isVisible",value: true}},{"calculate": {target: "total.value",formula: {"+": [1,2]}}},{"trigger": {event: "form_updated"}}]}

Custom Dependency Tracking

Flywheel provides powerful dependency tracking for custom operations and actions, ensuring proper cache invalidation and evaluation order.

Dependency Visitor Interfaces

// For custom logic operatorsinterfaceCustomLogicDependencyVisitor{visitLogic(params: {operator: string;operands: any;}): DependencyInfo;}// For custom actionsinterfaceCustomActionDependencyVisitor{visitAction(params: {actionType: string;payload: any;}): DependencyInfo;}interfaceDependencyInfo{dependencies: string[];// Fields this operation reads fromdependents: string[];// Fields this operation writes to}

Custom Action with Dependency Tracking

// Create a dependency visitor for a merge actionconstmergeActionVisitor: CustomActionDependencyVisitor={visitAction: ({ payload }): DependencyInfo=>{return{dependencies: payload.sources||[],// Fields being readdependents: payload.target ? [payload.target] : []// Fields being written};}};// Register action with dependency trackingengine.registerActionHandler({actionType: 'merge',handler: (payload,context,helpers)=>{constresult=payload.sources.map(field=>context[field]?.value||'').join(' ');helpers?.onFieldPropertySet?.(payload.target+'.value',result);},dependencyVisitor: mergeActionVisitor});// Now the engine properly tracks dependenciesconstruleSet={fullName: [{condition: true,action: {merge: {sources: ["firstName","lastName"],target: "fullName"}}asany,priority: 1}]};engine.loadRuleSet(ruleSet);engine.updateFieldValue({firstName: "John",lastName: "Doe"});// When firstName changes, fullName is automatically invalidated and re-evaluatedconstinvalidated=engine.updateFieldValue({firstName: "Jane"});console.log(invalidated);// includes 'fullName'

Custom Logic with Dependency Tracking

// Create a dependency visitor for a compareFields operatorconstcompareFieldsVisitor: CustomLogicDependencyVisitor={visitLogic: ({ operands }): DependencyInfo=>{// This operator compares two field valuesconst[field1,field2]=operands;return{dependencies: [field1,field2],// Reads from both fieldsdependents: []// Logic operators don't write to fields};}};// Register logic with dependency trackingengine.registerCustomLogic({operator: 'compareFields',handler: (args,context)=>{const[field1,field2]=args;constvalue1=context[field1]?.value;constvalue2=context[field2]?.value;returnvalue1===value2;},dependencyVisitor: compareFieldsVisitor});// Use in rulesconstvalidationRules={passwordMatch: [{condition: {compareFields: ["password","confirmPassword"]},action: {set: {target: "submitButton.isVisible",value: true}},priority: 1}]};engine.loadRuleSet(validationRules);// submitButton now properly depends on both password and confirmPassword fields

Benefits of Dependency Tracking

Automatic Cache Invalidation: When dependencies change, dependent fields are automatically invalidated and re-evaluated.

Correct Evaluation Order: Dependencies are resolved in the correct order, preventing stale data.

Performance Optimization: Only affected fields are re-evaluated when changes occur.

Debug Visibility: Use getDependenciesOf() to see the complete dependency graph including custom operations.

// Debug dependency relationshipsconsole.log(engine.getDependenciesOf("fullName"));// Output: ["firstName", "lastName"] - includes custom action dependencies

Without Dependency Tracking (Problems)

// ❌ Without dependency visitor - cache invalidation brokenengine.registerActionHandler({actionType: 'concat',handler: (payload,context,helpers)=>{// This works, but dependency tracking is brokenconstresult=payload.sources.map(s=>context[s]?.value||'').join('');helpers?.onFieldPropertySet?.(payload.target+'.value',result);}// No dependencyVisitor provided!});// When firstName changes, fullName won't be invalidated// Leading to stale data and incorrect results

With Dependency Tracking (Correct)

// ✅ With dependency visitor - proper cache invalidationconstconcatVisitor: CustomActionDependencyVisitor={visitAction: ({ payload })=>({dependencies: payload.sources||[],dependents: payload.target ? [payload.target] : []})};engine.registerActionHandler({actionType: 'concat',handler: (payload,context,helpers)=>{constresult=payload.sources.map(s=>context[s]?.value||'').join('');helpers?.onFieldPropertySet?.(payload.target+'.value',result);},dependencyVisitor: concatVisitor// ✅ Proper dependency tracking});// Now firstName changes correctly invalidate fullName

Examples

Field Initialization with Context

constengine=newRuleEngine({onFieldStateCreation: ()=>({theme: 'light',readOnly: false,customProps: {}})});constinitRules={"userSettings": [// Premium users get advanced settings{condition: {"==": [{"var": ["user.subscription"]},"premium"]},action: {"init": {fieldState: {isVisible: true,theme: 'dark',features: ['advanced-analytics','custom-themes','api-access'],maxExports: 'unlimited'},fieldValue: {theme: 'dark',notifications: true}}},priority: 0},// Free users get basic settings{condition: {"==": [1,1]},// Fallback ruleaction: {"init": {fieldState: {isVisible: true,theme: 'light',features: ['basic-analytics'],maxExports: 5},fieldValue: {theme: 'light',notifications: false}}},priority: 1}]};engine.loadRuleSet(initRules);engine.updateFieldValue({user: {subscription: 'premium'}});constsettings=engine.evaluateField("userSettings");console.log(settings);// {// isVisible: true,// theme: 'dark',// features: ['advanced-analytics', 'custom-themes', 'api-access'],// maxExports: 'unlimited',// readOnly: false,// customProps: {}// }

Dynamic Form Visibility

constengine=newRuleEngine();constformRules={"spouseInfo": [{condition: {"==": [{"var": ["maritalStatus.value"]},"married"]},action: {"set": {target: "spouseInfo.isVisible",value: true}},priority: 1}],"dependentCount": [{condition: {">": [{"var": ["children.value"]},0]},action: {"set": {target: "dependentCount.isVisible",value: true}},priority: 1}]};engine.loadRuleSet(formRules);// User selects "married" - spouse info becomes visibleengine.updateFieldValue({maritalStatus: "married"});console.log(engine.evaluateField("spouseInfo").isVisible);// true// User enters children count - dependent section appearsengine.updateFieldValue({children: 2});console.log(engine.evaluateField("dependentCount").isVisible);// true

Complex Calculations

constcalculationRules={"totalPrice": [{condition: true,// Always executeaction: {"calculate": {target: "totalPrice.calculatedValue",formula: {"+": [{"*": [{"var": ["quantity.value"]},{"var": ["unitPrice.value"]}]},{"if": [{">=": [{"var": ["quantity.value"]},10]},0,// No tax for bulk orders{"*": [{"*": [{"var": ["quantity.value"]},{"var": ["unitPrice.value"]}]},0.08]}]}]}}},priority: 1}],"submitButton": [{condition: {">": [{"fieldState": ["totalPrice.calculatedValue"]},0]},action: {"set": {target: "submitButton.isVisible",value: true}},priority: 1}]};engine.loadRuleSet(calculationRules);engine.updateFieldValue({quantity: 5,unitPrice: 20.00});consttotalField=engine.evaluateField("totalPrice");console.log(totalField.calculatedValue);// 108.00 (100 + 8% tax)constsubmitButton=engine.evaluateField("submitButton");console.log(submitButton.isVisible);// true

Lookup Table Integration

// Register product catalogengine.registerLookupTables([{name: "products",primaryKey: "id",table: [{id: "P001",name: "Laptop",category: "electronics",price: 999.99},{id: "P002",name: "Book",category: "media",price: 15.99},{id: "P003",name: "Shirt",category: "clothing",price: 29.99}]}]);constproductRules={"productName": [{condition: {"!=": [{"var": ["selectedProductId.value"]},""]},action: {"calculate": {target: "productName.calculatedValue",formula: {"varTable": "selectedProductId.value@products.name"}}},priority: 1}],"shippingSection": [{condition: {"==": [{"varTable": "selectedProductId.value@products.category"},"electronics"]},action: {"set": {target: "shippingSection.isVisible",value: true}},priority: 1}]};engine.loadRuleSet(productRules);engine.updateFieldValue({selectedProductId: "P001"});console.log(engine.evaluateField("productName").calculatedValue);// "Laptop"console.log(engine.evaluateField("shippingSection").isVisible);// true

Shared Rules and Complex Logic

// Register reusable business logicengine.registerSharedRules({"isAdult": {">=": [{"var": ["age.value"]},18]},"hasValidEmail": {"and": [{"!=": [{"var": ["email.value"]},""]},{"like": [{"var": ["email.value"]},"*@*.*"]}]},"isEligibleForDiscount": {"and": [{"$ref": "isAdult"},{">": [{"var": ["membershipYears.value"]},2]}]}});constmembershipRules={"discountField": [{condition: {"$ref": "isEligibleForDiscount"},action: {"set": {target: "discountField.isVisible",value: true}},priority: 1},{condition: {"$ref": "isEligibleForDiscount"},action: {"calculate": {target: "discountField.calculatedValue",formula: {"*": [{"var": ["orderTotal.value"]},0.1]}}},priority: 2}],"emailRequired": [{condition: {"not": [{"$ref": "hasValidEmail"}]},action: {"set": {target: "email.isRequired",value: true}},priority: 1}]};engine.loadRuleSet(membershipRules);engine.updateFieldValue({age: 25,membershipYears: 3,orderTotal: 100,email: ""});console.log(engine.evaluateField("discountField").isVisible);// trueconsole.log(engine.evaluateField("discountField").calculatedValue);// 10console.log(engine.evaluateField("email").isRequired);// true

Event Handling and Custom Actions

constengine=newRuleEngine({onEvent: (eventType,params)=>{switch(eventType){case'validation_error':
console.error('Validation failed:',params);break;case'calculation_complete':
console.log('Calculation result:',params.result);break;case'audit_log':
// Log to external systembreak;}}});// Register custom action with dependency trackingconstvalidateVisitor: CustomActionDependencyVisitor={visitAction: ({ payload })=>({dependencies: payload.field ? [payload.field] : [],// Reads from the field being validateddependents: []// Validation doesn't write to fields directly})};engine.registerActionHandler({actionType: 'validate',handler: (payload,context,helpers)=>{const{ field, rules }=payload;constfieldValue=engine.getFieldValue(field);constisValid=validateField(fieldValue,rules);if(!isValid){// Trigger event through the helper systemhelpers?.onEvent?.('validation_error',{ field });}},dependencyVisitor: validateVisitor});constvalidationRules={"passwordConfirm": [{condition: {"!=": [{"var": ["password.value"]},{"var": ["confirmPassword.value"]}]},action: {"trigger": {event: "validation_error",params: {field: "confirmPassword",message: "Passwords do not match"}}},priority: 1}],"emailField": [{condition: {"!=": [{"var": ["email.value"]},""]},action: {"validate": {field: "email",rules: ["required","email_format"]}},priority: 1}],"submitButton": [{condition: {"==": [{"var": ["password.value"]},{"var": ["confirmPassword.value"]}]},action: {"set": {target: "submitButton.isVisible",value: true}},priority: 1}]};engine.loadRuleSet(validationRules);// Trigger validation when email is enteredengine.updateFieldValue({email: "invalid-email",password: "secret123",confirmPassword: "secret123"});// This will trigger the custom 'validate' action for emailField

Advanced Features

Custom Field State Properties

constengine=newRuleEngine({onFieldStateCreation: (props)=>({
...props,// Add custom propertiespermissions: {read: true,write: true},validation: {errors: [],warnings: []},metadata: {lastModified: null}})});// Rules can now target custom propertiesconstcustomRules={"adminField": [{condition: {"==": [{"var": ["userRole.value"]},"admin"]},action: {"set": {target: "adminField.permissions.write",value: true}},priority: 1}]};

Custom Logic Operators

// Register custom operators with dependency trackingconstcontainsVisitor: CustomLogicDependencyVisitor={visitLogic: ({ operands })=>({// If first operand is a field reference, depend on itdependencies: typeofoperands[0]==='string'&&!operands[0].includes('.') ? [operands[0]] : [],dependents: []})};constcurrencyVisitor: CustomLogicDependencyVisitor={visitLogic: ({ operands })=>({dependencies: typeofoperands[0]==='string' ? [operands[0]] : [],dependents: []})};engine.registerCustomLogic({operator: 'contains',handler: (args,context)=>{const[haystack,needle]=args;returnString(haystack).includes(String(needle));},dependencyVisitor: containsVisitor});engine.registerCustomLogic({operator: 'currency',handler: (args,context)=>{const[amount]=args;returnnewIntl.NumberFormat('en-US',{style: 'currency',currency: 'USD'}).format(amount);},dependencyVisitor: currencyVisitor});// Use in rulesconstcustomLogicRules={"warningMessage": [{condition: {"contains": [{"var": ["description.value"]},"urgent"]},action: {"set": {target: "warningMessage.isVisible",value: true}},priority: 1}]};

Performance Optimization

Flywheel automatically optimizes performance through:

// 1. Dependency-based cachingengine.updateFieldValue({age: 25});// Only age-dependent fields re-evaluate// 2. Intelligent invalidationconstinvalidatedFields=engine.updateFieldValue({name: "John"});console.log(invalidatedFields);// ['displayName', 'greeting', ...]// 3. Debug utilities for performance analysisconsole.log(engine.getDependenciesOf("calculatedTotal"));// ['price', 'quantity', 'taxRate', 'discountPercent']

Debugging and Testing

// Comprehensive debugging utilitiesconstdependencies=engine.getDependenciesOf("totalPrice");console.log("totalPrice depends on:",dependencies);// Validation utilitiestry{engine.loadRuleSet(ruleSet);}catch(error){console.error("Rule validation failed:",error.message);}// Test rule evaluationengine.updateFieldValue({age: 30,email: "test@example.com"});constresult=engine.evaluateField("userProfile");expect(result.isVisible).toBe(true);

Troubleshooting Custom Dependencies

Problem: Custom actions don't trigger re-evaluation of dependent fields. Solution: Ensure you provide a dependencyVisitor when registering the action.

Problem: Fields show stale data after custom logic operations. Solution: Make sure your custom logic dependency visitor correctly identifies field dependencies.

Problem: Circular dependencies detected with custom operations. Solution: Review your dependency visitors to ensure they don't create circular references.

// ❌ Incorrect - missing dependenciesconstbadVisitor: CustomActionDependencyVisitor={visitAction: ()=>({dependencies: [],dependents: []})};// ✅ Correct - properly declares dependenciesconstgoodVisitor: CustomActionDependencyVisitor={visitAction: ({ payload })=>({dependencies: payload.sources||[],dependents: payload.target ? [payload.target] : []})};

Flywheel provides a comprehensive solution for complex dynamic form logic, business rule management, and field state orchestration. Its powerful yet intuitive API makes it easy to build sophisticated, reactive user interfaces with minimal code.

For more examples and advanced usage patterns, see the test files in the repository.

About

A powerful rule engine for dynamic field configuration using a condition-action pattern

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages