Goal
`TunnelHandle::shutdown()` is currently fire-and-forget — if the background task exits because the WSS died, the host process has no way to learn. Inbound calls silently stop arriving and the developer is confused why their tunnel "works" but routing doesn't.
Surface liveness so the host can react (reconnect, surface error, exit).
Sketch
Two reasonable shapes:
- JoinHandle exposed: the host `await`s it (or polls) to know the task ended.
```rust
pub(super) struct TunnelHandle {
pub session_id: String,
shutdown: Arc,
task: tokio::task::JoinHandle,
}
```
- Watch channel: `tokio::sync::watch` lets multiple callers observe state transitions (Connected, Disconnecting, Disconnected, Error).
(2) composes better with future health-UI (`mirrorstack dev status`) and surface-area-wise stays in dev/tunnel.rs. (1) is simpler.
Why this matters in practice
Without it, the silent-death paths the simplify reviewer flagged are actionable internally (we eprintln warnings) but the dev::run loop spawning the tunnel can't tell the tunnel died. A dev does `mirrorstack dev --tunnel`, the tunnel drops at minute 14 due to network blip, and 16 minutes of inbound calls silently 404 from dispatch's perspective.
Acceptance
- After tunnel-task exit (any reason), the host can detect within ~1s
- `shutdown()` followed by an immediate liveness-check returns Disconnecting/Disconnected
- Test: simulate stream close server-side, verify host observes the drop
Deps
Estimate
~80 LoC + tests. Probably ~45 min.
Goal
`TunnelHandle::shutdown()` is currently fire-and-forget — if the background task exits because the WSS died, the host process has no way to learn. Inbound calls silently stop arriving and the developer is confused why their tunnel "works" but routing doesn't.
Surface liveness so the host can react (reconnect, surface error, exit).
Sketch
Two reasonable shapes:
```rust
pub(super) struct TunnelHandle {
pub session_id: String,
shutdown: Arc,
task: tokio::task::JoinHandle,
}
```
(2) composes better with future health-UI (`mirrorstack dev status`) and surface-area-wise stays in dev/tunnel.rs. (1) is simpler.
Why this matters in practice
Without it, the silent-death paths the simplify reviewer flagged are actionable internally (we eprintln warnings) but the dev::run loop spawning the tunnel can't tell the tunnel died. A dev does `mirrorstack dev --tunnel`, the tunnel drops at minute 14 due to network blip, and 16 minutes of inbound calls silently 404 from dispatch's perspective.
Acceptance
Deps
Estimate
~80 LoC + tests. Probably ~45 min.