package socket

import (
	
	
	
	mrand 
	
	
	

	
	
	
	
	
	
)

// Relay actor timing constants, matching the Rust reference
// (iroh/src/socket/transports/relay/actor.rs).
const (
	// relayInactiveCleanupTime is how long a non-home relay connection may be
	// idle (no datagram sent) before it is closed. The home relay connection
	// never idles out. iroh/src/socket/transports/relay/actor.rs:67.
	relayInactiveCleanupTime = 60 * time.Second

	// pingInterval is how often the actor pings the relay to confirm the
	// connection is alive. It is stricter than the QUIC idle timeout so broken
	// relays are detected faster. iroh/src/socket/transports/relay/actor.rs:73.
	pingInterval = 15 * time.Second

	// sendDatagramBatchSize is the number of datagrams sent to the relay in one
	// batch. iroh/src/socket/transports/relay/actor.rs:80.
	sendDatagramBatchSize = 20

	// connectTimeout bounds establishing a relay connection (dial + handshake).
	// iroh/src/socket/transports/relay/actor.rs:86.
	connectTimeout = 10 * time.Second

	// undeliverableDatagramTimeout is how long datagrams queued while dialing
	// are held before being dropped. QUIC loss recovery retransmits.
	// iroh/src/socket/transports/relay/actor.rs:95.
	undeliverableDatagramTimeout = 3 * time.Second
)

// Backoff bounds for reconnection, matching the Rust ExponentialBuilder
// (iroh/src/socket/transports/relay/actor.rs:352).
const (
	backoffMinDelay = 10 * time.Millisecond
	backoffMaxDelay = 16 * time.Second
)

// Relay ping timeout bounds, matching iroh-relay's PingTracker.
const (
	relayPingTimeoutMin = 500 * time.Millisecond
	relayPingTimeoutMax = 5 * time.Second
)

// Channel depths, matching the Rust reference
// (iroh/src/socket/transports/relay.rs:46 and actor.rs:1254).
const (
	relayRecvQueueDepth   = 512
	relaySendChannelDepth = 256
	perRelaySendDepth     = 64
)

// RelayConnState is the connection state of a relay, published through the home
// relay watcher. It is the Go analog of the Rust RelayConnectionState
// (iroh/src/socket/transports/relay/actor.rs:897).
type RelayConnState int

const (
	// RelayConnecting means the actor is dialing or handshaking.
	RelayConnecting RelayConnState = iota
	// RelayConnected means the connection is established and handshaked.
	RelayConnected
	// RelayDisconnected means there is no connection: an attempt failed or a
	// previously-established connection was lost.
	RelayDisconnected
)

func ( RelayConnState) () string {
	switch  {
	case RelayConnecting:
		return "connecting"
	case RelayConnected:
		return "connected"
	case RelayDisconnected:
		return "disconnected"
	default:
		return "unknown"
	}
}

// RelayStatus is the connection status of a single home relay, observed through
// [RelayActor.HomeRelayStatus]. It is the Go analog of the Rust RelayStatus
// (iroh/src/endpoint.rs:1832).
//
// The zero value reports no home relay; use [RelayActor.HomeRelayStatus] to
// observe it.
type RelayStatus struct {
	// URL is the home relay URL.
	URL netaddr.RelayURL
	// State is the current connection state.
	State RelayConnState
	// LastError is the most recent connection error while disconnected, or nil.
	LastError error
}

// IsConnected reports whether the relay is connected.
func ( RelayStatus) () bool { return .State == RelayConnected }

// RelaySendItem is one or more datagrams to send to a remote endpoint via a
// relay. It is the Go analog of the Rust RelaySendItem
// (iroh/src/socket/transports/relay/actor.rs:846).
type RelaySendItem struct {
	// RemoteEndpoint is the destination endpoint.
	RemoteEndpoint key.EndpointID
	// URL is the relay through which to reach RemoteEndpoint.
	URL netaddr.RelayURL
	// Datagrams is the payload.
	Datagrams relayproto.Datagrams
}

// RelayRecvDatagram is a datagram received from a relay. It is the Go analog of
// the Rust RelayRecvDatagram (iroh/src/socket/transports/relay/actor.rs:1383).
type RelayRecvDatagram struct {
	// URL is the relay it arrived on.
	URL netaddr.RelayURL
	// Src is the endpoint that sent it.
	Src key.EndpointID
	// Datagrams is the payload.
	Datagrams relayproto.Datagrams
}

// relayDialer dials a relay client. It is an indirection point so tests can
// substitute an in-process relay without a real network dial. The default is
// [relayclient.Connect].
type relayDialer func(ctx context.Context, url netaddr.RelayURL, opts relayclient.Options) (relayClient, error)

// relayClient is the subset of [relayclient.Client] the actor uses. It exists so
// tests can supply a fake client implementing the same Send/Recv/Close surface.
type relayClient interface {
	Send(ctx context.Context, msg relayproto.ClientToRelayMsg) error
	Recv(ctx context.Context) (relayproto.RelayToClientMsg, error)
	Close() error
}

// defaultRelayDialer dials a real relay over WSS.
func defaultRelayDialer( context.Context,  netaddr.RelayURL,  relayclient.Options) (relayClient, error) {
	return relayclient.Connect(, , )
}

// RelayActorConfig configures a [RelayActor].
//
// SecretKey is required. The zero value is otherwise not usable; build a config
// and pass it to [NewRelayActor].
type RelayActorConfig struct {
	// SecretKey is the local endpoint's secret key, used to authenticate to
	// relays. Required.
	SecretKey key.SecretKey
	// Map is the relay map; consulted for per-relay auth tokens.
	Map *relay.Map
	// dialer overrides the relay dial function. nil uses [defaultRelayDialer].
	dialer relayDialer
}

// RelayActor manages connections to relay servers. It starts one [activeRelay]
// per relay URL on demand, routes outgoing datagrams to the right one, and
// surfaces received datagrams on a single queue. It tracks the home relay and
// publishes its status through a [watch.Value].
//
// It is the Go analog of the Rust RelayActor
// (iroh/src/socket/transports/relay/actor.rs:855). Create one with
// [NewRelayActor] and start it with [RelayActor.Run].
type RelayActor struct {
	cfg     RelayActorConfig
	dialer  relayDialer
	recvCh  chan RelayRecvDatagram
	sendCh  chan RelaySendItem
	homeURL *watch.Value[*RelayStatus]

	// metrics is the shared magic-socket counter set, or nil. It is set by the
	// owning MagicConn before the actor runs.
	metrics atomic.Pointer[Metrics]

	mu     sync.Mutex
	active map[string]*activeRelay // key: RelayURL.String()
	home   netaddr.RelayURL
	closed bool

	wg sync.WaitGroup
}

// setMetrics records the counter set frame handlers report into.
func ( *RelayActor) ( *Metrics) {
	if  != nil {
		.metrics.Store()
	}
}

// NewRelayActor returns a RelayActor ready to be started with [RelayActor.Run].
func ( RelayActorConfig) *RelayActor {
	 := .dialer
	if  == nil {
		 = defaultRelayDialer
	}
	if .Map == nil {
		.Map = relay.NewMap()
	} else {
		.Map = .Map.Clone()
	}
	return &RelayActor{
		cfg:     ,
		dialer:  ,
		recvCh:  make(chan RelayRecvDatagram, relayRecvQueueDepth),
		sendCh:  make(chan RelaySendItem, relaySendChannelDepth),
		homeURL: watch.NewValueFunc[*RelayStatus](nil, statusEqual),
		active:  make(map[string]*activeRelay),
	}
}

// Recv returns the queue of datagrams received from relays. A [RelayTransport]
// drains it; the channel is closed when the actor stops.
func ( *RelayActor) () <-chan RelayRecvDatagram { return .recvCh }

// HomeRelayStatus returns a watcher over the home relay's connection status. The
// value is nil until a home relay is set with [RelayActor.SetHomeRelay].
func ( *RelayActor) () watch.Observer[*RelayStatus] {
	return .homeURL.Watch()
}

// InsertRelay adds or replaces url's relay configuration, returning the
// previous config when one existed. If there is no home relay, url becomes home.
func ( *RelayActor) ( netaddr.RelayURL,  relay.Config) (relay.Config, bool) {
	.mu.Lock()
	defer .mu.Unlock()
	if .closed {
		return relay.Config{}, false
	}
	.URL = 
	,  := .cfg.Map.Insert()
	if .home.IsZero() {
		.home = 
		.homeURL.Set(&RelayStatus{URL: , State: RelayConnecting})
		.ensureActiveLocked(, true)
	}
	return , 
}

// RemoveRelay removes url's configuration, returning it when present. Any live
// non-home connection to url is stopped. If url was the home relay, the next
// configured relay (if any) becomes home.
func ( *RelayActor) ( netaddr.RelayURL) (relay.Config, bool) {
	.mu.Lock()
	defer .mu.Unlock()
	if .closed {
		return relay.Config{}, false
	}
	,  := .cfg.Map.Remove()
	if ! {
		return relay.Config{}, false
	}
	if  := .active[.String()];  != nil {
		.stop()
	}
	if !.home.Equal() {
		return , true
	}
	.home = netaddr.RelayURL{}
	.homeURL.Set(nil)
	if  := .cfg.Map.URLs(); len() > 0 {
		 := [0]
		.home = 
		.homeURL.Set(&RelayStatus{URL: , State: RelayConnecting})
		for ,  := range .active {
			.setHome( == .String())
		}
		.ensureActiveLocked(, true)
	}
	return , true
}

// Send queues item for delivery to its relay. It never blocks: if the queue is
// full the item is dropped (treated as datagram loss so QUIC's loss recovery
// retransmits), matching the Rust non-blocking send invariant
// (iroh/src/socket/transports.rs:1176). Send reports whether the item was
// queued; a false result is a dropped (lost) datagram, not an error.
func ( *RelayActor) ( RelaySendItem) bool {
	.mu.Lock()
	 := .closed
	.mu.Unlock()
	if  {
		return false
	}
	select {
	case .sendCh <- :
		return true
	default:
		return false
	}
}

// SetHomeRelay designates url as the home relay, ensuring an [activeRelay] for
// it (which then never idles out) and demoting any previous home relay. A zero
// url clears the home relay. It mirrors the Rust on_network_change /
// set_home_relay path (iroh/src/socket/transports/relay/actor.rs:1126).
func ( *RelayActor) ( netaddr.RelayURL) {
	.mu.Lock()
	defer .mu.Unlock()
	if .closed {
		return
	}
	if .IsZero() {
		.home = netaddr.RelayURL{}
		.homeURL.Set(nil)
		for ,  := range .active {
			.setHome(false)
		}
		return
	}
	if .home.Equal() {
		return
	}
	.home = 
	// Publish Connecting on the URL change; the active actor republishes its
	// real state when it becomes home.
	.homeURL.Set(&RelayStatus{URL: , State: RelayConnecting})
	for ,  := range .active {
		.setHome( == .String())
	}
	.ensureActiveLocked(, true)
}

// Run drives the actor until ctx is cancelled. It starts active relays on demand
// from queued send items. It blocks; run it in its own goroutine. When it
// returns it has closed the recv channel and stopped all active relays.
func ( *RelayActor) ( context.Context) {
	defer close(.recvCh)
	for {
		select {
		case <-.Done():
			.shutdown()
			return
		case  := <-.sendCh:
			.dispatch(, )
		}
	}
}

// dispatch routes a send item to the active relay for its URL, starting one if
// needed. If no actor exists for the item's URL but another active relay already
// knows a route to the endpoint, that relay is used, matching the Rust
// active_relay_handle_for_endpoint (actor.rs:1173).
func ( *RelayActor) ( context.Context,  RelaySendItem) {
	.mu.Lock()
	if .closed {
		.mu.Unlock()
		return
	}
	,  := .active[.URL.String()]
	if ! {
		if  := .routeForEndpointLocked(.RemoteEndpoint);  != nil {
			 = 
		} else {
			 = .ensureActiveLocked(.URL, .home.Equal(.URL))
		}
	}
	.mu.Unlock()
	.enqueue()
}

// routeForEndpointLocked returns an active relay already known to route to eid,
// or nil. a.mu must be held.
func ( *RelayActor) ( key.EndpointID) *activeRelay {
	for ,  := range .active {
		if .hasRoute() {
			return 
		}
	}
	return nil
}

// ensureActiveLocked returns the active relay for url, starting it if needed.
// a.mu must be held.
func ( *RelayActor) ( netaddr.RelayURL,  bool) *activeRelay {
	 := .String()
	if ,  := .active[];  {
		if  {
			.setHome(true)
		}
		return 
	}
	 := newActiveRelay(, , )
	.active[] = 
	.wg.Add(1)
	go func() {
		defer .wg.Done()
		.run()
		.mu.Lock()
		if .active[] ==  {
			delete(.active, )
		}
		.mu.Unlock()
	}()
	return 
}

// shutdown stops all active relays and waits for them to exit.
func ( *RelayActor) () {
	.mu.Lock()
	if .closed {
		.mu.Unlock()
		return
	}
	.closed = true
	 := make([]*activeRelay, 0, len(.active))
	for ,  := range .active {
		 = append(, )
	}
	.mu.Unlock()
	for ,  := range  {
		.stop()
	}
	.wg.Wait()
}

// publishStatus updates the home relay status if url is still the home relay.
// This guards against a demoted relay overwriting a newer home relay's status,
// matching the Rust HomeRelayWatch::set_status (actor.rs:985).
func ( *RelayActor) ( netaddr.RelayURL,  RelayConnState,  error) {
	.mu.Lock()
	 := .home.Equal()
	.mu.Unlock()
	if ! {
		return
	}
	.homeURL.Set(&RelayStatus{URL: , State: , LastError: })
}

// authTokenFor returns the configured auth token for url, if any.
func ( *RelayActor) ( netaddr.RelayURL) string {
	.mu.Lock()
	defer .mu.Unlock()
	if .cfg.Map == nil {
		return ""
	}
	if ,  := .cfg.Map.Get();  {
		return .AuthToken
	}
	return ""
}

// statusEqual compares two home relay statuses for the watcher's change
// suppression. Errors are compared by identity so a fresh error always notifies,
// matching the Rust Arc::ptr_eq comparison (actor.rs:931).
func statusEqual(,  *RelayStatus) bool {
	if  == nil ||  == nil {
		return  == 
	}
	return .URL.Equal(.URL) && .State == .State && .LastError == .LastError
}

// activeRelay manages the connection to a single relay server. It runs a state
// machine: dial (with exponential backoff) then connected (with ping/pong
// keepalive and idle close for non-home relays). It is the Go analog of the Rust
// ActiveRelayActor (iroh/src/socket/transports/relay/actor.rs:126).
type activeRelay struct {
	parent *RelayActor
	url    netaddr.RelayURL

	sendCh   chan RelaySendItem
	stopCh   chan struct{}
	stopOnce sync.Once

	mu      sync.Mutex
	isHome  bool
	routes  map[key.EndpointID]struct{}
	lastSrc key.EndpointID
	haveSrc bool
}

// newActiveRelay returns an active relay for url. home marks it the home relay
// (which never idles out).
func newActiveRelay( *RelayActor,  netaddr.RelayURL,  bool) *activeRelay {
	return &activeRelay{
		parent: ,
		url:    ,
		sendCh: make(chan RelaySendItem, perRelaySendDepth),
		stopCh: make(chan struct{}),
		isHome: ,
		routes: make(map[key.EndpointID]struct{}),
	}
}

// enqueue queues item for sending. It never blocks: a full queue drops the item
// (datagram loss; QUIC retransmits).
func ( *activeRelay) ( RelaySendItem) {
	select {
	case .sendCh <- :
	default:
	}
}

// stop signals the active relay to exit.
func ( *activeRelay) () {
	.stopOnce.Do(func() { close(.stopCh) })
}

// setHome marks (or unmarks) this as the home relay.
func ( *activeRelay) ( bool) {
	.mu.Lock()
	.isHome = 
	.mu.Unlock()
}

func ( *activeRelay) () bool {
	.mu.Lock()
	defer .mu.Unlock()
	return .isHome
}

// hasRoute reports whether eid has been seen on this relay.
func ( *activeRelay) ( key.EndpointID) bool {
	.mu.Lock()
	defer .mu.Unlock()
	,  := .routes[]
	return 
}

// noteRoute records that eid is reachable on this relay.
func ( *activeRelay) ( key.EndpointID) {
	.mu.Lock()
	defer .mu.Unlock()
	if .haveSrc && .lastSrc.Equal() {
		return
	}
	.lastSrc = 
	.haveSrc = true
	.routes[] = struct{}{}
}

// dropRoute removes eid (an EndpointGone frame).
func ( *activeRelay) ( key.EndpointID) {
	.mu.Lock()
	delete(.routes, )
	.mu.Unlock()
}

// run is the top-level state machine: it repeatedly dials and serves the
// connection, applying exponential backoff between failed attempts. Backoff is
// reset only after a connection becomes established (a pong was received),
// matching the Rust run loop (actor.rs:325).
func ( *activeRelay) () {
	 := backoffMinDelay
	for {
		select {
		case <-.stopCh:
			return
		default:
		}
		,  := .runOnce()
		if  == nil {
			// Clean shutdown (idle timeout or stop).
			return
		}
		.parent.publishStatus(.url, RelayDisconnected, )
		if  {
			// Reset backoff and reconnect immediately.
			 = backoffMinDelay
			continue
		}
		// Dial or pre-pong failure: back off.
		select {
		case <-.stopCh:
			return
		case <-time.After(jitter()):
		}
		 *= 2
		if  > backoffMaxDelay {
			 = backoffMaxDelay
		}
	}
}

// runOnce dials the relay and runs the connected loop. It returns whether the
// connection became established (a pong was received) and the error that ended
// it, or (false, nil) for a clean shutdown.
func ( *activeRelay) () ( bool,  error) {
	.parent.publishStatus(.url, RelayConnecting, nil)
	, ,  := .dial()
	if ! {
		// Stopped or idled out while dialing: clean shutdown.
		return false, nil
	}
	if  != nil {
		return false, 
	}
	defer .Close()
	.parent.publishStatus(.url, RelayConnected, nil)
	return .runConnected()
}

// dial attempts to connect, draining the send queue while it waits so stale
// datagrams are dropped after undeliverableDatagramTimeout. It returns
// (client, true, nil) on success, (nil, false, nil) on a clean stop/idle, and
// (nil, true, err) on a dial failure that should be retried with backoff.
func ( *activeRelay) () (relayClient, bool, error) {
	,  := context.WithTimeout(context.Background(), connectTimeout)
	defer ()

	type  struct {
		   relayClient
		 error
	}
	 := make(chan , 1)
	go func() {
		,  := .parent.dialer(, .url, relayclient.Options{
			SecretKey: .parent.cfg.SecretKey,
			AuthToken: .parent.authTokenFor(.url),
		})
		 <- {: , : }
	}()

	 := time.NewTicker(undeliverableDatagramTimeout)
	defer .Stop()
	 := time.NewTimer(relayInactiveCleanupTime)
	defer .Stop()

	for {
		select {
		case <-.stopCh:
			()
			<-
			return nil, false, nil
		case  := <-:
			if . != nil {
				return nil, true, .
			}
			return ., true, nil
		case <-.C:
			// Drop datagrams that have been waiting through a dial.
			drain(.sendCh)
		case <-.C:
			if !.home() {
				()
				<-
				return nil, false, nil
			}
			.Reset(relayInactiveCleanupTime)
		}
	}
}

// connectedState is the per-connection mutable state of the connected loop.
type connectedState struct {
	established bool
	pendingPong [8]byte
	havePong    bool
	pingSent    [8]byte
	pingSentAt  time.Time
	awaitingPng bool
	lastRTT     time.Duration
}

// runConnected serves an established connection: it reads frames from the relay,
// sends queued datagrams in batches, sends periodic pings, and detects a stalled
// connection by ping timeout. It returns whether the connection was established
// (a pong arrived) and the error that ended it, or (established, nil) for a
// clean shutdown. It mirrors the Rust run_connected (actor.rs:506).
func ( *activeRelay) ( relayClient) (bool, error) {
	// Receive loop: read frames in a goroutine and forward them on a channel.
	// relayclient.Client is not concurrent-safe across senders, but one Recv
	// goroutine plus the sends issued from this goroutine is the supported
	// pattern (one reader, one writer).
	,  := context.WithCancel(context.Background())
	defer ()
	 := make(chan relayproto.RelayToClientMsg, 16)
	 := make(chan error, 1)
	go func() {
		for {
			,  := .Recv()
			if  != nil {
				 <- 
				return
			}
			select {
			case  <- :
			case <-.Done():
				return
			}
		}
	}()

	 := &connectedState{}
	 := time.NewTicker(pingInterval)
	defer .Stop()
	 := time.NewTimer(pingInterval)
	.Stop()
	defer .Stop()
	 := time.NewTimer(relayInactiveCleanupTime)
	defer .Stop()

	// Send an initial ping immediately so we establish liveness.
	if  := .sendPing(, , );  != nil {
		return .established, 
	}

	 := make([]RelaySendItem, 0, sendDatagramBatchSize)
	for {
		// Send a pending pong ASAP.
		if .havePong {
			 := .pendingPong
			.havePong = false
			if  := .send(, relayproto.ClientToRelayMsg{
				Type: relayproto.FramePong, Ping: ,
			});  != nil {
				return .established, 
			}
		}

		select {
		case <-.stopCh:
			return .established, nil
		case  := <-:
			if errors.Is(, context.Canceled) {
				return .established, nil
			}
			return .established, 
		case  := <-:
			 := .awaitingPng
			.handleFrame(, )
			// A received message proves liveness; reset the ping interval.
			.Reset(pingInterval)
			// If this frame was the pong we were waiting for, disarm the
			// ping timeout. The timeout is shorter than pingInterval, so a
			// live connection would otherwise trip it before the next ping
			// re-arms it. Mirrors the Rust PingTracker, which cancels the
			// timeout on pong (actor.rs).
			if  && !.awaitingPng {
				stopTimer()
			}
		case <-.C:
			if  := .sendPing(, , );  != nil {
				return .established, 
			}
		case <-.C:
			return .established, errPingTimeout
		case <-.C:
			if !.home() {
				return .established, nil
			}
			.Reset(relayInactiveCleanupTime)
		case  := <-.sendCh:
			.Reset(relayInactiveCleanupTime)
			 = append([:0], )
			// Coalesce up to the batch size.
			for len() < sendDatagramBatchSize {
				select {
				case  := <-.sendCh:
					 = append(, )
				default:
					goto 
				}
			}
		:
			if  := .sendDatagrams(, );  != nil {
				return .established, 
			}
		}
	}
}

// send writes one frame to the relay, bounding the call by the per-send timeout
// (PING_INTERVAL, matching the Rust send timeout, actor.rs:744).
func ( *activeRelay) ( relayClient,  relayproto.ClientToRelayMsg) error {
	,  := context.WithTimeout(context.Background(), pingInterval)
	defer ()
	return .Send(, )
}

// sendPing sends a fresh ping and arms the ping-timeout.
func ( *activeRelay) ( relayClient,  *connectedState,  *time.Timer) error {
	var  [8]byte
	rand.Read([:])
	.pingSent = 
	.pingSentAt = time.Now()
	.awaitingPng = true
	stopTimer()
	.Reset(pingTimeoutDuration())
	return .send(, relayproto.ClientToRelayMsg{Type: relayproto.FramePing, Ping: })
}

// stopTimer stops t and drains its channel if the stop lost the race, so a
// stale fire cannot be observed after a subsequent Reset.
func stopTimer( *time.Timer) {
	if !.Stop() {
		select {
		case <-.C:
		default:
		}
	}
}

func pingTimeoutDuration( *connectedState) time.Duration {
	if  != nil && .lastRTT > 0 {
		return min(max(.lastRTT*3, relayPingTimeoutMin), relayPingTimeoutMax)
	}
	return relayPingTimeoutMax
}

// sendDatagrams sends a batch of queued datagrams as client-to-relay datagram
// frames, one frame per item (each item already carries its own batch encoding).
func ( *activeRelay) ( relayClient,  []RelaySendItem) error {
	for ,  := range  {
		 := .send(, relayproto.ClientToRelayMsg{
			Type:          relayproto.FrameClientToRelayDatagram,
			DstEndpointID: .RemoteEndpoint,
			Datagrams:     .Datagrams,
		})
		if  != nil {
			return 
		}
	}
	return nil
}

// handleFrame processes one relay-to-client frame, matching the Rust
// handle_relay_msg (actor.rs:664).
func ( *activeRelay) ( relayproto.RelayToClientMsg,  *connectedState) {
	.handleFrameAt(, , time.Now())
}

func ( *activeRelay) ( relayproto.RelayToClientMsg,  *connectedState,  time.Time) {
	switch .Type {
	case relayproto.FrameRelayToClientDatagram, relayproto.FrameRelayToClientDatagramBat:
		.noteRoute(.RemoteEndpointID)
		select {
		case .parent.recvCh <- RelayRecvDatagram{
			URL: .url, Src: .RemoteEndpointID, Datagrams: .Datagrams,
		}:
		default:
			// Recv queue full: drop (loss; QUIC retransmits).
		}
	case relayproto.FrameEndpointGone:
		.dropRoute(.EndpointGone)
	case relayproto.FramePing:
		.pendingPong = .Ping
		.havePong = true
	case relayproto.FramePong:
		if .awaitingPng && .pingSent == .Ping {
			.awaitingPng = false
			if !.pingSentAt.IsZero() {
				.lastRTT = .Sub(.pingSentAt)
			}
		}
		.established = true
	case relayproto.FrameStatus:
		// Rate limiting is worth surfacing — the relay is throttling our
		// outbound traffic; other statuses are informational.
		if .Status == relayproto.StatusRateLimited {
			if  := .parent.metrics.Load();  != nil {
				.relayRateLimited.Add(1)
			}
		}
	case relayproto.FrameHealth, relayproto.FrameRestarting:
		// Informational; ignored. Status/Health are version-gated by the parser.
	}
}

// errPingTimeout is returned when a ping is not answered within pingInterval.
var errPingTimeout = errors.New("relay: ping timeout")

// jitter returns d scaled by a random factor in [0.5, 1.5), matching the
// jittered exponential backoff in the Rust ExponentialBuilder (actor.rs:356).
func jitter( time.Duration) time.Duration {
	return time.Duration(float64() * (0.5 + mrand.Float64()))
}

// drain removes all currently-queued items from ch without blocking.
func drain[ any]( chan ) {
	for {
		select {
		case <-:
		default:
			return
		}
	}
}