- Integration test: smoke test against in-process EchoServer (hello handshake round-trip). Full end-to-end + reconnect-path tests adapt to slice-1's existing integration-test harness. - examples/echo_brain/: Python reference brain (~80 lines, websockets lib). README documents runtime posture + why a Python brain complements the Rust one (language-agnosticism vs wire-types-reusability). - LEARNING.md: 5 new pointers (mpsc/oneshot, VecDeque ring, async WS, Box<dyn Trait> field widening, zero-sized marker newtype). Spec ref: 2026-06-28-slice-2-agent-tap-design.md §8.4, §8.5 #7.
90 lines
4.8 KiB
Markdown
90 lines
4.8 KiB
Markdown
# LEARNING.md — to learn concept X, read file Y
|
|
|
|
This index maps a Rust concept you might be learning to the file where
|
|
slice 1 makes the concept concrete. Each entry is a worked example you
|
|
can read in `cargo doc --open` plus the source file itself.
|
|
|
|
## Concepts + pointers
|
|
|
|
- **Newtype pattern (type-safety via single-field wrappers)** →
|
|
`crates/rutster-call-model/src/lib.rs` — `ChannelId(Uuid)`. The newtype
|
|
stops us from mixing up a `ChannelId` with some future `SessionId` at
|
|
the type-system level. Compile-enforced where a comment could only ask.
|
|
|
|
- **`enum` for closed state sets + exhaustive `match`** →
|
|
`crates/rutster-call-model/src/lib.rs` — `ChannelState` (New →
|
|
Connecting → Connected → Closing → Closed). Exhaustiveness checking
|
|
forces every `match` to consider each state; adding a state later
|
|
surfaces every site that needs handling.
|
|
|
|
- **Sans-IO pattern (no I/O inside the library; input via method calls,
|
|
output via return values)** → `crates/rutster-media/src/loop_driver.rs`
|
|
— the str0m poll loop. `Rtc::handle_input` takes a network packet as a
|
|
struct argument, not from a socket the library owns; `poll_output`
|
|
returns `Transmit` packets the caller sends. Fully testable without a
|
|
network — str0m integration tests use this property to drive faster
|
|
than realtime.
|
|
|
|
- **Trait design for extension points (a futures-compatible seam)** →
|
|
`crates/rutster-media/src/pcm.rs` — the `AudioSource` / `AudioSink`
|
|
traits. Slice 1 wires an `EchoAudioPipe` between them; step 2 swaps
|
|
that for a real WSS tap client without touching `RtcSession`. The
|
|
traits describe *what* the splice point does, not *how* it's filled.
|
|
|
|
- **Error enums with `thiserror` + hot-path match-and-continue** →
|
|
`crates/rutster-media/src/lib.rs` (`MediaError`) and
|
|
`crates/rutster-media/src/opus_codec.rs` (`OpusDecoder::decode` returns
|
|
`Option<PcmFrame>`). Cold path: `thiserror`-derived enum + `?`. Hot
|
|
path: match-and-continue, never `?`, never panic — "drop + observe,
|
|
don't crash" (spec §3.8).
|
|
|
|
- **`Arc<Mutex<T>>` vs `Arc<RwLock<T>>` — when each is right** →
|
|
`crates/rutster/src/session_map.rs`. The `RtcSession` lives behind
|
|
`Arc<Mutex<...>>` because every access mutates it (str0m's `&mut self`
|
|
contract) — `RwLock`'s read-mode would be useless. Comment on the
|
|
struct explains the trade-off.
|
|
|
|
- **`DashMap` for sharded concurrent maps** →
|
|
`crates/rutster/src/session_map.rs`. `DashMap` shards its inner map so
|
|
two handlers operating on different `ChannelId`s don't contend;
|
|
`HashMap` wrapped in a single `Mutex` would serialize every access.
|
|
|
|
- **str0m 0.21's single-mutation invariant** →
|
|
`crates/rutster-media/src/loop_driver.rs`. Mutate (handle_input /
|
|
Writer::write) → drain `poll_output` to `Output::Timeout` → next
|
|
mutate. Violating this leaves str0m in an inconsistent state.
|
|
|
|
- **tokio graceful shutdown via signal handlers** →
|
|
`crates/rutster/src/main.rs` (`shutdown_signal`). Ctrl-C / SIGTERM
|
|
drops the AppState; the AppState drops the DashMap; the DashMap drops
|
|
every RtcSession. No in-flight call preservation in slice 1.
|
|
|
|
- **`include_str!` for embedding static assets** →
|
|
`crates/rutster/src/routes.rs` (`include_str!("../static/index.html")`).
|
|
The HTML test client is compiled into the binary at build time — no
|
|
separate file to ship, no disk IO to serve it.
|
|
|
|
- **`mpsc` + `oneshot` for cold-path task supervision** → `crates/rutster/src/tap_engine.rs`
|
|
— how a spawned tokio task is supervised + cancelled via `oneshot::Receiver`.
|
|
- **`VecDeque` as a bounded playout ring with drop-oldest policy** → `crates/rutster-tap/src/tap_audio_pipe.rs`
|
|
— why a manual ring (not `mpsc`) when the overflow policy is drop-oldest, not drop-newest.
|
|
- **Async WS connect + `Sink`/`Stream` traits** → `crates/rutster-tap/src/tap_client.rs`
|
|
— `tokio_tungstenite::connect_async`, `WebSocketStream`, the `SinkExt`/`StreamExt`
|
|
extension traits, `tokio::select!` over inbound + outbound + close.
|
|
- **`Box<dyn Trait + Send>` field widening (the seam test)** → `crates/rutster-media/src/rtc_session.rs`
|
|
— why the `pipe` field type changed from `EchoAudioPipe` to
|
|
`Box<dyn AudioSource + AudioSink + Send>` so `loop_driver`'s call sites
|
|
are byte-identical (slice-2 §8.5 #6).
|
|
- **Zero-sized marker newtype for state flags** → `crates/rutster-call-model/src/lib.rs`
|
|
— `TapHandle(())` compiles `Option<TapHandle>` to a single `bool`; no
|
|
runtime cost for the type-system marker.
|
|
|
|
## How to read
|
|
|
|
1. `cargo doc --open` — every module has a `//!` doc comment; the doc
|
|
tree is the high-level map.
|
|
2. Pick a concept above; open the named file. The first occurrence of
|
|
each non-obvious pattern has a `//` comment explaining *why*.
|
|
3. Cross-ref back to the spec sections cited inline (`spec §3.8`,
|
|
`ADR-0002`, etc.) for the architecture-level rationale.
|