From 7e63ffd3573e77dc59f0f9091a95955b31da2f4c Mon Sep 17 00:00:00 2001 From: "Claude Opus 4.7" Date: Thu, 21 May 2026 01:03:06 -0500 Subject: [PATCH] client: chirp offer every 5s while unpaired MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- client/main.go | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/client/main.go b/client/main.go index f13f621..0c6d930 100644 --- a/client/main.go +++ b/client/main.go @@ -21,6 +21,7 @@ import ( "os" "runtime" "strings" + "sync" "time" "github.com/atotto/clipboard" @@ -179,7 +180,10 @@ func handleMessage(ev string, m Message) { // ── WebRTC ──────────────────────────────────────────────────────────────── -var incomingSignal = make(chan Message, 16) +var ( + incomingSignal = make(chan Message, 16) + rtcConnected = make(chan struct{}) +) func runRTCPeer(server string) { api := webrtc.NewAPI() @@ -200,8 +204,10 @@ func runRTCPeer(server string) { return } + var once sync.Once dc.OnOpen(func() { fmt.Fprintln(os.Stderr, "rtc: DataChannel OPEN — P2P live") + once.Do(func() { close(rtcConnected) }) }) dc.OnMessage(func(msg webrtc.DataChannelMessage) { text := string(msg.Data) @@ -225,7 +231,7 @@ func runRTCPeer(server string) { 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) if err != nil { log.Printf("rtc: create offer: %v", err) @@ -237,7 +243,24 @@ func runRTCPeer(server string) { } payload, _ := json.Marshal(SignalPayload{Kind: "offer", SDP: &offer}) 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 for {