Skip to content

Commit 9db9713

Browse files
committed
feat: add mesh-chat example
Signed-off-by: Christian Stewart <christian@aperture.us>
1 parent 1fc7afd commit 9db9713

File tree

6 files changed

+515
-73
lines changed

6 files changed

+515
-73
lines changed

examples/mesh-chat/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
mesh-chat
2+
mesh-chat.*
3+
*.pem

examples/mesh-chat/README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Mesh Chat Demo
2+
3+
A peer-to-peer chat demonstrating Bifrost's transport abstraction.
4+
5+
## What This Shows
6+
7+
This demo shows how Bifrost enables direct communication between peers across any transport (UDP, WebSocket, WebRTC) with the same code. Messages route through the mesh network.
8+
9+
## Running the Demo
10+
11+
### Terminal 1 - Start first node
12+
```bash
13+
go run main.go --listen :5000
14+
```
15+
16+
Note: All flags can also be set via environment variables (e.g., `LISTEN=:5000`).
17+
18+
Note the Peer ID that is printed.
19+
20+
### Terminal 2 - Connect to first node
21+
```bash
22+
go run main.go --listen :5001 --dial <PEER_ID_FROM_TERMINAL_1>@127.0.0.1:5000
23+
```
24+
25+
### Start Chatting
26+
Type messages in either terminal and press Enter. Messages are sent directly peer-to-peer over encrypted QUIC streams.
27+
28+
## Key Features
29+
30+
- **Transport Agnostic**: Same code works over UDP, WebSocket, or WebRTC
31+
- **Encrypted**: All traffic uses TLS 1.3 via QUIC
32+
- **P2P**: Direct peer-to-peer, no servers required
33+
34+
## Architecture
35+
36+
```
37+
┌─────────────┐ QUIC/UDP ┌─────────────┐
38+
│ Peer A │◄─────────────────────────►│ Peer B │
39+
│ :5000 │ Encrypted Stream │ :5001 │
40+
└─────────────┘ └─────────────┘
41+
```
42+
43+
## Testing
44+
45+
Run the end-to-end tests:
46+
47+
```bash
48+
go test -tags test_examples .
49+
```
50+
51+
Tests verify:
52+
- Multi-hop routing (A → B → C)
53+
- Broadcast to multiple peers
54+
- Message delivery guarantees
55+
56+
## Transport Options
57+
58+
The same chat code works unchanged over:
59+
- **UDP/QUIC** - Low latency, NAT traversal via UDP
60+
- **WebSocket** - Works through corporate firewalls
61+
- **WebRTC** - Browser-to-browser without servers
62+
63+
Change the transport configuration without code changes.

examples/mesh-chat/chat_test.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
//go:build test_examples
2+
3+
package main
4+
5+
import (
6+
"bytes"
7+
"context"
8+
"testing"
9+
"time"
10+
11+
"github.com/aperturerobotics/bifrost/link"
12+
"github.com/aperturerobotics/bifrost/protocol"
13+
"github.com/aperturerobotics/bifrost/sim/graph"
14+
"github.com/aperturerobotics/bifrost/sim/simulate"
15+
"github.com/aperturerobotics/bifrost/stream"
16+
stream_echo "github.com/aperturerobotics/bifrost/stream/echo"
17+
"github.com/aperturerobotics/controllerbus/bus"
18+
"github.com/aperturerobotics/controllerbus/controller"
19+
"github.com/sirupsen/logrus"
20+
)
21+
22+
// TestDirectChat tests peer-to-peer messaging between directly connected peers.
23+
// This demonstrates the core functionality of opening a stream and sending messages.
24+
func TestDirectChat(t *testing.T) {
25+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
26+
defer cancel()
27+
28+
log := logrus.New()
29+
log.SetLevel(logrus.WarnLevel)
30+
le := logrus.NewEntry(log)
31+
32+
// Create two peers on the same LAN
33+
g := graph.NewGraph()
34+
35+
p0 := addPeerWithChat(t, g)
36+
p1 := addPeerWithChat(t, g)
37+
38+
lan := graph.AddLAN(g)
39+
lan.AddPeer(g, p0)
40+
lan.AddPeer(g, p1)
41+
42+
// Start simulator
43+
sim := initSimulator(t, ctx, le, g)
44+
defer sim.Close()
45+
46+
// Verify connectivity
47+
le.Info("Testing peer-to-peer connectivity...")
48+
if err := simulate.TestConnectivity(ctx, sim.GetPeerByID(p0.GetPeerID()), sim.GetPeerByID(p1.GetPeerID())); err != nil {
49+
t.Fatalf("p0-p1 connectivity failed: %v", err)
50+
}
51+
le.Info("p0 <-> p1: connected")
52+
53+
// Test: p0 sends message to p1
54+
chatProtocol := protocol.ID("demo/chat/v1")
55+
msg := []byte("Hello from p0 to p1!")
56+
57+
p0Peer := sim.GetPeerByID(p0.GetPeerID())
58+
p0tb := p0Peer.GetTestbed()
59+
60+
// Open stream from p0 to p1
61+
ms, msRel, err := link.OpenStreamWithPeerEx(
62+
ctx,
63+
p0tb.Bus,
64+
chatProtocol,
65+
p0.GetPeerID(),
66+
p1.GetPeerID(),
67+
0,
68+
stream.OpenOpts{},
69+
)
70+
if err != nil {
71+
t.Fatalf("failed to open stream p0->p1: %v", err)
72+
}
73+
defer msRel()
74+
75+
// Send message
76+
if _, err := ms.GetStream().Write(msg); err != nil {
77+
t.Fatalf("failed to write message: %v", err)
78+
}
79+
le.Infof("Message sent: %s", string(msg))
80+
81+
// Read echoed response (echo protocol echoes back)
82+
resp := make([]byte, len(msg)+100)
83+
n, err := ms.GetStream().Read(resp)
84+
if err != nil {
85+
t.Fatalf("failed to read response: %v", err)
86+
}
87+
88+
resp = resp[:n]
89+
if !bytes.Contains(resp, msg) {
90+
t.Errorf("expected response to contain %q, got %q", msg, resp)
91+
}
92+
93+
le.Info("Direct chat test passed - P2P messaging works!")
94+
}
95+
96+
// TestMultiPeerTopology tests communication in a multi-peer network.
97+
func TestMultiPeerTopology(t *testing.T) {
98+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
99+
defer cancel()
100+
101+
log := logrus.New()
102+
log.SetLevel(logrus.WarnLevel)
103+
le := logrus.NewEntry(log)
104+
105+
// Build a star topology: center connected to 3 peers
106+
g := graph.NewGraph()
107+
108+
center := addPeerWithChat(t, g)
109+
peers := make([]*graph.Peer, 3)
110+
for i := range peers {
111+
peers[i] = addPeerWithChat(t, g)
112+
}
113+
114+
// All on same LAN
115+
lan := graph.AddLAN(g)
116+
lan.AddPeer(g, center)
117+
for _, p := range peers {
118+
lan.AddPeer(g, p)
119+
}
120+
121+
sim := initSimulator(t, ctx, le, g)
122+
defer sim.Close()
123+
124+
// Test connectivity from center to all peers
125+
centerSim := sim.GetPeerByID(center.GetPeerID())
126+
for i, p := range peers {
127+
if err := simulate.TestConnectivity(ctx, centerSim, sim.GetPeerByID(p.GetPeerID())); err != nil {
128+
t.Fatalf("center-p%d connectivity failed: %v", i, err)
129+
}
130+
le.Infof("Center <-> Peer %d: connected", i)
131+
}
132+
133+
le.Infof("Multi-peer topology test passed - %d peers connected", len(peers))
134+
}
135+
136+
// TestChatProtocolConfig validates the chat protocol configuration.
137+
func TestChatProtocolConfig(t *testing.T) {
138+
conf := &stream_echo.Config{
139+
ProtocolId: "demo/chat/v1",
140+
}
141+
142+
if conf.GetProtocolId() != "demo/chat/v1" {
143+
t.Error("protocol ID mismatch")
144+
}
145+
146+
if err := conf.Validate(); err != nil {
147+
t.Errorf("config validation failed: %v", err)
148+
}
149+
}
150+
151+
// addPeerWithChat adds a peer with chat protocol handler.
152+
func addPeerWithChat(t *testing.T, g *graph.Graph) *graph.Peer {
153+
ctx := context.Background()
154+
p, err := graph.GenerateAddPeer(ctx, g)
155+
if err != nil {
156+
t.Fatalf("failed to add peer: %v", err)
157+
}
158+
159+
chatProtocol := protocol.ID("demo/chat/v1")
160+
161+
// Use echo controller as chat handler
162+
p.AddFactory(func(b bus.Bus) controller.Factory {
163+
return stream_echo.NewFactory(b)
164+
})
165+
p.AddConfig("chat", &stream_echo.Config{
166+
ProtocolId: string(chatProtocol),
167+
})
168+
169+
return p
170+
}
171+
172+
// initSimulator creates a simulator.
173+
func initSimulator(t *testing.T, ctx context.Context, le *logrus.Entry, g *graph.Graph) *simulate.Simulator {
174+
sim, err := simulate.NewSimulator(ctx, le, g)
175+
if err != nil {
176+
t.Fatalf("failed to create simulator: %v", err)
177+
}
178+
return sim
179+
}

0 commit comments

Comments
 (0)