Files
client-rs/CLAUDE_TODO.md
2026-04-06 21:15:35 -06:00

87 lines
3.0 KiB
Markdown

# Add non-blocking `connect` variant
## Problem
`connect(None, timeout)` in `src/connect.rs` has a three-step fallback:
1. `DONGLORA_MUX_TCP` env var -> TCP mux
2. Unix socket mux (if socket file exists)
3. Direct USB serial via `find_port().unwrap_or_else(wait_for_device)`
Step 3 calls `wait_for_device()` which polls `find_port()` every 500ms
**indefinitely** until a USB device appears. This makes `connect(None, ...)`
unsuitable for callers that need to retry with backoff or report disconnected
status (e.g. donglora-bridge's reconnect loop).
Today, donglora-bridge works around this by manually reimplementing the
fallback chain:
```rust
if let Ok(c) = connect_mux_auto(timeout) {
(c, "mux".to_string())
} else {
let port_path = find_port()
.ok_or_else(|| anyhow!("no mux or USB dongle found"))?;
let c = connect(Some(&port_path), timeout)?;
(c, shorten_path(&port_path))
}
```
This duplicates logic from `connect()` (steps 1-2 via `connect_mux_auto`, step
3 minus the blocking wait) and won't pick up any future changes to the
`connect()` fallback order.
## Proposed solution
Add a `try_connect` (or similar) that runs the same mux -> serial fallback as
`connect(None, timeout)` but replaces `wait_for_device()` with a single
`find_port()` call, returning an error immediately if no device is found:
```rust
/// Like [`connect`] but returns an error instead of blocking when no USB
/// device is present. Suitable for callers with their own retry logic.
pub fn try_connect(timeout: Duration) -> Result<Client<AnyTransport>> {
// Steps 1-2: identical to connect() — try TCP mux, then Unix socket mux
// Step 3: non-blocking USB scan
let port_path = discovery::find_port()
.ok_or_else(|| anyhow!("no DongLoRa device found (no mux, no USB device)"))?;
let transport = SerialTransport::open(&port_path, timeout)?;
Ok(Client::new(AnyTransport::Serial(transport)))
}
```
This would let donglora-bridge collapse its connection code to:
```rust
let (mut client, device) = if let Some(port) = port {
let c = connect(Some(port), timeout)?;
(c, shorten_path(port))
} else {
let c = try_connect(timeout)?;
let device = match c.transport() {
AnyTransport::Mux(_) => "mux".to_string(),
AnyTransport::Serial(_) => find_port()
.map(|p| shorten_path(&p))
.unwrap_or_else(|| "serial".to_string()),
};
(c, device)
};
```
### Device name from transport
The bridge also needs to know *what* it connected to for display purposes. Two
options:
1. Match on `AnyTransport` variant after connecting (shown above) -- works but
requires a redundant `find_port()` call for the display name since
`SerialTransport` has private fields.
2. Have `try_connect` return the port path alongside the client, e.g.
`-> Result<(Client<AnyTransport>, ConnectionInfo)>` where `ConnectionInfo`
is an enum like `Mux { socket_path }` | `Serial { port_path }`. Cleaner
but a larger API change.
Either works. Option 2 would also benefit `connect()` callers who want to log
what they connected to.