call-model: Channel + ChannelId + ChannelState (signaling embryo)
rutster-call-model is real-but-minimal (spec §5): the unifying leg object the future API exposes. ChannelId is a Uuid newtype for type-safety (the slice-1 worked example of the newtype pattern). Channel is signaling-state only — media lives in rutster-media as a leaf concern of the Channel, surfaced only when a second consumer needs to observe it (spec §5.3). ChannelState matches the New→Connecting→ Connected→Closing→Closed flow from §5.4.
This commit is contained in:
192
Cargo.lock
generated
192
Cargo.lock
generated
@@ -2,6 +2,119 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "rutster-call-model"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rutster-signaling-sip"
|
||||
version = "0.0.0"
|
||||
@@ -13,3 +126,82 @@ version = "0.0.0"
|
||||
[[package]]
|
||||
name = "rutster-tap"
|
||||
version = "0.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.23.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/rutster-call-model",
|
||||
"crates/rutster-signaling-sip",
|
||||
"crates/rutster-tap",
|
||||
"crates/rutster-spend",
|
||||
|
||||
13
crates/rutster-call-model/Cargo.toml
Normal file
13
crates/rutster-call-model/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
# crates/rutster-call-model/Cargo.toml
|
||||
[package]
|
||||
name = "rutster-call-model"
|
||||
version = "0.0.0"
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
description = "The Channel / leg object embryo (signaling-state only in slice 1)."
|
||||
|
||||
[dependencies]
|
||||
uuid = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
153
crates/rutster-call-model/src/lib.rs
Normal file
153
crates/rutster-call-model/src/lib.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
//! # rutster-call-model
|
||||
//!
|
||||
//! The unifying leg object: a `Channel` is one peer / one leg, the object
|
||||
//! the future API will model (PORT_PLAN §3 — "the unifying leg object").
|
||||
//! Building a throwaway `LoopbackPeer` for slice 1 and refactoring it
|
||||
//! later is the exact failure mode the design rules warn against, so the
|
||||
//! slice-1 peer *is* a `Channel` (spec §5.2).
|
||||
//!
|
||||
//! Slice 1 ships the signaling-state embryo only (spec §5.4). Media state
|
||||
//! is internal to `rutster-media`; the split — "Channel = signaling state;
|
||||
//! media = leaf concern" — matches ARCHITECTURE.md's "call model as the
|
||||
//! unifying object." Media state moves UP into the `Channel` only when a
|
||||
//! second consumer (the API, the tap, an audiohook) needs to observe it.
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// ChannelId must be a newtype around Uuid, NOT a bare Uuid — the
|
||||
/// newtype pattern prevents us from mixing up a ChannelId with some
|
||||
/// future SessionId at the type-system level. The compiler enforces
|
||||
/// what a comment can only ask for.
|
||||
#[test]
|
||||
fn channel_id_is_a_newtype() {
|
||||
let id = ChannelId::new();
|
||||
// Newtype wraps Uuid; we can reach the inner id but the outer
|
||||
// type is what the API surface speaks in.
|
||||
let _inner: Uuid = id.0;
|
||||
assert_eq!(format!("{}", id.0).len(), 36); // canonical UUID v4 length
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_starts_in_new_state() {
|
||||
let ch = Channel::new_inbound();
|
||||
assert_eq!(ch.state, ChannelState::New);
|
||||
assert_eq!(ch.direction, Direction::Inbound);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_state_transitions_match_spec_5_4() {
|
||||
let mut ch = Channel::new_inbound();
|
||||
assert_eq!(ch.state, ChannelState::New);
|
||||
ch.state = ChannelState::Connecting;
|
||||
ch.state = ChannelState::Connected;
|
||||
ch.state = ChannelState::Closing;
|
||||
ch.state = ChannelState::Closed;
|
||||
}
|
||||
}
|
||||
|
||||
use std::time::Instant;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Newtype wrapping a `Uuid` for the channel id.
|
||||
///
|
||||
/// # Why a newtype (not a bare `Uuid`?)
|
||||
/// Newtypes give zero-cost type safety. If we used bare `Uuid` everywhere,
|
||||
/// nothing in the type system would stop us from passing a `SessionId`
|
||||
/// into a function expecting a `ChannelId`. With `ChannelId(Uuid)`, the
|
||||
/// compiler rejects that mixup at the call site. The pattern is taught
|
||||
/// in the Rust Book's "Using the Newtype Pattern for Type Safety and
|
||||
/// Abstraction" section — `ChannelId` is the slice-1 worked example.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ChannelId(pub Uuid);
|
||||
|
||||
impl ChannelId {
|
||||
/// Mint a fresh `ChannelId`. Slice 1 uses UUID v4 — opaque, random,
|
||||
/// no coordination. A future multi-tenant deployment would scope by
|
||||
/// tenant prefix; that lands with authz (step 6).
|
||||
pub fn new() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ChannelId {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ChannelId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Signaling state machine for a `Channel` (spec §5.4, slice 1).
|
||||
///
|
||||
/// `New → Connecting → Connected → Closing → Closed`
|
||||
///
|
||||
/// # Why an enum (not a struct with a `kind: &str` field?)
|
||||
/// Enums model a closed set of states; exhaustiveness checking forces
|
||||
/// every `match` to consider each state explicitly. When step 4 adds
|
||||
/// `Closing`'s sub-state for "graceful close in flight," it'll be a new
|
||||
/// variant or a wrapping struct; either way, the compiler tells us
|
||||
/// every site that needs updating. A `kind: String` field would let
|
||||
/// new states slip in silently.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ChannelState {
|
||||
/// `POST /v1/sessions` created the Channel; no offer yet.
|
||||
New,
|
||||
/// Offer received, ICE gathering / DTLS handshake in progress.
|
||||
Connecting,
|
||||
/// ICE+DTLS connected, RTP flowing, audio echoing.
|
||||
Connected,
|
||||
/// `DELETE /v1/sessions/:id` or peerconnectionclose; cleaning up.
|
||||
Closing,
|
||||
/// Resources dropped, entry removed from the DashMap.
|
||||
Closed,
|
||||
}
|
||||
|
||||
/// Direction of the leg (spec §5.1).
|
||||
///
|
||||
/// Slice 1 is browser-initiated → `Inbound` only. `Outbound` lands with
|
||||
/// the dialer (later rung). The enum exists now so the API has a stable
|
||||
/// shape — adding `Outbound` later is a non-breaking addition.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Direction {
|
||||
Inbound,
|
||||
// Outbound lands with the dialer (later). NOT present in slice 1.
|
||||
}
|
||||
|
||||
/// The unifying leg object — one peer = one `Channel` (spec §5.1).
|
||||
///
|
||||
/// Slice 1 carries signaling state only. Fields that arrive later, listed
|
||||
/// in spec §5.6, are absent by design — adding them is a backwards-
|
||||
/// compatible field add:
|
||||
/// - `media: Option<MediaLeg>` — second consumer.
|
||||
/// - `audiohooks: Vec<AudiohookHandle>` — escalation rung 2.
|
||||
/// - `tap: Option<TapHandle>` — step 2.
|
||||
#[derive(Debug)]
|
||||
pub struct Channel {
|
||||
pub id: ChannelId,
|
||||
pub state: ChannelState,
|
||||
pub direction: Direction,
|
||||
/// For the 60 s idle timeout (spec §4.5). `Instant` is a monotonic
|
||||
/// clock — choosing it over `SystemTime` means we're measuring
|
||||
/// elapsed wall-time within this process, NOT a calendar time the
|
||||
/// 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,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
/// Construct a fresh inbound channel — the only slice-1 path.
|
||||
pub fn new_inbound() -> Self {
|
||||
Self {
|
||||
id: ChannelId::new(),
|
||||
state: ChannelState::New,
|
||||
direction: Direction::Inbound,
|
||||
created_at: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user