client: chirp offer every 5s while unpaired

Solves the late-subscriber problem — browsers that load the page after
the peer's startup offer would never see one. Now the peer re-broadcasts
the offer every 5 seconds until the DataChannel opens, then stops.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Claude Opus 4.7
2026-05-21 01:03:06 -05:00
parent ce020d641a
commit 7e63ffd357

View File

@@ -21,6 +21,7 @@ import (
"os" "os"
"runtime" "runtime"
"strings" "strings"
"sync"
"time" "time"
"github.com/atotto/clipboard" "github.com/atotto/clipboard"
@@ -179,7 +180,10 @@ func handleMessage(ev string, m Message) {
// ── WebRTC ──────────────────────────────────────────────────────────────── // ── WebRTC ────────────────────────────────────────────────────────────────
var incomingSignal = make(chan Message, 16) var (
incomingSignal = make(chan Message, 16)
rtcConnected = make(chan struct{})
)
func runRTCPeer(server string) { func runRTCPeer(server string) {
api := webrtc.NewAPI() api := webrtc.NewAPI()
@@ -200,8 +204,10 @@ func runRTCPeer(server string) {
return return
} }
var once sync.Once
dc.OnOpen(func() { dc.OnOpen(func() {
fmt.Fprintln(os.Stderr, "rtc: DataChannel OPEN — P2P live") fmt.Fprintln(os.Stderr, "rtc: DataChannel OPEN — P2P live")
once.Do(func() { close(rtcConnected) })
}) })
dc.OnMessage(func(msg webrtc.DataChannelMessage) { dc.OnMessage(func(msg webrtc.DataChannelMessage) {
text := string(msg.Data) text := string(msg.Data)
@@ -225,7 +231,7 @@ func runRTCPeer(server string) {
fmt.Fprintf(os.Stderr, "rtc: state=%s\n", s) fmt.Fprintf(os.Stderr, "rtc: state=%s\n", s)
}) })
// Create offer + post via signaling bus // Create offer + post once immediately
offer, err := pc.CreateOffer(nil) offer, err := pc.CreateOffer(nil)
if err != nil { if err != nil {
log.Printf("rtc: create offer: %v", err) log.Printf("rtc: create offer: %v", err)
@@ -237,7 +243,24 @@ func runRTCPeer(server string) {
} }
payload, _ := json.Marshal(SignalPayload{Kind: "offer", SDP: &offer}) payload, _ := json.Marshal(SignalPayload{Kind: "offer", SDP: &offer})
send(server, myLabel, "", "signal", payload) send(server, myLabel, "", "signal", payload)
fmt.Fprintln(os.Stderr, "rtc: offer posted, waiting for answer...") fmt.Fprintln(os.Stderr, "rtc: offer posted, will chirp every 5s until paired...")
// "Chirp": re-post the offer every 5s until DataChannel opens. Catches
// late-joining browsers without needing server-side history of signals.
go func() {
t := time.NewTicker(5 * time.Second)
defer t.Stop()
for {
select {
case <-rtcConnected:
fmt.Fprintln(os.Stderr, "rtc: paired — chirping stopped")
return
case <-t.C:
p, _ := json.Marshal(SignalPayload{Kind: "offer", SDP: &offer})
send(server, myLabel, "", "signal", p)
}
}
}()
// Process incoming signals // Process incoming signals
for { for {