Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions npm/packages/raft/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"name": "@ruvector/raft",
"version": "0.1.0",
"description": "Raft consensus implementation for distributed systems - leader election, log replication, and fault tolerance",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
},
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build",
"test": "node --test test/*.test.js",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist"
},
"devDependencies": {
"@types/node": "^20.19.30",
"typescript": "^5.9.3"
},
"dependencies": {
"eventemitter3": "^5.0.4"
},
"keywords": [
"raft",
"consensus",
"distributed-systems",
"leader-election",
"log-replication",
"fault-tolerance",
"distributed-consensus",
"ruvector",
"cluster"
],
"author": "rUv Team <team@ruv.io>",
"license": "MIT OR Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/ruvnet/ruvector.git",
"directory": "npm/packages/raft"
},
"homepage": "https://github.com/ruvnet/ruvector/tree/main/crates/ruvector-raft",
"bugs": {
"url": "https://github.com/ruvnet/ruvector/issues"
},
"engines": {
"node": ">= 18"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "public"
},
"files": [
"dist",
"README.md"
]
}
78 changes: 78 additions & 0 deletions npm/packages/raft/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
135 changes: 135 additions & 0 deletions npm/packages/raft/src/log.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading
Loading