nexus-async-net 0.7.1

Async WebSocket adapter for nexus-net. Tokio-compatible, zero-copy, SIMD-accelerated.
Documentation
# Async WebSocket

The async WebSocket type is `ws::WsStream<S>`. It's structurally
identical to nexus-net's `Client<S>` but with `async fn` on every
I/O method.

## Connect and stream

```rust
use nexus_async_net::ws::WsStreamBuilder;
use nexus_net::ws::{Message, CloseCode};
use nexus_net::tls::TlsConfig;
use std::time::Duration;

#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
    let tls = TlsConfig::new()?;

    let mut ws = WsStreamBuilder::new()
        .tls(&tls)
        .disable_nagle()
        .buffer_capacity(1 << 20)
        .max_message_size(16 << 20)
        .connect_timeout(Duration::from_secs(3))
        .connect("wss://stream.binance.com:9443/ws")
        .await?;

    ws.send_text(r#"{"method":"SUBSCRIBE","params":["btcusdt@trade"],"id":1}"#)
        .await?;

    loop {
        match ws.recv().await? {
            Some(Message::Text(json))    => handle(json),
            Some(Message::Binary(bytes)) => handle_binary(bytes),
            Some(Message::Ping(data))    => ws.send_pong(data).await?,
            Some(Message::Pong(_))       => {}
            Some(Message::Close(_))      => {
                ws.close(CloseCode::Normal, "bye").await?;
                break;
            }
            None => break,  // EOF
        }
    }
    Ok(())
}

fn handle(_: &str) {}
fn handle_binary(_: &[u8]) {}
```

`WsStream::recv()` returns `Result<Option<Message<'_>>, WsError>`:

- `Ok(Some(msg))` — a full message (all continuation frames
  reassembled, UTF-8 validated if text)
- `Ok(None)` — EOF or the peer cleanly closed
- `Err(e)` — protocol violation, IO error, or TLS error

The returned `Message<'_>` borrows from the internal `ReadBuf` inside
`ws` — you cannot hold it across another `.await` on `ws`. Copy
(`.into_owned()` or `.to_string()`) if you need to.

## Builder options

```rust
WsStreamBuilder::new()
    .tls(&tls)                       // TLS config (feature "tls")
    .disable_nagle()                  // TCP_NODELAY
    .buffer_capacity(1 << 20)         // ReadBuf size
    .max_read_size(64 << 10)          // max bytes per AsyncRead::read
    .compact_at(0.5)                  // compaction trigger
    .max_frame_size(16 << 20)
    .max_message_size(16 << 20)
    .write_buffer_capacity(64 << 10)
    .connect_timeout(Duration::from_secs(3))
    .tcp_keepalive(Duration::from_secs(60))     // feature "socket-opts"
    .recv_buffer_size(1 << 20)                   // feature "socket-opts"
    .send_buffer_size(1 << 20);                  // feature "socket-opts"
```

See [tuning.md](./tuning.md) for the recv-path knobs (`buffer_capacity`,
`max_read_size`, `compact_at`) in detail.

## Server side: `accept`

```rust
use tokio::net::TcpListener;
use nexus_async_net::ws::WsStreamBuilder;
use nexus_net::ws::Message;

let listener = TcpListener::bind("0.0.0.0:9001").await?;
loop {
    let (sock, _) = listener.accept().await?;
    sock.set_nodelay(true)?;
    tokio::task::spawn(async move {
        let mut ws = match WsStreamBuilder::new().accept(sock).await {
            Ok(ws) => ws,
            Err(e) => { eprintln!("handshake: {e}"); return; }
        };
        while let Ok(Some(msg)) = ws.recv().await {
            if let Message::Text(s) = msg {
                let _ = ws.send_text(s).await;
            }
        }
    });
}
```

## `Stream` and `Sink` (tokio backend only)

When the `tokio-rt` feature is enabled, `WsStream<S>` implements
`futures_core::Stream<Item = Result<OwnedMessage, WsError>>` and
`futures_sink::Sink<OwnedMessage>`. This lets you compose with
`futures::StreamExt`, `tokio_util::codec`, and other ecosystem
crates.

```rust
use futures::{SinkExt, StreamExt};
use nexus_net::ws::OwnedMessage;

while let Some(item) = ws.next().await {
    match item? {
        OwnedMessage::Text(s) => { /* ... */ }
        OwnedMessage::Binary(b) => { /* ... */ }
        _ => {}
    }
}

ws.send(OwnedMessage::Text("hi".into())).await?;
```

**Cost note.** The `Stream`/`Sink` path goes through `OwnedMessage`,
which allocates (`bytes::Bytes`). The direct `recv()`/`send_text()`
path is zero-copy. For trading hot paths, prefer the direct methods.

The `nexus` backend intentionally **does not** implement
`Stream`/`Sink` — the owned-message conversion would defeat the
zero-alloc guarantees that backend exists to provide.

## Errors and poisoning

`WsError` has the same structure as nexus-net's `ws::Error` — see
[nexus-net/docs/errors.md](../../nexus-net/docs/errors.md). An IO
error during a send poisons the `WsStream`; you must reconnect.
Unlike the REST pool, there is no automatic WebSocket reconnect
at the library layer — WebSocket connections are session-stateful
(subscriptions, auth), so the application must re-establish state.

See [patterns.md — Exchange client with reconnect](./patterns.md#exchange-client-with-reconnect)
for a production reconnect loop.