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)
    };
  }
}