<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dmytro Bardonov</title>
    <description>The latest articles on DEV Community by Dmytro Bardonov (@__b4700c753b04).</description>
    <link>https://dev.to/__b4700c753b04</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2115913%2Ff12562eb-ff67-40ae-8dfc-8bd03e22537f.png</url>
      <title>DEV Community: Dmytro Bardonov</title>
      <link>https://dev.to/__b4700c753b04</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/__b4700c753b04"/>
    <language>en</language>
    <item>
      <title>Building an Embedded Raft SDK for Existing Node.js Services</title>
      <dc:creator>Dmytro Bardonov</dc:creator>
      <pubDate>Tue, 22 Sep 2026 15:41:05 +0000</pubDate>
      <link>https://dev.to/__b4700c753b04/building-an-embedded-raft-sdk-for-existing-nodejs-services-4d2l</link>
      <guid>https://dev.to/__b4700c753b04/building-an-embedded-raft-sdk-for-existing-nodejs-services-4d2l</guid>
      <description>&lt;p&gt;Sometimes an application needs Raft semantics, but it does not need another general-purpose&lt;br&gt;
distributed system.&lt;/p&gt;

&lt;p&gt;An existing scheduler, control plane, metadata service, or coordination service may already have its&lt;br&gt;
own domain model and API. What it lacks is a safe way for several instances to elect a leader, agree&lt;br&gt;
on an ordered command stream, and apply that stream to local state.&lt;/p&gt;

&lt;p&gt;The usual choices are not always comfortable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;implement Raft inside the application;&lt;/li&gt;
&lt;li&gt;redesign the application around an external system such as etcd;&lt;/li&gt;
&lt;li&gt;or operate a larger platform whose data model and purpose do not match the application.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal of &lt;a href="https://github.com/dmitry-bardonov/node-raft-rsm" rel="noopener noreferrer"&gt;&lt;code&gt;node-raft-rsm&lt;/code&gt;&lt;/a&gt; is to offer another&lt;br&gt;
option: an embedded TypeScript SDK that lets an existing Node.js service use Raft primitives through&lt;br&gt;
a focused API.&lt;/p&gt;

&lt;p&gt;The application should keep ownership of its commands, state machine, storage strategy, transport,&lt;br&gt;
and public API. The SDK should own the difficult consensus mechanics: terms, elections, log&lt;br&gt;
replication, quorum commitment, recovery ordering, and serialized application.&lt;/p&gt;

&lt;p&gt;Raft itself is often introduced with three reassuring words: &lt;em&gt;leader, log, majority&lt;/em&gt;. That sounds&lt;br&gt;
simple until you try to answer the questions that matter in a real implementation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When is a log entry safe to apply?&lt;/li&gt;
&lt;li&gt;What must be durable before a node sends an acknowledgement?&lt;/li&gt;
&lt;li&gt;What happens when a response is duplicated, delayed, or dropped?&lt;/li&gt;
&lt;li&gt;Can a client treat a timeout as failure?&lt;/li&gt;
&lt;li&gt;How do you test all of this without waiting for real clocks and real network failures?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I built &lt;code&gt;node-raft-rsm&lt;/code&gt; to put those details behind a small set of typed contracts. It is accompanied&lt;br&gt;
by a deterministic test harness and an interactive React visualizer, but the central product is the&lt;br&gt;
SDK and its integration boundary.&lt;/p&gt;

&lt;p&gt;It is not production-ready. That is worth saying at the beginning, not hiding at the end. The&lt;br&gt;
durable adapter, complete snapshot lifecycle, authenticated multi-process transport, dynamic&lt;br&gt;
membership, linearizable reads, and a larger fault matrix are still work in progress.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Project status:&lt;/strong&gt; &lt;code&gt;node-raft-rsm&lt;/code&gt; is under active development. The current release is for&lt;br&gt;
evaluation, deterministic testing, API design, and continued implementation—not production data.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What exists today is a useful implementation slice for integrating and evaluating the API, learning&lt;br&gt;
Raft mechanics, and discussing the boundaries between consensus, durability, and application state.&lt;/p&gt;
&lt;h2&gt;
  
  
  The integration experience I want
&lt;/h2&gt;

&lt;p&gt;Adding Raft to an existing system should not require rewriting that system around a generic&lt;br&gt;
key-value store. The intended integration flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define the application's commands and typed results.&lt;/li&gt;
&lt;li&gt;Implement a deterministic state machine for those commands.&lt;/li&gt;
&lt;li&gt;Provide storage and transport adapters appropriate for the deployment.&lt;/li&gt;
&lt;li&gt;Create a &lt;code&gt;RaftNode&lt;/code&gt; with stable cluster membership and identity.&lt;/li&gt;
&lt;li&gt;Route existing mutation paths through &lt;code&gt;node.propose()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Keep the application's existing HTTP, RPC, or message-based API in front of the SDK.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The public surface is deliberately centered on a few operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;RaftNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;propose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;commandId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;consistency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;local&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SDK does not decide what an account, job, lock, configuration update, or scheduling decision&lt;br&gt;
looks like. It provides the consensus mechanism that orders those domain operations.&lt;/p&gt;

&lt;p&gt;The current implementation is a Node.js library, not a standalone cross-language Raft daemon. A&lt;br&gt;
non-Node program would access consensus through the host service's existing API. A dedicated network&lt;br&gt;
API could be built later, but it should remain a thin boundary over the same SDK contracts.&lt;/p&gt;
&lt;h2&gt;
  
  
  A concrete SDK integration
&lt;/h2&gt;

&lt;p&gt;Imagine an existing scheduling service. It already has HTTP endpoints, validation, domain types,&lt;br&gt;
metrics, and a local scheduler state. We want every replica to apply the same job mutations in the&lt;br&gt;
same order.&lt;/p&gt;

&lt;p&gt;First, create one &lt;code&gt;RaftNode&lt;/code&gt; in each service process. Identity and membership must come from stable&lt;br&gt;
configuration rather than being generated at startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;clusterId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nodeId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@node-raft-rsm/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;JsonCommandCodec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RaftNode&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@node-raft-rsm/node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SchedulerStateMachine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;validateSchedulerCommand&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./scheduler-state-machine.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;storage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./raft-storage.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;transport&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./raft-transport.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raftMembers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nodeId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;machine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SchedulerStateMachine&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;codec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;JsonCommandCodec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;validateSchedulerCommand&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;RaftNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;nodeId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;nodeId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodeId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;clusterId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;clusterId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scheduler-cluster&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;heartbeatInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;electionTimeoutMinMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;electionTimeoutMaxMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;stateMachine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;machine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;codec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;onEvent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recordRaftEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;raft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;storage&lt;/code&gt; and &lt;code&gt;transport&lt;/code&gt; are application-provided adapters. The repository currently ships&lt;br&gt;
deterministic in-memory implementations for development and tests; production-grade durable storage&lt;br&gt;
and authenticated transport are still under development.&lt;/p&gt;

&lt;p&gt;Then route an existing mutation endpoint through &lt;code&gt;propose()&lt;/code&gt; instead of mutating scheduler state&lt;br&gt;
directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;randomUUID&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NotLeaderError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ProposalTimeoutError&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@node-raft-rsm/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/jobs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commandId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idempotency-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;schedule-job&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;runAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;raft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;propose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;commandId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;NotLeaderError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NOT_LEADER&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;leaderHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;leaderHint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;ProposalTimeoutError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;504&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OUTCOME_UNKNOWN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;commandId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Retry identical command bytes with the same command ID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service keeps its &lt;code&gt;/jobs&lt;/code&gt; API and domain model. Raft is an internal consistency mechanism rather&lt;br&gt;
than a replacement public API.&lt;/p&gt;

&lt;p&gt;Reads must also communicate their guarantee. Only explicitly local reads exist today:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;raft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;machine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;consistency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;local&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That result may be stale, including when read from a former leader isolated by a partition. The SDK&lt;br&gt;
does not yet expose a linearizable ReadIndex-style operation.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why not just use etcd?
&lt;/h2&gt;

&lt;p&gt;etcd is a mature distributed key-value store with a broad operational ecosystem. If its data model&lt;br&gt;
and API fit the problem, using it is usually safer than adopting an experimental consensus library.&lt;/p&gt;

&lt;p&gt;This project is aimed at a different integration shape. It is useful when Raft should be part of the&lt;br&gt;
application runtime and the replicated state machine is application-specific. The host service may&lt;br&gt;
need typed commands, domain-level conflict results, custom snapshots, or state that should not be&lt;br&gt;
modeled as an external generic key-value store.&lt;/p&gt;

&lt;p&gt;The goal is not to recreate every etcd feature. It is to provide a smaller, composable Raft layer for&lt;br&gt;
systems that already know what their data and APIs should look like.&lt;/p&gt;
&lt;h2&gt;
  
  
  When do you actually need embedded Raft?
&lt;/h2&gt;

&lt;p&gt;Most web applications do not.&lt;/p&gt;

&lt;p&gt;If several stateless API processes use an authoritative shared database, the database already&lt;br&gt;
provides the consistency boundary. Adding Raft to the application tier would usually create more&lt;br&gt;
operational complexity without improving the system.&lt;/p&gt;

&lt;p&gt;Embedded Raft makes sense when every process owns a local copy of state and the processes must agree&lt;br&gt;
on one ordered mutation stream. Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;replicated schedulers;&lt;/li&gt;
&lt;li&gt;metadata and configuration services;&lt;/li&gt;
&lt;li&gt;control planes;&lt;/li&gt;
&lt;li&gt;coordination systems;&lt;/li&gt;
&lt;li&gt;research and simulation tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mental model is not “make my service highly available with one library call.” It is “give every&lt;br&gt;
replica of my existing service the same committed command sequence.”&lt;/p&gt;
&lt;h2&gt;
  
  
  The command lifecycle
&lt;/h2&gt;

&lt;p&gt;The most important line in the project README is this one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;propose → append → persist → replicate → quorum → commit → apply → return result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those steps are deliberately separate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Append&lt;/strong&gt; means the leader added a command to its local log. The command is not committed yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commit&lt;/strong&gt; means a quorum has stored the entry and the leader has advanced its commit index according&lt;br&gt;
to Raft's current-term rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apply&lt;/strong&gt; means the committed command has been executed by the local replicated state machine.&lt;/p&gt;

&lt;p&gt;In this SDK, &lt;code&gt;propose()&lt;/code&gt; resolves after the leader has committed and applied the entry locally. It&lt;br&gt;
does not wait for every follower to apply it. A slow or partitioned follower can catch up later.&lt;/p&gt;

&lt;p&gt;This distinction is much easier to understand when it is visible:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb27fo4uncksxkcnsdsdt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb27fo4uncksxkcnsdsdt.png" alt="Raft command replication trace" width="800" height="347"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  A pure core and an impure runtime
&lt;/h2&gt;

&lt;p&gt;The implementation separates consensus decisions from I/O.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;raft-core&lt;/code&gt; is a deterministic transition engine. It owns terms, roles, votes, log matching, peer&lt;br&gt;
progress, elections, and commitment. It does not import timers, sockets, filesystems, or databases.&lt;br&gt;
It receives an event and produces state changes plus effects.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;raft-node&lt;/code&gt; is the runtime around that core. It owns lifecycle, timers, transport, storage, command&lt;br&gt;
encoding, proposal completion, and serialized state-machine application.&lt;/p&gt;

&lt;p&gt;The boundary between them is a Ready/Advance cycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;event
  ↓
RaftCore.step()
  ↓
Ready { hard state, log changes, messages, committed entries }
  ↓
persist → send → apply
  ↓
advance()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runtime processes each Ready batch conservatively:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Persist hard state, truncations, entries, and snapshot metadata atomically.&lt;/li&gt;
&lt;li&gt;Wait for durability.&lt;/li&gt;
&lt;li&gt;Send outbound messages.&lt;/li&gt;
&lt;li&gt;Apply committed entries in index order.&lt;/li&gt;
&lt;li&gt;Persist the applied position.&lt;/li&gt;
&lt;li&gt;Advance the core.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That order matters. A node must not grant a vote or report a successful append if the corresponding&lt;br&gt;
state exists only in volatile memory. A crash between “send success” and “persist” can break the&lt;br&gt;
assumptions Raft relies on.&lt;/p&gt;
&lt;h2&gt;
  
  
  The application is a deterministic state machine
&lt;/h2&gt;

&lt;p&gt;The SDK does not replicate arbitrary JavaScript memory. It replicates commands.&lt;/p&gt;

&lt;p&gt;Here is the shape of the included key-value example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;KvCommand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;put&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;compare-and-set&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;KvStateMachine&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;ReplicatedStateMachine&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;KvCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;KvResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Readonly&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;KvCommand&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;KvResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;put&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;applied&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;applied&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;previous&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;compare-and-set&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentValue&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;conflict&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentValue&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;applied&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;apply&lt;/code&gt; method must not use &lt;code&gt;Date.now()&lt;/code&gt;, randomness, local files, environment variables, or&lt;br&gt;
remote calls to decide replicated state. Any nondeterministic input must be captured in the command&lt;br&gt;
before it is proposed.&lt;/p&gt;

&lt;p&gt;Business rejection is also data, not an exception. A failed compare-and-set still occupies a&lt;br&gt;
committed log position so that every replica reaches the same decision in the same order.&lt;/p&gt;
&lt;h2&gt;
  
  
  Timeouts are ambiguous
&lt;/h2&gt;

&lt;p&gt;One of the least intuitive distributed-systems lessons is that a client timeout does not prove an&lt;br&gt;
operation failed.&lt;/p&gt;

&lt;p&gt;The leader may have committed a command just before the response was lost. Retrying it with a new ID&lt;br&gt;
could apply the mutation twice. &lt;code&gt;node-raft-rsm&lt;/code&gt; therefore accepts a command ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;propose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;commandId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idempotency-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After a timeout, the safe retry is the identical command bytes with the same command ID. The current&lt;br&gt;
deduplication support is bounded and in-process; durable deduplication remains a release blocker.&lt;/p&gt;
&lt;h2&gt;
  
  
  Node.js pauses are part of the failure model
&lt;/h2&gt;

&lt;p&gt;Running Raft inside Node.js introduces an important operational constraint: heartbeats and election&lt;br&gt;
timeouts are scheduled on the event loop.&lt;/p&gt;

&lt;p&gt;A long synchronous computation, synchronous I/O, a large serialization operation, or a garbage&lt;br&gt;
collection pause can prevent timers and message handlers from running on time. A follower may start&lt;br&gt;
an unnecessary election because it did not process the leader's heartbeat. A leader may fail to send&lt;br&gt;
heartbeats quickly enough, creating term churn and temporary loss of availability.&lt;/p&gt;

&lt;p&gt;A pause should not by itself violate Raft's safety rules in a correct implementation, but this&lt;br&gt;
project does not yet have enough multi-process and long-pause evidence to present that as a production&lt;br&gt;
guarantee. Event-loop stalls are one of the explicit areas still under development and testing.&lt;/p&gt;

&lt;p&gt;An eventual production deployment should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;choose election timeouts comfortably above normal network, disk, event-loop, and GC latency tails;&lt;/li&gt;
&lt;li&gt;monitor event-loop delay alongside role, term, proposal latency, peer lag, and storage latency;&lt;/li&gt;
&lt;li&gt;avoid synchronous I/O and CPU-heavy work on the Raft process's main thread;&lt;/li&gt;
&lt;li&gt;move expensive application work to worker threads or separate processes;&lt;/li&gt;
&lt;li&gt;bound command sizes and state-machine apply time;&lt;/li&gt;
&lt;li&gt;test deliberate process pauses, not only message loss and network partitions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is also why timeout values copied from a LAN example are not universal defaults. A deployment&lt;br&gt;
must tune them using its own worst-case latency measurements.&lt;/p&gt;
&lt;h2&gt;
  
  
  Making the network controllable
&lt;/h2&gt;

&lt;p&gt;Testing Raft against a normal in-memory event emitter is not enough. A useful simulator must let us&lt;br&gt;
control the uncomfortable cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;delay a message;&lt;/li&gt;
&lt;li&gt;deliver one message at a time;&lt;/li&gt;
&lt;li&gt;duplicate or drop a message;&lt;/li&gt;
&lt;li&gt;reverse the pending queue;&lt;/li&gt;
&lt;li&gt;partition two groups of nodes;&lt;/li&gt;
&lt;li&gt;stop and restart a node while retaining its durable state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The in-memory transport exposes those controls, while seeded randomness and virtual time make&lt;br&gt;
scenarios repeatable.&lt;/p&gt;

&lt;p&gt;The React visualizer uses the real &lt;code&gt;RaftNode&lt;/code&gt; runtime and that deterministic transport. It is not a&lt;br&gt;
separate toy implementation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft9k6ndpsbqjg6sb6dooa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft9k6ndpsbqjg6sb6dooa.png" alt="Raft laboratory cluster view" width="799" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From the UI, you can start an election, inspect RequestVote and AppendEntries messages, manually&lt;br&gt;
deliver or drop them, partition the cluster, disable a node, restart it, and propose key-value&lt;br&gt;
commands. The timeline separates network events, election events, and command lifecycle events.&lt;/p&gt;

&lt;p&gt;You can also click a node to select it or press and hold to reposition it. That small interaction&lt;br&gt;
turned out to be surprisingly useful when explaining a partition to someone else.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it locally
&lt;/h2&gt;

&lt;p&gt;The current development baseline is Node.js 24 and pnpm 9:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/dmitry-bardonov/node-raft-rsm.git
&lt;span class="nb"&gt;cd &lt;/span&gt;node-raft-rsm
nvm use
pnpm &lt;span class="nb"&gt;install
&lt;/span&gt;pnpm check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the deterministic three-node key-value example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm example:kv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or start the visualizer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm visualizer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;a href="http://127.0.0.1:3000" rel="noopener noreferrer"&gt;http://127.0.0.1:3000&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A useful first scenario is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start an election on &lt;code&gt;node-1&lt;/code&gt; and drain the vote messages.&lt;/li&gt;
&lt;li&gt;Propose &lt;code&gt;PUT counter = 2&lt;/code&gt; and drain replication messages.&lt;/li&gt;
&lt;li&gt;Partition &lt;code&gt;node-1&lt;/code&gt; away from the other two nodes.&lt;/li&gt;
&lt;li&gt;Elect a new leader on the majority side.&lt;/li&gt;
&lt;li&gt;Heal the partition and watch the old leader step down and repair its log.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What is intentionally missing
&lt;/h2&gt;

&lt;p&gt;The project currently lacks several things I would require before trusting it with production data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a durable SQLite storage adapter;&lt;/li&gt;
&lt;li&gt;a real authenticated transport;&lt;/li&gt;
&lt;li&gt;a complete snapshot and recovery pipeline;&lt;/li&gt;
&lt;li&gt;durable command deduplication;&lt;/li&gt;
&lt;li&gt;dynamic membership and joint consensus;&lt;/li&gt;
&lt;li&gt;linearizable ReadIndex-style reads;&lt;/li&gt;
&lt;li&gt;broad property, restart, corruption, and multi-process fault testing;&lt;/li&gt;
&lt;li&gt;operational evidence under event-loop stalls and GC pauses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Raft also does not solve every distributed-systems problem. It does not provide Byzantine fault&lt;br&gt;
tolerance, exactly-once external side effects, transparent distributed transactions, or automatic&lt;br&gt;
consistency for arbitrary process memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;The algorithm is only part of a consensus implementation. The difficult boundaries are where the&lt;br&gt;
algorithm meets durability, transport, recovery, application code, and client expectations.&lt;/p&gt;

&lt;p&gt;Three lessons have shaped this project so far:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Make ordering constraints explicit.&lt;/strong&gt; “Persist before send” should be visible in the
architecture, not buried in a callback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make nondeterminism injectable.&lt;/strong&gt; Clocks, randomness, and delivery order must be controllable if
failures are going to be reproducible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make incomplete guarantees obvious.&lt;/strong&gt; A local read is not a linearizable read, and a timeout is
not proof of failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protect the host application's architecture.&lt;/strong&gt; An embedded consensus SDK should adapt to the
application's domain model instead of forcing the application to become a client of a different
general-purpose system.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The visualizer helped too. Watching terms, message queues, commit indexes, and applied indexes change&lt;br&gt;
made several design mistakes more obvious than another page of logs would have.&lt;/p&gt;

&lt;p&gt;The long-term question behind the project is: how small and predictable can the integration surface&lt;br&gt;
be while still giving an existing service correct Raft elections, replication, commitment, recovery,&lt;br&gt;
and state-machine application?&lt;/p&gt;

&lt;p&gt;If you maintain a system that could benefit from embedded consensus, I would appreciate feedback on&lt;br&gt;
the SDK boundaries and integration experience:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/dmitry-bardonov/node-raft-rsm" rel="noopener noreferrer"&gt;github.com/dmitry-bardonov/node-raft-rsm&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What would an embedded Raft API need before you would integrate it into an existing service?&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>node</category>
      <category>distributed</category>
    </item>
  </channel>
</rss>
