Skip to content

Instantly share code, notes, and snippets.

@algesten
Created January 17, 2026 18:48
Show Gist options
  • Select an option

  • Save algesten/417fdae74e0b6b05680e0ba8e880205f to your computer and use it in GitHub Desktop.

Select an option

Save algesten/417fdae74e0b6b05680e0ba8e880205f to your computer and use it in GitHub Desktop.
//! # Proposed API Changes for str0m
//!
//! Type-state pattern enforcing correct API usage at compile time.
//!
//! ## Core Principles
//!
//! 1. **Time is driven externally** — caller provides `now: Instant`
//! 2. **Every mutation requires poll-to-timeout** — type system enforces this
//! 3. **All state changes are mutations** — media writes, DirectApi, SDP
//!
//! ## Usage
//!
//! ```ignore
//! let tx = rtc.begin(now);
//! let tx = tx.media(mid).write(rtp_time, payload)?;
//!
//! loop {
//! match tx.poll() {
//! Output::Timeout(when) => break,
//! Output::Transmit(pkt) => send(pkt),
//! Output::Event(evt) => handle(evt),
//! }
//! }
//! ```
use std::marker::PhantomData;
use std::time::Instant;
/// The main WebRTC session object.
///
/// All interaction happens through transactions started with [`begin()`](Self::begin).
pub struct Rtc {}
impl Rtc {
/// Begins a new transaction, advancing time to `now`.
///
/// Processes pending timeouts, then returns an [`RtcTx<Mutation>`] for
/// performing a mutation. Must poll to timeout before starting another.
pub fn begin(&mut self, now: Instant) -> RtcTx<'_, Mutation> {
self.handle_timeout(now);
RtcTx {
rtc: Some(self),
_ph: PhantomData,
polled_to_timeout: false,
}
}
fn handle_timeout(&mut self, _now: Instant) {
todo!()
}
}
/// A transaction handle that borrows the [`Rtc`] session.
///
/// The type parameter `S` tracks state:
/// - [`Mutation`]: can perform a mutation (media write, DirectApi, etc.)
/// - [`PollToTimeout`]: must poll until timeout
///
/// Dropping without completing the transaction panics.
pub struct RtcTx<'a, S> {
/// Option enables transferring the borrow to `MediaWriter`/`DirectApi` via `.take()`.
/// This sidesteps borrow checker issues with Drop + returning the reference.
rtc: Option<&'a mut Rtc>,
_ph: PhantomData<S>,
polled_to_timeout: bool,
}
/// Type-state: can perform a mutation.
pub struct Mutation;
/// Type-state: must poll until timeout.
pub struct PollToTimeout;
/// Methods take `self` to enforce single mutation per transaction.
impl<'a> RtcTx<'a, Mutation> {
/// Gets a writer for the specified media track.
pub fn media(mut self, _mid: Mid) -> MediaWriter<'a> {
MediaWriter {
rtc: self.rtc.take().unwrap(),
}
}
/// Gets the direct API for changes without SDP negotiation.
pub fn direct_api(mut self) -> DirectApi<'a> {
DirectApi {
rtc: self.rtc.take().unwrap(),
}
}
/// Skips mutation, transitions directly to polling.
pub fn timeout_only(mut self) -> RtcTx<'a, PollToTimeout> {
RtcTx {
rtc: self.rtc.take(),
polled_to_timeout: false,
_ph: PhantomData,
}
}
}
/// Takes `&mut self` to allow repeated polling until timeout.
impl<'a> RtcTx<'a, PollToTimeout> {
/// Polls for next output. Call in a loop until `Output::Timeout`.
pub fn poll(&mut self) -> Output {
let is_timeout = todo!();
// If we encounter timeout, mark as polled for Drop trait.
if is_timeout {
self.polled_to_timeout = true;
}
todo!()
}
}
/// Panics if dropped without polling to timeout.
///
/// This catches the case where a transaction is abandoned mid-way, e.g. due to
/// an early return or `?` operator. The panic ensures bugs are caught during
/// development rather than causing silent state corruption.
///
/// When `rtc` is `None`, ownership was transferred (e.g. to `MediaWriter`),
/// so the new owner is responsible for completing the transaction.
impl<'a, S> Drop for RtcTx<'a, S> {
fn drop(&mut self) {
if self.rtc.is_some() && !self.polled_to_timeout {
panic!("RtcTx dropped without polling to timeout");
}
}
}
/// Handle for writing media to a track. Obtained from [`RtcTx::media()`].
pub struct MediaWriter<'a> {
rtc: &'a mut Rtc,
}
impl<'a> MediaWriter<'a> {
/// Writes media data. Returns `RtcTx<PollToTimeout>` that must be polled.
pub fn write(
self,
_rtp_time: Ts,
_payload: Vec<u8>,
) -> Result<RtcTx<'a, PollToTimeout>, Error> {
Ok(RtcTx {
rtc: Some(self.rtc),
_ph: PhantomData,
polled_to_timeout: false,
})
}
}
/// Direct API for changes without SDP. Obtained from [`RtcTx::direct_api()`].
///
/// Methods take `&mut self` to allow multiple mutations before [`commit()`](Self::commit).
pub struct DirectApi<'a> {
rtc: &'a mut Rtc,
}
impl<'a> DirectApi<'a> {
/// Declares a data channel.
pub fn declare_data_channel(&mut self, _id: String) {
todo!()
}
/// Commits changes. Returns `RtcTx<PollToTimeout>` that must be polled.
pub fn commit(self) -> RtcTx<'a, PollToTimeout> {
RtcTx {
rtc: Some(self.rtc),
_ph: PhantomData,
polled_to_timeout: false,
}
}
}
/// Media identifier for a track.
pub struct Mid(String);
/// Errors from mutations.
pub enum Error {}
/// RTP timestamp.
pub struct Ts(u64);
/// Output from polling. Poll until `Timeout`.
pub enum Output {
// Transmit(Packet),
// Event(Event),
// Timeout(Instant),
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment