package client import ( "context" "time" "gitea.libretechconsulting.com/rmcguire/wingbits/pkg/types/readsb" "gitea.libretechconsulting.com/rmcguire/wingbits/pkg/types/wingbits" ) // Update carries one streamed sample of T, or the error from the fetch that // produced it. Exactly one of Value and Err is meaningful per send. type Update[T any] struct { Value *T Err error } // StreamAircraft polls aircraft.json every interval and sends each decoded, // filtered report on the returned channel until ctx is cancelled. The first // sample is sent immediately. The channel is closed when ctx ends. func (c *Client) StreamAircraft(ctx context.Context, interval time.Duration, filters ...readsb.AircraftFilter) <-chan Update[readsb.AircraftReport] { return stream(ctx, c.streamBuf, interval, func(ctx context.Context) (*readsb.AircraftReport, error) { return c.Aircraft(ctx, filters...) }) } // StreamStats polls stats.json every interval. func (c *Client) StreamStats(ctx context.Context, interval time.Duration) <-chan Update[readsb.Stats] { return stream(ctx, c.streamBuf, interval, c.Stats) } // StreamReceiver polls receiver.json every interval. func (c *Client) StreamReceiver(ctx context.Context, interval time.Duration) <-chan Update[readsb.Receiver] { return stream(ctx, c.streamBuf, interval, c.Receiver) } // StreamOutline polls outline.json every interval. func (c *Client) StreamOutline(ctx context.Context, interval time.Duration) <-chan Update[readsb.Outline] { return stream(ctx, c.streamBuf, interval, c.Outline) } // StreamDiagnostics polls /network/diagnostics every interval. func (c *Client) StreamDiagnostics(ctx context.Context, interval time.Duration) <-chan Update[wingbits.Diagnostics] { return stream(ctx, c.streamBuf, interval, c.Diagnostics) } // StreamMetrics polls /metrics every interval. func (c *Client) StreamMetrics(ctx context.Context, interval time.Duration) <-chan Update[wingbits.Metrics] { return stream(ctx, c.streamBuf, interval, c.Metrics) } // StreamStatus polls the Tailscale status every interval. func (c *Client) StreamStatus(ctx context.Context, interval time.Duration) <-chan Update[wingbits.TailscaleStatus] { return stream(ctx, c.streamBuf, interval, c.Status) } // stream drives a generic poll loop: it calls fetch immediately, then once per // interval tick, forwarding each result as an Update on a buffered channel. func stream[T any](ctx context.Context, buf int, interval time.Duration, fetch func(context.Context) (*T, error)) <-chan Update[T] { ch := make(chan Update[T], buf) go func() { defer close(ch) tick := time.NewTicker(interval) defer tick.Stop() for { send(ctx, ch, fetch) select { case <-ctx.Done(): return case <-tick.C: } } }() return ch } // send runs one fetch and delivers the result, respecting cancellation so a // full channel cannot wedge the loop past ctx's lifetime. func send[T any](ctx context.Context, ch chan<- Update[T], fetch func(context.Context) (*T, error)) { v, err := fetch(ctx) select { case ch <- Update[T]{Value: v, Err: err}: case <-ctx.Done(): } }