Tag each WG timeout event with the watcher's context and discard it at
dispatch time when that context is cancelled. This replaces the earlier
generation counter and the mailbox-level epoch filter: cancelling the
watcher is now the single act that both stops it and retires its pending
timeouts, so the two can no longer diverge, and the staleness check moves
to dispatch time so a cancel performed by an earlier event in the same
drained batch already suppresses a superseded watcher's timeout.
The rebase carried #6664's WGWatcher changes into our wg_watcher package
(single-shot, no enabled flag). Adapt conn.go to match: create a fresh
watcher per connection attempt in enableWgWatcherIfNeeded, drop it in
disableWgWatcherIfNeeded, nil-guard resetEndpoint.
Guard stale WG timeouts on the event loop instead of #6664's conn.mu
recheck: onWGDisconnected only checked watcherCtx on the watcher
goroutine, racing the loop that cancels it and processes the timeout. A
loop-owned wgWatcherGen tags evWGTimeout; handleWGTimeout drops events
from a superseded generation, so the check and the teardown happen
atomically on the single loop.
Relocate WorkerICE (renamed worker.ICE) and WorkerRelay plus ConnPriority
into client/internal/peer/worker. To break the peer<->worker cycle the
workers no longer take *Conn or ConnConfig: callbacks are passed as plain
functions (Conn's unexported methods as method values), and each worker
receives only the fields it needs (key, ICE config, isController) plus a
small services struct. Context is passed to OnNewOffer instead of stored.
Move the worker connection-status helper the other way, out of the worker
package into peer as worker_status.go (WorkerStatus / AtomicWorkerStatus),
since only Conn uses it.
Move the signaling protocol out of the peer package into
client/internal/peer/signaling: OfferAnswer, IceCredentials, Handshaker
and Signaler. Break the peer<->signaling cycle by giving the Handshaker
a plain Config and an ICEWorker interface instead of *Conn/*WorkerICE,
and by passing the relay manager directly rather than the relay worker.
Combine the ICE worker's local-credentials and session-id accessors into
a single Credentials() returning a Credentials struct.
Move the ICE session id to the ice package as ice.SessionID, since it
identifies an ICE agent session and is minted there alongside
GenerateICECredentials; signaling only carries it.
The conntype package held only ConnPriority and its constants and was
imported solely by peer. Move it into the peer package as priority.go
and drop the conntype. qualifier from conn.go, event.go and worker_ice.go.
Replace the mutex-guarded callback model of peer.Conn with a per-peer
event loop that exclusively owns all mutable connection state. External
callers and transport workers post typed events into a non-blocking,
coalescing mailbox instead of contending on conn.mu:
- offers/answers coalesce to the newest message, a new offer flushes
queued candidates of the superseded session
- candidates are applied in arrival order from a bounded FIFO
- transport state changes are never dropped
- the blocking relay dial runs on a helper goroutine with a single dial
in flight; signaling I/O (offer/answer sends) runs off the loop
conn.mu now only guards the open/close lifecycle. Close posts a close
event and waits for the loop teardown; the loop also tears down on
engine context cancellation and releases resources of unprocessed
events.
Delete the Handshaker listener machinery (Listen loop, unbuffered
drop-on-busy channels, AsyncOfferListener with its double-processing of
the first offer), the never-wired dispatcher package and the unused
ICEMonitor.ReconnectCh. Fix a goroutine leak in the WG watcher test
that raced with tests mutating the package-level check timing vars.