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

@goplasmatic/datalogic-node

v5.8.1

Published

JSONLogic (json-logic) rules engine for Node.js — native Rust core via N-API, compile-once/evaluate-many. A fast alternative to json-logic-js with identical semantics across Node, browsers/WASM, Python, Go, Java, .NET, PHP, and Rust.

Readme

@goplasmatic/datalogic-node

npm CI License: Apache 2.0

Part of datalogic-rs: one engine, every runtime.

Native Node.js bindings for datalogic-rs, a fast Rust implementation of JSONLogic. Same rules, same semantics as the Rust crate, with the compile-once / evaluate-many pattern exposed natively: compile a rule once and evaluate it against thousands of data inputs without re-parsing. Every binding runs the same core and passes the same 2,128-case conformance battery (66 suites).

For the cross-runtime overview and the API-tier model every binding implements, see the repo README.

No v4 package. This native binding started with v5. If you ran JSONLogic under Node through v4's @goplasmatic/datalogic (WASM), install this package for production Node services. See MIGRATION.md for the full cookbook.

Two npm packages, one engine. @goplasmatic/datalogic-wasm is the WebAssembly build and runs in browsers, Node, Deno, and Bun. This package (@goplasmatic/datalogic-node) is the native Node build via napi-rs, pulling in the same Rust engine through a per-platform prebuilt .node artifact. Pick this one on Node when throughput matters; pick the WASM package when you need to run in the browser or want a single artifact across runtimes.

Install

npm install @goplasmatic/datalogic-node

Prebuilt platform binaries ship as optionalDependencies, so npm pulls only the .node file matching the consumer's platform:

| Platform | Architectures | |---|---| | Linux (glibc) | x64, arm64 | | Linux (musl) | x64, arm64 | | macOS | x64, arm64 | | Windows | x64, arm64 |

The package supports Node 18 and newer.

If require('@goplasmatic/datalogic-node') or import fails with MODULE_NOT_FOUND, upgrade to 5.8.0: versions 5.1.1 through 5.7.1 were published without the package's index.js loader and index.d.ts typings.

Quick start

import { apply } from '@goplasmatic/datalogic-node';

const result = apply(
  { if: [{ '>': [{ var: 'score' }, 50] }, 'pass', 'fail'] },
  { score: 75 }
);
// -> "pass"

Rules and data are plain JS values, and both arguments also accept JSON text: the binding parses a JS string passed as rule or data as JSON rather than treating it as a string value. To evaluate against a data document that is a JSON string, pass it encoded (rule.evaluate(JSON.stringify('hello'))), or use the string-in methods (evaluateStr) or a DataHandle.

Object keys come back sorted when an object crosses the boundary as a JS value, in either direction (a rule, data or DataHandle.fromValue object, or a result from evaluate). To keep key order, pass JSON text and call a string-out method (evalStr, evaluateStr, evaluateDataStr).

Compile-once / evaluate-many

For repeated evaluations of the same rule, compile once and hold the Rule instance:

import { Engine } from '@goplasmatic/datalogic-node';

const engine = new Engine();
const rule = engine.compile({ '+': [{ var: 'x' }, 1] });

for (const payload of inputs) {
  console.log(rule.evaluate(payload));
}

Rule has no thread affinity on the Rust side, but napi class instances cannot be posted or transferred between worker_threads: a Rule sent through postMessage arrives as an empty plain object with no evaluate. Each worker must load the module and compile its own Rule (compiling is cheap). For parallelism from a single thread, rule.evaluateStrAsync(json) evaluates on the libuv pool (see Async evaluation).

Templating mode per compile

new Engine({ templating: true }) makes every compile treat a multi-key object as an output template. To choose per rule instead, engine.compileTemplate(rule) compiles in templating mode and engine.compileStrict(rule) outside it, whatever the engine was built with. Both use the engine's custom operators and config:

const engine = new Engine();
engine.compileTemplate({ name: { var: 'user' }, active: true }).evaluate({ user: 'Alice' });
// { active: true, name: 'Alice' } (keys sorted: a JS-value result)

Checking a rule before it runs

compile checks the JSON and the rule's structure only: an unknown operator compiles and fails at evaluation with errorType: 'InvalidOperator'. engine.check(rule, mode?) reports every problem the engine can see before the rule runs, each with an RFC 6901 JSON Pointer into the rule. mode is 'engine' (the default), 'strict' or 'template':

engine.check({ if: [true, { vr: 'x' }, { map: [1] }] });
// [
//   { code: 'UnknownOperator', severity: 'error', pointer: '/if/1', operator: 'vr',
//     message: 'unknown operator `vr`; did you mean `var`?' },
//   { code: 'ArgumentCount', severity: 'error', pointer: '/if/2', operator: 'map',
//     message: '`map` takes exactly 2 arguments, not 1' }
// ]

engine.check({ a: { var: 'x' }, b: 1 }, 'template'); // []

An 'error' will fail; a 'warning' runs but is probably a mistake (for example an argument the operator never evaluates). engine.compileChecked(rule) compiles only a rule with no error diagnostic and otherwise throws an Error named CompileError whose diagnostics holds the same objects:

try {
  engine.compileChecked({ vr: 'x' });
} catch (e) {
  e.name;                // 'CompileError'
  e.diagnostics[0].code; // 'UnknownOperator'
}

Sessions: hot-loop arena reuse

A Session reuses one bump arena across evaluations and resets between calls to bound peak memory. Open one per worker thread:

const sess = engine.session();
for (const payload of inputs) {
  sess.evaluate(rule, payload);
}

Sessions hold non-Sync state, so don't share them between worker threads; open one per worker.

Data handles, typed results, and batch evaluation

These mirror the C ABI v2 tiers. A DataHandle is an immutable, pre-parsed JSON document: parse a payload once with new DataHandle(json) and every evaluation against it skips JSON parsing. Handles are engine-independent (one handle can feed rules compiled by different engines), and evaluation never consumes or mutates them. They are per-JS-thread: the underlying parsed tree is Send but not Sync, which matches JS single-threaded semantics. You cannot share a handle across worker threads, so parse one per worker.

import { Engine, DataHandle } from '@goplasmatic/datalogic-node';

const handle = new DataHandle('{"age": 25, "status": "active"}'); // throws ParseError on bad JSON
const fromObj = DataHandle.fromValue({ age: 25, status: 'active' }); // no JSON round trip
handle.allocatedBytes;              // arena bytes (input copy + tree)

rule.evaluateData(handle);          // JS value out, no parse per call
rule.evaluateDataStr(handle);       // JSON string out
sess.evaluateData(rule, handle);    // hot path: session arena + no parse
sess.evaluateDataStr(rule, handle); // fastest: no parse, no JS materialisation

For predicates and scalar results, the typed session evaluations skip the JSON result round trip too:

sess.evaluateBool(rule, handle);   // strict JSON boolean
sess.evaluateInt(rule, handle);    // a whole number a JS number holds exactly
sess.evaluateFloat(rule, handle);  // any JSON number
sess.evaluateTruthy(rule, handle); // JSONLogic truthiness, never mismatches

evaluateBool, evaluateInt and evaluateFloat throw an EvaluateError with errorType: 'TypeMismatch' when the rule evaluates fine but the result is not of the requested type (the message names the actual type). evaluateTruthy coerces any result through the engine's configured truthiness rules (the same coercion if/and/or apply). evaluateNumber is the deprecated name of evaluateFloat and is removed in 6.0.

The batch entry points evaluate a whole set in one native call and report outcomes per item in the Promise.allSettled shape, so one bad input never poisons its neighbours:

// One rule, many payloads:
const outcomes = sess.evaluateBatch(rule, [h0, h1, h2]);
// Many rules, one payload (the rule-set / feature-flag shape):
const flags = sess.evaluateMany([r0, r1], handle);

for (const [i, o] of outcomes.entries()) {
  if (o.status === 'rejected') {
    console.log(`item ${i} failed: ${o.reason.message} (${o.reason.tag})`);
    continue;
  }
  console.log(`item ${i}: ${o.value}`); // result as a JSON string
}

Item failures land in { status: 'rejected', reason: { tag, message, operator? } } and never throw; argument errors (a non-handle in the array, a null rule, ...) do throw. The session resets its arena between items.

One Node-specific note on engines: a Rule carries a reference to the engine that compiled it, but every Session method evaluates the rule's compiled logic with the session's engine: its configuration and custom operators apply, and (unlike the C ABI) the binding performs no engine-identity check. Compile rules and open sessions on the same engine unless you want that substitution.

Async evaluation

Rule.evaluateStrAsync(dataJson) evaluates on the libuv thread pool and returns a Promise<string>:

const result = await rule.evaluateStrAsync('{"age": 25}');

It is not faster per operation than evaluateStr. The gain is event-loop hygiene: a large payload's parse + evaluate + serialize runs off the JS thread, so reach for it when payloads are big enough to cause noticeable event-loop stalls or to overlap evaluation with other work. String input only (a DataHandle is pinned to the JS thread and cannot cross to the pool). Rejections carry the same structured fields as synchronous throws (name, errorType, operator, nodeIds, path). Rules from engines with custom operators reject if evaluation reaches a JS-backed operator, because the callback is pinned to the JS thread.

API surface

| Symbol | Description | |---|---| | apply(rule, data) | One-shot compile + evaluate; convenience | | builtinOperatorNames() | Every built-in operator name this build accepts (includes the aliases var, ?:, match) | | Engine | Construct once; holds compile state, opens sessions | | Engine.compile(rule) → Rule | Parse a rule into a reusable handle | | Engine.compileTemplate(rule) / Engine.compileStrict(rule) → Rule | Compile in or outside templating mode, whatever the engine's default | | Engine.compileChecked(rule) → Rule | Compile only a rule check finds no error in; otherwise throw CompileError | | Engine.check(rule, mode?) | Every problem the engine can see before the rule runs, as diagnostics | | Engine.eval(rule, data) | One-shot, returns JS value | | Engine.evalStr(rule, data) | One-shot, returns JSON string | | Engine.evalMetered(rule, data, budget?) | One-shot with an operation count, returns { result, ops } | | Engine.evaluateWithTrace(logic, data, mode?) | One-shot with execution trace, JSON strings in and out | | Engine.session() → Session | Open a hot-loop arena | | Engine.operators() | The operator catalogue: one row per built-in operator this engine evaluates | | Engine.truthy(value) | The engine's truthiness applied to a value (a string is JSON text) | | Engine.customOperatorNames() | Names of the custom operators registered on this engine | | new DataHandle(json) | Parse a payload once into a reusable handle | | DataHandle.fromValue(value) | Build a handle from a JS value, copied once (later changes to the value are not seen) | | DataHandle.allocatedBytes | Arena bytes held by the handle | | Rule.evaluate(data) | Evaluate, returns JS value | | Rule.evaluateStr(data) | Evaluate, returns JSON string | | Rule.evaluateMetered(data, budget?) | Evaluate with an operation count, returns { result, ops } | | Rule.evaluateData(handle) | Evaluate a pre-parsed handle, returns JS value | | Rule.evaluateDataStr(handle) | Same, returns JSON string | | Rule.evaluateStrAsync(dataJson) | Evaluate on the libuv pool, returns Promise<string> | | Rule.facts() | What the rule reads and calls | | Session.evaluate(rule, data) | Evaluate with arena reuse | | Session.evaluateStr(rule, data) | Same, returns JSON string | | Session.evaluateData(rule, handle) | Handle in, JS value out, arena reuse | | Session.evaluateDataStr(rule, handle) | Handle in, JSON string out; fastest path | | Session.evaluateBool(rule, handle) | Strict boolean result (TypeMismatch otherwise) | | Session.evaluateInt(rule, handle) | Whole-number result a JS number holds exactly, \|n\| <= 2^53 - 1 (TypeMismatch otherwise) | | Session.evaluateFloat(rule, handle) | Any JSON number result (TypeMismatch otherwise) | | Session.evaluateNumber(rule, handle) | Deprecated name of evaluateFloat; removed in 6.0 | | Session.evaluateTruthy(rule, handle) | Engine-truthiness boolean; never mismatches | | Session.evaluateBatch(rule, handles) | One rule × many handles, allSettled-style items | | Session.evaluateMany(rules, handle) | Many rules × one handle, allSettled-style items | | Session.reset() | Explicit arena reset (optional) | | Session.allocatedBytes() | Bytes held by the arena's chunks (exact past 4 GiB) |

Constructor options:

new Engine({
  templating: true,
  templateKeyEscape: '$',
  config: { preset: 'strict' },
  strictOperatorNames: true,
  families: ['ExtString', 'DateTime'],
})

templating: true enables the engine's output-shaping templating mode: multi-key objects in a rule compile to templates with embedded JSONLogic. templateKeyEscape is an optional single-character prefix (see below). config sets the engine's evaluation configuration, and families limits the operator families it evaluates; see Engine configuration. strictOperatorNames refuses a custom operator named like a built-in (see Custom operators). The options bag reads only these five keys: new Engine({ preset: 'strict' }) builds a default engine, because preset belongs under config.

Emitting keys that are operator names

A single-key object is an operator invocation, so in templating mode a key naming a built-in (type, map, if, length, …) or a registered custom operator runs the operator instead of becoming an output field. There is no error, only the wrong result. Set templateKeyEscape to a single-character prefix to recover those keys: exactly one leading prefix is stripped from every template key, and an escaped key is never resolved as an operator.

const engine = new Engine({ templating: true, templateKeyEscape: '$' });

engine.compile({ $type: { var: 'x' } }).evaluate({ x: 1 });   // { type: 1 }
engine.compile({ $$type: 1 }).evaluate({});                   // { $type: 1 }
engine.compile({ type: { var: 'x' } }).evaluate({ x: 1 });    // 'number' (operator)

Unset by default, so $-prefixed keys otherwise pass through verbatim. The prefix is one character of your choosing, so payloads that already use $ keys (MongoDB documents, JSON Schema output) can pick ~ or # instead. Anything other than a one-character string throws at construction with errorType: 'InvalidArguments'.

Custom operators

Register host-language operators by passing a { name: fn } map as the second constructor argument. Each callback receives the operator's pre-evaluated arguments as a JSON-array string and returns a JSON-value string:

import { Engine } from '@goplasmatic/datalogic-node';

const engine = new Engine({}, {
  double: (argsJson) => String(JSON.parse(argsJson)[0] * 2),
});
const rule = engine.compile({ double: [21] });
rule.evaluate({});             // 42
engine.customOperatorNames();  // ['double']

Callbacks run synchronously on the thread that created the engine. Built-ins win: registering a name that collides with a built-in operator (+, if, var, ...) has no effect. With strictOperatorNames: true, that registration throws at construction with errorType: 'ConfigurationError' instead. An engine carrying custom operators is not safe to share across worker threads (the JS callback is pinned to its originating thread); create one per worker. If a custom operator is ever invoked from a different thread than the one that registered it (as rule.evaluateStrAsync does, on the libuv pool), evaluation fails with an EvaluateError naming the operator rather than risking undefined behavior. No napi class instance (Engine, Rule, Session, DataHandle) can be posted to a worker thread, so every worker builds its own either way.

Introspection

Operator names

Tooling that validates or autocompletes rules (editors, linters, palettes) can ask the binding for its vocabulary instead of keeping a hand-maintained list:

import { builtinOperatorNames, Engine } from '@goplasmatic/datalogic-node';

const names = builtinOperatorNames();
names.length;               // 87: the 84 built-in operators plus the aliases var, ?:, match
names.includes('group_by'); // true
names.includes('preserve'); // false (removed in v5)

const engine = new Engine({}, { double: (a) => String(JSON.parse(a)[0] * 2) });
engine.customOperatorNames(); // ['double']

builtinOperatorNames() mirrors Engine::builtin_operator_names() in the Rust crate and is derived from the compiler's own lookup table, so it cannot drift from dispatch; this binding enables every operator feature, so the list is the full set. engine.customOperatorNames() lists the operators passed as the second constructor argument (order not guaranteed). For an engine with every operator family, the union of the two is its full vocabulary, which matters under templating mode, where an unknown key is not an error but echoes back as data. On an engine built with families, engine.operators() lists the built-ins that engine evaluates.

Operator catalogue, rule facts and truthiness

Three more calls describe the engine and a rule without running anything:

const engine = new Engine();

engine.operators().find((op) => op.name === 'reduce');
// { name: 'reduce', aliases: [], family: 'Core', feature: null, min_args: 2, max_args: 3,
//   reads_context: false, effect: 'pure', cost: 'per_item', scoped_arg: 1 }

engine.compile({ if: [{ '>': [{ var: 'user.age' }, 18] }, 'adult', { var: 'fallback' }] }).facts();
// { reads: [['fallback'], ['user', 'age']], computed_reads: false, reads_complete: true,
//   reads_data: true, operators: ['>', 'if', 'val'], custom_operators: [],
//   deterministic: true }

engine.truthy({});   // false: an empty object is falsy, like an empty array
engine.truthy('[]'); // false: a string is JSON text

operators() returns one row per built-in operator the engine evaluates, in the schema of the operator catalogue: aliases, family and gating feature, the argument counts it reads, whether it reads the data context, its effect (pure, clock, throws, catches), its cost class, and which argument runs once per element. facts() lists each data path the rule reads from the root as its segments, after the optimizer, so a folded branch is not listed. reads_complete is false when a path is computed at runtime or a custom operator runs, and deterministic is false for now and any custom operator. truthy applies the engine's configured truthiness, the rules if, and, or and evaluateTruthy use.

Engine configuration

The config constructor option changes evaluation semantics. It accepts a plain object or a JSON-encoded string; both use the wire format every binding shares, parsed by the core crate's EvaluationConfig::from_json_str. All keys are optional:

| Key | Values | |-----|--------| | preset | "default", "safe_arithmetic", "strict" | | arithmetic_nan_handling | "throw_error", "ignore_value", "coerce_to_zero", "return_null" | | division_by_zero | "return_saturated", "throw_error", "return_null", "return_infinity" | | loose_equality_errors | bool | | missing_var | "null" (default: a missing variable reads as null), "error" (raises VariableNotFound) | | truthy_evaluator | "javascript", "python", "strict_boolean" | | numeric_coercion | object of bools: empty_string_to_zero, null_to_zero, bool_to_number, reject_non_numeric | | max_recursion_depth | integer >= 1 | | ops_budget | integer >= 1, or null for unbounded (caps the work one evaluation may do; crossing it raises BudgetExceeded) |

preset selects the starting point and the remaining keys override individual fields on top of it:

const engine = new Engine({ config: { preset: 'strict' } });

// The default engine coerces booleans to numbers; strict rejects them:
engine.evalStr('{"+": [1, true]}', 'null'); // throws EvaluateError

With missing_var: 'error', a var / val read that finds nothing throws errorType: 'VariableNotFound', so a typo in a path fails instead of flowing on as null. A default ({ var: ['x', 0] }), a present null, missing, missing_some and exists are not misses, and try catches the error:

const checked = new Engine({ config: { missing_var: 'error' } });
checked.eval({ var: 'user.nmae' }, { user: { name: 'Ana' } });                // throws VariableNotFound
checked.eval({ var: ['user.nmae', 'anonymous'] }, { user: { name: 'Ana' } }); // 'anonymous'

Unknown keys or values throw at construction with errorType: 'ConfigurationError', so typos fail at startup.

Operator families

families keeps the engine to the JSONLogic core plus the families you name, using the family names of engine.operators(): 'DateTime', 'ExtString', 'ExtArray', 'ExtObject', 'ExtControl', 'ErrorHandling', 'ExtMath', 'Tensor', 'Flagd'. Unset, the engine has every family. A family you leave out is not there for that engine: its names compile as unknown operators, which fail at evaluation with errorType: 'InvalidOperator' (and are errors in check and compileChecked), and a custom operator may take them. An unknown family name throws at construction with errorType: 'ConfigurationError'.

const stringsOnly = new Engine({ families: ['ExtString'] });
stringsOnly.eval({ upper: 'abc' }, null); // 'ABC'
stringsOnly.eval({ abs: -1 }, null);      // throws, errorType 'InvalidOperator'

Metering: what a rule costs

evalMetered returns { result, ops } (the result as a JSON string and the operations the evaluation charged), so you can see what a rule costs whether or not a budget is set. Rule.evaluateMetered(data, budget?) is the same thing on an already-compiled rule.

const engine = new Engine();
const rule = { map: [{ var: 'xs' }, { '*': [{ var: '' }, 2] }] };
engine.evalMetered(rule, { xs: [1, 2, 3] });
// { result: '[2,4,6]', ops: 10 }

// An optional third argument caps the operations for that one call,
// overriding the engine's `config.ops_budget`.
engine.evalMetered(rule, { xs: [1, 2, 3] }, 100_000);

One operation is one node the engine dispatches, one item an iterator walks, or whatever an operator charges for the data it moves (the tensor family prices itself in elements). Literals and constant-folded subtrees cost nothing. A budget must be a whole number >= 1. Exceeding it throws an EvaluateError with errorType: 'BudgetExceeded', carrying budget and spent. The engine refuses the evaluation before doing the work, and a try in the rule cannot recover from it.

Errors

Failures throw plain JS Error instances with structured fields attached:

try {
  rule.evaluate(data);
} catch (e) {
  if (e.name === 'ParseError') {
    // Malformed rule or data JSON
  } else if (e.name === 'EvaluateError') {
    console.log(e.errorType);  // stable tag (e.g. "TypeError", "Thrown")
    console.log(e.operator);   // innermost failing operator
    console.log(e.nodeIds);    // leaf-to-root breadcrumb
    console.log(e.path);       // resolved root-to-leaf step list
  }
}

| Property | Contents | |---|---| | name | 'ParseError', 'EvaluateError', 'CompileError' (compileChecked) or 'InternalError' (a caught panic) | | errorType | Stable tag: 'ParseError', 'InvalidOperator', 'InvalidArguments', 'VariableNotFound', 'Thrown', 'TypeError', 'ConfigurationError', 'BudgetExceeded', ... plus 'TypeMismatch' (typed session methods), 'CompileError' and 'InternalError' | | operator | Innermost failing operator, custom operators included, or null | | nodeIds | Breadcrumb of compiled-node ids from the failure site toward the root | | path | Root-to-leaf { nodeId, operator, argIndex, jsonPointer } steps, or null when the error has no compiled rule to resolve against | | budget / spent | BudgetExceeded only | | diagnostics | CompileError only: the objects check returns |

A panic inside the native engine never takes the process down. It is caught at the binding boundary and thrown (or, for evaluateStrAsync, rejected) as an Error with name and errorType set to "InternalError" and the panic message as message. The engine, rule and session stay usable. An InternalError always means a bug; please report it.

Threading

| Type | Pattern | |---|---| | Engine | Build once per thread (the main thread or each worker) and share it across calls on that thread | | Rule | Compile once per thread; evaluateStrAsync evaluates it on the libuv pool | | Session | One per thread | | DataHandle | Parse once per thread; immutable, evaluation never mutates it |

napi class instances cannot be posted or transferred between worker_threads, so each worker loads the module and builds its own.

Tracing

Engine.evaluateWithTrace(logic, data, mode?) evaluates with a step-by-step execution trace. Both arguments are JSON strings. The return value is a JSON string with the same envelope the WASM package (@goplasmatic/datalogic-wasm) produces, so trace consumers such as the React debugger component accept output from either package:

const engine = new Engine();
const run = JSON.parse(
  engine.evaluateWithTrace('{"+": [1, 2, 3]}', 'null')
);
run.result;          // 6
run.steps;           // per-node log: { step_id, node_id, context, result, ... }
run.expression_tree; // compile-time tree: { id, expression, children }
run.pointers;        // { "1": "/+/0", "2": "/+/1", "3": "/+/2", "4": "" }

pointers maps each node id to the RFC 6901 JSON Pointer of the rule value it was compiled from, so a debugger can place every step in the rule as written; it is absent when the rule does not compile. The result keeps its object key order. mode is 'engine' (the default), 'strict' or 'template', as for check, so a rule you compile with compileTemplate can be traced as one.

Failures do not throw. Instead result is null, error carries the message, and structured_error the structured form. The binding compiles the rule with optimization disabled so every operator surfaces a step; expect it to be slower than evalStr. Use it for debugging, not hot paths.

Performance

Geomean across 51 operator benchmark suites (Apple M2 Pro, median of 3 runs; pairwise shared-suite ratios per the methodology): the native Rust core evaluates at 10.3 ns/op, 7.0× faster than json-logic-engine (compiled, the fastest JS engine), 28.1× faster than jsonlogic-rs (the closest Rust alternative), and 83.6× faster than the json-logic-js reference implementation. The WASM build under Node measures 900.5 ns geomean (88× native); on Node servers, prefer @goplasmatic/datalogic-node.

The napi-rs boundary adds a small per-call marshalling cost on top of the core numbers; the measured per-call boundary overhead for this binding, per API tier, lives in BINDINGS-OVERHEAD.md.

Pick the path by your data's shape. Three rules of thumb: compile once and reuse the Rule; when your data is already a JSON string, call evaluateStr (the string path parses JSON text into the engine's arena with no intermediate JS value, the fastest way across the boundary at every payload size); and when the same payload feeds multiple evaluations, parse it once into a DataHandle (the handle paths skip the per-call parse and are the fastest tier of all). If your data lives as plain JS objects and your rules are small, a well-optimized pure-JS engine (e.g. json-logic-engine's compiled mode) runs with zero boundary cost and can beat any native binding on raw ns/op for that shape. Reach for this package when you need string payloads straight from the wire, full conformance including the extension operators, deterministic latency and bounded memory, parallel evaluation across worker threads, or the same engine behaving identically across languages.

Building from source

cd bindings/node
npm install
npx napi build --platform --release
npm test

This produces a local datalogic-node.<platform-triple>.node, plus index.js and index.d.ts loaders. The .node, index.js, and index.d.ts files are gitignored; napi build regenerates them.

Learn more

License

Apache-2.0. See the main repository for source and contribution guidelines.