peerpigeon
Version:
WebRTC-based peer-to-peer mesh networking library with intelligent routing and signaling server
261 lines (218 loc) • 10.2 kB
JavaScript
import { EventEmitter } from './EventEmitter.js';
import DebugLogger from './DebugLogger.js';
/**
* Simple eviction manager for mesh topology optimization
* Evicts farthest peers when at capacity to maintain optimal XOR distance topology
*/
export class EvictionManager extends EventEmitter {
constructor(mesh, connectionManager) {
super();
this.debug = DebugLogger.create('EvictionManager');
this.mesh = mesh;
this.connectionManager = connectionManager;
}
shouldEvictForPeer(newPeerId) {
// Only evict for mesh topology optimization
if (!this.mesh.evictionStrategy) {
this.debug.log('Eviction disabled - evictionStrategy is false');
return null;
}
// Check total peer count (including connecting peers) to prevent race conditions
const totalPeerCount = this.connectionManager.peers.size;
const connectedCount = this.connectionManager.getConnectedPeerCount();
// STRICT RULE: Never evict if we're under max capacity, unless we're completely isolated
if (totalPeerCount < this.mesh.maxPeers) {
this.debug.log(`No eviction needed: ${totalPeerCount}/${this.mesh.maxPeers} peers (connected: ${connectedCount})`);
return null;
}
// Special case: if we have 0 connected peers (isolated), be aggressive about making room
if (connectedCount === 0) {
this.debug.log(`Isolated peer scenario: finding any peer to evict for ${newPeerId.substring(0, 8)}...`);
// Find any peer to evict (even connecting ones) to make room for this connection
const anyPeerId = Array.from(this.connectionManager.peers.keys())[0];
if (anyPeerId) {
this.debug.log(`Will evict any peer ${anyPeerId.substring(0, 8)}... to escape isolation for ${newPeerId.substring(0, 8)}...`);
return anyPeerId;
}
}
// If XOR routing is enabled, use distance-based eviction
if (this.mesh.xorRouting) {
const newPeerDistance = this.calculateXorDistance(this.mesh.peerId, newPeerId);
const farthestPeerId = this.findFarthestPeer();
if (!farthestPeerId) {
this.debug.log('No eviction candidate found (no peers to evict)');
return null;
}
const farthestDistance = this.calculateXorDistance(this.mesh.peerId, farthestPeerId);
this.debug.log(`Eviction check: new peer ${newPeerId.substring(0, 8)}... (distance: ${newPeerDistance.toString(16).substring(0, 8)}) vs farthest ${farthestPeerId.substring(0, 8)}... (distance: ${farthestDistance.toString(16).substring(0, 8)})`);
// STRICT RULE: Only evict if we're at max capacity AND the new peer is closer
// Removed the aggressive eviction condition that could evict when under capacity
const shouldEvict = newPeerDistance < farthestDistance;
if (shouldEvict) {
this.debug.log(`Will evict ${farthestPeerId.substring(0, 8)}... for closer peer ${newPeerId.substring(0, 8)}... (${totalPeerCount}/${this.mesh.maxPeers} peers)`);
return farthestPeerId;
}
this.debug.log(`Not evicting for ${newPeerId.substring(0, 8)}... - new peer is not closer (${totalPeerCount}/${this.mesh.maxPeers} peers)`);
return null;
} else {
// If XOR routing is disabled, use simple FIFO eviction - evict oldest peer
this.debug.log(`XOR routing disabled - using FIFO eviction for ${newPeerId.substring(0, 8)}...`);
const oldestPeerId = this.findOldestPeer();
if (oldestPeerId) {
this.debug.log(`Will evict oldest peer ${oldestPeerId.substring(0, 8)}... for new peer ${newPeerId.substring(0, 8)}... (${totalPeerCount}/${this.mesh.maxPeers} peers)`);
return oldestPeerId;
}
this.debug.log('No eviction candidate found (no peers to evict)');
return null;
}
}
async evictPeer(peerId, reason = 'topology optimization') {
const peerConnection = this.connectionManager.getPeer(peerId);
if (!peerConnection) {
this.debug.log(`Cannot evict ${peerId.substring(0, 8)}... - peer not found`);
return;
}
this.debug.log(`Evicting ${peerId.substring(0, 8)}... (${reason})`);
// Send eviction notice
try {
peerConnection.sendMessage({
type: 'eviction',
reason,
from: this.mesh.peerId
});
} catch (error) {
this.debug.log('Failed to send eviction notice:', error.message);
}
// Close connection and clean up
peerConnection.close();
this.connectionManager.peers.delete(peerId);
this.mesh.peerDiscovery.clearConnectionAttempt(peerId);
this.mesh.emit('peerDisconnected', { peerId, reason: `evicted: ${reason}` });
this.connectionManager.emit('peersUpdated');
}
handleEvictionNotice(message, fromPeerId) {
this.debug.log(`Evicted by ${fromPeerId.substring(0, 8)}... (${message.reason})`);
// Clean up the connection
this.connectionManager.peers.delete(fromPeerId);
this.mesh.peerDiscovery.clearConnectionAttempt(fromPeerId);
this.mesh.emit('peerEvicted', { fromPeerId, reason: message.reason });
this.connectionManager.emit('peersUpdated');
// The evicted peer will naturally discover other peers and connect to those it's closer to
// No complex reconnection logic needed - let peer discovery handle it
}
calculateXorDistance(peerId1, peerId2) {
let distance = 0n;
for (let i = 0; i < Math.min(peerId1.length, peerId2.length); i += 2) {
const byte1 = parseInt(peerId1.substr(i, 2), 16);
const byte2 = parseInt(peerId2.substr(i, 2), 16);
const xor = byte1 ^ byte2;
distance = (distance << 8n) | BigInt(xor);
}
return distance;
}
findFarthestPeer() {
if (this.connectionManager.peers.size === 0) {
this.debug.log('No peers available for eviction');
return null;
}
let farthestPeer = null;
let maxDistance = 0n;
let candidateCount = 0;
this.connectionManager.peers.forEach((peerConnection, peerId) => {
const status = peerConnection.getStatus();
// LOOSENED: Accept more peer states as eviction candidates
if (status === 'connected' ||
status === 'channel-connecting' ||
status === 'connecting' ||
status === 'channel-open') {
candidateCount++;
const distance = this.calculateXorDistance(this.mesh.peerId, peerId);
if (distance > maxDistance) {
maxDistance = distance;
farthestPeer = peerId;
}
}
});
// FALLBACK: If no candidates found with strict criteria, try ANY peer
if (!farthestPeer && this.connectionManager.peers.size > 0) {
this.debug.log('No eviction candidates with connected/connecting status, trying any peer...');
this.connectionManager.peers.forEach((peerConnection, peerId) => {
candidateCount++;
const distance = this.calculateXorDistance(this.mesh.peerId, peerId);
if (distance > maxDistance) {
maxDistance = distance;
farthestPeer = peerId;
}
});
}
if (farthestPeer) {
this.debug.log(`Found farthest peer for eviction: ${farthestPeer.substring(0, 8)}... (distance: ${maxDistance.toString(16).substring(0, 8)}, ${candidateCount} candidate peers)`);
} else {
this.debug.log(`No peers available for eviction (${candidateCount} candidate peers)`);
}
return farthestPeer;
}
findOldestPeer() {
if (this.connectionManager.peers.size === 0) {
this.debug.log('No peers available for FIFO eviction');
return null;
}
let oldestPeer = null;
let oldestTime = Date.now();
let candidateCount = 0;
this.connectionManager.peers.forEach((peerConnection, peerId) => {
const status = peerConnection.getStatus();
// LOOSENED: Accept more peer states as eviction candidates
if (status === 'connected' ||
status === 'channel-connecting' ||
status === 'connecting' ||
status === 'channel-open') {
candidateCount++;
const connectionTime = peerConnection.connectionStartTime || Date.now();
if (connectionTime < oldestTime) {
oldestTime = connectionTime;
oldestPeer = peerId;
}
}
});
// FALLBACK: If no candidates found with strict criteria, try ANY peer
if (!oldestPeer && this.connectionManager.peers.size > 0) {
this.debug.log('No FIFO eviction candidates with connected/connecting status, trying any peer...');
this.connectionManager.peers.forEach((peerConnection, peerId) => {
candidateCount++;
const connectionTime = peerConnection.connectionStartTime || Date.now();
if (connectionTime < oldestTime) {
oldestTime = connectionTime;
oldestPeer = peerId;
}
});
}
if (oldestPeer) {
this.debug.log(`Found oldest peer for eviction: ${oldestPeer.substring(0, 8)}... (connected at: ${new Date(oldestTime).toLocaleTimeString()}, ${candidateCount} candidates)`);
} else {
this.debug.log(`No peers available for FIFO eviction (${candidateCount} candidates)`);
}
return oldestPeer;
}
disconnectExcessPeers() {
if (this.connectionManager.peers.size <= this.mesh.maxPeers) return;
const peerEntries = Array.from(this.connectionManager.peers.entries())
.filter(([_, peerConnection]) => peerConnection.connectionStartTime)
.sort((a, b) => a[1].connectionStartTime - b[1].connectionStartTime);
const toDisconnect = peerEntries.slice(0, this.connectionManager.peers.size - this.mesh.maxPeers);
toDisconnect.forEach(([peerId, peerConnection]) => {
this.debug.log(`Disconnecting ${peerId.substring(0, 8)}... (over max peers limit)`);
peerConnection.close();
this.connectionManager.peers.delete(peerId);
this.mesh.peerDiscovery.clearConnectionAttempt(peerId);
this.mesh.emit('peerDisconnected', { peerId, reason: 'over max peers limit' });
});
this.connectionManager.emit('peersUpdated');
}
clearEvictionTracking(_peerId) {
// No eviction tracking in simplified system
}
cleanup() {
// Simple cleanup - no complex state to manage
}
}