npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

mairon

v1.1.1

Published

Mairon is a lightweight, type-safe rule engine. Define complex business rules declaratively and evaluate them dynamically at runtime.

Downloads

108

Readme

Mairon

One engine to rule them all

npm version codecov License: MIT TypeScript

Mairon is a lightweight, type-safe rule engine. Define complex business rules declaratively and evaluate them dynamically at runtime.

Features

Declarative Rules: Define rules as JSON-like objects with all, any, and not logic

🔍 45+ Built-in Operators: Comparison, string, array, type-checking, change detection, and more

🧩 Custom Operators: Register your own sync/async operators with alias support

🎯 Type-Safe: Full TypeScript support with generic types

🔄 Change Detection: Track changes between data states

📝 Templates: Dynamic values with time expressions and data references

🎨 Event System: Hook into the evaluation lifecycle

🔒 Immutable Mode: Protect original data from mutation

Async Operators: Support for operators that call external APIs

🔗 Rule Chaining: Trigger dependent rules automatically

🔍 Explainability: Debug why rules matched or didn't match

💾 Serialization: Export/import rules and engine state as JSON

Installation

npm install mairon
# or
yarn add mairon
# or
pnpm add mairon

Quick Start

import Mairon from 'mairon';

// Create engine
const engine = new Mairon();

// Register action handlers
engine.registerHandler('notify', (context, params) => {
  console.log(`Notification: ${params.message}`);
});

// Define a rule
engine.addRule({
  id: 'welcome-new-users',
  name: 'Welcome new users',
  conditions: {
    all: [
      { field: 'isNew', operator: 'equals', value: true },
      { field: 'age', operator: 'greaterThanOrEqual', value: 18 }
    ]
  },
  actions: [
    { type: 'notify', params: { message: 'Welcome!' } }
  ]
});

// Evaluate
const results = await engine.evaluate({
  data: { isNew: true, age: 25 }
});

console.log(results[0].matched); // true

Example: Task Management

This example shows how to build automation on top of a Todo object using Mairon.

import Mairon, { type Rule } from 'mairon';

type Todo = {
  id: string;
  title: string;
  dueAt?: number;
  priority: 'low' | 'normal' | 'high';
  tags: string[];
  completed: boolean;
  assignee?: string;
};

const engine = new Mairon<Todo>({ enableIndexing: true });

const notifications: string[] = [];

engine.registerHandlers({
  addTag: ({ evaluationContext }, params) => {
    const todo = evaluationContext.data;
    const tag = String(params.tag);
    if (!todo.tags.includes(tag)) {
      todo.tags.push(tag);
    }
  },
  assign: ({ evaluationContext }, params) => {
    evaluationContext.data.assignee = String(params.user);
  },
  notify: ({ evaluationContext }, params) => {
    const todo = evaluationContext.data;
    notifications.push(`${params.message}: ${todo.title}`);
  },
});

const rules: Rule<Todo>[] = [
  {
    id: 'overdue-tasks',
    name: 'Mark overdue tasks',
    priority: 100,
    enabled: true,
    conditions: {
      all: [
        { field: 'completed', operator: 'equals', value: false },
        { field: 'dueAt', operator: 'lessThan', value: '{{ now }}' }
      ]
    },
    actions: [
      { type: 'addTag', params: { tag: 'overdue' } },
      { type: 'notify', params: { message: 'Task is overdue' } }
    ]
  },
  {
    id: 'assign-high-priority',
    name: 'Auto-assign high priority tasks',
    priority: 90,
    enabled: true,
    conditions: {
      all: [
        { field: 'priority', operator: 'equals', value: 'high' },
        { field: 'assignee', operator: 'isUndefined' }
      ]
    },
    actions: [
      { type: 'assign', params: { user: 'team-lead' } }
    ]
  }
];

engine.addRules(rules);

// Evaluate a todo
const todo: Todo = {
  id: '1',
  title: 'Complete project',
  dueAt: Date.now() - 1000,  // Overdue
  priority: 'high',
  tags: [],
  completed: false
};

const results = await engine.evaluate({ data: todo });
console.log(`Matched ${results.filter(r => r.matched).length} rules`);
console.log('Notifications:', notifications);

Core Concepts

Rules

A rule consists of:

  • Conditions: Logic tree that evaluates to true/false
  • Actions: Operations to perform when conditions match
  • Priority: Higher priority rules execute first
  • Triggers: Other rules to chain when this rule matches
  • Metadata: Tags, description, and custom data
{
  id: 'rule-id',
  name: 'Human readable name',
  priority: 100,
  conditions: {
    all: [  // AND logic
      { field: 'status', operator: 'equals', value: 'active' },
      { field: 'age', operator: 'greaterThan', value: 18 }
    ]
  },
  actions: [
    { type: 'actionName', params: { key: 'value' } }
  ],
  triggers: ['another-rule-id']  // Optional: chain to other rules
}

Conditions

Simple Conditions:

{ field: 'age', operator: 'greaterThan', value: 21 }

Logical Groups (all = AND, any = OR, not = NOT):

{
  any: [  // OR logic
    { field: 'role', operator: 'equals', value: 'admin' },
    {
      all: [  // Nested AND
        { field: 'role', operator: 'equals', value: 'moderator' },
        { field: 'verified', operator: 'equals', value: true }
      ]
    }
  ]
}

// NOT logic
{
  not: { field: 'banned', operator: 'equals', value: true }
}

Operators

Mairon includes 45+ operators across multiple categories:

  • Comparison: equals, greaterThan, lessThan, between, etc.
  • String: contains, startsWith, endsWith, matches (regex)
  • Array: includes, includesAll, includesAny, isEmpty
  • Existence: exists, isNull, isDefined, isUndefined
  • Type: isString, isNumber, isBoolean, isArray, isObject
  • Change: changed, changedFrom, changedTo, increased, decreased
  • Membership: in, notIn
  • Length: lengthEquals, lengthGreaterThan, etc.

See Operators Guide for complete reference.

Actions & Handlers

Actions are executed when rules match. Register handlers to define behavior:

engine.registerHandler('sendEmail', async (context, params) => {
  await emailService.send({
    to: params.recipient,
    subject: params.subject,
    body: params.body
  });
});

// Use in rules
{
  actions: [
    {
      type: 'sendEmail',
      params: {
        recipient: '[email protected]',
        subject: 'Welcome!',
        body: 'Thanks for signing up'
      }
    }
  ]
}

Templates

Dynamic values using {{ }} syntax:

Time Expressions:

{ field: 'dueAt', operator: 'lessThan', value: '{{ now }}' }
{ field: 'createdAt', operator: 'greaterThan', value: '{{ now - 7d }}' }

Data References:

{ field: 'confirmEmail', operator: 'equals', value: '{{ data.email }}' }

In Actions:

{
  type: 'notify',
  params: {
    message: 'Welcome {{ data.name }}! Your ID is {{ data.id }}'
  }
}

See Templates Guide for complete reference.

Change Detection

Compare current and previous states:

const results = await engine.evaluate({
  data: { status: 'active', lastLogin: Date.now() },
  previousData: { status: 'pending', lastLogin: Date.now() - 86400000 }
});

// Use change operators
{ field: 'status', operator: 'changed' }
{ field: 'status', operator: 'changedFrom', value: 'pending' }
{ field: 'status', operator: 'changedTo', value: 'active' }
{ field: 'loginCount', operator: 'increased' }

API Overview

// Create engine
const engine = new Mairon<DataType>(config);

// Add rules
engine.addRule(rule);
engine.addRules([rule1, rule2]);

// Manage rules
engine.updateRule('rule-id', { enabled: false });
engine.removeRule('rule-id');
engine.enableRule('rule-id');
engine.disableRule('rule-id');

// Query rules
const rule = engine.getRule('rule-id');
const all = engine.getRules();
const enabled = engine.getRules({ enabled: true });
const priority = engine.getRules({ priority: { min: 50 } });

// Register handlers
engine.registerHandler('actionType', handler);
engine.registerHandlers({ action1: handler1, action2: handler2 });
engine.unregisterHandler('actionType');
const handlers = engine.getRegisteredHandlers(); // ['action1', 'action2']

// Custom operators
engine.registerOperator('isWeekend', (value) => {
  const day = new Date(value).getDay();
  return day === 0 || day === 6;
});

// Evaluate
let results = await engine.evaluate({ data });
results = await engine.evaluate({ data, previousData, context });

// Explain (debug why rules matched/didn't match)
const explanations = await engine.explain({ data });

// Serialization
const snapshot = engine.toJSON();
engine.loadJSON(snapshot);
const rules = engine.exportRules();
engine.importRules(rules, { replace: true });

// Events
engine.on('ruleMatched', (data) => console.log(data));
engine.on('actionExecuted', (data) => console.log(data));

// Stats (evaluations, rules, actions)
const stats = engine.getStats();

Configuration

const engine = new Mairon({
  strict: true,              // Throw on missing handlers
  immutable: true,           // Protect original data from mutation
  enableIndexing: true,      // Performance optimization for large rule sets
  maxRulesPerExecution: 100, // Limit rules per evaluation
  stopOnFirstError: false,   // Continue on action errors
});

Advanced Features

Event System

Hook into the evaluation lifecycle:

engine.on('beforeEvaluate', (data) => {
  console.log(`Evaluating ${data.ruleCount} rules`);
});

engine.on('ruleMatched', (data) => {
  console.log(`Rule ${data.rule.name} matched`);
});

engine.on('ruleTriggered', (data) => {
  console.log(`${data.sourceRule.name} triggered ${data.triggeredRule.name}`);
});

engine.on('actionFailed', (data) => {
  console.error(`Action failed:`, data.error);
});

engine.on('afterEvaluate', (data) => {
  console.log(`Completed in ${data.duration}ms`);
});

Rule Filtering

Query specific subsets of rules:

// By enabled status
engine.getRules({ enabled: true });

// By priority range
engine.getRules({ priority: { min: 50, max: 100 } });

// By tags
engine.getRules({ tags: ['critical', 'security'] });

// By IDs
engine.getRules({ ids: ['rule-1', 'rule-2'] });

Custom Context

Pass additional data for evaluation:

await engine.evaluate({
  data: order,
  context: {
    userId: 'user-123',
    requestId: 'req-456',
    environment: 'production',
    features: { betaAccess: true }
  }
});

// Access in templates
{ field: 'environment', operator: 'equals', value: '{{ context.environment }}' }

Immutable Mode

Protect your data from accidental mutation by action handlers:

const engine = new Mairon({ immutable: true });

engine.registerHandler('modify', (ctx) => {
  ctx.data.value = 999; // This modifies a clone, not original
});

const data = { value: 1 };
await engine.evaluate({ data });
console.log(data.value); // Still 1

Custom Operators

Add domain-specific operators, with optional aliases:

engine.registerOperator('isWeekend', (value) => {
  const day = new Date(value).getDay();
  return day === 0 || day === 6;
});

// Use in rules
{ field: 'timestamp', operator: 'isWeekend' }

// With aliases
engine.registerOperator('greaterThan', (a, cond) => a > cond.value, {
  aliases: ['gt', 'moreThan']
});

// Both work
{ field: 'age', operator: 'gt', value: 18 }

Async Operators

Operators can call external services:

engine.registerOperator('hasPermission', async (value, condition, ctx) => {
  const perms = await fetchPermissions(value);
  return perms.includes(condition.value);
});

Rule Chaining

Trigger dependent rules automatically:

engine.addRule({
  id: 'calculate-discount',
  name: 'Calculate Discount',
  conditions: { field: 'total', operator: 'greaterThan', value: 100 },
  actions: [{ type: 'applyDiscount' }],
  triggers: ['send-notification', 'update-loyalty-points']
});

Explainability

Debug why rules matched or didn't:

const explanations = await engine.explain({ data: user });

for (const exp of explanations) {
  console.log(`Rule: ${exp.ruleName}, Matched: ${exp.matched}`);
  // Inspect exp.explanation for detailed condition breakdown
}

Serialization

Export and import rules:

// Export
const snapshot = engine.toJSON();
fs.writeFileSync('rules.json', JSON.stringify(snapshot));

// Import
engine.loadJSON(JSON.parse(fs.readFileSync('rules.json')));

// Or just rules
const rules = engine.exportRules();
engine.importRules(rules, { replace: true });

License

MIT