Skip to content

predatorray/dandelion-mesh

Repository files navigation

Description of image Dandelion Mesh

License Build Status codecov NPM Version Doc

Serverless mesh network for browsers using WebRTC.

Connect, Broadcast, and Sync State without a central server.

Overview

dandelion-mesh is a fault-tolerant P2P service mesh library for browser applications.

It combines:

  • WebRTC data channels for transport,
  • RSA hybrid encryption for private messaging,
  • and the Raft consensus algorithm for leader election and ordered log replication

All without requiring a dedicated server.

Usage

Install

npm install dandelion-mesh

Basic example

import { PeerJSTransport, DandelionMesh } from 'dandelion-mesh';

// Create a transport and mesh instance
const transport = new PeerJSTransport({ peerId: 'alice' });
const mesh = new DandelionMesh(transport, {
  bootstrapPeers: ['bob', 'charlie'],
});

// Listen for events
mesh.on('ready', (id) => {
  console.log('My peer ID:', id);
});

mesh.on('message', (msg) => {
  if (msg.type === 'public') {
    console.log(`[${msg.sender}]: ${JSON.stringify(msg.data)}`);
  }
  if (msg.type === 'private') {
    console.log(`[private from ${msg.sender}]: ${JSON.stringify(msg.data)}`);
  }
});

mesh.on('leaderChanged', (leaderId) => {
  console.log('Current leader:', leaderId);
});

mesh.on('peersChanged', (peers) => {
  console.log('Connected peers:', peers);
});

// Send messages
await mesh.sendPublic({ action: 'bet', amount: 100 });
await mesh.sendPrivate('bob', { cards: ['Ah', 'Kd'] });

Using localStorage for durable sessions

import {
  PeerJSTransport,
  DandelionMesh,
  LocalStorageRaftLog,
} from 'dandelion-mesh';

const transport = new PeerJSTransport({ peerId: 'alice' });
const mesh = new DandelionMesh(transport, {
  bootstrapPeers: ['bob', 'charlie'],
  raftLog: new LocalStorageRaftLog('my-game-room'),
});

// Raft state (term, votedFor, log) persists across page refreshes,
// allowing a peer to rejoin and catch up from where it left off.

Custom transport

import { Transport, DandelionMesh } from 'dandelion-mesh';

class MyWebSocketTransport implements Transport {
  // Implement the Transport interface with your own
  // connection management and message passing logic.
  // ...
}

const transport = new MyWebSocketTransport();
const mesh = new DandelionMesh(transport);

High-level architecture

graph TB
    subgraph "dandelion-mesh"
        direction TB
        API["DandelionMesh API<br/><i>sendPublic() · sendPrivate() · on('message')</i>"]

        subgraph layers [" "]
            direction LR
            RAFT["Raft Consensus<br/><i>Leader Election<br/>Log Replication</i>"]
            CRYPTO["Crypto Service<br/><i>RSA-OAEP + AES-GCM<br/>Hybrid Encryption</i>"]
        end

        TRANSPORT["Transport Layer<br/><i>PeerJS (default) · pluggable</i>"]
    end

    API --> RAFT
    API --> CRYPTO
    CRYPTO --> RAFT
    RAFT --> TRANSPORT
    TRANSPORT <-->|WebRTC<br/>Data Channels| TRANSPORT
Loading

Low-level architecture

Message flow

sequenceDiagram
    participant App as Application
    participant Mesh as DandelionMesh
    participant Raft as Raft Leader
    participant Peers as Other Peers

    Note over App,Peers: Public message
    App->>Mesh: sendPublic(data)
    Mesh->>Raft: propose(PublicMessageEntry)
    Raft->>Peers: AppendEntries (log replication)
    Peers-->>Raft: success (majority)
    Raft->>Mesh: committed
    Mesh->>App: on('message', PublicMessage)
    Raft->>Peers: leaderCommit updated
    Note over Peers: Each peer applies & emits 'message'

    Note over App,Peers: Private message
    App->>Mesh: sendPrivate(recipientId, data)
    Mesh->>Mesh: encrypt with recipient's RSA public key
    Mesh->>Raft: propose(EncryptedPrivateMessage)
    Raft->>Peers: AppendEntries (encrypted payload in log)
    Peers-->>Raft: success (majority)
    Note over Peers: Only recipient can decrypt
Loading

Leader election

stateDiagram-v2
    [*] --> Follower
    Follower --> Candidate: election timeout<br/>(no heartbeat received)
    Candidate --> Leader: received votes<br/>from majority
    Candidate --> Candidate: election timeout<br/>(split vote)
    Candidate --> Follower: discovered higher term<br/>or current leader
    Leader --> Follower: discovered higher term
    Leader --> Leader: sends heartbeats<br/>to prevent elections
Loading

Key Design Decisions

  • Transport abstraction — The Transport interface decouples the mesh from PeerJS. Any P2P transport (WebSocket, libp2p, etc.) can be plugged in by implementing the interface.

  • Raft consensus — Full implementation per the Raft paper:

    • Leader election with randomized timeouts (2000–4000ms default)
    • Log replication with AppendEntries consistency checks
    • Commitment only for current-term entries (Figure 8 safety)
    • Dynamic membership updates as peers join/leave
  • Three log backendsInMemoryRaftLog for ephemeral sessions, LocalStorageRaftLog for peers that need to survive page refreshes and rejoin, and SessionStorageRaftLog for per-tab durability that clears when the tab is closed.

  • Hybrid encryption — Private messages use RSA-OAEP to wrap a random AES-256-GCM key. Public keys are exchanged as Raft log entries, so every peer receives them through the same ordered replication path. All peers see the encrypted log entry, but only the intended recipient can decrypt it.

  • Ordered delivery via Raft — All messages (public and encrypted private) go through Raft as log entries. Non-leader peers forward proposals to the leader. Once committed, public messages are delivered to all; encrypted messages are decrypted only by the intended recipient. This guarantees total ordering of all events across the cluster.

Support & Bug Report

If you find any bugs or have suggestions, please feel free to open an issue.

License

This project is licensed under the MIT License.

About

Serverless mesh network for browsers using WebRTC. Connect, broadcast, and sync state without a central server.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors