UNPKG

peerpigeon

Version:

WebRTC-based peer-to-peer mesh networking library with intelligent routing and signaling server

390 lines (324 loc) 10.6 kB
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) }; } }