peerpigeon
Version:
WebRTC-based peer-to-peer mesh networking library with intelligent routing and signaling server
390 lines (324 loc) • 10.6 kB
JavaScript
import { EventEmitter } from './EventEmitter.js';
import DebugLogger from './DebugLogger.js';
/**
* SimpleWebDHT - Efficient Distributed Hash Table for millions of WebRTC peers
*
* Design principles:
* 1. SIMPLE: Minimal complexity, maximum reliability
* 2. EFFICIENT: Optimized for millions of peers with O(log N) operations
* 3. SCALABLE: Consistent hashing with automatic load balancing
* 4. RELIABLE: Gossip-based replication with eventual consistency
*
* Key features:
* - Consistent hashing for efficient key distribution
* - Minimal routing table (only closest peers)
* - Gossip-based replication (fire-and-forget)
* - Automatic peer discovery and failure handling
* - No complex Kademlia routing - just simple consistent hashing
*/
export class SimpleWebDHT extends EventEmitter {
constructor(mesh) {
super();
this.debug = DebugLogger.create('SimpleWebDHT');
this.mesh = mesh;
this.peerId = mesh.peerId;
// Simple local storage
this.storage = new Map();
// Minimal routing: just track closest peers for efficient forwarding
this.closestPeers = new Set();
// Simple replication factor
this.replicationFactor = 3;
// Hash ring position for this peer
this.hashPosition = this.hashPeerId(this.peerId);
this.debug.log(`SimpleWebDHT initialized for peer ${this.peerId.substring(0, 8)} at position ${this.hashPosition.toString(16).substring(0, 8)}`);
this.setupMessageHandling();
this.startMaintenance();
}
/**
* Simple hash function for consistent hashing
*/
async hash(data) {
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(typeof data === 'string' ? data : JSON.stringify(data));
const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer);
const hashArray = new Uint8Array(hashBuffer);
// Convert to number for position on hash ring
let hash = 0;
for (let i = 0; i < 4; i++) {
hash = (hash * 256 + hashArray[i]) >>> 0;
}
return hash;
}
/**
* Hash peer ID to position on ring
*/
hashPeerId(peerId) {
// Simple deterministic hash of peer ID
let hash = 0;
for (let i = 0; i < peerId.length; i++) {
hash = ((hash << 5) - hash + peerId.charCodeAt(i)) >>> 0;
}
return hash;
}
/**
* Calculate distance between two positions on hash ring
*/
ringDistance(pos1, pos2) {
const diff = Math.abs(pos1 - pos2);
const maxUint32 = 0xFFFFFFFF;
return Math.min(diff, maxUint32 - diff);
}
/**
* Find closest peers to a hash position
*/
findClosestPeers(targetHash, count = this.replicationFactor) {
const connectedPeers = Array.from(this.mesh.connectionManager.peers.keys())
.filter(peerId => this.mesh.connectionManager.peers.get(peerId).getStatus() === 'connected');
if (connectedPeers.length === 0) {
return [];
}
// Calculate distances and sort
const peersWithDistance = connectedPeers.map(peerId => ({
peerId,
distance: this.ringDistance(targetHash, this.hashPeerId(peerId))
}));
peersWithDistance.sort((a, b) => a.distance - b.distance);
return peersWithDistance.slice(0, count).map(p => p.peerId);
}
/**
* Store key-value pair
*/
async put(key, value) {
const keyHash = await this.hash(key);
const storeData = {
key,
value,
timestamp: Date.now(),
publisher: this.peerId
};
// Always store locally first
this.storage.set(key, storeData);
this.debug.log(`PUT: Stored ${key} locally`);
// Find closest peers for replication
const targetPeers = this.findClosestPeers(keyHash, this.replicationFactor);
// Simple gossip replication - fire and forget
const replicationPromises = targetPeers.map(async (peerId) => {
if (peerId !== this.peerId) {
try {
this.sendMessage(peerId, 'dht_store', storeData);
} catch (error) {
// Silent failure - gossip is best effort
this.debug.warn(`Replication to ${peerId.substring(0, 8)} failed:`, error.message);
}
}
});
// Don't wait for replication - fire and forget for speed
Promise.allSettled(replicationPromises);
this.debug.log(`PUT: ${key} replicated to ${targetPeers.length} peers`);
return true;
}
/**
* Retrieve value by key
*/
async get(key, options = {}) {
const forceRefresh = options.forceRefresh || false;
// Check local storage first (unless force refresh)
if (!forceRefresh && this.storage.has(key)) {
const data = this.storage.get(key);
this.debug.log(`GET: Found ${key} locally`);
return data.value;
}
// If not found locally or force refresh, query the network
const keyHash = await this.hash(key);
const targetPeers = this.findClosestPeers(keyHash, this.replicationFactor);
this.debug.log(`GET: Querying ${targetPeers.length} peers for ${key}`);
// Query peers in parallel
const queryPromises = targetPeers.map(async (peerId) => {
if (peerId === this.peerId) return null;
try {
return await this.queryPeer(peerId, key);
} catch (error) {
this.debug.warn(`Query to ${peerId.substring(0, 8)} failed:`, error.message);
return null;
}
});
const results = await Promise.allSettled(queryPromises);
// Find the first successful result
for (const result of results) {
if (result.status === 'fulfilled' && result.value) {
const data = result.value;
// Cache locally for future use
this.storage.set(key, data);
this.debug.log(`GET: Found ${key} from network`);
return data.value;
}
}
this.debug.log(`GET: ${key} not found`);
return null;
}
/**
* Query a specific peer for a key
*/
async queryPeer(peerId, key) {
return new Promise((resolve, reject) => {
const requestId = Math.random().toString(36).substring(7);
// Set timeout for query
const timeout = setTimeout(() => {
this.responseHandlers.delete(requestId);
reject(new Error('Query timeout'));
}, 5000);
// Store response handler
this.responseHandlers.set(requestId, (response) => {
clearTimeout(timeout);
this.responseHandlers.delete(requestId);
if (response.found) {
resolve(response.data);
} else {
resolve(null);
}
});
// Send query
this.sendMessage(peerId, 'dht_query', { key, requestId });
});
}
/**
* Send message to peer through connection manager
*/
sendMessage(peerId, type, data) {
const message = {
type: 'dht',
messageType: type,
data,
from: this.peerId,
timestamp: Date.now()
};
const peer = this.mesh.connectionManager.peers.get(peerId);
if (peer && peer.getStatus() === 'connected') {
peer.send(message);
} else {
throw new Error(`Peer ${peerId} not connected`);
}
}
/**
* Setup message handling
*/
setupMessageHandling() {
this.responseHandlers = new Map();
}
/**
* Handle incoming DHT message
*/
async handleMessage(message, fromPeerId) {
const { messageType, data } = message;
switch (messageType) {
case 'dht_store':
this.handleStore(data, fromPeerId);
break;
case 'dht_query':
await this.handleQuery(data, fromPeerId);
break;
case 'dht_query_response':
this.handleQueryResponse(data);
break;
default:
this.debug.warn(`Unknown DHT message type: ${messageType}`);
}
}
/**
* Handle store request from peer
*/
handleStore(data, fromPeerId) {
const { key, value, timestamp, publisher } = data;
// Simple conflict resolution: latest timestamp wins
if (this.storage.has(key)) {
const existing = this.storage.get(key);
if (existing.timestamp >= timestamp) {
return; // Ignore older data
}
}
// Store the data
this.storage.set(key, { key, value, timestamp, publisher });
this.debug.log(`STORE: Received ${key} from ${fromPeerId.substring(0, 8)}`);
}
/**
* Handle query request from peer
*/
async handleQuery(data, fromPeerId) {
const { key, requestId } = data;
const response = {
requestId,
found: false,
data: null
};
if (this.storage.has(key)) {
response.found = true;
response.data = this.storage.get(key);
}
// Send response
this.sendMessage(fromPeerId, 'dht_query_response', response);
}
/**
* Handle query response
*/
handleQueryResponse(data) {
const { requestId } = data;
const handler = this.responseHandlers.get(requestId);
if (handler) {
handler(data);
}
}
/**
* Update closest peers for efficient routing
*/
updateClosestPeers() {
const connectedPeers = Array.from(this.mesh.connectionManager.peers.keys())
.filter(peerId => this.mesh.connectionManager.peers.get(peerId).getStatus() === 'connected');
// Keep track of closest peers for efficient routing
const peersWithDistance = connectedPeers.map(peerId => ({
peerId,
distance: this.ringDistance(this.hashPosition, this.hashPeerId(peerId))
}));
peersWithDistance.sort((a, b) => a.distance - b.distance);
// Keep top 10 closest peers for efficient routing
this.closestPeers = new Set(
peersWithDistance.slice(0, 10).map(p => p.peerId)
);
}
/**
* Periodic maintenance
*/
startMaintenance() {
// Update routing table every 30 seconds
setInterval(() => {
this.updateClosestPeers();
}, 30000);
// Clean up old data every 5 minutes
setInterval(() => {
this.cleanupOldData();
}, 300000);
}
/**
* Clean up old data (optional TTL support)
*/
cleanupOldData() {
const now = Date.now();
const maxAge = 24 * 60 * 60 * 1000; // 24 hours
for (const [key, data] of this.storage) {
if (now - data.timestamp > maxAge) {
this.storage.delete(key);
this.debug.log(`Cleaned up old data: ${key}`);
}
}
}
/**
* Get DHT statistics
*/
getStats() {
return {
localKeys: this.storage.size,
connectedPeers: this.mesh.connectionManager.getConnectedPeerCount(),
closestPeers: this.closestPeers.size,
hashPosition: this.hashPosition.toString(16).substring(0, 8)
};
}
}