wingfoil 6.0.4

graph based stream processing framework
Documentation
# web Adapter

Bidirectional WebSocket streaming between a wingfoil graph and one or more
browsers. Publishes graph values to clients (`web_pub`) and exposes client
frames as a wingfoil source stream (`web_sub`).

## Module Structure

```
web/
  mod.rs               # Re-exports, module-level //! docs, example usage
  codec.rs             # Envelope + bincode/JSON encoding (backed by wingfoil-wire-types)
  server.rs            # WebServer + axum router + per-connection task
  write.rs             # web_pub() sink + WebPubOperators fluent trait
  read.rs              # web_sub() source
  integration_tests.rs # Ordinary `#[cfg(test)]`; in-process server + tungstenite client
  CLAUDE.md            # This file
```

## Key Design Decisions

- **Protocol: WebSocket (binary frames).** Bidirectional, kHz-capable, universal
  browser support. Not SSE (one-way, text-only) and not WebTransport (browser
  support still gated; complex TLS story).
- **Default codec: bincode** via [`wingfoil_wire_types::Envelope`]. JSON is
  available via `.codec(CodecKind::Json)` for debugging in browser devtools.
- **Shared wire types** live in the top-level `wingfoil-wire-types` crate so the
  server and the `wingfoil-wasm` browser client can't drift — wire mismatches
  become compile errors.
- **Axum 0.8** with `tokio-tungstenite` under the hood. Adds HTTP routing and
  `tower-http::services::ServeDir` for hosting the UI bundle on the same
  origin as the WebSocket endpoint.
- **Dedicated thread + current-thread tokio runtime** for the HTTP server
  (same pattern as `PrometheusExporter::serve`). Bind is synchronous so port
  conflicts surface before the graph starts.
- **Broadcast per publish topic** (`tokio::sync::broadcast`). Slow consumers
  are lossy — `broadcast::error::RecvError::Lagged` drops oldest frames so a
  frozen browser tab can never back-pressure the graph.
- **Bounded mpsc per connection outbound queue** and **per subscribe-topic
  listener queue**, both with `try_send` + drop-newest-under-overload so a
  misbehaving client cannot grow memory without bound or push graph latency.
- **Control plane on topic `"$ctrl"`**: `Hello { codec, version }` is sent by
  the server on upgrade; clients send `Subscribe { topics }` /
  `Unsubscribe { topics }` to manage forwarders.
- **Historical-mode safety**: `WebServerBuilder::start_historical()` returns
  a no-op server. Both `web_pub` and `web_sub` become no-ops so the same
  graph can run in `RunMode::HistoricalFrom(...)` without touching the
  network — mirrors `PrometheusMetricNode`'s `historical` flag.
- **Optional TLS via the `web-tls` feature**. Adds a `.tls(cert, key)`
  builder method that loads PEM files synchronously at `start()` time
  (so a missing/malformed cert surfaces alongside bind errors, before
  the graph starts) and serves over HTTPS / WSS. Implementation
  delegates the per-connection TLS handshake to `axum-server`'s
  rustls integration; the same axum `Router` (and therefore the
  existing `/ws` upgrade handler) is reused. The crypto provider is
  pinned to `ring` to match the FIX adapter.

## Threading Model

- Graph publishers and subscribers are async consumers/producers (via
  `consume_async` / `produce_async`). They run on the graph's shared tokio
  runtime.
- The HTTP/WS server runs on its own dedicated current-thread tokio runtime
  on a named `wingfoil-web` OS thread. The server is bound when `start()`
  is called and shut down when the `WebServer` handle drops.
- Per-connection handler spawns one writer task (drains outbound mpsc →
  WebSocket) and one forwarder task per subscribed publish topic (reads
  broadcast receiver → outbound mpsc).

## Wire Format

Every WebSocket binary frame is an `Envelope`:

```rust
pub struct Envelope {
    pub topic: String,   // e.g. "order_book" or "$ctrl"
    pub time_ns: u64,    // graph engine time (0 for client → server frames)
    pub payload: Vec<u8>, // bincode(T) or serde_json(T) of the user type
}
```

Control topic `"$ctrl"` carries a `ControlMessage`:

```rust
pub enum ControlMessage {
    Hello { codec: CodecKind, version: u16 }, // server → client on upgrade
    Subscribe   { topics: Vec<String> },      // client → server
    Unsubscribe { topics: Vec<String> },      // client → server
}
```

## Pre-Commit Requirements

```bash
# 1. Standard checks
cargo fmt --all
cargo clippy --workspace --all-targets --all-features -- -D warnings

# 2. Unit + integration tests (no external service required)
cargo test --features web -p wingfoil \
  -- --test-threads=1 adapters::web

# 3. TLS round-trip (rcgen-generated self-signed cert + rustls client)
cargo test --features web-tls-integration-test -p wingfoil \
  -- --test-threads=1 adapters::web::integration_tests::test_pub_round_trip_tls
```

Integration tests run entirely in-process — there is no Docker container
dependency. Tests that need both a server and a client use a dedicated thread
with its own `tokio::runtime::Runtime` for the client so the graph's `Rc`
nodes remain on the original thread.

## Gotchas

- The WebSocket endpoint is `GET /ws`. Set your client URL to
  `ws://HOST:PORT/ws`.
- `WebServer::bind("127.0.0.1:0")` is the recommended pattern for tests —
  read back the bound port via `WebServer::port()` afterwards.
- `broadcast::channel` refuses to `send` when there are no subscribers; this
  is expected and silently ignored in `web_pub`. A graph can publish with
  zero clients connected — values are simply dropped.
- A `web_sub::<T>` listener's mpsc is registered eagerly at construction
  time, so frames sent by a client that connects *before* the graph starts
  are buffered (up to the mpsc capacity) rather than lost.
- Tests use `--test-threads=1` only as a safety measure; they bind port 0
  and are independent, so parallel execution also works. The `=1` matches
  the rest of the wingfoil adapter test suite.

## wingfoil-wasm browser client

The sibling `wingfoil-wasm` crate (at the workspace root, excluded from the
default workspace because it targets `wasm32-unknown-unknown`) provides a
Rust-compiled-to-wasm decoder/encoder so JS / TS apps can consume and emit
frames without maintaining hand-written schemas. The `wingfoil-js` npm
package wraps that wasm module and provides reactive-framework adapters
(Solid.js, Svelte).