
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
raft-logic
Advanced tools
raft-logic is a Node.js library that wraps the etcd/tikv Raft implementation (raft-rs) via WebAssembly, exposing a small, promise-based ES module API for running Raft nodes in JavaScript.
Use it to:
Highlights:
Looking for runnable snippets? Skip to the examples section: Examples.
Installation
Requirements
Quick start (single node)
import { RaftNode, InMemoryTransport, InMemoryStorage } from 'raft-logic';
async function run() {
const transport = new InMemoryTransport();
const storage = new InMemoryStorage();
const applied = [];
const node = new RaftNode({
id: '1',
peers: ['1'], // single-node cluster
electionTick: 10,
heartbeatTick: 1,
transport,
storage,
apply: async (entry) => {
if (entry.data) {
const data = Buffer.from(entry.data, 'base64').toString('utf8');
applied.push(data);
console.log('[apply] committed entry:', data);
}
},
tickIntervalMs: 50,
});
await node.start();
// Wait until a leader exists (for single node this should be quick)
await node.waitForLeader(5000);
// Ergonomic client request: auto-forward (if follower), and optionally wait for local apply
await node.clientRequest('hello-from-node', { waitFor: 'apply', timeout: 2000 });
console.log('Applied entries:', applied);
await node.stop();
}
run().catch((e) => {
console.error(e);
process.exit(1);
});
Threaded quick start (worker thread)
import { ThreadedRaftNode, InMemoryTransport } from 'raft-logic';
async function run() {
const transport = new InMemoryTransport();
const applied = [];
const node = new ThreadedRaftNode({
id: '1',
peers: ['1'],
electionTick: 10,
heartbeatTick: 1,
transport,
apply: async (entry) => {
if (entry.data) {
const data = Buffer.from(entry.data, 'base64').toString('utf8');
applied.push(data);
console.log('[apply/main-thread] committed entry:', data);
}
},
tickIntervalMs: 50,
preVote: true,
});
await node.start();
await node.waitForLeader(5000);
// Propose via clientRequest and wait for local apply
const res = await node.clientRequest('hello-from-threaded', { waitFor: 'apply', timeout: 2000 });
console.log('Proposed at index', res.index, 'term', res.term);
await node.stop();
}
run().catch((e) => { console.error(e); process.exit(1); });
Threaded worker apply/storage (optional)
import { ThreadedRaftNode, InMemoryTransport } from 'raft-logic';
import { fileURLToPath } from 'node:url';
const applyModule = fileURLToPath(new URL('./apply.mjs', import.meta.url));
const node = new ThreadedRaftNode({
id: '1',
peers: ['1'],
electionTick: 10,
heartbeatTick: 1,
transport: new InMemoryTransport(),
workerApply: { module: applyModule, export: 'apply' }, // runs apply inside the worker
workerStorage: { kind: 'sqlite', options: { file: './data/node-1.sqlite' } },
});
API overview (high level)
class RaftNode(options)
class ThreadedRaftNode(options)
Adapters
Typed errors
Status/observability
Examples: metrics and readIndex
import { RaftNode, InMemoryTransport, SimpleMetrics } from 'raft-logic';
const transport = new InMemoryTransport();
const metrics = new SimpleMetrics();
const node = new RaftNode({
id: '1',
peers: ['1'],
electionTick: 10,
heartbeatTick: 1,
transport,
apply: () => {},
metrics, // enable metrics hooks
});
await node.start();
await node.waitForLeader(5000);
// Metrics counters increment on proposals and rejections
await node.clientRequest('ex', { waitFor: 'commit', timeout: 2000 });
console.log('metrics snapshot', metrics.snapshot());
// Linearizable read (lease-based): returns a safe commit index
const safeIndex = await node.readIndex({ timeout: 2000 });
// perform your application read knowing state ≥ safeIndex
await node.stop({ drainApply: true, drainTicks: true });
Deterministic testing helpers
Example: deterministic leadership transfer and step down
import { ThreadedRaftNode, InMemoryTransport } from 'raft-logic';
const transport = new InMemoryTransport();
const peers = ['1','2','3'];
const common = {
peers,
electionTick: 10,
heartbeatTick: 1,
preVote: true,
checkQuorum: false,
transport,
apply: async () => {},
tickIntervalMs: 0 // manual ticking for determinism
};
const n1 = new ThreadedRaftNode({ id: '1', ...common });
const n2 = new ThreadedRaftNode({ id: '2', ...common });
const n3 = new ThreadedRaftNode({ id: '3', ...common });
await Promise.all([n1.start(), n2.start(), n3.start()]);
// helper to drive ticks across the cluster
async function driveTicks(rounds = 100) {
for (let i = 0; i < rounds; i++) {
await Promise.all([n1.manualTick(), n2.manualTick(), n3.manualTick()]);
}
}
// elect a leader deterministically
await driveTicks(200);
const leaderId = await n1.runUntilStableLeader(5000);
console.log('Leader elected:', leaderId);
// transfer leadership to node 2
if (leaderId !== '2') {
const leaderNode = leaderId === '1' ? n1 : (leaderId === '2' ? n2 : n3);
await leaderNode.transferLeadership('2', 5000);
await driveTicks(200);
}
// ask node 2 to step down and observe a new leader
await n2.stepDown(5000);
await driveTicks(200);
const nextLeader = await n1.runUntilStableLeader(5000);
console.log('New leader:', nextLeader);
await Promise.all([n1.stop(), n2.stop(), n3.stop()]);
Durable storage: SqliteStorage
Usage (durable)
import { RaftNode, InMemoryTransport, SqliteStorage } from 'raft-logic';
const transport = new InMemoryTransport();
const storage = new SqliteStorage({
file: './data/node-1.sqlite',
onOpen(db) {
db.exec('CREATE TABLE IF NOT EXISTS outbox(id INTEGER PRIMARY KEY, payload TEXT)');
}
});
storage.open();
const node = new RaftNode({
id: '1',
peers: ['1','2','3'],
electionTick: 10,
heartbeatTick: 1,
transport,
storage,
apply: async (entry) => { /* your state machine */ },
});
await node.start();
await node.waitForLeader(5000);
const { index, term } = await node.clientRequest('do-something', { waitFor: 'commit', timeout: 2000 });
console.log('Committed at index', index, 'term', term);
await node.stop();
storage.close();
Notes on WASM
Implementation notes
Changelog (recent)
New APIs have been added to give developers explicit control and visibility over the WebAssembly runtime lifecycle.
These are especially useful in long-lived or test-driven environments where multiple Raft clusters are created and destroyed in one process.
import { disableAutoFree } from "raft-logic/loader.mjs";
disableAutoFree(); // Keeps the WASM runtime alive for the entire process
or equivalently:
import { retainWasm } from "raft-logic/loader.mjs";
retainWasm(true);
Ensures all RaftNodes share the same WASM instance:
import { getWasmInstance } from "raft-logic/loader.mjs";
const wasm = await getWasmInstance();
Inspect the current WASM runtime state:
import { getWasmStatus } from "raft-logic/loader.mjs";
console.log(getWasmStatus()); // { refCount: 0, freed: false, autoFreeDisabled: true }
When auto-free is disabled, the runtime will not be freed automatically:
import { controlledShutdownWasm } from "raft-logic/loader.mjs";
await controlledShutdownWasm(); // Skips freeing if disableAutoFree() was called
These APIs make raft-logic more robust for frameworks, test suites, and long-lived processes.
A new diagnostic test has been added to help verify multi-cluster Raft behavior and isolate issues such as worker lifecycle or election stalls.
npm test --silent -- test/multi-instance-diagnostics.test.mjs
This test creates two independent Raft clusters and drives them manually using manualTick().
It verifies that both clusters elect leaders independently and remain isolated.
The diagnostic test supports an enableLogs option to toggle detailed Raft logs:
const clusterA = await makeCluster('A', { enableLogs: true });
const clusterB = await makeCluster('B', { enableLogs: false });
When enableLogs is true, detailed [apply ...] and Raft debug logs are printed.
When false, the test runs silently except for high-level diagnostic messages.
License
FAQs
Node.js wrapper around a WASM build of tikv/raft-rs (via wasm-bindgen).
We found that raft-logic demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.