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

@divmain/jdm-asm

v0.2.3

Published

JDM to AssemblyScript WASM Compiler - Transforms JSON Decision Models into efficient WebAssembly

Readme

jdm-asm

A high-performance JDM (JSON Decision Model) compiler that transforms decision models into WebAssembly modules using AssemblyScript. Compatible with GoRules zen-engine JDM format with sub-10 microsecond decision latency and 5-9x faster execution than zen-engine.

Table of Contents

Overview

jdm-asm compiles JSON Decision Models (JDM) into optimized WebAssembly modules. This approach moves expensive parsing and interpretation to compile-time, resulting in significantly faster runtime execution compared to traditional JavaScript rule engines.

Key Features:

  • Ultra-Low Latency: 3.5-7 microsecond execution for typical decisions in single-threaded mode
  • High Performance: 5-9x faster than zen-engine across all scenarios
  • Type Safety: TypeBox schemas for input/output validation
  • zen-engine Compatible: Drop-in replacement for most JDM files
  • Extended Hit Policies: Supports DMN-standard policies (unique, ruleOrder, outputOrder, priority)
  • Multi-threaded: Worker pool support for high-throughput batch processing

Installation

npm install jdm-asm

Requirements:

  • Node.js 18+
  • TypeScript 5.0+ (for TypeBox schemas)

Quick Start

import { Type } from '@sinclair/typebox';
import { compile, createCompiledDecision } from 'jdm-asm';

// 1. Define input/output schemas
const InputSchema = Type.Object({
  customerType: Type.String(),
  orderAmount: Type.Number(),
});

const OutputSchema = Type.Object({
  discount: Type.Number(),
});

// 2. Define your decision model (or load from file)
const jdm = {
  nodes: [
    { id: 'input', type: 'inputNode', name: 'Request', position: { x: 0, y: 0 } },
    {
      id: 'table',
      type: 'decisionTableNode',
      name: 'Discount Rules',
      position: { x: 200, y: 0 },
      content: {
        hitPolicy: 'first',
        inputs: [
          { id: 'in1', name: 'Customer', field: 'customerType' },
          { id: 'in2', name: 'Amount', field: 'orderAmount' },
        ],
        outputs: [{ id: 'out1', name: 'Discount', field: 'discount' }],
        rules: [
          { _id: 'r1', in1: '"premium"', in2: '> 100', out1: '0.15' },
          { _id: 'r2', in1: '"standard"', in2: '> 100', out1: '0.10' },
          { _id: 'r3', in1: '', in2: '', out1: '0' },
        ],
      },
    },
    { id: 'output', type: 'outputNode', name: 'Response', position: { x: 400, y: 0 } },
  ],
  edges: [
    { id: 'e1', sourceId: 'input', targetId: 'table', type: 'edge' },
    { id: 'e2', sourceId: 'table', targetId: 'output', type: 'edge' },
  ],
};

// 3. Compile to WebAssembly
const compiled = await compile({
  jdm,
  inputSchema: InputSchema,
  outputSchema: OutputSchema,
});

// 4. Create decision instance
const decision = await createCompiledDecision(compiled);

// 5. Evaluate
const result = await decision.evaluate({
  customerType: 'premium',
  orderAmount: 150,
});

console.log(result); // { discount: 0.15 }

// 6. Clean up when done
decision.dispose();

Usage Guide

Defining Schemas

jdm-asm uses TypeBox schemas to define the structure of input and output data. Schemas enable:

  • Compile-time type checking
  • Runtime input validation
  • Optimized memory layout for WASM
import { Type } from '@sinclair/typebox';

// Simple flat schema
const InputSchema = Type.Object({
  age: Type.Number(),
  name: Type.String(),
  active: Type.Boolean(),
});

// Nested schema
const InputSchema = Type.Object({
  customer: Type.Object({
    id: Type.String(),
    tier: Type.String(),
    loyaltyYears: Type.Number(),
  }),
  order: Type.Object({
    total: Type.Number(),
    items: Type.Array(Type.Object({
      sku: Type.String(),
      price: Type.Number(),
      quantity: Type.Number(),
    })),
  }),
});

// Output schema
const OutputSchema = Type.Object({
  eligible: Type.Boolean(),
  discount: Type.Number(),
  message: Type.String(),
});

Creating JDM Files

JDM files are JSON documents containing nodes and edges that form a decision graph:

{
  "nodes": [
    {
      "id": "unique-node-id",
      "type": "inputNode|outputNode|expressionNode|decisionTableNode|switchNode|decisionNode",
      "name": "Human-readable name",
      "position": { "x": 0, "y": 0 },
      "content": { }
    }
  ],
  "edges": [
    {
      "id": "unique-edge-id",
      "type": "edge",
      "sourceId": "source-node-id",
      "targetId": "target-node-id"
    }
  ]
}

Every JDM must have:

  • Exactly one inputNode (entry point)
  • At least one outputNode (exit point)
  • A connected path from input to output

Compiling Decisions

The compile() function transforms JDM into WebAssembly:

import { compile } from 'jdm-asm';

const result = await compile({
  // Required
  jdm: jdmObject,           // JDM as object or JSON string
  inputSchema: InputSchema,  // TypeBox schema
  outputSchema: OutputSchema,

  // Optional
  optimize: true,            // Enable WASM optimizations (default: true)
  debug: false,              // Include AS source and WAT in output
  noMatchBehavior: {         // When no rules match:
    type: 'returnNull'       // 'returnNull' | 'throwError' | 'returnDefault'
  },
  loadDecision: (key) => {}, // Custom loader for sub-decisions
});

Compilation Result:

interface CompilationResult {
  wasm: Uint8Array;        // Compiled WASM binary
  schemaHash: bigint;      // Hash for runtime validation
  marshalCode: string;     // JS code for data marshaling
  validationCode: string;  // JS code for input validation
  wat?: string;            // WAT text format (debug mode)
  assemblyScript?: string; // Generated AS source (debug mode)
}

Executing Decisions

Use createCompiledDecision() to instantiate and execute compiled decisions:

import { createCompiledDecision } from 'jdm-asm';

// Create instance (instantiates WASM module)
const decision = await createCompiledDecision(compiledResult);

// Evaluate with input data
const output = await decision.evaluate({
  customerType: 'premium',
  orderAmount: 150,
});

// Reuse for multiple evaluations (recommended for performance)
for (const input of inputs) {
  const result = await decision.evaluate(input);
  processResult(result);
}

// Release resources when done
decision.dispose();

CompiledDecision API:

class CompiledDecision {
  // Evaluate with input
  evaluate<I, O>(input: I): Promise<O>;

  // Get WASM memory (debugging)
  getMemory(): WebAssembly.Memory | null;

  // Memory statistics
  getMemoryStats(): { current: number; maximum: number; used: number };

  // Release resources
  dispose(): void;
}

Configuration Options

No-Match Behavior

Configure what happens when no rules match:

// Return null (default)
{ type: 'returnNull' }

// Set error code and return null
{ type: 'throwError' }

// Return a specific default value
{ type: 'returnDefault', value: { discount: 0, message: 'No match' } }

Can be set globally via compile options or per-node in the JDM content.

Compilation Cache

jdm-asm caches WASM compilations to disk for faster subsequent builds:

import {
  clearCache,
  getCacheStats,
  pruneCache,
  getCacheDirectory,
} from 'jdm-asm';

// Clear all cached compilations
clearCache();

// Get cache statistics
const stats = getCacheStats();

// Remove old entries
pruneCache();

JDM Format Reference

Node Types

Input Node

Entry point for the decision graph.

{
  "id": "input-1",
  "type": "inputNode",
  "name": "Request"
}

Output Node

Exit point that returns accumulated context data.

{
  "id": "output-1",
  "type": "outputNode",
  "name": "Response"
}

Expression Node

Evaluates expressions and sets output fields.

{
  "id": "expr-1",
  "type": "expressionNode",
  "name": "Calculate",
  "content": {
    "expressions": [
      { "id": "e1", "key": "total", "value": "price * quantity" },
      { "id": "e2", "key": "tax", "value": "total * 0.08" },
      { "id": "e3", "key": "grandTotal", "value": "$.total + $.tax" }
    ],
    "passThrough": true,        // Include input fields in output
    "inputField": "order",      // Scope to nested field
    "outputPath": "result"      // Store results under this key
  }
}

Self-Referential Expressions: Use $ to reference earlier computed values within the same expression node (e.g., $.total refers to the total computed above).

Decision Table Node

Rule-based decision table with conditions and outputs.

{
  "id": "table-1",
  "type": "decisionTableNode",
  "name": "Discount Rules",
  "content": {
    "hitPolicy": "first",
    "inputs": [
      { "id": "in1", "name": "Customer Type", "field": "customerType" },
      { "id": "in2", "name": "Order Amount", "field": "orderAmount" }
    ],
    "outputs": [
      { "id": "out1", "name": "Discount", "field": "discount" }
    ],
    "rules": [
      { "_id": "r1", "in1": "\"premium\"", "in2": "> 100", "out1": "0.15" },
      { "_id": "r2", "in1": "\"standard\"", "in2": "> 100", "out1": "0.10" },
      { "_id": "r3", "in1": "", "in2": "", "out1": "0" }
    ],
    "passThrough": false
  }
}

Unary Mode: In decision table cells, expressions are implicitly compared against the column's input value ($):

| Cell Value | Interpreted As | |------------|----------------| | "admin" | $ == "admin" | | > 100 | $ > 100 | | >= 18, < 65 | $ >= 18 and $ < 65 | | [1..10] | $ >= 1 and $ <= 10 | | "a", "b", "c" | $ == "a" or $ == "b" or $ == "c" | | ["gold", "platinum"] | $ in ["gold", "platinum"] | | `` (empty) or - | true (always matches) |

Switch Node

Conditional branching based on expressions.

{
  "id": "switch-1",
  "type": "switchNode",
  "name": "Route",
  "content": {
    "statements": [
      { "id": "s1", "condition": "status == 'active'" },
      { "id": "s2", "condition": "status == 'pending'" },
      { "id": "s3", "condition": "" }
    ],
    "hitPolicy": "first"
  }
}

Edges from switch nodes use sourceHandle to specify which branch they belong to (matching the statement id).

Decision Node

References external sub-decisions for modular composition.

{
  "id": "sub-1",
  "type": "decisionNode",
  "name": "Calculate Shipping",
  "content": {
    "key": "shipping-rules.json"
  }
}

The loadDecision option in compile controls how sub-decisions are loaded.

Expression Language

jdm-asm supports the ZEN expression language:

Literals

42, 3.14, -5           // Numbers
"hello", 'world'       // Strings
true, false            // Booleans
null                   // Null
[1, 2, 3]              // Arrays
{key: value}           // Objects

Operators

| Category | Operators | |----------|-----------| | Arithmetic | +, -, *, /, %, ^ (power) | | Comparison | ==, !=, <, >, <=, >= | | Logical | and, or, not | | Null coalescing | ?? | | Membership | in, not in | | Ternary | condition ? then : else |

Access Patterns

object.property              // Member access
object["key"]                // Bracket access
array[0]                     // Index access
nested.deeply.nested.value   // Chained access

Template Literals

`Hello, ${name}!`
`Order ${id}: ${items.length} items totaling ${total}`

Interval Notation (DMN-style)

[1..10]    // 1 <= x <= 10 (inclusive)
(1..10)    // 1 < x < 10 (exclusive)
[1..10)    // 1 <= x < 10
(1..10]    // 1 < x <= 10

Built-in Functions

Array Functions

| Function | Description | Example | |----------|-------------|---------| | sum(array) | Sum numeric values | sum([1, 2, 3])6 | | avg(array) | Average of values | avg([1, 2, 3])2 | | min(array) | Minimum value | min([3, 1, 2])1 | | max(array) | Maximum value | max([3, 1, 2])3 | | count(array) | Array length | count([1, 2, 3])3 | | sort(array) | Sort ascending | sort([3, 1, 2])[1, 2, 3] | | flat(array, depth?) | Flatten nested arrays | flat([[1], [2, 3]])[1, 2, 3] | | contains(array, val) | Check membership | contains([1, 2], 2)true |

Higher-Order Functions

| Function | Description | Example | |----------|-------------|---------| | filter(array, predicate) | Filter elements | filter(items, # > 5) | | map(array, transform) | Transform elements | map(items, # * 2) | | reduce(array, expr, init) | Reduce to value | reduce(nums, acc + #, 0) | | all(array, predicate) | All match | all(scores, # >= 60) | | some(array, predicate) | Any match | some(items, # == "gold") |

Note: Use # to reference the current array element in predicates.

String Functions

| Function | Description | Example | |----------|-------------|---------| | upper(str) | Uppercase | upper("hello")"HELLO" | | lower(str) | Lowercase | lower("HELLO")"hello" | | trim(str) | Remove whitespace | trim(" hi ")"hi" | | substring(str, start, end?) | Extract substring | substring("hello", 0, 2)"he" | | indexOf(str, search) | Find index | indexOf("hello", "l")2 | | startsWith(str, prefix) | Check prefix | startsWith("hello", "he")true | | endsWith(str, suffix) | Check suffix | endsWith("hello", "lo")true | | split(str, delim) | Split to array | split("a,b,c", ",")["a", "b", "c"] | | join(array, delim) | Join to string | join(["a", "b"], "-")"a-b" | | replace(str, find, repl) | Replace first | replace("hello", "l", "L")"heLlo" | | replaceAll(str, find, repl) | Replace all | replaceAll("hello", "l", "L")"heLLo" | | contains(str, substr) | Check substring | contains("hello", "ell")true |

Math Functions

| Function | Description | Example | |----------|-------------|---------| | abs(num) | Absolute value | abs(-5)5 | | floor(num) | Round down | floor(3.7)3 | | ceil(num) | Round up | ceil(3.2)4 | | round(num) | Round nearest | round(3.5)4 |

Date/Time Functions

| Function | Description | Example | |----------|-------------|---------| | date(str) | Parse ISO date to timestamp (seconds) | date("2025-03-20T10:30:00Z") | | date("now") | Current timestamp | date("now") | | time(str) | Parse time to seconds since midnight | time("10:30:00")37800 | | duration(str) | Parse duration string | duration("24h")86400 |

Type Functions

| Function | Description | Example | |----------|-------------|---------| | number(str) | Parse to number | number("42")42 | | string(val) | Convert to string | string(42)"42" | | keys(obj) | Get object keys | keys({a: 1})["a"] | | values(obj) | Get object values | values({a: 1})[1] |

Decision Table Hit Policies

| Policy | Behavior | zen-engine | |--------|----------|------------| | first | Return first matching rule | Yes | | collect | Return all matches as array | Yes | | unique | Return single match (error if multiple) | No | | ruleOrder | Return all matches in rule definition order | No | | outputOrder | Return all matches sorted by output value | No | | priority | Return highest priority match | No |

Architecture

Compile Phase vs Execution Phase

jdm-asm operates in two distinct phases:

Compile Phase (~250ms-10s depending on complexity/hardware):

  1. Parse JDM JSON and validate structure
  2. Build dependency graph from nodes/edges
  3. Topologically sort for evaluation order
  4. Generate AssemblyScript code for each node
  5. Compile AssemblyScript to WebAssembly
  6. Generate JavaScript marshaling code

Execution Phase (~3-1000 microseconds):

  1. Validate input against schema
  2. Marshal JavaScript object to WASM linear memory
  3. Call WASM evaluate() function
  4. Unmarshal result from WASM memory to JavaScript

This separation enables high performance: expensive compilation happens once, while fast execution occurs many times.

Data Marshaling

Data flows between JavaScript and WASM via typed memory:

JavaScript                    WASM Linear Memory
──────────                    ──────────────────
{ name: "John",     ──►      [ValueMap]
  age: 30,                     ├─ length: 2
  tags: ["a","b"] }            ├─ key0 → "name" (UTF-16)
                               ├─ val0 → [STRING, ptr → "John"]
                               ├─ key1 → "age"
                               ├─ val1 → [FLOAT, 30.0]
                               ├─ key2 → "tags"
                               └─ val2 → [ARRAY, ptr → [...]]

Value Types (tagged union):

| Tag | Type | Storage | |-----|------|---------| | 0 | Null | Tag only | | 1 | Boolean | Tag + u8 | | 2 | Integer | Tag + i64 | | 3 | Float | Tag + f64 | | 4 | String | Tag + pointer to UTF-16 data | | 5 | Array | Tag + pointer to element array | | 6 | Object | Tag + pointer to ValueMap |

Performance

Benchmark Results

Benchmarks run on Apple M3 Max, Node.js 24.7, comparing jdm-asm to @gorules/zen-engine:

Single-Threaded Latency (Best-Case)

In single-threaded mode, jdm-asm achieves microsecond-level latency:

| Scenario | jdm-asm | zen-engine | Speedup | |----------|---------|------------|---------| | Simple (5 rules) | 3.5 µs | 32.4 µs | 9.2x faster | | Moderate (12 rules) | 7.2 µs | 36.2 µs | 5.0x faster | | Complex (~8000 rules) | 492 µs | 1.47 ms | 3x faster |

For typical decision tables (5-50 rules), expect sub-10 microsecond execution times.

Single-Threaded Latency Percentiles

| Scenario | p50 | p95 | p99 | |----------|-----|-----|-----| | Simple | 3.5 µs | 4.2 µs | 4.7 µs | | Moderate | 7.2 µs | 8.0 µs | 9.7 µs | | Complex | 492 µs | 1.1 ms | 1.6 ms |

Throughput (decisions/second)

| Mode | Scenario | zen-engine | jdm-asm | |------|----------|------------|---------| | Single-threaded | Simple | 34.6K | 277.6K | | Single-threaded | Moderate | 27.6K | 139.1K | | Single-threaded | Complex | 682 | 892 | | Multi-thread (16) | Simple | 119K | 180K | | Multi-thread (16) | Moderate | 98K | 178K | | Multi-thread (16) | Complex | 5.6K | 20.6K |

Multi-Threaded Batch Latency

When using worker threads (includes IPC overhead):

| Scenario | jdm-asm (16 threads) | zen-engine | |----------|----------------------|------------| | Simple | 2.7ms / 4.9ms / 6.7ms | 4.4ms / 6.3ms / 7.5ms | | Moderate | 2.7ms / 5.0ms / 5.9ms | 5.3ms / 8.2ms / 8.9ms | | Complex | 20.9ms / 38.1ms / 43.9ms | 121.9ms / 159.9ms / 173.8ms |

Compilation Time

| Scenario | WASM Size | Compile Time | |----------|-----------|--------------| | Simple | 34 KB | 1.4s | | Moderate | 55 KB | 0.8s | | Complex | 6.1 MB | 9.5s |

When to Use jdm-asm

Best for:

  • Ultra-low latency requirements (sub-10 µs for typical decisions)
  • High-throughput APIs evaluating hundreds of thousands of decisions per second
  • Latency-sensitive applications where p99 matters
  • Simple to moderate complexity rules (5-50 rules)
  • Cases where rules change infrequently (amortize compilation cost)

Consider zen-engine when:

  • Rules change frequently (compilation cost is high)
  • Using function nodes with complex JavaScript logic
  • Quick prototyping without schema definitions

Compatibility with zen-engine

jdm-asm is designed as a drop-in replacement for zen-engine with some differences:

Fully Supported:

  • All node types (input, output, expression, decision table, switch, decision)
  • Expression language (operators, functions, access patterns)
  • Decision table hit policies: first, collect
  • Sub-decision composition

Extended Features (not in zen-engine):

  • DMN-standard hit policies: unique, ruleOrder, outputOrder, priority
  • Compile-time optimization
  • Multi-threaded execution

Limitations:

  • Function nodes: Parsed but not executed (returns placeholder)
  • Division by zero: Returns null instead of Infinity
  • Full ISO 8601 durations: Simple formats (24h, 30d) work; complex format (P1Y2M3D) not supported

Development

# Install dependencies
npm install

# Build TypeScript
npm run build

# Build AssemblyScript runtime
npm run asbuild

# Run tests
npm run test

# Run benchmarks
npm run bench

# Lint code
npm run lint

# Type check
npx tsc --noEmit

Project Structure

jdm-asm/
├── src/
│   ├── compiler/       # TypeScript compiler code
│   └── runtime/        # AssemblyScript runtime
├── tests/
│   ├── unit/           # Unit tests
│   ├── integration/    # Integration tests
│   └── helpers/        # Test utilities
├── test-data/          # Ported zen-engine test fixtures
├── benchmarks/         # Performance benchmarks
├── build/              # Compiled WASM output
└── dist/               # Built npm package

License

MIT License - see LICENSE for details.