Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAGH,OAAO,EACL,MAAM,EACN,IAAI,EACJ,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,eAAe,EACf,aAAa,EACb,WAAW,EACX,cAAc,EACd,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,qBAAqB,EACrB,SAAS,EACT,aAAa,EACb,SAAS,EACT,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAGnC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAGvC,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC"}

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;;;AAEH,QAAQ;AACR,uCAoBoB;AAhBlB,qGAAA,SAAS,OAAA;AAUT,qGAAA,SAAS,OAAA;AACT,yGAAA,aAAa,OAAA;AACb,qGAAA,SAAS,OAAA;AAMX,MAAM;AACN,mCAAmC;AAA1B,iGAAA,OAAO,OAAA;AAEhB,QAAQ;AACR,uCAAuC;AAA9B,qGAAA,SAAS,OAAA;AAElB,OAAO;AACP,qCAAkE;AAAzD,mGAAA,QAAQ,OAAA"}

View File

@@ -0,0 +1,78 @@
/**
* @ruvector/raft - Raft Consensus Implementation
*
* A TypeScript implementation of the Raft consensus algorithm for
* distributed systems, providing leader election, log replication,
* and fault tolerance.
*
* @example
* ```typescript
* import { RaftNode, RaftTransport, NodeState } from '@ruvector/raft';
*
* // Create a Raft node
* const node = new RaftNode({
* nodeId: 'node-1',
* peers: ['node-2', 'node-3'],
* electionTimeout: [150, 300],
* heartbeatInterval: 50,
* maxEntriesPerRequest: 100,
* });
*
* // Set up transport for RPC communication
* node.setTransport(myTransport);
*
* // Set up state machine for applying commands
* node.setStateMachine(myStateMachine);
*
* // Listen for events
* node.on('stateChange', (event) => {
* console.log(`State changed: ${event.previousState} -> ${event.newState}`);
* });
*
* node.on('leaderElected', (event) => {
* console.log(`New leader: ${event.leaderId} in term ${event.term}`);
* });
*
* // Start the node
* node.start();
*
* // Propose a command (only works if leader)
* if (node.isLeader) {
* await node.propose({ type: 'SET', key: 'foo', value: 'bar' });
* }
* ```
*
* @packageDocumentation
*/
// Types
export {
NodeId,
Term,
LogIndex,
NodeState,
LogEntry,
PersistentState,
VolatileState,
LeaderState,
RaftNodeConfig,
RequestVoteRequest,
RequestVoteResponse,
AppendEntriesRequest,
AppendEntriesResponse,
RaftError,
RaftErrorCode,
RaftEvent,
StateChangeEvent,
LeaderElectedEvent,
LogCommittedEvent,
} from './types.js';
// Log
export { RaftLog } from './log.js';
// State
export { RaftState } from './state.js';
// Node
export { RaftNode, RaftTransport, StateMachine } from './node.js';

View File

@@ -0,0 +1,44 @@
/**
* Raft Log Implementation
* Manages the replicated log with persistence support
*/
import { LogEntry, LogIndex, Term } from './types.js';
/** In-memory log storage with optional persistence callback */
export declare class RaftLog<T = unknown> {
private entries;
private persistCallback?;
constructor(options?: {
onPersist?: (entries: LogEntry<T>[]) => Promise<void>;
});
/** Get the last log index */
get lastIndex(): LogIndex;
/** Get the last log term */
get lastTerm(): Term;
/** Get log length */
get length(): number;
/** Get entry at index */
get(index: LogIndex): LogEntry<T> | undefined;
/** Get term at index */
termAt(index: LogIndex): Term | undefined;
/** Append entries to log */
append(entries: LogEntry<T>[]): Promise<void>;
/** Append a single command, returning the new entry */
appendCommand(term: Term, command: T): Promise<LogEntry<T>>;
/** Get entries starting from index */
getFrom(startIndex: LogIndex, maxCount?: number): LogEntry<T>[];
/** Get entries in range [start, end] */
getRange(startIndex: LogIndex, endIndex: LogIndex): LogEntry<T>[];
/** Truncate log from index (remove index and all following) */
truncateFrom(index: LogIndex): void;
/** Check if log is at least as up-to-date as given term/index */
isUpToDate(lastLogTerm: Term, lastLogIndex: LogIndex): boolean;
/** Check if log contains entry at index with matching term */
containsEntry(index: LogIndex, term: Term): boolean;
/** Get all entries */
getAll(): LogEntry<T>[];
/** Clear all entries */
clear(): void;
/** Load entries from storage */
load(entries: LogEntry<T>[]): void;
}
//# sourceMappingURL=log.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"log.d.ts","sourceRoot":"","sources":["log.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAEtD,+DAA+D;AAC/D,qBAAa,OAAO,CAAC,CAAC,GAAG,OAAO;IAC9B,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,eAAe,CAAC,CAA4C;gBAExD,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE;IAI/E,6BAA6B;IAC7B,IAAI,SAAS,IAAI,QAAQ,CAExB;IAED,4BAA4B;IAC5B,IAAI,QAAQ,IAAI,IAAI,CAEnB;IAED,qBAAqB;IACrB,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,yBAAyB;IACzB,GAAG,CAAC,KAAK,EAAE,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,SAAS;IAI7C,wBAAwB;IACxB,MAAM,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,GAAG,SAAS;IAMzC,4BAA4B;IACtB,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BnD,uDAAuD;IACjD,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IAWjE,sCAAsC;IACtC,OAAO,CAAC,UAAU,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,EAAE;IAW/D,wCAAwC;IACxC,QAAQ,CAAC,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC,EAAE;IAIjE,+DAA+D;IAC/D,YAAY,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI;IAInC,iEAAiE;IACjE,UAAU,CAAC,WAAW,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,GAAG,OAAO;IAO9D,8DAA8D;IAC9D,aAAa,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO;IAMnD,sBAAsB;IACtB,MAAM,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE;IAIvB,wBAAwB;IACxB,KAAK,IAAI,IAAI;IAIb,gCAAgC;IAChC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI;CAInC"}

View File

@@ -0,0 +1,122 @@
"use strict";
/**
* Raft Log Implementation
* Manages the replicated log with persistence support
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.RaftLog = void 0;
/** In-memory log storage with optional persistence callback */
class RaftLog {
constructor(options) {
this.entries = [];
this.persistCallback = options?.onPersist;
}
/** Get the last log index */
get lastIndex() {
return this.entries.length > 0 ? this.entries[this.entries.length - 1].index : 0;
}
/** Get the last log term */
get lastTerm() {
return this.entries.length > 0 ? this.entries[this.entries.length - 1].term : 0;
}
/** Get log length */
get length() {
return this.entries.length;
}
/** Get entry at index */
get(index) {
return this.entries.find((e) => e.index === index);
}
/** Get term at index */
termAt(index) {
if (index === 0)
return 0;
const entry = this.get(index);
return entry?.term;
}
/** Append entries to log */
async append(entries) {
if (entries.length === 0)
return;
// Find where to start appending (handle conflicting entries)
for (const entry of entries) {
const existing = this.get(entry.index);
if (existing) {
if (existing.term !== entry.term) {
// Conflict: delete this and all following entries
this.truncateFrom(entry.index);
}
else {
// Same entry, skip
continue;
}
}
this.entries.push(entry);
}
// Sort by index to maintain order
this.entries.sort((a, b) => a.index - b.index);
if (this.persistCallback) {
await this.persistCallback(this.entries);
}
}
/** Append a single command, returning the new entry */
async appendCommand(term, command) {
const entry = {
term,
index: this.lastIndex + 1,
command,
timestamp: Date.now(),
};
await this.append([entry]);
return entry;
}
/** Get entries starting from index */
getFrom(startIndex, maxCount) {
const result = [];
for (const entry of this.entries) {
if (entry.index >= startIndex) {
result.push(entry);
if (maxCount && result.length >= maxCount)
break;
}
}
return result;
}
/** Get entries in range [start, end] */
getRange(startIndex, endIndex) {
return this.entries.filter((e) => e.index >= startIndex && e.index <= endIndex);
}
/** Truncate log from index (remove index and all following) */
truncateFrom(index) {
this.entries = this.entries.filter((e) => e.index < index);
}
/** Check if log is at least as up-to-date as given term/index */
isUpToDate(lastLogTerm, lastLogIndex) {
if (this.lastTerm !== lastLogTerm) {
return this.lastTerm > lastLogTerm;
}
return this.lastIndex >= lastLogIndex;
}
/** Check if log contains entry at index with matching term */
containsEntry(index, term) {
if (index === 0)
return true;
const entry = this.get(index);
return entry?.term === term;
}
/** Get all entries */
getAll() {
return [...this.entries];
}
/** Clear all entries */
clear() {
this.entries = [];
}
/** Load entries from storage */
load(entries) {
this.entries = [...entries];
this.entries.sort((a, b) => a.index - b.index);
}
}
exports.RaftLog = RaftLog;
//# sourceMappingURL=log.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"log.js","sourceRoot":"","sources":["log.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,135 @@
/**
* Raft Log Implementation
* Manages the replicated log with persistence support
*/
import { LogEntry, LogIndex, Term } from './types.js';
/** In-memory log storage with optional persistence callback */
export class RaftLog<T = unknown> {
private entries: LogEntry<T>[] = [];
private persistCallback?: (entries: LogEntry<T>[]) => Promise<void>;
constructor(options?: { onPersist?: (entries: LogEntry<T>[]) => Promise<void> }) {
this.persistCallback = options?.onPersist;
}
/** Get the last log index */
get lastIndex(): LogIndex {
return this.entries.length > 0 ? this.entries[this.entries.length - 1].index : 0;
}
/** Get the last log term */
get lastTerm(): Term {
return this.entries.length > 0 ? this.entries[this.entries.length - 1].term : 0;
}
/** Get log length */
get length(): number {
return this.entries.length;
}
/** Get entry at index */
get(index: LogIndex): LogEntry<T> | undefined {
return this.entries.find((e) => e.index === index);
}
/** Get term at index */
termAt(index: LogIndex): Term | undefined {
if (index === 0) return 0;
const entry = this.get(index);
return entry?.term;
}
/** Append entries to log */
async append(entries: LogEntry<T>[]): Promise<void> {
if (entries.length === 0) return;
// Find where to start appending (handle conflicting entries)
for (const entry of entries) {
const existing = this.get(entry.index);
if (existing) {
if (existing.term !== entry.term) {
// Conflict: delete this and all following entries
this.truncateFrom(entry.index);
} else {
// Same entry, skip
continue;
}
}
this.entries.push(entry);
}
// Sort by index to maintain order
this.entries.sort((a, b) => a.index - b.index);
if (this.persistCallback) {
await this.persistCallback(this.entries);
}
}
/** Append a single command, returning the new entry */
async appendCommand(term: Term, command: T): Promise<LogEntry<T>> {
const entry: LogEntry<T> = {
term,
index: this.lastIndex + 1,
command,
timestamp: Date.now(),
};
await this.append([entry]);
return entry;
}
/** Get entries starting from index */
getFrom(startIndex: LogIndex, maxCount?: number): LogEntry<T>[] {
const result: LogEntry<T>[] = [];
for (const entry of this.entries) {
if (entry.index >= startIndex) {
result.push(entry);
if (maxCount && result.length >= maxCount) break;
}
}
return result;
}
/** Get entries in range [start, end] */
getRange(startIndex: LogIndex, endIndex: LogIndex): LogEntry<T>[] {
return this.entries.filter((e) => e.index >= startIndex && e.index <= endIndex);
}
/** Truncate log from index (remove index and all following) */
truncateFrom(index: LogIndex): void {
this.entries = this.entries.filter((e) => e.index < index);
}
/** Check if log is at least as up-to-date as given term/index */
isUpToDate(lastLogTerm: Term, lastLogIndex: LogIndex): boolean {
if (this.lastTerm !== lastLogTerm) {
return this.lastTerm > lastLogTerm;
}
return this.lastIndex >= lastLogIndex;
}
/** Check if log contains entry at index with matching term */
containsEntry(index: LogIndex, term: Term): boolean {
if (index === 0) return true;
const entry = this.get(index);
return entry?.term === term;
}
/** Get all entries */
getAll(): LogEntry<T>[] {
return [...this.entries];
}
/** Clear all entries */
clear(): void {
this.entries = [];
}
/** Load entries from storage */
load(entries: LogEntry<T>[]): void {
this.entries = [...entries];
this.entries.sort((a, b) => a.index - b.index);
}
}

View File

@@ -0,0 +1,71 @@
/**
* Raft Node Implementation
* Core Raft consensus algorithm implementation
*/
import EventEmitter from 'eventemitter3';
import { NodeId, Term, LogIndex, NodeState, RaftNodeConfig, RequestVoteRequest, RequestVoteResponse, AppendEntriesRequest, AppendEntriesResponse, LogEntry, PersistentState } from './types.js';
/** Transport interface for sending RPCs to peers */
export interface RaftTransport<T = unknown> {
/** Send RequestVote RPC to a peer */
requestVote(peerId: NodeId, request: RequestVoteRequest): Promise<RequestVoteResponse>;
/** Send AppendEntries RPC to a peer */
appendEntries(peerId: NodeId, request: AppendEntriesRequest<T>): Promise<AppendEntriesResponse>;
}
/** State machine interface for applying committed entries */
export interface StateMachine<T = unknown, R = void> {
/** Apply a committed command to the state machine */
apply(command: T): Promise<R>;
}
/** Raft consensus node */
export declare class RaftNode<T = unknown, R = void> extends EventEmitter {
private readonly config;
private readonly state;
private nodeState;
private leaderId;
private transport;
private stateMachine;
private electionTimer;
private heartbeatTimer;
private running;
constructor(config: RaftNodeConfig);
/** Get node ID */
get nodeId(): NodeId;
/** Get current state */
get currentState(): NodeState;
/** Get current term */
get currentTerm(): Term;
/** Get current leader ID */
get leader(): NodeId | null;
/** Check if this node is the leader */
get isLeader(): boolean;
/** Get commit index */
get commitIndex(): LogIndex;
/** Set transport for RPC communication */
setTransport(transport: RaftTransport<T>): void;
/** Set state machine for applying commands */
setStateMachine(stateMachine: StateMachine<T, R>): void;
/** Start the Raft node */
start(): void;
/** Stop the Raft node */
stop(): void;
/** Propose a command to be replicated (only works if leader) */
propose(command: T): Promise<LogEntry<T>>;
/** Handle RequestVote RPC from a candidate */
handleRequestVote(request: RequestVoteRequest): Promise<RequestVoteResponse>;
/** Handle AppendEntries RPC from leader */
handleAppendEntries(request: AppendEntriesRequest<T>): Promise<AppendEntriesResponse>;
/** Load persistent state */
loadState(state: PersistentState<T>): void;
/** Get current persistent state */
getState(): PersistentState<T>;
private transitionTo;
private getRandomElectionTimeout;
private resetElectionTimer;
private clearTimers;
private startElection;
private startHeartbeat;
private replicateToFollowers;
private replicateToPeer;
private applyCommitted;
}
//# sourceMappingURL=node.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["node.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,352 @@
"use strict";
/**
* Raft Node Implementation
* Core Raft consensus algorithm implementation
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RaftNode = void 0;
const eventemitter3_1 = __importDefault(require("eventemitter3"));
const types_js_1 = require("./types.js");
const state_js_1 = require("./state.js");
/** Default configuration values */
const DEFAULT_CONFIG = {
electionTimeout: [150, 300],
heartbeatInterval: 50,
maxEntriesPerRequest: 100,
};
/** Raft consensus node */
class RaftNode extends eventemitter3_1.default {
constructor(config) {
super();
this.nodeState = types_js_1.NodeState.Follower;
this.leaderId = null;
this.transport = null;
this.stateMachine = null;
this.electionTimer = null;
this.heartbeatTimer = null;
this.running = false;
this.config = { ...DEFAULT_CONFIG, ...config };
this.state = new state_js_1.RaftState(config.nodeId, config.peers);
}
/** Get node ID */
get nodeId() {
return this.config.nodeId;
}
/** Get current state */
get currentState() {
return this.nodeState;
}
/** Get current term */
get currentTerm() {
return this.state.currentTerm;
}
/** Get current leader ID */
get leader() {
return this.leaderId;
}
/** Check if this node is the leader */
get isLeader() {
return this.nodeState === types_js_1.NodeState.Leader;
}
/** Get commit index */
get commitIndex() {
return this.state.commitIndex;
}
/** Set transport for RPC communication */
setTransport(transport) {
this.transport = transport;
}
/** Set state machine for applying commands */
setStateMachine(stateMachine) {
this.stateMachine = stateMachine;
}
/** Start the Raft node */
start() {
if (this.running)
return;
this.running = true;
this.resetElectionTimer();
}
/** Stop the Raft node */
stop() {
this.running = false;
this.clearTimers();
}
/** Propose a command to be replicated (only works if leader) */
async propose(command) {
if (this.nodeState !== types_js_1.NodeState.Leader) {
throw types_js_1.RaftError.notLeader();
}
const entry = await this.state.log.appendCommand(this.state.currentTerm, command);
this.emit(types_js_1.RaftEvent.LogAppended, entry);
// Immediately replicate to followers
await this.replicateToFollowers();
return entry;
}
/** Handle RequestVote RPC from a candidate */
async handleRequestVote(request) {
// If request term is higher, update term and become follower
if (request.term > this.state.currentTerm) {
await this.state.setTerm(request.term);
this.transitionTo(types_js_1.NodeState.Follower);
}
// Deny vote if request term is less than current term
if (request.term < this.state.currentTerm) {
return { term: this.state.currentTerm, voteGranted: false };
}
// Check if we can vote for this candidate
const canVote = (this.state.votedFor === null || this.state.votedFor === request.candidateId) &&
this.state.log.isUpToDate(request.lastLogTerm, request.lastLogIndex);
if (canVote) {
await this.state.vote(request.term, request.candidateId);
this.resetElectionTimer();
this.emit(types_js_1.RaftEvent.VoteGranted, { candidateId: request.candidateId, term: request.term });
return { term: this.state.currentTerm, voteGranted: true };
}
return { term: this.state.currentTerm, voteGranted: false };
}
/** Handle AppendEntries RPC from leader */
async handleAppendEntries(request) {
// If request term is higher, update term
if (request.term > this.state.currentTerm) {
await this.state.setTerm(request.term);
this.transitionTo(types_js_1.NodeState.Follower);
}
// Reject if term is less than current term
if (request.term < this.state.currentTerm) {
return { term: this.state.currentTerm, success: false };
}
// Valid leader - reset election timer
this.leaderId = request.leaderId;
this.resetElectionTimer();
// If not follower, become follower
if (this.nodeState !== types_js_1.NodeState.Follower) {
this.transitionTo(types_js_1.NodeState.Follower);
}
this.emit(types_js_1.RaftEvent.Heartbeat, { leaderId: request.leaderId, term: request.term });
// Check if log contains entry at prevLogIndex with prevLogTerm
if (request.prevLogIndex > 0 && !this.state.log.containsEntry(request.prevLogIndex, request.prevLogTerm)) {
return { term: this.state.currentTerm, success: false };
}
// Append entries
if (request.entries.length > 0) {
await this.state.log.append(request.entries);
}
// Update commit index
if (request.leaderCommit > this.state.commitIndex) {
this.state.setCommitIndex(Math.min(request.leaderCommit, this.state.log.lastIndex));
await this.applyCommitted();
}
return {
term: this.state.currentTerm,
success: true,
matchIndex: this.state.log.lastIndex,
};
}
/** Load persistent state */
loadState(state) {
this.state.loadPersistentState(state);
}
/** Get current persistent state */
getState() {
return this.state.getPersistentState();
}
// Private methods
transitionTo(newState) {
const previousState = this.nodeState;
if (previousState === newState)
return;
this.nodeState = newState;
this.clearTimers();
if (newState === types_js_1.NodeState.Leader) {
this.state.initLeaderState();
this.leaderId = this.config.nodeId;
this.startHeartbeat();
this.emit(types_js_1.RaftEvent.LeaderElected, {
leaderId: this.config.nodeId,
term: this.state.currentTerm,
});
}
else {
this.state.clearLeaderState();
if (newState === types_js_1.NodeState.Follower) {
this.leaderId = null;
this.resetElectionTimer();
}
}
this.emit(types_js_1.RaftEvent.StateChange, {
previousState,
newState,
term: this.state.currentTerm,
});
}
getRandomElectionTimeout() {
const [min, max] = this.config.electionTimeout;
return min + Math.random() * (max - min);
}
resetElectionTimer() {
if (this.electionTimer) {
clearTimeout(this.electionTimer);
}
if (!this.running)
return;
this.electionTimer = setTimeout(() => {
this.startElection();
}, this.getRandomElectionTimeout());
}
clearTimers() {
if (this.electionTimer) {
clearTimeout(this.electionTimer);
this.electionTimer = null;
}
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
async startElection() {
if (!this.running)
return;
// Increment term and become candidate
await this.state.setTerm(this.state.currentTerm + 1);
await this.state.vote(this.state.currentTerm, this.config.nodeId);
this.transitionTo(types_js_1.NodeState.Candidate);
this.emit(types_js_1.RaftEvent.VoteRequested, {
term: this.state.currentTerm,
candidateId: this.config.nodeId,
});
// Start with 1 vote (self)
let votesReceived = 1;
const majority = Math.floor((this.config.peers.length + 1) / 2) + 1;
// Request votes from all peers
if (!this.transport) {
this.resetElectionTimer();
return;
}
const votePromises = this.config.peers.map(async (peerId) => {
try {
const response = await this.transport.requestVote(peerId, {
term: this.state.currentTerm,
candidateId: this.config.nodeId,
lastLogIndex: this.state.log.lastIndex,
lastLogTerm: this.state.log.lastTerm,
});
// If response term is higher, become follower
if (response.term > this.state.currentTerm) {
await this.state.setTerm(response.term);
this.transitionTo(types_js_1.NodeState.Follower);
return;
}
if (response.voteGranted && this.nodeState === types_js_1.NodeState.Candidate) {
votesReceived++;
if (votesReceived >= majority) {
this.transitionTo(types_js_1.NodeState.Leader);
}
}
}
catch {
// Peer unavailable, continue
}
});
await Promise.allSettled(votePromises);
// If still candidate, restart election timer
if (this.nodeState === types_js_1.NodeState.Candidate) {
this.resetElectionTimer();
}
}
startHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
}
// Send immediate heartbeat
this.replicateToFollowers();
// Start periodic heartbeat
this.heartbeatTimer = setInterval(() => {
if (this.nodeState === types_js_1.NodeState.Leader) {
this.replicateToFollowers();
}
}, this.config.heartbeatInterval);
}
async replicateToFollowers() {
if (!this.transport || this.nodeState !== types_js_1.NodeState.Leader)
return;
const replicationPromises = this.config.peers.map(async (peerId) => {
await this.replicateToPeer(peerId);
});
await Promise.allSettled(replicationPromises);
// Update commit index if majority have replicated
if (this.state.updateCommitIndex()) {
this.emit(types_js_1.RaftEvent.LogCommitted, {
index: this.state.commitIndex,
term: this.state.currentTerm,
});
await this.applyCommitted();
}
}
async replicateToPeer(peerId) {
if (!this.transport || this.nodeState !== types_js_1.NodeState.Leader)
return;
const nextIndex = this.state.getNextIndex(peerId);
const prevLogIndex = nextIndex - 1;
const prevLogTerm = this.state.log.termAt(prevLogIndex) ?? 0;
const entries = this.state.log.getFrom(nextIndex, this.config.maxEntriesPerRequest);
try {
const response = await this.transport.appendEntries(peerId, {
term: this.state.currentTerm,
leaderId: this.config.nodeId,
prevLogIndex,
prevLogTerm,
entries,
leaderCommit: this.state.commitIndex,
});
if (response.term > this.state.currentTerm) {
await this.state.setTerm(response.term);
this.transitionTo(types_js_1.NodeState.Follower);
return;
}
if (response.success) {
if (response.matchIndex !== undefined) {
this.state.setNextIndex(peerId, response.matchIndex + 1);
this.state.setMatchIndex(peerId, response.matchIndex);
}
else if (entries.length > 0) {
const lastEntry = entries[entries.length - 1];
this.state.setNextIndex(peerId, lastEntry.index + 1);
this.state.setMatchIndex(peerId, lastEntry.index);
}
}
else {
// Decrement nextIndex and retry
this.state.setNextIndex(peerId, nextIndex - 1);
}
}
catch {
// Peer unavailable, will retry on next heartbeat
}
}
async applyCommitted() {
while (this.state.lastApplied < this.state.commitIndex) {
const nextIndex = this.state.lastApplied + 1;
const entry = this.state.log.get(nextIndex);
if (entry && this.stateMachine) {
try {
await this.stateMachine.apply(entry.command);
this.state.setLastApplied(nextIndex);
this.emit(types_js_1.RaftEvent.LogApplied, entry);
}
catch (error) {
this.emit(types_js_1.RaftEvent.Error, error);
break;
}
}
else {
this.state.setLastApplied(nextIndex);
}
}
}
}
exports.RaftNode = RaftNode;
//# sourceMappingURL=node.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,435 @@
/**
* Raft Node Implementation
* Core Raft consensus algorithm implementation
*/
import EventEmitter from 'eventemitter3';
import {
NodeId,
Term,
LogIndex,
NodeState,
RaftNodeConfig,
RequestVoteRequest,
RequestVoteResponse,
AppendEntriesRequest,
AppendEntriesResponse,
LogEntry,
RaftError,
RaftEvent,
StateChangeEvent,
LeaderElectedEvent,
LogCommittedEvent,
PersistentState,
} from './types.js';
import { RaftState } from './state.js';
/** Transport interface for sending RPCs to peers */
export interface RaftTransport<T = unknown> {
/** Send RequestVote RPC to a peer */
requestVote(peerId: NodeId, request: RequestVoteRequest): Promise<RequestVoteResponse>;
/** Send AppendEntries RPC to a peer */
appendEntries(peerId: NodeId, request: AppendEntriesRequest<T>): Promise<AppendEntriesResponse>;
}
/** State machine interface for applying committed entries */
export interface StateMachine<T = unknown, R = void> {
/** Apply a committed command to the state machine */
apply(command: T): Promise<R>;
}
/** Default configuration values */
const DEFAULT_CONFIG: Partial<RaftNodeConfig> = {
electionTimeout: [150, 300],
heartbeatInterval: 50,
maxEntriesPerRequest: 100,
};
/** Raft consensus node */
export class RaftNode<T = unknown, R = void> extends EventEmitter {
private readonly config: Required<RaftNodeConfig>;
private readonly state: RaftState<T>;
private nodeState: NodeState = NodeState.Follower;
private leaderId: NodeId | null = null;
private transport: RaftTransport<T> | null = null;
private stateMachine: StateMachine<T, R> | null = null;
private electionTimer: ReturnType<typeof setTimeout> | null = null;
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
private running = false;
constructor(config: RaftNodeConfig) {
super();
this.config = { ...DEFAULT_CONFIG, ...config } as Required<RaftNodeConfig>;
this.state = new RaftState<T>(config.nodeId, config.peers);
}
/** Get node ID */
get nodeId(): NodeId {
return this.config.nodeId;
}
/** Get current state */
get currentState(): NodeState {
return this.nodeState;
}
/** Get current term */
get currentTerm(): Term {
return this.state.currentTerm;
}
/** Get current leader ID */
get leader(): NodeId | null {
return this.leaderId;
}
/** Check if this node is the leader */
get isLeader(): boolean {
return this.nodeState === NodeState.Leader;
}
/** Get commit index */
get commitIndex(): LogIndex {
return this.state.commitIndex;
}
/** Set transport for RPC communication */
setTransport(transport: RaftTransport<T>): void {
this.transport = transport;
}
/** Set state machine for applying commands */
setStateMachine(stateMachine: StateMachine<T, R>): void {
this.stateMachine = stateMachine;
}
/** Start the Raft node */
start(): void {
if (this.running) return;
this.running = true;
this.resetElectionTimer();
}
/** Stop the Raft node */
stop(): void {
this.running = false;
this.clearTimers();
}
/** Propose a command to be replicated (only works if leader) */
async propose(command: T): Promise<LogEntry<T>> {
if (this.nodeState !== NodeState.Leader) {
throw RaftError.notLeader();
}
const entry = await this.state.log.appendCommand(this.state.currentTerm, command);
this.emit(RaftEvent.LogAppended, entry);
// Immediately replicate to followers
await this.replicateToFollowers();
return entry;
}
/** Handle RequestVote RPC from a candidate */
async handleRequestVote(request: RequestVoteRequest): Promise<RequestVoteResponse> {
// If request term is higher, update term and become follower
if (request.term > this.state.currentTerm) {
await this.state.setTerm(request.term);
this.transitionTo(NodeState.Follower);
}
// Deny vote if request term is less than current term
if (request.term < this.state.currentTerm) {
return { term: this.state.currentTerm, voteGranted: false };
}
// Check if we can vote for this candidate
const canVote =
(this.state.votedFor === null || this.state.votedFor === request.candidateId) &&
this.state.log.isUpToDate(request.lastLogTerm, request.lastLogIndex);
if (canVote) {
await this.state.vote(request.term, request.candidateId);
this.resetElectionTimer();
this.emit(RaftEvent.VoteGranted, { candidateId: request.candidateId, term: request.term });
return { term: this.state.currentTerm, voteGranted: true };
}
return { term: this.state.currentTerm, voteGranted: false };
}
/** Handle AppendEntries RPC from leader */
async handleAppendEntries(request: AppendEntriesRequest<T>): Promise<AppendEntriesResponse> {
// If request term is higher, update term
if (request.term > this.state.currentTerm) {
await this.state.setTerm(request.term);
this.transitionTo(NodeState.Follower);
}
// Reject if term is less than current term
if (request.term < this.state.currentTerm) {
return { term: this.state.currentTerm, success: false };
}
// Valid leader - reset election timer
this.leaderId = request.leaderId;
this.resetElectionTimer();
// If not follower, become follower
if (this.nodeState !== NodeState.Follower) {
this.transitionTo(NodeState.Follower);
}
this.emit(RaftEvent.Heartbeat, { leaderId: request.leaderId, term: request.term });
// Check if log contains entry at prevLogIndex with prevLogTerm
if (request.prevLogIndex > 0 && !this.state.log.containsEntry(request.prevLogIndex, request.prevLogTerm)) {
return { term: this.state.currentTerm, success: false };
}
// Append entries
if (request.entries.length > 0) {
await this.state.log.append(request.entries);
}
// Update commit index
if (request.leaderCommit > this.state.commitIndex) {
this.state.setCommitIndex(
Math.min(request.leaderCommit, this.state.log.lastIndex),
);
await this.applyCommitted();
}
return {
term: this.state.currentTerm,
success: true,
matchIndex: this.state.log.lastIndex,
};
}
/** Load persistent state */
loadState(state: PersistentState<T>): void {
this.state.loadPersistentState(state);
}
/** Get current persistent state */
getState(): PersistentState<T> {
return this.state.getPersistentState();
}
// Private methods
private transitionTo(newState: NodeState): void {
const previousState = this.nodeState;
if (previousState === newState) return;
this.nodeState = newState;
this.clearTimers();
if (newState === NodeState.Leader) {
this.state.initLeaderState();
this.leaderId = this.config.nodeId;
this.startHeartbeat();
this.emit(RaftEvent.LeaderElected, {
leaderId: this.config.nodeId,
term: this.state.currentTerm,
} as LeaderElectedEvent);
} else {
this.state.clearLeaderState();
if (newState === NodeState.Follower) {
this.leaderId = null;
this.resetElectionTimer();
}
}
this.emit(RaftEvent.StateChange, {
previousState,
newState,
term: this.state.currentTerm,
} as StateChangeEvent);
}
private getRandomElectionTimeout(): number {
const [min, max] = this.config.electionTimeout;
return min + Math.random() * (max - min);
}
private resetElectionTimer(): void {
if (this.electionTimer) {
clearTimeout(this.electionTimer);
}
if (!this.running) return;
this.electionTimer = setTimeout(() => {
this.startElection();
}, this.getRandomElectionTimeout());
}
private clearTimers(): void {
if (this.electionTimer) {
clearTimeout(this.electionTimer);
this.electionTimer = null;
}
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
private async startElection(): Promise<void> {
if (!this.running) return;
// Increment term and become candidate
await this.state.setTerm(this.state.currentTerm + 1);
await this.state.vote(this.state.currentTerm, this.config.nodeId);
this.transitionTo(NodeState.Candidate);
this.emit(RaftEvent.VoteRequested, {
term: this.state.currentTerm,
candidateId: this.config.nodeId,
});
// Start with 1 vote (self)
let votesReceived = 1;
const majority = Math.floor((this.config.peers.length + 1) / 2) + 1;
// Request votes from all peers
if (!this.transport) {
this.resetElectionTimer();
return;
}
const votePromises = this.config.peers.map(async (peerId) => {
try {
const response = await this.transport!.requestVote(peerId, {
term: this.state.currentTerm,
candidateId: this.config.nodeId,
lastLogIndex: this.state.log.lastIndex,
lastLogTerm: this.state.log.lastTerm,
});
// If response term is higher, become follower
if (response.term > this.state.currentTerm) {
await this.state.setTerm(response.term);
this.transitionTo(NodeState.Follower);
return;
}
if (response.voteGranted && this.nodeState === NodeState.Candidate) {
votesReceived++;
if (votesReceived >= majority) {
this.transitionTo(NodeState.Leader);
}
}
} catch {
// Peer unavailable, continue
}
});
await Promise.allSettled(votePromises);
// If still candidate, restart election timer
if (this.nodeState === NodeState.Candidate) {
this.resetElectionTimer();
}
}
private startHeartbeat(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
}
// Send immediate heartbeat
this.replicateToFollowers();
// Start periodic heartbeat
this.heartbeatTimer = setInterval(() => {
if (this.nodeState === NodeState.Leader) {
this.replicateToFollowers();
}
}, this.config.heartbeatInterval);
}
private async replicateToFollowers(): Promise<void> {
if (!this.transport || this.nodeState !== NodeState.Leader) return;
const replicationPromises = this.config.peers.map(async (peerId) => {
await this.replicateToPeer(peerId);
});
await Promise.allSettled(replicationPromises);
// Update commit index if majority have replicated
if (this.state.updateCommitIndex()) {
this.emit(RaftEvent.LogCommitted, {
index: this.state.commitIndex,
term: this.state.currentTerm,
} as LogCommittedEvent);
await this.applyCommitted();
}
}
private async replicateToPeer(peerId: NodeId): Promise<void> {
if (!this.transport || this.nodeState !== NodeState.Leader) return;
const nextIndex = this.state.getNextIndex(peerId);
const prevLogIndex = nextIndex - 1;
const prevLogTerm = this.state.log.termAt(prevLogIndex) ?? 0;
const entries = this.state.log.getFrom(nextIndex, this.config.maxEntriesPerRequest);
try {
const response = await this.transport.appendEntries(peerId, {
term: this.state.currentTerm,
leaderId: this.config.nodeId,
prevLogIndex,
prevLogTerm,
entries,
leaderCommit: this.state.commitIndex,
});
if (response.term > this.state.currentTerm) {
await this.state.setTerm(response.term);
this.transitionTo(NodeState.Follower);
return;
}
if (response.success) {
if (response.matchIndex !== undefined) {
this.state.setNextIndex(peerId, response.matchIndex + 1);
this.state.setMatchIndex(peerId, response.matchIndex);
} else if (entries.length > 0) {
const lastEntry = entries[entries.length - 1];
this.state.setNextIndex(peerId, lastEntry.index + 1);
this.state.setMatchIndex(peerId, lastEntry.index);
}
} else {
// Decrement nextIndex and retry
this.state.setNextIndex(peerId, nextIndex - 1);
}
} catch {
// Peer unavailable, will retry on next heartbeat
}
}
private async applyCommitted(): Promise<void> {
while (this.state.lastApplied < this.state.commitIndex) {
const nextIndex = this.state.lastApplied + 1;
const entry = this.state.log.get(nextIndex);
if (entry && this.stateMachine) {
try {
await this.stateMachine.apply(entry.command);
this.state.setLastApplied(nextIndex);
this.emit(RaftEvent.LogApplied, entry);
} catch (error) {
this.emit(RaftEvent.Error, error);
break;
}
} else {
this.state.setLastApplied(nextIndex);
}
}
}
}

View File

@@ -0,0 +1,63 @@
/**
* Raft State Management
* Manages persistent and volatile state for Raft consensus
*/
import type { NodeId, Term, LogIndex, PersistentState, VolatileState, LeaderState, LogEntry } from './types.js';
import { RaftLog } from './log.js';
/** State manager for a Raft node */
export declare class RaftState<T = unknown> {
private readonly nodeId;
private readonly peers;
private _currentTerm;
private _votedFor;
private _commitIndex;
private _lastApplied;
private _leaderState;
readonly log: RaftLog<T>;
constructor(nodeId: NodeId, peers: NodeId[], options?: {
onPersist?: (state: PersistentState<T>) => Promise<void>;
onLogPersist?: (entries: LogEntry<T>[]) => Promise<void>;
});
private persistCallback?;
/** Get current term */
get currentTerm(): Term;
/** Get voted for */
get votedFor(): NodeId | null;
/** Get commit index */
get commitIndex(): LogIndex;
/** Get last applied */
get lastApplied(): LogIndex;
/** Get leader state (null if not leader) */
get leaderState(): LeaderState | null;
/** Update term (with persistence) */
setTerm(term: Term): Promise<void>;
/** Record vote (with persistence) */
vote(term: Term, candidateId: NodeId): Promise<void>;
/** Update commit index */
setCommitIndex(index: LogIndex): void;
/** Update last applied */
setLastApplied(index: LogIndex): void;
/** Initialize leader state */
initLeaderState(): void;
/** Clear leader state */
clearLeaderState(): void;
/** Update nextIndex for a peer */
setNextIndex(peerId: NodeId, index: LogIndex): void;
/** Update matchIndex for a peer */
setMatchIndex(peerId: NodeId, index: LogIndex): void;
/** Get nextIndex for a peer */
getNextIndex(peerId: NodeId): LogIndex;
/** Get matchIndex for a peer */
getMatchIndex(peerId: NodeId): LogIndex;
/** Update commit index based on match indices (for leader) */
updateCommitIndex(): boolean;
/** Get persistent state */
getPersistentState(): PersistentState<T>;
/** Get volatile state */
getVolatileState(): VolatileState;
/** Load persistent state */
loadPersistentState(state: PersistentState<T>): void;
/** Persist state */
private persist;
}
//# sourceMappingURL=state.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["state.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,MAAM,EACN,IAAI,EACJ,QAAQ,EACR,eAAe,EACf,aAAa,EACb,WAAW,EACX,QAAQ,EACT,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAEnC,oCAAoC;AACpC,qBAAa,SAAS,CAAC,CAAC,GAAG,OAAO;IAU9B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IAVxB,OAAO,CAAC,YAAY,CAAW;IAC/B,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,YAAY,CAA4B;IAEhD,SAAgB,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;gBAGb,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EAAE,EAChC,OAAO,CAAC,EAAE;QACR,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;QACzD,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KAC1D;IAMH,OAAO,CAAC,eAAe,CAAC,CAA+C;IAEvE,uBAAuB;IACvB,IAAI,WAAW,IAAI,IAAI,CAEtB;IAED,oBAAoB;IACpB,IAAI,QAAQ,IAAI,MAAM,GAAG,IAAI,CAE5B;IAED,uBAAuB;IACvB,IAAI,WAAW,IAAI,QAAQ,CAE1B;IAED,uBAAuB;IACvB,IAAI,WAAW,IAAI,QAAQ,CAE1B;IAED,4CAA4C;IAC5C,IAAI,WAAW,IAAI,WAAW,GAAG,IAAI,CAEpC;IAED,qCAAqC;IAC/B,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAQxC,qCAAqC;IAC/B,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAM1D,0BAA0B;IAC1B,cAAc,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI;IAMrC,0BAA0B;IAC1B,cAAc,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI;IAMrC,8BAA8B;IAC9B,eAAe,IAAI,IAAI;IAcvB,yBAAyB;IACzB,gBAAgB,IAAI,IAAI;IAIxB,kCAAkC;IAClC,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,IAAI;IAMnD,mCAAmC;IACnC,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,IAAI;IAMpD,+BAA+B;IAC/B,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ;IAItC,gCAAgC;IAChC,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ;IAIvC,8DAA8D;IAC9D,iBAAiB,IAAI,OAAO;IA6B5B,2BAA2B;IAC3B,kBAAkB,IAAI,eAAe,CAAC,CAAC,CAAC;IAQxC,yBAAyB;IACzB,gBAAgB,IAAI,aAAa;IAOjC,4BAA4B;IAC5B,mBAAmB,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,IAAI;IAMpD,oBAAoB;YACN,OAAO;CAKtB"}

View File

@@ -0,0 +1,158 @@
"use strict";
/**
* Raft State Management
* Manages persistent and volatile state for Raft consensus
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.RaftState = void 0;
const log_js_1 = require("./log.js");
/** State manager for a Raft node */
class RaftState {
constructor(nodeId, peers, options) {
this.nodeId = nodeId;
this.peers = peers;
this._currentTerm = 0;
this._votedFor = null;
this._commitIndex = 0;
this._lastApplied = 0;
this._leaderState = null;
this.log = new log_js_1.RaftLog({ onPersist: options?.onLogPersist });
this.persistCallback = options?.onPersist;
}
/** Get current term */
get currentTerm() {
return this._currentTerm;
}
/** Get voted for */
get votedFor() {
return this._votedFor;
}
/** Get commit index */
get commitIndex() {
return this._commitIndex;
}
/** Get last applied */
get lastApplied() {
return this._lastApplied;
}
/** Get leader state (null if not leader) */
get leaderState() {
return this._leaderState;
}
/** Update term (with persistence) */
async setTerm(term) {
if (term > this._currentTerm) {
this._currentTerm = term;
this._votedFor = null;
await this.persist();
}
}
/** Record vote (with persistence) */
async vote(term, candidateId) {
this._currentTerm = term;
this._votedFor = candidateId;
await this.persist();
}
/** Update commit index */
setCommitIndex(index) {
if (index > this._commitIndex) {
this._commitIndex = index;
}
}
/** Update last applied */
setLastApplied(index) {
if (index > this._lastApplied) {
this._lastApplied = index;
}
}
/** Initialize leader state */
initLeaderState() {
const nextIndex = new Map();
const matchIndex = new Map();
for (const peer of this.peers) {
// Initialize nextIndex to leader's last log index + 1
nextIndex.set(peer, this.log.lastIndex + 1);
// Initialize matchIndex to 0
matchIndex.set(peer, 0);
}
this._leaderState = { nextIndex, matchIndex };
}
/** Clear leader state */
clearLeaderState() {
this._leaderState = null;
}
/** Update nextIndex for a peer */
setNextIndex(peerId, index) {
if (this._leaderState) {
this._leaderState.nextIndex.set(peerId, Math.max(1, index));
}
}
/** Update matchIndex for a peer */
setMatchIndex(peerId, index) {
if (this._leaderState) {
this._leaderState.matchIndex.set(peerId, index);
}
}
/** Get nextIndex for a peer */
getNextIndex(peerId) {
return this._leaderState?.nextIndex.get(peerId) ?? this.log.lastIndex + 1;
}
/** Get matchIndex for a peer */
getMatchIndex(peerId) {
return this._leaderState?.matchIndex.get(peerId) ?? 0;
}
/** Update commit index based on match indices (for leader) */
updateCommitIndex() {
if (!this._leaderState)
return false;
// Find the highest index N such that a majority have matchIndex >= N
// and log[N].term == currentTerm
const matchIndices = Array.from(this._leaderState.matchIndex.values());
matchIndices.push(this.log.lastIndex); // Include self
matchIndices.sort((a, b) => b - a); // Sort descending
const majority = Math.floor((this.peers.length + 1) / 2) + 1;
for (const index of matchIndices) {
if (index <= this._commitIndex)
break;
const term = this.log.termAt(index);
if (term === this._currentTerm) {
// Count how many have this index or higher
const count = matchIndices.filter((m) => m >= index).length + 1; // +1 for self
if (count >= majority) {
this._commitIndex = index;
return true;
}
}
}
return false;
}
/** Get persistent state */
getPersistentState() {
return {
currentTerm: this._currentTerm,
votedFor: this._votedFor,
log: this.log.getAll(),
};
}
/** Get volatile state */
getVolatileState() {
return {
commitIndex: this._commitIndex,
lastApplied: this._lastApplied,
};
}
/** Load persistent state */
loadPersistentState(state) {
this._currentTerm = state.currentTerm;
this._votedFor = state.votedFor;
this.log.load(state.log);
}
/** Persist state */
async persist() {
if (this.persistCallback) {
await this.persistCallback(this.getPersistentState());
}
}
}
exports.RaftState = RaftState;
//# sourceMappingURL=state.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"state.js","sourceRoot":"","sources":["state.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,200 @@
/**
* Raft State Management
* Manages persistent and volatile state for Raft consensus
*/
import type {
NodeId,
Term,
LogIndex,
PersistentState,
VolatileState,
LeaderState,
LogEntry,
} from './types.js';
import { RaftLog } from './log.js';
/** State manager for a Raft node */
export class RaftState<T = unknown> {
private _currentTerm: Term = 0;
private _votedFor: NodeId | null = null;
private _commitIndex: LogIndex = 0;
private _lastApplied: LogIndex = 0;
private _leaderState: LeaderState | null = null;
public readonly log: RaftLog<T>;
constructor(
private readonly nodeId: NodeId,
private readonly peers: NodeId[],
options?: {
onPersist?: (state: PersistentState<T>) => Promise<void>;
onLogPersist?: (entries: LogEntry<T>[]) => Promise<void>;
},
) {
this.log = new RaftLog({ onPersist: options?.onLogPersist });
this.persistCallback = options?.onPersist;
}
private persistCallback?: (state: PersistentState<T>) => Promise<void>;
/** Get current term */
get currentTerm(): Term {
return this._currentTerm;
}
/** Get voted for */
get votedFor(): NodeId | null {
return this._votedFor;
}
/** Get commit index */
get commitIndex(): LogIndex {
return this._commitIndex;
}
/** Get last applied */
get lastApplied(): LogIndex {
return this._lastApplied;
}
/** Get leader state (null if not leader) */
get leaderState(): LeaderState | null {
return this._leaderState;
}
/** Update term (with persistence) */
async setTerm(term: Term): Promise<void> {
if (term > this._currentTerm) {
this._currentTerm = term;
this._votedFor = null;
await this.persist();
}
}
/** Record vote (with persistence) */
async vote(term: Term, candidateId: NodeId): Promise<void> {
this._currentTerm = term;
this._votedFor = candidateId;
await this.persist();
}
/** Update commit index */
setCommitIndex(index: LogIndex): void {
if (index > this._commitIndex) {
this._commitIndex = index;
}
}
/** Update last applied */
setLastApplied(index: LogIndex): void {
if (index > this._lastApplied) {
this._lastApplied = index;
}
}
/** Initialize leader state */
initLeaderState(): void {
const nextIndex = new Map<NodeId, LogIndex>();
const matchIndex = new Map<NodeId, LogIndex>();
for (const peer of this.peers) {
// Initialize nextIndex to leader's last log index + 1
nextIndex.set(peer, this.log.lastIndex + 1);
// Initialize matchIndex to 0
matchIndex.set(peer, 0);
}
this._leaderState = { nextIndex, matchIndex };
}
/** Clear leader state */
clearLeaderState(): void {
this._leaderState = null;
}
/** Update nextIndex for a peer */
setNextIndex(peerId: NodeId, index: LogIndex): void {
if (this._leaderState) {
this._leaderState.nextIndex.set(peerId, Math.max(1, index));
}
}
/** Update matchIndex for a peer */
setMatchIndex(peerId: NodeId, index: LogIndex): void {
if (this._leaderState) {
this._leaderState.matchIndex.set(peerId, index);
}
}
/** Get nextIndex for a peer */
getNextIndex(peerId: NodeId): LogIndex {
return this._leaderState?.nextIndex.get(peerId) ?? this.log.lastIndex + 1;
}
/** Get matchIndex for a peer */
getMatchIndex(peerId: NodeId): LogIndex {
return this._leaderState?.matchIndex.get(peerId) ?? 0;
}
/** Update commit index based on match indices (for leader) */
updateCommitIndex(): boolean {
if (!this._leaderState) return false;
// Find the highest index N such that a majority have matchIndex >= N
// and log[N].term == currentTerm
const matchIndices = Array.from(this._leaderState.matchIndex.values());
matchIndices.push(this.log.lastIndex); // Include self
matchIndices.sort((a, b) => b - a); // Sort descending
const majority = Math.floor((this.peers.length + 1) / 2) + 1;
for (const index of matchIndices) {
if (index <= this._commitIndex) break;
const term = this.log.termAt(index);
if (term === this._currentTerm) {
// Count how many have this index or higher
const count =
matchIndices.filter((m) => m >= index).length + 1; // +1 for self
if (count >= majority) {
this._commitIndex = index;
return true;
}
}
}
return false;
}
/** Get persistent state */
getPersistentState(): PersistentState<T> {
return {
currentTerm: this._currentTerm,
votedFor: this._votedFor,
log: this.log.getAll(),
};
}
/** Get volatile state */
getVolatileState(): VolatileState {
return {
commitIndex: this._commitIndex,
lastApplied: this._lastApplied,
};
}
/** Load persistent state */
loadPersistentState(state: PersistentState<T>): void {
this._currentTerm = state.currentTerm;
this._votedFor = state.votedFor;
this.log.load(state.log);
}
/** Persist state */
private async persist(): Promise<void> {
if (this.persistCallback) {
await this.persistCallback(this.getPersistentState());
}
}
}

View File

@@ -0,0 +1,154 @@
/**
* Raft Consensus Types
* Based on the Raft paper specification
*/
/** Unique identifier for a node in the cluster */
export type NodeId = string;
/** Monotonically increasing term number */
export type Term = number;
/** Index into the replicated log */
export type LogIndex = number;
/** Possible states of a Raft node */
export declare enum NodeState {
Follower = "follower",
Candidate = "candidate",
Leader = "leader"
}
/** Entry in the replicated log */
export interface LogEntry<T = unknown> {
/** Term when entry was received by leader */
term: Term;
/** Index in the log */
index: LogIndex;
/** Command to be applied to state machine */
command: T;
/** Timestamp when entry was created */
timestamp: number;
}
/** Persistent state on all servers (updated on stable storage before responding to RPCs) */
export interface PersistentState<T = unknown> {
/** Latest term server has seen */
currentTerm: Term;
/** CandidateId that received vote in current term (or null if none) */
votedFor: NodeId | null;
/** Log entries */
log: LogEntry<T>[];
}
/** Volatile state on all servers */
export interface VolatileState {
/** Index of highest log entry known to be committed */
commitIndex: LogIndex;
/** Index of highest log entry applied to state machine */
lastApplied: LogIndex;
}
/** Volatile state on leaders (reinitialized after election) */
export interface LeaderState {
/** For each server, index of the next log entry to send to that server */
nextIndex: Map<NodeId, LogIndex>;
/** For each server, index of highest log entry known to be replicated on server */
matchIndex: Map<NodeId, LogIndex>;
}
/** Configuration for a Raft node */
export interface RaftNodeConfig {
/** Unique identifier for this node */
nodeId: NodeId;
/** List of all node IDs in the cluster */
peers: NodeId[];
/** Election timeout range in milliseconds [min, max] */
electionTimeout: [number, number];
/** Heartbeat interval in milliseconds */
heartbeatInterval: number;
/** Maximum entries per AppendEntries RPC */
maxEntriesPerRequest: number;
}
/** Request for RequestVote RPC */
export interface RequestVoteRequest {
/** Candidate's term */
term: Term;
/** Candidate requesting vote */
candidateId: NodeId;
/** Index of candidate's last log entry */
lastLogIndex: LogIndex;
/** Term of candidate's last log entry */
lastLogTerm: Term;
}
/** Response for RequestVote RPC */
export interface RequestVoteResponse {
/** Current term, for candidate to update itself */
term: Term;
/** True means candidate received vote */
voteGranted: boolean;
}
/** Request for AppendEntries RPC */
export interface AppendEntriesRequest<T = unknown> {
/** Leader's term */
term: Term;
/** So follower can redirect clients */
leaderId: NodeId;
/** Index of log entry immediately preceding new ones */
prevLogIndex: LogIndex;
/** Term of prevLogIndex entry */
prevLogTerm: Term;
/** Log entries to store (empty for heartbeat) */
entries: LogEntry<T>[];
/** Leader's commitIndex */
leaderCommit: LogIndex;
}
/** Response for AppendEntries RPC */
export interface AppendEntriesResponse {
/** Current term, for leader to update itself */
term: Term;
/** True if follower contained entry matching prevLogIndex and prevLogTerm */
success: boolean;
/** Hint for next index to try (optimization) */
matchIndex?: LogIndex;
}
/** Raft error types */
export declare class RaftError extends Error {
readonly code: RaftErrorCode;
constructor(message: string, code: RaftErrorCode);
static notLeader(): RaftError;
static noLeader(): RaftError;
static electionTimeout(): RaftError;
static logInconsistency(): RaftError;
}
export declare enum RaftErrorCode {
NotLeader = "NOT_LEADER",
NoLeader = "NO_LEADER",
InvalidTerm = "INVALID_TERM",
InvalidLogIndex = "INVALID_LOG_INDEX",
ElectionTimeout = "ELECTION_TIMEOUT",
LogInconsistency = "LOG_INCONSISTENCY",
SnapshotFailed = "SNAPSHOT_FAILED",
ConfigError = "CONFIG_ERROR",
Internal = "INTERNAL"
}
/** Event types emitted by RaftNode */
export declare enum RaftEvent {
StateChange = "stateChange",
LeaderElected = "leaderElected",
LogAppended = "logAppended",
LogCommitted = "logCommitted",
LogApplied = "logApplied",
VoteRequested = "voteRequested",
VoteGranted = "voteGranted",
Heartbeat = "heartbeat",
Error = "error"
}
/** State change event data */
export interface StateChangeEvent {
previousState: NodeState;
newState: NodeState;
term: Term;
}
/** Leader elected event data */
export interface LeaderElectedEvent {
leaderId: NodeId;
term: Term;
}
/** Log committed event data */
export interface LogCommittedEvent {
index: LogIndex;
term: Term;
}
//# sourceMappingURL=types.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,61 @@
"use strict";
/**
* Raft Consensus Types
* Based on the Raft paper specification
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.RaftEvent = exports.RaftErrorCode = exports.RaftError = exports.NodeState = void 0;
/** Possible states of a Raft node */
var NodeState;
(function (NodeState) {
NodeState["Follower"] = "follower";
NodeState["Candidate"] = "candidate";
NodeState["Leader"] = "leader";
})(NodeState || (exports.NodeState = NodeState = {}));
/** Raft error types */
class RaftError extends Error {
constructor(message, code) {
super(message);
this.code = code;
this.name = 'RaftError';
}
static notLeader() {
return new RaftError('Node is not the leader', RaftErrorCode.NotLeader);
}
static noLeader() {
return new RaftError('No leader available', RaftErrorCode.NoLeader);
}
static electionTimeout() {
return new RaftError('Election timeout', RaftErrorCode.ElectionTimeout);
}
static logInconsistency() {
return new RaftError('Log inconsistency detected', RaftErrorCode.LogInconsistency);
}
}
exports.RaftError = RaftError;
var RaftErrorCode;
(function (RaftErrorCode) {
RaftErrorCode["NotLeader"] = "NOT_LEADER";
RaftErrorCode["NoLeader"] = "NO_LEADER";
RaftErrorCode["InvalidTerm"] = "INVALID_TERM";
RaftErrorCode["InvalidLogIndex"] = "INVALID_LOG_INDEX";
RaftErrorCode["ElectionTimeout"] = "ELECTION_TIMEOUT";
RaftErrorCode["LogInconsistency"] = "LOG_INCONSISTENCY";
RaftErrorCode["SnapshotFailed"] = "SNAPSHOT_FAILED";
RaftErrorCode["ConfigError"] = "CONFIG_ERROR";
RaftErrorCode["Internal"] = "INTERNAL";
})(RaftErrorCode || (exports.RaftErrorCode = RaftErrorCode = {}));
/** Event types emitted by RaftNode */
var RaftEvent;
(function (RaftEvent) {
RaftEvent["StateChange"] = "stateChange";
RaftEvent["LeaderElected"] = "leaderElected";
RaftEvent["LogAppended"] = "logAppended";
RaftEvent["LogCommitted"] = "logCommitted";
RaftEvent["LogApplied"] = "logApplied";
RaftEvent["VoteRequested"] = "voteRequested";
RaftEvent["VoteGranted"] = "voteGranted";
RaftEvent["Heartbeat"] = "heartbeat";
RaftEvent["Error"] = "error";
})(RaftEvent || (exports.RaftEvent = RaftEvent = {}));
//# sourceMappingURL=types.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAWH,qCAAqC;AACrC,IAAY,SAIX;AAJD,WAAY,SAAS;IACnB,kCAAqB,CAAA;IACrB,oCAAuB,CAAA;IACvB,8BAAiB,CAAA;AACnB,CAAC,EAJW,SAAS,yBAAT,SAAS,QAIpB;AAoGD,uBAAuB;AACvB,MAAa,SAAU,SAAQ,KAAK;IAClC,YACE,OAAe,EACC,IAAmB;QAEnC,KAAK,CAAC,OAAO,CAAC,CAAC;QAFC,SAAI,GAAJ,IAAI,CAAe;QAGnC,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;IAC1B,CAAC;IAED,MAAM,CAAC,SAAS;QACd,OAAO,IAAI,SAAS,CAAC,wBAAwB,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC;IAC1E,CAAC;IAED,MAAM,CAAC,QAAQ;QACb,OAAO,IAAI,SAAS,CAAC,qBAAqB,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;IACtE,CAAC;IAED,MAAM,CAAC,eAAe;QACpB,OAAO,IAAI,SAAS,CAAC,kBAAkB,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAC1E,CAAC;IAED,MAAM,CAAC,gBAAgB;QACrB,OAAO,IAAI,SAAS,CAAC,4BAA4B,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;IACrF,CAAC;CACF;AAxBD,8BAwBC;AAED,IAAY,aAUX;AAVD,WAAY,aAAa;IACvB,yCAAwB,CAAA;IACxB,uCAAsB,CAAA;IACtB,6CAA4B,CAAA;IAC5B,sDAAqC,CAAA;IACrC,qDAAoC,CAAA;IACpC,uDAAsC,CAAA;IACtC,mDAAkC,CAAA;IAClC,6CAA4B,CAAA;IAC5B,sCAAqB,CAAA;AACvB,CAAC,EAVW,aAAa,6BAAb,aAAa,QAUxB;AAED,sCAAsC;AACtC,IAAY,SAUX;AAVD,WAAY,SAAS;IACnB,wCAA2B,CAAA;IAC3B,4CAA+B,CAAA;IAC/B,wCAA2B,CAAA;IAC3B,0CAA6B,CAAA;IAC7B,sCAAyB,CAAA;IACzB,4CAA+B,CAAA;IAC/B,wCAA2B,CAAA;IAC3B,oCAAuB,CAAA;IACvB,4BAAe,CAAA;AACjB,CAAC,EAVW,SAAS,yBAAT,SAAS,QAUpB"}

View File

@@ -0,0 +1,189 @@
/**
* Raft Consensus Types
* Based on the Raft paper specification
*/
/** Unique identifier for a node in the cluster */
export type NodeId = string;
/** Monotonically increasing term number */
export type Term = number;
/** Index into the replicated log */
export type LogIndex = number;
/** Possible states of a Raft node */
export enum NodeState {
Follower = 'follower',
Candidate = 'candidate',
Leader = 'leader',
}
/** Entry in the replicated log */
export interface LogEntry<T = unknown> {
/** Term when entry was received by leader */
term: Term;
/** Index in the log */
index: LogIndex;
/** Command to be applied to state machine */
command: T;
/** Timestamp when entry was created */
timestamp: number;
}
/** Persistent state on all servers (updated on stable storage before responding to RPCs) */
export interface PersistentState<T = unknown> {
/** Latest term server has seen */
currentTerm: Term;
/** CandidateId that received vote in current term (or null if none) */
votedFor: NodeId | null;
/** Log entries */
log: LogEntry<T>[];
}
/** Volatile state on all servers */
export interface VolatileState {
/** Index of highest log entry known to be committed */
commitIndex: LogIndex;
/** Index of highest log entry applied to state machine */
lastApplied: LogIndex;
}
/** Volatile state on leaders (reinitialized after election) */
export interface LeaderState {
/** For each server, index of the next log entry to send to that server */
nextIndex: Map<NodeId, LogIndex>;
/** For each server, index of highest log entry known to be replicated on server */
matchIndex: Map<NodeId, LogIndex>;
}
/** Configuration for a Raft node */
export interface RaftNodeConfig {
/** Unique identifier for this node */
nodeId: NodeId;
/** List of all node IDs in the cluster */
peers: NodeId[];
/** Election timeout range in milliseconds [min, max] */
electionTimeout: [number, number];
/** Heartbeat interval in milliseconds */
heartbeatInterval: number;
/** Maximum entries per AppendEntries RPC */
maxEntriesPerRequest: number;
}
/** Request for RequestVote RPC */
export interface RequestVoteRequest {
/** Candidate's term */
term: Term;
/** Candidate requesting vote */
candidateId: NodeId;
/** Index of candidate's last log entry */
lastLogIndex: LogIndex;
/** Term of candidate's last log entry */
lastLogTerm: Term;
}
/** Response for RequestVote RPC */
export interface RequestVoteResponse {
/** Current term, for candidate to update itself */
term: Term;
/** True means candidate received vote */
voteGranted: boolean;
}
/** Request for AppendEntries RPC */
export interface AppendEntriesRequest<T = unknown> {
/** Leader's term */
term: Term;
/** So follower can redirect clients */
leaderId: NodeId;
/** Index of log entry immediately preceding new ones */
prevLogIndex: LogIndex;
/** Term of prevLogIndex entry */
prevLogTerm: Term;
/** Log entries to store (empty for heartbeat) */
entries: LogEntry<T>[];
/** Leader's commitIndex */
leaderCommit: LogIndex;
}
/** Response for AppendEntries RPC */
export interface AppendEntriesResponse {
/** Current term, for leader to update itself */
term: Term;
/** True if follower contained entry matching prevLogIndex and prevLogTerm */
success: boolean;
/** Hint for next index to try (optimization) */
matchIndex?: LogIndex;
}
/** Raft error types */
export class RaftError extends Error {
constructor(
message: string,
public readonly code: RaftErrorCode,
) {
super(message);
this.name = 'RaftError';
}
static notLeader(): RaftError {
return new RaftError('Node is not the leader', RaftErrorCode.NotLeader);
}
static noLeader(): RaftError {
return new RaftError('No leader available', RaftErrorCode.NoLeader);
}
static electionTimeout(): RaftError {
return new RaftError('Election timeout', RaftErrorCode.ElectionTimeout);
}
static logInconsistency(): RaftError {
return new RaftError('Log inconsistency detected', RaftErrorCode.LogInconsistency);
}
}
export enum RaftErrorCode {
NotLeader = 'NOT_LEADER',
NoLeader = 'NO_LEADER',
InvalidTerm = 'INVALID_TERM',
InvalidLogIndex = 'INVALID_LOG_INDEX',
ElectionTimeout = 'ELECTION_TIMEOUT',
LogInconsistency = 'LOG_INCONSISTENCY',
SnapshotFailed = 'SNAPSHOT_FAILED',
ConfigError = 'CONFIG_ERROR',
Internal = 'INTERNAL',
}
/** Event types emitted by RaftNode */
export enum RaftEvent {
StateChange = 'stateChange',
LeaderElected = 'leaderElected',
LogAppended = 'logAppended',
LogCommitted = 'logCommitted',
LogApplied = 'logApplied',
VoteRequested = 'voteRequested',
VoteGranted = 'voteGranted',
Heartbeat = 'heartbeat',
Error = 'error',
}
/** State change event data */
export interface StateChangeEvent {
previousState: NodeState;
newState: NodeState;
term: Term;
}
/** Leader elected event data */
export interface LeaderElectedEvent {
leaderId: NodeId;
term: Term;
}
/** Log committed event data */
export interface LogCommittedEvent {
index: LogIndex;
term: Term;
}