switch to iterators and client polling
Publish / release (push) Failing after 17m20s

This commit is contained in:
2026-06-22 22:20:32 -04:00
parent 24fe4258f7
commit f174c2922c
7 changed files with 154 additions and 143 deletions
+33 -11
View File
@@ -39,8 +39,8 @@ and validated against real hardware.
lives under `/readsb/` on `:8088` and under `/data/` on the tar1090 root `:8504`.
- **Composable aircraft filters** — position, altitude, range, squawk, emergency,
callsign, category, MLAT, signal strength, and more, AND/ORcomposable.
- **Streaming built in.** Any endpoint becomes a `<-chan Update[T]` with a
configurable poll interval and buffer.
- **Streaming built in.** Any endpoint becomes a Go 1.23 iterator
(`iter.Seq2[*T, error]`) you can `range` over at a configurable interval.
- **Bring your own everything** — `context.Context` on every call, pluggable
`*http.Client`, perendpoint TLS, custom ports/schemes/paths.
- **Zero nonstdlib dependencies**, including a small Prometheus text parser for
@@ -152,24 +152,47 @@ plus the `Not` and `Any` combinators.
### Streaming
Every endpoint has a `Stream*` variant. It emits immediately, then on each tick,
and closes the channel when the context is cancelled. Each `Update[T]` carries
either a value or the error from that poll.
Every endpoint has a `Poll*` variant that returns a Go 1.23 iterator
(`iter.Seq2[*T, error]`). It yields a fresh result immediately, then once per
interval, until the context is cancelled or you `break`. Each step yields either
a value or the error from that poll.
```go
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for u := range c.StreamAircraft(ctx, 2*time.Second, readsb.WithPosition()) {
if u.Err != nil {
log.Printf("poll failed: %v", u.Err)
for report, err := range c.PollAircraft(ctx, 2*time.Second, readsb.WithPosition()) {
if err != nil {
log.Printf("poll failed: %v", err)
continue
}
log.Printf("%d aircraft in view", len(u.Value.Aircraft))
log.Printf("%d aircraft in view", len(report.Aircraft))
}
```
Set the channel buffer with `client.WithStreamBufferSize(n)`.
Because the iterator runs in your goroutine, breaking the loop stops polling
immediately — there is no background goroutine to leak and no buffer to tune.
Need a channel for fan-out or `select`? Wrap the iterator at the call site.
The `Poll*` methods are thin wrappers over the exported generic engine,
`client.Poll`, so you never re-implement the interval loop — apply the same
cadence to any custom or composed fetch:
```go
for v, err := range client.Poll(ctx, time.Second, func(ctx context.Context) (int, error) {
r, err := c.Aircraft(ctx, readsb.InEmergency())
if err != nil {
return 0, err
}
return len(r.Aircraft), nil
}) {
log.Printf("emergencies: %d (err=%v)", v, err)
}
```
Filtering exposes an iterator too: `report.All(filters...)` yields matching
aircraft lazily (handy with `break` or `slices.Collect`), while
`report.Filter(...)` is the eager slice form.
### Station health & feeder telemetry
@@ -245,7 +268,6 @@ c, err := client.New(
Host: "station.example", Port: 8504, DataPath: "data",
}),
client.WithHTTPClient(myHTTPClient), // optional; TLS above is ignored if set
client.WithStreamBufferSize(8),
client.WithUserAgent("my-app/1.0"),
)
```