feat: live mesh peer gossip

When a peer's hello is verified, send a gossip burst of currently
connected peers so the new peer can discover and connect to nodes it
hasn't met yet — without relying on the anchor for a full peer-join
notification for each one.

- mesh: add PendingConnect chan for gossip-discovered peer IDs
- peer: send gossip burst after hello; push unknown peers from incoming
  gossip onto PendingConnect (skip self + already-connected)
- anchor: drain PendingConnect per runOnce, initiate startOffer for
  unknown peers using the same lexicographic tiebreak as anchor join;
  drain goroutine is scoped to the runOnce lifetime to avoid using stale
  sessions after a reconnect

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-06-22 16:02:11 +02:00
parent d02e18e212
commit f1498697b6
3 changed files with 98 additions and 7 deletions

View File

@@ -100,6 +100,46 @@ func runOnce(ctx context.Context, anchorURL, netHash string, id *crypto.Identity
s := &sender{id: id, sendCh: sendCh}
// Drain gossip-discovered peers and initiate offers. Stopped when runOnce
// returns (via drainDone) so stale sessions from a dead connection are never reused.
drainDone := make(chan struct{})
defer close(drainDone)
go func() {
for {
select {
case pid, ok := <-m.PendingConnect:
if !ok {
return
}
mu.RLock()
_, already := sessions[pid]
mu.RUnlock()
if already {
continue
}
// Use the same lexicographic tiebreak as anchor join to avoid
// both sides trying to offer simultaneously.
if strings.Compare(string(id.PeerID()), string(pid)) <= 0 {
continue
}
go func(pid proto.PeerID) {
sess, err := startOffer(ctx, pid, id, m, s)
if err != nil {
log.Printf("anchor: gossip offer to %s: %v", pid.Short(), err)
return
}
mu.Lock()
sessions[pid] = sess
mu.Unlock()
}(pid)
case <-drainDone:
return
case <-ctx.Done():
return
}
}
}()
for {
var msg proto.AnchorMessage
if err := wsjson.Read(ctx, conn, &msg); err != nil {