2025-09-08 14:26:09 -07:00
/ * *
* Distributed Coordinator for Brainy 3.0
* Provides leader election , consensus , and coordination for distributed instances
* /
feat: add node: protocol to all Node.js built-in imports for bundler compatibility
- Updated all fs, path, crypto, os, url, util, events, http, https, net, child_process, stream, and zlib imports
- Changed both static imports and dynamic imports to use node: protocol
- This makes Brainy more bundler-friendly by explicitly marking Node.js built-ins
- Prevents bundlers from attempting to polyfill or bundle these modules
- Reduces bundle size for web applications using Brainy
- Improves tree-shaking and dead code elimination
Benefits for external bundlers:
- Clear distinction between Node.js built-ins and external dependencies
- No ambiguity about what needs polyfilling
- Smaller bundles for browser builds
- Better compatibility with modern bundlers (Webpack 5, Vite, Rollup, esbuild)
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-17 14:20:21 -07:00
import { EventEmitter } from 'node:events'
2025-09-11 16:23:32 -07:00
import { NetworkTransport , NetworkMessage } from './networkTransport.js'
feat: add node: protocol to all Node.js built-in imports for bundler compatibility
- Updated all fs, path, crypto, os, url, util, events, http, https, net, child_process, stream, and zlib imports
- Changed both static imports and dynamic imports to use node: protocol
- This makes Brainy more bundler-friendly by explicitly marking Node.js built-ins
- Prevents bundlers from attempting to polyfill or bundle these modules
- Reduces bundle size for web applications using Brainy
- Improves tree-shaking and dead code elimination
Benefits for external bundlers:
- Clear distinction between Node.js built-ins and external dependencies
- No ambiguity about what needs polyfilling
- Smaller bundles for browser builds
- Better compatibility with modern bundlers (Webpack 5, Vite, Rollup, esbuild)
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-17 14:20:21 -07:00
import { createHash } from 'node:crypto'
2025-09-08 14:26:09 -07:00
export interface NodeInfo {
id : string
address : string
port : number
2025-09-11 16:23:32 -07:00
lastSeen : number
status : 'active' | 'inactive' | 'suspected'
state ? : 'follower' | 'candidate' | 'leader'
lastHeartbeat? : number
2025-09-08 14:26:09 -07:00
}
export interface CoordinatorConfig {
nodeId? : string
address? : string
port? : number
heartbeatInterval? : number
electionTimeout? : number
nodes? : string [ ]
}
export interface ConsensusState {
term : number
votedFor : string | null
leader : string | null
state : 'follower' | 'candidate' | 'leader'
}
2025-09-11 16:23:32 -07:00
export interface RaftMessage {
type : 'requestVote' | 'voteResponse' | 'appendEntries' | 'appendResponse'
term : number
from : string
to? : string
data? : any
}
2025-09-08 14:26:09 -07:00
/ * *
* Distributed Coordinator implementing Raft - like consensus
* /
export class DistributedCoordinator extends EventEmitter {
private nodeId : string
private nodes : Map < string , NodeInfo > = new Map ( )
private consensusState : ConsensusState
private heartbeatInterval : number
private electionTimeout : number
private electionTimer? : NodeJS.Timeout
private heartbeatTimer? : NodeJS.Timeout
private isRunning : boolean = false
2025-09-11 16:23:32 -07:00
private networkTransport? : NetworkTransport
private votesReceived : Set < string > = new Set ( )
private currentTerm : number = 0
private lastLogIndex : number = - 1
private lastLogTerm : number = 0
private logEntries : Array < { term : number ; index : number ; data : any } > = [ ]
private transport : any = null // For migration proposals
private pendingMigrations = new Map < string , any > ( )
private committedMigrations = new Set < string > ( )
2025-09-08 14:26:09 -07:00
constructor ( config : CoordinatorConfig = { } ) {
super ( )
// Generate node ID if not provided
this . nodeId = config . nodeId || this . generateNodeId ( )
// Configuration
this . heartbeatInterval = config . heartbeatInterval || 1000
this . electionTimeout = config . electionTimeout || 5000
// Initialize consensus state
this . consensusState = {
term : 0 ,
votedFor : null ,
leader : null ,
state : 'follower'
}
// Register this node
this . nodes . set ( this . nodeId , {
id : this.nodeId ,
address : config.address || 'localhost' ,
port : config.port || 3000 ,
2025-09-11 16:23:32 -07:00
lastSeen : Date.now ( ) ,
status : 'active' ,
state : 'follower' ,
2025-09-08 14:26:09 -07:00
lastHeartbeat : Date.now ( )
} )
// Register other nodes if provided
if ( config . nodes ) {
this . registerNodes ( config . nodes )
}
}
/ * *
* Start the coordinator
* /
2025-09-11 16:23:32 -07:00
async start ( networkTransport? : NetworkTransport ) : Promise < void > {
2025-09-08 14:26:09 -07:00
if ( this . isRunning ) return
this . isRunning = true
2025-09-11 16:23:32 -07:00
this . networkTransport = networkTransport
// Setup network message handlers if transport is provided
if ( this . networkTransport ) {
this . setupNetworkHandlers ( )
}
2025-09-08 14:26:09 -07:00
this . emit ( 'started' , { nodeId : this.nodeId } )
// Start as follower
this . becomeFollower ( )
}
2025-09-11 16:23:32 -07:00
/ * *
* Setup network message handlers
* /
private setupNetworkHandlers ( ) : void {
if ( ! this . networkTransport ) return
// Register message handlers directly on the messageHandlers map
const handlers = ( this . networkTransport as any ) . messageHandlers as Map < string , ( msg : NetworkMessage ) = > Promise < any > >
// Handle vote requests
handlers . set ( 'requestVote' , async ( msg : NetworkMessage ) = > {
const response = await this . handleVoteRequest ( msg )
// Send response back
if ( this . networkTransport ) {
await this . networkTransport . sendToNode ( msg . from , 'voteResponse' , response )
}
return response
} )
// Handle vote responses
handlers . set ( 'voteResponse' , async ( msg : NetworkMessage ) = > {
this . handleVoteResponse ( msg )
} )
// Handle heartbeats/append entries
handlers . set ( 'appendEntries' , async ( msg : NetworkMessage ) = > {
const response = await this . handleAppendEntries ( msg )
// Send response back
if ( this . networkTransport ) {
await this . networkTransport . sendToNode ( msg . from , 'appendResponse' , response )
}
return response
} )
// Handle append responses
handlers . set ( 'appendResponse' , async ( msg : NetworkMessage ) = > {
this . handleAppendResponse ( msg )
} )
}
2025-09-08 14:26:09 -07:00
/ * *
* Stop the coordinator
* /
async stop ( ) : Promise < void > {
if ( ! this . isRunning ) return
this . isRunning = false
if ( this . electionTimer ) {
clearTimeout ( this . electionTimer )
this . electionTimer = undefined
}
if ( this . heartbeatTimer ) {
clearInterval ( this . heartbeatTimer )
this . heartbeatTimer = undefined
}
2025-09-11 16:23:32 -07:00
this . emit ( 'stopped' )
2025-09-08 14:26:09 -07:00
}
/ * *
2025-09-11 16:23:32 -07:00
* Register additional nodes
2025-09-08 14:26:09 -07:00
* /
2025-09-11 16:23:32 -07:00
registerNodes ( nodes : string [ ] ) : void {
for ( const node of nodes ) {
const [ address , port ] = node . split ( ':' )
const nodeId = this . generateNodeId ( node )
2025-09-08 14:26:09 -07:00
2025-09-11 16:23:32 -07:00
if ( ! this . nodes . has ( nodeId ) ) {
2025-09-08 14:26:09 -07:00
this . nodes . set ( nodeId , {
id : nodeId ,
2025-09-11 16:23:32 -07:00
address ,
port : parseInt ( port || '3000' ) ,
lastSeen : Date.now ( ) ,
status : 'active' ,
state : 'follower'
2025-09-08 14:26:09 -07:00
} )
}
}
}
/ * *
* Become a follower
* /
private becomeFollower ( ) : void {
this . consensusState . state = 'follower'
const node = this . nodes . get ( this . nodeId )
if ( node ) {
2025-09-11 16:23:32 -07:00
node . state = 'follower'
2025-09-08 14:26:09 -07:00
}
this . resetElectionTimeout ( )
2025-09-11 16:23:32 -07:00
this . emit ( 'stateChange' , 'follower' )
2025-09-08 14:26:09 -07:00
}
/ * *
* Become a candidate and start election
* /
2025-09-11 16:23:32 -07:00
private async becomeCandidate ( ) : Promise < void > {
2025-09-08 14:26:09 -07:00
this . consensusState . state = 'candidate'
2025-09-11 16:23:32 -07:00
this . currentTerm ++
this . consensusState . term = this . currentTerm
2025-09-08 14:26:09 -07:00
this . consensusState . votedFor = this . nodeId
2025-09-11 16:23:32 -07:00
this . votesReceived = new Set ( [ this . nodeId ] )
2025-09-08 14:26:09 -07:00
const node = this . nodes . get ( this . nodeId )
if ( node ) {
2025-09-11 16:23:32 -07:00
node . state = 'candidate'
2025-09-08 14:26:09 -07:00
}
2025-09-11 16:23:32 -07:00
this . emit ( 'stateChange' , 'candidate' )
// Request votes from all other nodes
await this . requestVotes ( )
2025-09-08 14:26:09 -07:00
2025-09-11 16:23:32 -07:00
// Reset election timeout
this . resetElectionTimeout ( )
2025-09-08 14:26:09 -07:00
}
/ * *
* Become the leader
* /
private becomeLeader ( ) : void {
this . consensusState . state = 'leader'
this . consensusState . leader = this . nodeId
const node = this . nodes . get ( this . nodeId )
if ( node ) {
2025-09-11 16:23:32 -07:00
node . state = 'leader'
2025-09-08 14:26:09 -07:00
}
2025-09-11 16:23:32 -07:00
// Stop election timer as leader
2025-09-08 14:26:09 -07:00
if ( this . electionTimer ) {
clearTimeout ( this . electionTimer )
this . electionTimer = undefined
}
2025-09-11 16:23:32 -07:00
this . emit ( 'stateChange' , 'leader' )
this . emit ( 'leaderElected' , this . nodeId )
2025-09-08 14:26:09 -07:00
// Start sending heartbeats
this . startHeartbeat ( )
}
/ * *
2025-09-11 16:23:32 -07:00
* Request votes from all nodes
2025-09-08 14:26:09 -07:00
* /
2025-09-11 16:23:32 -07:00
private async requestVotes ( ) : Promise < void > {
if ( ! this . networkTransport ) {
// Simulate vote for testing
this . checkVoteMajority ( )
return
}
const voteRequest = {
type : 'requestVote' as const ,
term : this.currentTerm ,
candidateId : this.nodeId ,
lastLogIndex : this.getLastLogIndex ( ) ,
lastLogTerm : this.getLastLogTerm ( )
}
2025-09-08 14:26:09 -07:00
2025-09-11 16:23:32 -07:00
// Send vote requests to all other nodes
2025-09-08 14:26:09 -07:00
for ( const [ nodeId ] of this . nodes ) {
if ( nodeId !== this . nodeId ) {
2025-09-11 16:23:32 -07:00
try {
await this . networkTransport . sendToNode ( nodeId , 'requestVote' , voteRequest )
} catch ( err ) {
console . error ( ` Failed to request vote from ${ nodeId } : ` , err )
2025-09-08 14:26:09 -07:00
}
2025-09-11 16:23:32 -07:00
}
}
}
/ * *
* Handle vote request from another node
* /
private async handleVoteRequest ( msg : NetworkMessage ) : Promise < any > {
const { term , candidateId , lastLogIndex , lastLogTerm } = msg . data
// Update term if necessary
if ( term > this . currentTerm ) {
this . currentTerm = term
this . consensusState . term = term
this . consensusState . votedFor = null
this . becomeFollower ( )
}
// Grant vote if conditions are met
let voteGranted = false
if ( term >= this . currentTerm &&
( ! this . consensusState . votedFor || this . consensusState . votedFor === candidateId ) &&
this . isLogUpToDate ( lastLogIndex , lastLogTerm ) ) {
this . consensusState . votedFor = candidateId
voteGranted = true
this . resetElectionTimeout ( )
}
return {
type : 'voteResponse' ,
term : this.currentTerm ,
voteGranted
}
}
/ * *
* Handle vote response
* /
private handleVoteResponse ( msg : NetworkMessage ) : void {
const { term , voteGranted } = msg . data
// Ignore old responses
if ( term < this . currentTerm ) return
// Update term if necessary
if ( term > this . currentTerm ) {
this . currentTerm = term
this . consensusState . term = term
this . becomeFollower ( )
return
}
// Count vote if granted
if ( voteGranted && this . consensusState . state === 'candidate' ) {
this . votesReceived . add ( msg . from )
this . checkVoteMajority ( )
}
}
/ * *
* Check if we have majority votes
* /
private checkVoteMajority ( ) : void {
const majority = Math . floor ( this . nodes . size / 2 ) + 1
if ( this . votesReceived . size >= majority ) {
this . becomeLeader ( )
}
}
/ * *
* Handle append entries ( heartbeat ) from leader
* /
private async handleAppendEntries ( msg : NetworkMessage ) : Promise < any > {
const { term , leaderId , prevLogIndex , prevLogTerm , entries , leaderCommit } = msg . data
// Update term if necessary
if ( term > this . currentTerm ) {
this . currentTerm = term
this . consensusState . term = term
this . consensusState . votedFor = null
this . becomeFollower ( )
}
// Reset election timeout when receiving valid heartbeat
if ( term >= this . currentTerm ) {
this . consensusState . leader = leaderId
this . resetElectionTimeout ( )
// Update leader node's last heartbeat
const leaderNode = this . nodes . get ( leaderId )
if ( leaderNode ) {
leaderNode . lastHeartbeat = Date . now ( )
leaderNode . lastSeen = Date . now ( )
}
}
// Check log consistency
let success = false
if ( term >= this . currentTerm ) {
if ( this . checkLogConsistency ( prevLogIndex , prevLogTerm ) ) {
// Append new entries if any
if ( entries && entries . length > 0 ) {
this . appendLogEntries ( prevLogIndex , entries )
2025-09-08 14:26:09 -07:00
}
2025-09-11 16:23:32 -07:00
success = true
2025-09-08 14:26:09 -07:00
}
}
2025-09-11 16:23:32 -07:00
return {
type : 'appendResponse' ,
term : this.currentTerm ,
success
}
}
/ * *
* Handle append response from follower
* /
private handleAppendResponse ( msg : NetworkMessage ) : void {
const { term , success } = msg . data
// Update term if necessary
if ( term > this . currentTerm ) {
this . currentTerm = term
this . consensusState . term = term
this . becomeFollower ( )
}
// Process successful append
if ( success && this . consensusState . state === 'leader' ) {
// Update follower's match index
// In a real implementation, this would track replication progress
}
2025-09-08 14:26:09 -07:00
}
/ * *
2025-09-11 16:23:32 -07:00
* Send heartbeat to followers
2025-09-08 14:26:09 -07:00
* /
2025-09-11 16:23:32 -07:00
private async sendHeartbeat ( ) : Promise < void > {
if ( ! this . networkTransport ) {
// Fallback for testing
await new Promise ( resolve = > setTimeout ( resolve , 10 ) )
return
}
const heartbeat = {
type : 'appendEntries' as const ,
term : this.currentTerm ,
leaderId : this.nodeId ,
prevLogIndex : this.getLastLogIndex ( ) ,
prevLogTerm : this.getLastLogTerm ( ) ,
entries : [ ] ,
leaderCommit : 0
}
// Send heartbeat to all followers
for ( const [ nodeId ] of this . nodes ) {
if ( nodeId !== this . nodeId ) {
try {
await this . networkTransport . sendToNode ( nodeId , 'appendEntries' , heartbeat )
} catch ( err ) {
// Node might be down, mark as suspected
const node = this . nodes . get ( nodeId )
if ( node ) {
node . status = 'suspected'
}
}
}
}
2025-09-08 14:26:09 -07:00
}
/ * *
2025-09-11 16:23:32 -07:00
* Start heartbeat timer as leader
2025-09-08 14:26:09 -07:00
* /
private startHeartbeat ( ) : void {
if ( this . heartbeatTimer ) {
clearInterval ( this . heartbeatTimer )
}
this . heartbeatTimer = setInterval ( ( ) = > {
this . sendHeartbeat ( )
} , this . heartbeatInterval )
// Send immediate heartbeat
this . sendHeartbeat ( )
}
/ * *
* Reset election timeout
* /
private resetElectionTimeout ( ) : void {
if ( this . electionTimer ) {
clearTimeout ( this . electionTimer )
}
// Randomize timeout to prevent split votes
const timeout = this . electionTimeout + Math . random ( ) * this . electionTimeout
this . electionTimer = setTimeout ( ( ) = > {
if ( this . consensusState . state === 'follower' ) {
this . becomeCandidate ( )
}
} , timeout )
}
/ * *
2025-09-11 16:23:32 -07:00
* Check if log is up to date
2025-09-08 14:26:09 -07:00
* /
2025-09-11 16:23:32 -07:00
private isLogUpToDate ( lastLogIndex : number , lastLogTerm : number ) : boolean {
const myLastLogTerm = this . getLastLogTerm ( )
const myLastLogIndex = this . getLastLogIndex ( )
if ( lastLogTerm > myLastLogTerm ) return true
if ( lastLogTerm < myLastLogTerm ) return false
return lastLogIndex >= myLastLogIndex
}
/ * *
* Check log consistency
* /
private checkLogConsistency ( prevLogIndex : number , prevLogTerm : number ) : boolean {
if ( prevLogIndex === - 1 ) return true
if ( prevLogIndex >= this . logEntries . length ) return false
const entry = this . logEntries [ prevLogIndex ]
return entry && entry . term === prevLogTerm
}
/ * *
* Append log entries
* /
private appendLogEntries ( prevLogIndex : number , entries : any [ ] ) : void {
// Remove conflicting entries
this . logEntries = this . logEntries . slice ( 0 , prevLogIndex + 1 )
// Append new entries
for ( const entry of entries ) {
this . logEntries . push ( {
term : entry.term ,
index : this.logEntries.length ,
data : entry.data
} )
}
this . lastLogIndex = this . logEntries . length - 1
if ( this . lastLogIndex >= 0 ) {
this . lastLogTerm = this . logEntries [ this . lastLogIndex ] . term
2025-09-08 14:26:09 -07:00
}
}
2025-09-11 16:23:32 -07:00
/ * *
* Get last log index
* /
private getLastLogIndex ( ) : number {
return this . lastLogIndex
}
/ * *
* Get last log term
* /
private getLastLogTerm ( ) : number {
return this . lastLogTerm
}
2025-09-08 14:26:09 -07:00
/ * *
* Generate a unique node ID
* /
private generateNodeId ( seed? : string ) : string {
const source = seed || ` ${ process . pid } - ${ Date . now ( ) } - ${ Math . random ( ) } `
return createHash ( 'sha256' ) . update ( source ) . digest ( 'hex' ) . substring ( 0 , 16 )
}
/ * *
* Get current leader
* /
getLeader ( ) : string | null {
return this . consensusState . leader
}
2025-09-11 16:23:32 -07:00
/ * *
* Propose a shard migration to the cluster
* /
async proposeMigration ( migration : {
shardId : string
fromNode : string
toNode : string
migrationId : string
} ) : Promise < void > {
if ( ! this . isLeader ( ) ) {
throw new Error ( 'Only leader can propose migrations' )
}
// Broadcast migration proposal to all nodes
const message = {
type : 'migration-proposal' ,
migration ,
timestamp : Date.now ( )
}
await this . transport . broadcast ( 'migration' , message )
// Store migration as pending
this . pendingMigrations . set ( migration . migrationId , {
. . . migration ,
status : 'pending'
} )
}
/ * *
* Get migration status
* /
async getMigrationStatus ( migrationId : string ) : Promise < 'pending' | 'committed' | 'rejected' > {
const migration = this . pendingMigrations . get ( migrationId )
if ( ! migration ) {
// Check if it was committed
return this . committedMigrations . has ( migrationId ) ? 'committed' : 'rejected'
}
return migration . status || 'pending'
}
2025-09-08 14:26:09 -07:00
/ * *
* Check if this node is the leader
* /
isLeader ( ) : boolean {
return this . consensusState . state === 'leader'
}
/ * *
* Get all nodes in the cluster
* /
getNodes ( ) : NodeInfo [ ] {
return Array . from ( this . nodes . values ( ) )
}
/ * *
* Get cluster health status
* /
getHealth ( ) : { healthy : boolean ; leader : string | null ; nodes : number ; activeNodes : number } {
const now = Date . now ( )
const activeNodes = Array . from ( this . nodes . values ( ) ) . filter (
2025-09-11 16:23:32 -07:00
node = > now - node . lastSeen < this . electionTimeout
2025-09-08 14:26:09 -07:00
) . length
return {
healthy : this.consensusState.leader !== null && activeNodes > this . nodes . size / 2 ,
leader : this.consensusState.leader ,
nodes : this.nodes.size ,
activeNodes
}
}
2025-09-11 16:23:32 -07:00
/ * *
* Propose a command to the cluster
* /
async proposeCommand ( command : any ) : Promise < void > {
if ( this . consensusState . state !== 'leader' ) {
throw new Error ( ` Not the leader. Current leader: ${ this . consensusState . leader } ` )
}
// Add to log
const entry = {
term : this.currentTerm ,
index : this.logEntries.length ,
data : command
}
this . logEntries . push ( entry )
this . lastLogIndex = entry . index
this . lastLogTerm = entry . term
// Replicate to followers
await this . sendHeartbeat ( )
this . emit ( 'commandProposed' , command )
}
/ * *
* Get current state
* /
getState ( ) : ConsensusState {
return { . . . this . consensusState }
}
2025-09-08 14:26:09 -07:00
}
/ * *
* Create a coordinator instance
* /
export function createCoordinator ( config? : CoordinatorConfig ) : DistributedCoordinator {
return new DistributedCoordinator ( config )
}