diff --git a/crates/rutster-call-model/src/lib.rs b/crates/rutster-call-model/src/lib.rs index ca5291b..6770fd2 100644 --- a/crates/rutster-call-model/src/lib.rs +++ b/crates/rutster-call-model/src/lib.rs @@ -45,6 +45,18 @@ mod tests { ch.state = ChannelState::Closing; ch.state = ChannelState::Closed; } + + #[test] + fn channel_starts_with_no_tap() { + let ch = Channel::new_inbound(); + assert_eq!(ch.tap, None); + } + + #[test] + fn tap_handle_is_zero_sized() { + // Zero-sized type — confirms the "no extra allocation" claim. + assert_eq!(std::mem::size_of::(), 0); + } } use std::time::Instant; @@ -138,6 +150,9 @@ pub struct Channel { /// user could change mid-call. The monotonic clock is the right /// tool for "has this peer been silent for 60 seconds?" pub created_at: Instant, + /// NEW (slice-2, spec §5.2, §6): None until Connected, set on Connected, + /// cleared on Closing. State invariant — see the table below. + pub tap: Option, } impl Channel { @@ -148,6 +163,43 @@ impl Channel { state: ChannelState::New, direction: Direction::Inbound, created_at: Instant::now(), + tap: None, } } } + +/// Zero-cost marker that a tap is attached to this `Channel`. +/// +/// # Why a zero-sized newtype (not a `TapClient` handle / mpsc sender / UUID?) +/// +/// `Channel` lives in `rutster-call-model`, which is a leaf — no tokio dep +/// (spec §6, slice-1 §5.3). The live tap connection (mpsc handles, WS +/// state) lives in the binary's `DashMap` and is +/// looked up by the channel's existing `ChannelId` (which is also the +/// wire `session_id` per spec §5.3). The `TapHandle` here is just the +/// type-system marker that "a tap is attached" — `Option` +/// compiles to a single bool, no allocation. +/// +/// Multi-tap-per-channel (e.g. a recording tap beside a brain tap) is a +/// future-rung concern; when it appears that's the trigger to mint a +/// separate `TapId(Uuid)` newtype and key the registry by it. For slice-2 +/// (one brain per call) `ChannelId` is sufficient — YAGNI (spec §6). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TapHandle(()); + +impl TapHandle { + /// Construct a marker. Public so the binary (Task 7) can mint one + /// when the TapEngine spawns; the doc comment + the zero-sized + /// payload make accidental misuse low-stakes. `pub(crate)` would + /// restrict to `rutster-call-model` and block the binary; we have no + /// reason to gate this from the binary that owns the spawn lifecycle. + pub fn new() -> Self { + Self(()) + } +} + +impl Default for TapHandle { + fn default() -> Self { + Self::new() + } +}