{"_id":"@distributed-cordis/raft-logic","_rev":"2-f8cd9b77e7d071fa182b9252a31aa898","name":"@distributed-cordis/raft-logic","dist-tags":{"latest":"0.3.15"},"versions":{"0.0.0":{"name":"@distributed-cordis/raft-logic","version":"0.0.0","_id":"@distributed-cordis/raft-logic@0.0.0","maintainers":[{"name":"clinal","email":"clinal@anil.lc"},{"name":"anillc","email":"void@anillc.cn"}],"dist":{"shasum":"9785eb056358ead42729d08ac816e7b3a679085c","tarball":"https://registry.npmjs.org/@distributed-cordis/raft-logic/-/raft-logic-0.0.0.tgz","fileCount":1,"integrity":"sha512-0sWJn0/ZWn2D3PUCwnhiBrpqUYMkWwC75Ehzjl6MayH1shqXJzFuCRiWOG7gLlZPZmsONLPEyoHWJOmwEJJB+g==","signatures":[{"sig":"MEUCIQCIk0Vi9PdqD4FEHeyda1NvFqlY7xD/97YceVkpBX1ILgIgXsL4gn7D+mOka9LTQ94ikF90LcPwnCtsdkTtw1rALYI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":69},"_npmUser":{"name":"anillc","email":"void@anillc.cn"},"_npmVersion":"11.6.2","directories":{},"_nodeVersion":"24.13.0","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/raft-logic_0.0.0_1786966426287_0.29982050159282214","host":"s3://npm-registry-packages-npm-production"}},"0.3.15":{"name":"@distributed-cordis/raft-logic","version":"0.3.15","description":"Node.js wrapper around a WASM build of tikv/raft-rs (via wasm-bindgen).","license":"MIT OR Apache-2.0","repository":{"type":"git","url":"ssh://git@codeberg.org/psvensson/raft-logic.git"},"keywords":["raft","consensus","etcd","tikv","wasm","webassembly","node"],"type":"module","main":"./index.mjs","module":"./index.mjs","types":"./index.d.ts","exports":{".":{"types":"./index.d.ts","import":"./index.mjs","default":"./index.mjs"},"./loader.mjs":{"types":"./loader.d.ts","import":"./loader.mjs","default":"./loader.mjs"},"./*":"./*"},"engines":{"node":">=22.14"},"scripts":{"build:wasm":"bash -c 'set -euo pipefail; if [ -d native/raft-wasm ]; then cd native/raft-wasm; elif [ -d packages/raft-wasm ]; then cd packages/raft-wasm; else echo \"No raft-wasm crate found (expected native/raft-wasm or packages/raft-wasm)\"; exit 1; fi; wasm-pack build --release --target nodejs --out-name raft_wasm --out-dir ../../wasm; cd - >/dev/null; rm -f wasm/package.json wasm/.gitignore; if [ -f wasm/raft_wasm.js ]; then mv wasm/raft_wasm.js wasm/raft_wasm.cjs; fi'","prepack":"bash -c 'set -e; if [ -f wasm/raft_wasm.cjs ] && [ -f wasm/raft_wasm_bg.wasm ]; then echo \"Using existing wasm artifacts\"; else npm run build:wasm; fi'","prepare":"npm run build:wasm || true","example:single":"node examples/single-node.mjs","example:single-threaded":"node examples/single-node-threaded.mjs","example:three":"node examples/three-node.mjs","example:three-sqlite":"node examples/three-node-sqlite.mjs","example:three-sqlite-restart":"node examples/three-node-sqlite-restart.mjs","test":"node --test","test:types":"tsc --project tsconfig.types.json"},"devDependencies":{"typescript":"^5.9.2"},"directories":{"example":"examples"},"author":{"name":"psvensson@gmail.com"},"gitHead":"de7989276cd9b69907e2f7959061b03bcd10c48d","_id":"@distributed-cordis/raft-logic@0.3.15","_nodeVersion":"24.19.0","_npmVersion":"11.17.0","dist":{"integrity":"sha512-UM9jVKNAGpjsZJzVJmPduDWWxQHo0tOWN8xRS2pGVwnuOmGU8X/YvOtKZhFEOEAKaC2ht85PRjNOfFYZaBhZ2A==","shasum":"21777c375299b5a38277f4fc0cb2259735e1ab36","tarball":"https://registry.npmjs.org/@distributed-cordis/raft-logic/-/raft-logic-0.3.15.tgz","fileCount":19,"unpackedSize":848842,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIFkyZ5S7SxteKmV5wr4QytLnZAWeZX7thI/FAfwf9OVVAiEAgGmrs9SOwahqIahTiCyqHXvfBPADIrFdxOy3Mxse/qg="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:dcfa3388-aed4-459a-b523-f69f7a32de0d"}},"maintainers":[{"name":"clinal","email":"clinal@anil.lc"},{"name":"anillc","email":"void@anillc.cn"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/raft-logic_0.3.15_1786969660650_0.12155292178292498"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-17T11:33:46.092Z","modified":"2026-08-17T12:27:41.762Z","0.0.0":"2026-08-17T11:33:46.415Z","0.3.15":"2026-08-17T12:27:40.820Z"},"maintainers":[{"name":"clinal","email":"clinal@anil.lc"},{"name":"anillc","email":"void@anillc.cn"}],"readme":"# raft-logic\n\nraft-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.\n\nUse it to:\n- Embed consensus in a service without running a separate Raft daemon.\n- Build replicated state machines for metadata, coordination, or job scheduling.\n- Prototype or test Raft behaviors with deterministic, controllable ticks.\n\nHighlights:\n- ESM-only (Node 22.14+)\n- Minimal in-memory transport and storage for tests/examples\n- Bundled WASM artifacts (no external download needed)\n- Deterministic helpers: transferLeadership, stepDown, runUntilStableLeader\n- Event signals and waiters: onRoleChange, onCommitAdvanced, onQuorumActive; waitForLeaderStable(), waitForHeartbeatRound(), waitForFirstStableLeader()\n\nLooking for runnable snippets? Skip to the examples section: [Examples](#examples).\n\nInstallation\n- npm install raft-logic\n\nRequirements\n- Node.js >= 22.14 (uses the built-in `node:sqlite` module)\n- ESM environment (either use .mjs files or set \"type\": \"module\" in your package.json)\n\nQuick start (single node)\n```js\nimport { RaftNode, InMemoryTransport, InMemoryStorage } from '@distributed-cordis/raft-logic';\n\nasync function run() {\n  const transport = new InMemoryTransport();\n  const storage = new InMemoryStorage();\n\n  const applied = [];\n  const node = new RaftNode({\n    id: '1',\n    peers: ['1'],          // single-node cluster\n    electionTick: 10,\n    heartbeatTick: 1,\n    transport,\n    storage,\n    apply: async (entry) => {\n      if (entry.data) {\n        const data = Buffer.from(entry.data, 'base64').toString('utf8');\n        applied.push(data);\n        console.log('[apply] committed entry:', data);\n      }\n    },\n    tickIntervalMs: 50,\n  });\n\n  await node.start();\n\n  // Wait until a leader exists (for single node this should be quick)\n  await node.waitForLeader(5000);\n\n  // Ergonomic client request: auto-forward (if follower), and optionally wait for local apply\n  await node.clientRequest('hello-from-node', { waitFor: 'apply', timeout: 2000 });\n\n  console.log('Applied entries:', applied);\n  await node.stop();\n}\n\nrun().catch((e) => {\n  console.error(e);\n  process.exit(1);\n});\n```\n\nThreaded quick start (worker thread)\n```js\nimport { ThreadedRaftNode, InMemoryTransport } from '@distributed-cordis/raft-logic';\n\nasync function run() {\n  const transport = new InMemoryTransport();\n\n  const applied = [];\n  const node = new ThreadedRaftNode({\n    id: '1',\n    peers: ['1'],\n    electionTick: 10,\n    heartbeatTick: 1,\n    transport,\n    apply: async (entry) => {\n      if (entry.data) {\n        const data = Buffer.from(entry.data, 'base64').toString('utf8');\n        applied.push(data);\n        console.log('[apply/main-thread] committed entry:', data);\n      }\n    },\n    tickIntervalMs: 50,\n    preVote: true,\n  });\n\n  await node.start();\n  await node.waitForLeader(5000);\n\n  // Propose via clientRequest and wait for local apply\n  const res = await node.clientRequest('hello-from-threaded', { waitFor: 'apply', timeout: 2000 });\n  console.log('Proposed at index', res.index, 'term', res.term);\n\n  await node.stop();\n}\n\nrun().catch((e) => { console.error(e); process.exit(1); });\n```\n\nThreaded worker apply/storage (optional)\n```js\nimport { ThreadedRaftNode, InMemoryTransport } from '@distributed-cordis/raft-logic';\nimport { fileURLToPath } from 'node:url';\n\nconst applyModule = fileURLToPath(new URL('./apply.mjs', import.meta.url));\n\nconst node = new ThreadedRaftNode({\n  id: '1',\n  peers: ['1'],\n  electionTick: 10,\n  heartbeatTick: 1,\n  transport: new InMemoryTransport(),\n  workerApply: { module: applyModule, export: 'apply' }, // runs apply inside the worker\n  workerStorage: { kind: 'sqlite', options: { file: './data/node-1.sqlite' } },\n});\n```\n\nAPI overview (high level)\n- class RaftNode(options)\n  - options:\n    - id: string (stringified u64)\n    - peers: string[] (initial voter set, stringified u64)\n    - electionTick: number\n    - heartbeatTick: number\n    - transport: { send(fromId: string, msgs: object[]): Promise<void>, register?: (node: RaftNode) => void }\n    - storage?: { initialState?(id: string): Promise<object>, persistReady?(id: string, ready: any): Promise<void> }\n    - apply(entry): function called for each committed entry\n    - tickIntervalMs?: number (default ~100ms)\n    - preVote?: boolean (default false)\n    - checkQuorum?: boolean (default false)\n    - onStateUpdate?(snapshot): optional callback to observe internal state cache updates\n    - onDrain?(): optional callback when the Ready queue is drained\n    - metrics?: { onPropose?(): void, onRejected?(reason: string, meta?: object): void, onRoleChange?(prev: string, next: string): void }\n  - methods:\n    - start(): Promise<void>\n    - stop(opts?): Promise<void>\n      - opts: { drainApply?: boolean, drainTicks?: boolean } to quiesce before stopping\n    - status(): Promise<{ role, term, lead, commitIndex, lastApplied, lastLogIndex, raft_state } | null>\n    - propose(data: Uint8Array | ArrayBuffer | string): Promise<void>\n      - Throws NotLeaderError on follower/candidate; safe to call on single-node or when leader hint points to self\n    - addNode(id, opts?): Promise<{ index, term, members }> — commit a new voting member\n    - removeNode(id, opts?): Promise<{ index, term, members }> — remove a voting member\n      - Membership changes must be submitted to the leader; `opts` accepts `timeout` and `abortSignal`.\n    - schedulePropose(data, opts?): Promise<{ index, term }>\n      - Defers propose out of apply() context; safe alternative to propose() in re-entrant scenarios\n    - clientRequest(data, opts?): Promise<{ index, term }>\n      - opts: { autoForward?: boolean=true, waitFor?: 'none'|'commit'|'apply'='none', timeout?: number, abortSignal?: AbortSignal }\n      - Auto-forwards to current leader if called on a follower (when transport supports it). Optionally waits for commit or local apply.\n    - waitForLeader(timeoutMs?): Promise<string> — resolves with leaderId when a stable leader exists\n    - waitForLeaderStable(options?): Promise<void> — event-driven waiter; options { requireQuorum=true, minTermResidencyTicks=0, signal?: AbortSignal }\n    - waitForHeartbeatRound(n=1, options?): Promise<void> — resolves after n majority heartbeat rounds in current term\n    - waitForCommit(index, { localApply?: boolean, timeout?: number, abortSignal?: AbortSignal }): Promise<void>\n      - Waits for index to be committed cluster-wide and (optionally) applied locally\n    - readIndex(opts?): Promise<number> — leader-gated lease read; returns safe commit index\n    - barrier(): Promise<void>\n      - Leader: resolves when all prior proposals are committed\n      - Follower: resolves when all prior proposals are committed and applied locally\n      - manualTick(): Promise<void>, advanceTicks(n): Promise<void>\n        - Deterministic testing helpers to manually drive ticks\n      - step(msg: object): Promise<void>\n      - campaign(): Promise<void>\n      - transferLeadership(targetId, timeoutMs?): Promise<void> — deterministic leader handoff to a target peer (test helper)\n      - stepDown(timeoutMs?): Promise<void> — relinquish leadership and trigger re-election/transfer\n      - runUntilStableLeader(timeoutMs?): Promise<string> — wait for a stable leader and a short stability window\n\n- class ThreadedRaftNode(options)\n  - Same high-level behavior as RaftNode but runs the raft core (WASM + storage + drain loop) inside a worker thread.\n  - Options: same as RaftNode; apply() is required unless workerApply/applyMode 'worker' is set.\n    - applyMode: 'main' | 'worker' (workerApply implies 'worker')\n    - workerApply: { module: string, export?: string, options?: any } to run apply inside the worker.\n    - workerStorage: { kind: 'inmemory'|'sqlite', options?: object } or { module: string, export?: string, options?: object } for worker storage.\n  - Additional methods and signals:\n    - onStateChange(cb), onBecameLeader(cb), onBecameFollower(cb): subscribe to state updates\n    - transferLeadership(targetId, timeoutMs?): Promise<void> — test helper via MsgTransferLeader\n    - stepDown(timeoutMs?): Promise<void> — test helper via MsgTimeoutNow\n    - runUntilStableLeader(timeoutMs?): Promise<string> — waits for a leader and a stability window (no role/lead changes)\n    - readIndex(opts?): Promise<number> — leader-gated lease read; returns safe commit index\n    - stop(opts?): Promise<void> — accepts { drainApply?: boolean, drainTicks?: boolean }\n\n- Adapters\n  - InMemoryTransport: routes messages within the same process for testing.\n    - sendClientRequest(leaderId, data, opts) — in-memory convenience used by clientRequest auto-forwarding\n  - Utilities:\n    - leaderForwarder(transport, getLeaderId): returns async function to route clientRequest to the current leader\n    - waitForFirstStableLeader(nodes, options?): Promise<Node> — Promise.any over nodes’ waitForLeaderStable to pick the first stable leader\n    - waitForReadyLeader(node, options?): Promise<void> — composite helper that waits for a leader, drains backlog, and (optionally) enforces a lease-based read barrier before treating the node as ready for client operations.\n  - InMemoryStorage: demonstrates the persist-before-advance contract (not durable)\n  - SqliteStorage: production-oriented durable storage\n    - getDb(): returns the underlying `node:sqlite` DatabaseSync handle for co-located app tables\n    - onOpen(db): optional callback invoked when the DB is opened to initialize co-located tables\n\n- Typed errors\n  - NotLeaderError: { code: 'NotLeader', term: number, leaderId?: string, forwardHint?: { id?: string } }\n  - TimeoutError: { code: 'Timeout' }\n  - RejectedError: { code: 'Rejected', reason?: 'Reentrancy' | 'NotLeader' | ... }\n\nStatus/observability\n- status(): returns a richer snapshot: { role: 'leader'|'follower'|'candidate', term, lead, commitIndex, lastApplied, lastLogIndex, raft_state }\n- Event-style callbacks on ThreadedRaftNode: onStateChange, onBecameLeader, onBecameFollower\n- Barrier and waiter APIs for deterministic sequencing in tests and app logic\n\nExamples: metrics and readIndex\n```js\nimport { RaftNode, InMemoryTransport, SimpleMetrics } from '@distributed-cordis/raft-logic';\n\nconst transport = new InMemoryTransport();\nconst metrics = new SimpleMetrics();\n\nconst node = new RaftNode({\n  id: '1',\n  peers: ['1'],\n  electionTick: 10,\n  heartbeatTick: 1,\n  transport,\n  apply: () => {},\n  metrics, // enable metrics hooks\n});\n\nawait node.start();\nawait node.waitForLeader(5000);\n\n// Metrics counters increment on proposals and rejections\nawait node.clientRequest('ex', { waitFor: 'commit', timeout: 2000 });\nconsole.log('metrics snapshot', metrics.snapshot());\n\n// Linearizable read (lease-based): returns a safe commit index\nconst safeIndex = await node.readIndex({ timeout: 2000 });\n// perform your application read knowing state ≥ safeIndex\n\nawait node.stop({ drainApply: true, drainTicks: true });\n```\n\nDeterministic testing helpers\n- Set tickIntervalMs in RaftNode to 0 (or use ThreadedRaftNode helpers) and drive ticks via manualTick()/advanceTicks(n) to remove wall clock coupling.\n- transferLeadership(targetId), stepDown() allow deterministic failover in tests.\n- runUntilStableLeader(timeout) blocks until a stable leader is observed (and holds through a short stability window).\n\nExample: deterministic leadership transfer and step down\n```js\nimport { ThreadedRaftNode, InMemoryTransport } from '@distributed-cordis/raft-logic';\n\nconst transport = new InMemoryTransport();\nconst peers = ['1','2','3'];\n\nconst common = {\n  peers,\n  electionTick: 10,\n  heartbeatTick: 1,\n  preVote: true,\n  checkQuorum: false,\n  transport,\n  apply: async () => {},\n  tickIntervalMs: 0 // manual ticking for determinism\n};\n\nconst n1 = new ThreadedRaftNode({ id: '1', ...common });\nconst n2 = new ThreadedRaftNode({ id: '2', ...common });\nconst n3 = new ThreadedRaftNode({ id: '3', ...common });\n\nawait Promise.all([n1.start(), n2.start(), n3.start()]);\n\n// helper to drive ticks across the cluster\nasync function driveTicks(rounds = 100) {\n  for (let i = 0; i < rounds; i++) {\n    await Promise.all([n1.manualTick(), n2.manualTick(), n3.manualTick()]);\n  }\n}\n\n// elect a leader deterministically\nawait driveTicks(200);\nconst leaderId = await n1.runUntilStableLeader(5000);\nconsole.log('Leader elected:', leaderId);\n\n// transfer leadership to node 2\nif (leaderId !== '2') {\n  const leaderNode = leaderId === '1' ? n1 : (leaderId === '2' ? n2 : n3);\n  await leaderNode.transferLeadership('2', 5000);\n  await driveTicks(200);\n}\n\n// ask node 2 to step down and observe a new leader\nawait n2.stepDown(5000);\nawait driveTicks(200);\nconst nextLeader = await n1.runUntilStableLeader(5000);\nconsole.log('New leader:', nextLeader);\n\nawait Promise.all([n1.stop(), n2.stop(), n3.stop()]);\n```\n\nDurable storage: SqliteStorage\n- Upholds persist-before-advance:\n  1) snapshot (compaction: delete entries <= snapshot.index)\n  2) entries (tail replacement: delete entries >= firstNew, then insert)\n  3) hardState (term, vote, commit)\n- Durable PRAGMAs on open: WAL + synchronous=FULL by default (configurable).\n- getDb(): gives access to the same database handle for co-located app tables.\n- onOpen(db): optional hook to set up your co-located schema.\n- Optional applyWithDb(entry, db): if provided, called during apply with the same handle used by raft-logic. This enables efficient, consistent side-effects (best-effort; user manages app-level transactions).\n\nUsage (durable)\n```js\nimport { RaftNode, InMemoryTransport, SqliteStorage } from '@distributed-cordis/raft-logic';\n\nconst transport = new InMemoryTransport();\nconst storage = new SqliteStorage({\n  file: './data/node-1.sqlite',\n  onOpen(db) {\n    db.exec('CREATE TABLE IF NOT EXISTS outbox(id INTEGER PRIMARY KEY, payload TEXT)');\n  }\n});\nstorage.open();\n\nconst node = new RaftNode({\n  id: '1',\n  peers: ['1','2','3'],\n  electionTick: 10,\n  heartbeatTick: 1,\n  transport,\n  storage,\n  apply: async (entry) => { /* your state machine */ },\n});\nawait node.start();\nawait node.waitForLeader(5000);\n\nconst { index, term } = await node.clientRequest('do-something', { waitFor: 'commit', timeout: 2000 });\nconsole.log('Committed at index', index, 'term', term);\n\nawait node.stop();\nstorage.close();\n```\n\nNotes on WASM\n- The package bundles the wasm-bindgen output under ./wasm and the loader automatically initializes it in Node.js.\n- You normally do not need to call loadWasm() directly; RaftNode.start() will initialize it if needed.\n- Low-level helpers:\n  - loadWasm(customUrl?): Promise<Exports>\n  - wasmReady(): Exports\n  - ready(customUrl?): Promise<void>\n\n## Examples\n- Single node (threaded):\n  - npm run example:single-threaded\n- Three nodes (in-memory):\n  - npm run example:three\n- Three nodes + SQLite:\n  - npm run example:three-sqlite\n- Restart + recovery demo:\n  - npm run example:three-sqlite-restart\n\nImplementation notes\n- Schema avoids reserved SQLite keywords:\n  - hard_state.commit_index (not \"commit\")\n  - snapshot.snap_index (not \"index\")\n- Entries persist the entry type as etype; the host JSON boundary uses entryType.\n\nChangelog (recent)\n- New event signals: onRoleChange, onCommitAdvanced, onQuorumActive\n- New waiters: waitForLeaderStable(options), waitForHeartbeatRound(n), and waitForFirstStableLeader(nodes, options)\n- loader.ready(): Promise ensures WASM initialized without polling\n- Added clientRequest(entry, opts) with auto-forwarding, waitFor=commit/apply, timeout/abort.\n- propose() returns NotLeaderError on followers (with leaderId, term, forwardHint).\n- Leadership signals and barriers:\n  - onBecameLeader/onBecameFollower/onStateChange\n  - waitForLeader(), barrier()\n- Waiters: waitForCommit(index, { localApply, timeout, abortSignal })\n- Deterministic testing: manualTick(), advanceTicks(), transferLeadership(), stepDown(), runUntilStableLeader()\n- SqliteStorage: getDb(), onOpen(db), optional applyWithDb(entry, db)\n- Richer status(): role, term, lead, commitIndex, lastApplied, lastLogIndex\n- Typed errors: NotLeaderError, TimeoutError, RejectedError\n- Metrics hooks: metrics.onPropose(), metrics.onRejected(reason, meta), metrics.onRoleChange(prev, next)\n- readIndex(): leader-gated lease-based linearizable read; returns commit index\n- stop(opts): { drainApply, drainTicks } to quiesce before freeing\n- Utilities: leaderForwarder(transport, getLeaderId)\n\n## WASM Lifecycle Control and Diagnostics\n\nNew APIs have been added to give developers explicit control and visibility over the WebAssembly runtime lifecycle.  \nThese are especially useful in long-lived or test-driven environments where multiple Raft clusters are created and destroyed in one process.\n\n### Prevent premature freeing\n```js\nimport { disableAutoFree } from \"@distributed-cordis/raft-logic/loader.mjs\";\ndisableAutoFree(); // Keeps the WASM runtime alive for the entire process\n```\n\nor equivalently:\n```js\nimport { retainWasm } from \"@distributed-cordis/raft-logic/loader.mjs\";\nretainWasm(true);\n```\n\n### Global singleton loader\nEnsures all RaftNodes share the same WASM instance:\n```js\nimport { getWasmInstance } from \"@distributed-cordis/raft-logic/loader.mjs\";\nconst wasm = await getWasmInstance();\n```\n\n### Diagnostics\nInspect the current WASM runtime state:\n```js\nimport { getWasmStatus } from \"@distributed-cordis/raft-logic/loader.mjs\";\nconsole.log(getWasmStatus()); // { refCount: 0, freed: false, autoFreeDisabled: true }\n```\n\n### Controlled shutdown\nWhen auto-free is disabled, the runtime will not be freed automatically:\n```js\nimport { controlledShutdownWasm } from \"@distributed-cordis/raft-logic/loader.mjs\";\nawait controlledShutdownWasm(); // Skips freeing if disableAutoFree() was called\n```\n\nThese APIs make `raft-logic` more robust for frameworks, test suites, and long-lived processes.\n\n## Diagnostic Tests and Optional Logging\n\nA new diagnostic test has been added to help verify multi-cluster Raft behavior and isolate issues such as worker lifecycle or election stalls.\n\n### Running the diagnostic test\n```bash\nnpm test --silent -- test/multi-instance-diagnostics.test.mjs\n```\n\nThis test creates two independent Raft clusters and drives them manually using `manualTick()`.  \nIt verifies that both clusters elect leaders independently and remain isolated.\n\n### Optional logging\nThe diagnostic test supports an `enableLogs` option to toggle detailed Raft logs:\n```js\nconst clusterA = await makeCluster('A', { enableLogs: true });\nconst clusterB = await makeCluster('B', { enableLogs: false });\n```\n\nWhen `enableLogs` is `true`, detailed `[apply ...]` and Raft debug logs are printed.  \nWhen `false`, the test runs silently except for high-level diagnostic messages.\n\n---\n\nLicense\n- MIT OR Apache-2.0\n","readmeFilename":"README.md","description":"Node.js wrapper around a WASM build of tikv/raft-rs (via wasm-bindgen).","keywords":["raft","consensus","etcd","tikv","wasm","webassembly","node"],"repository":{"type":"git","url":"ssh://git@codeberg.org/psvensson/raft-logic.git"},"author":{"name":"psvensson@gmail.com"},"license":"MIT OR Apache-2.0"}