// Package netstate tracks OS-reported network availability for the client. // // A State instance is owned by the platform integration (e.g. the Android or // iOS bindings, fed from ConnectivityManager callbacks or NWPathMonitor) and // is injected into the connection retry loops (management, signal, relay, // peer guards and the top-level connect loop), which consult it to avoid // burning CPU and battery on reconnect attempts while the device has no // network at all (e.g. airplane mode), and to reset their backoff as soon as // the network returns. // // Consumers hold a *State that may be nil — every non-mobile platform leaves // it unset. The read methods are safe on a nil receiver: they report online // and never block, so consumers behave as if this package did not exist. package netstate import ( "context" "sync" log "github.com/sirupsen/logrus" ) // State holds the OS-reported network availability. The zero value is not // usable; create instances with New. type State struct { mu sync.Mutex online bool changed chan struct{} } // New creates a State that starts online. Platforms without network tracking // pass a nil *State instead: the read methods treat nil as always online and // never block, so consumers need no nil guards. func New() *State { return &State{ online: true, changed: make(chan struct{}), } } // Set records whether the OS reports any usable network. Transitions wake up // all Wait callers immediately. Unlike the read methods, Set is not nil-safe: // it is only for the platform owner that created the State with New. func (s *State) Set(online bool) { s.mu.Lock() defer s.mu.Unlock() if s.online == online { return } s.online = online close(s.changed) s.changed = make(chan struct{}) log.Infof("OS network availability changed: online=%t", online) } // IsOnline reports whether the OS reports at least one usable network. On a // nil receiver — no State injected — it reports online. func (s *State) IsOnline() bool { if s == nil { return true } s.mu.Lock() defer s.mu.Unlock() return s.online } // Changed returns a channel closed on the next availability transition, for // callers that already own a select loop and cannot block in Wait. Re-read it // after every fire: each transition installs a fresh channel. On a nil // receiver — no State injected — it returns nil, which blocks forever in a // select, so the caller simply never observes a transition. func (s *State) Changed() <-chan struct{} { if s == nil { return nil } s.mu.Lock() defer s.mu.Unlock() return s.changed } // Wait blocks while the network is offline. It reports whether it had to // wait, so callers can reset their backoff after an outage. It returns early // with the context error when ctx is done. On a nil receiver — no State // injected — it returns immediately. func (s *State) Wait(ctx context.Context) (bool, error) { if s == nil { return false, nil } waited := false for { s.mu.Lock() if s.online { s.mu.Unlock() return waited, nil } ch := s.changed s.mu.Unlock() if !waited { waited = true log.Debugf("network is offline, pausing connection attempts") } select { case <-ctx.Done(): return waited, ctx.Err() case <-ch: } } }