vesper-player-plugin 0.5.1

Safe Rust author SDK for Vesper native plugins.
Documentation
use std::mem;
use std::sync::{Arc, Mutex};

use player_plugin_abi::VESPER_MAX_SESSIONS_PER_INTERFACE;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum SessionRegistryError {
    Stale,
    Busy,
    Exhausted,
}

enum SessionState<S> {
    Open(S),
    Busy,
    Closed,
}

struct SessionSlot<S> {
    generation: u32,
    state: Arc<Mutex<SessionState<S>>>,
}

pub(super) struct SessionRegistry<S> {
    slots: Vec<SessionSlot<S>>,
}

impl<S> Default for SessionRegistry<S> {
    fn default() -> Self {
        Self { slots: Vec::new() }
    }
}

impl<S> SessionRegistry<S> {
    pub(super) fn insert(&mut self, value: S) -> Result<u64, (SessionRegistryError, S)> {
        for (index, slot) in self.slots.iter_mut().enumerate() {
            if slot.generation == u32::MAX {
                continue;
            }
            let state = slot.state.lock().unwrap_or_else(|error| error.into_inner());
            if !matches!(*state, SessionState::Closed) {
                continue;
            }
            drop(state);
            let generation = slot.generation + 1;
            let token = match encode_token(index, generation) {
                Ok(token) => token,
                Err(error) => return Err((error, value)),
            };
            slot.generation = generation;
            slot.state = Arc::new(Mutex::new(SessionState::Open(value)));
            return Ok(token);
        }

        if self.slots.len() >= VESPER_MAX_SESSIONS_PER_INTERFACE {
            return Err((SessionRegistryError::Exhausted, value));
        }
        let generation = 1;
        let index = self.slots.len();
        let token = match encode_token(index, generation) {
            Ok(token) => token,
            Err(error) => return Err((error, value)),
        };
        self.slots.push(SessionSlot {
            generation,
            state: Arc::new(Mutex::new(SessionState::Open(value))),
        });
        Ok(token)
    }

    pub(super) fn acquire(&self, token: u64) -> Result<SessionGuard<S>, SessionRegistryError> {
        let state = self.resolve(token)?;
        let mut locked = state.lock().unwrap_or_else(|error| error.into_inner());
        match mem::replace(&mut *locked, SessionState::Busy) {
            SessionState::Open(value) => {
                drop(locked);
                Ok(SessionGuard {
                    state,
                    value: Some(value),
                })
            }
            SessionState::Busy => {
                *locked = SessionState::Busy;
                Err(SessionRegistryError::Busy)
            }
            SessionState::Closed => {
                *locked = SessionState::Closed;
                Err(SessionRegistryError::Stale)
            }
        }
    }

    pub(super) fn begin_close(
        &self,
        token: u64,
    ) -> Result<Option<SessionCloseGuard<S>>, SessionRegistryError> {
        let state = self.resolve(token)?;
        let mut locked = state.lock().unwrap_or_else(|error| error.into_inner());
        match mem::replace(&mut *locked, SessionState::Busy) {
            SessionState::Open(value) => {
                drop(locked);
                Ok(Some(SessionCloseGuard {
                    state,
                    value: Some(value),
                    committed: false,
                }))
            }
            SessionState::Closed => {
                *locked = SessionState::Closed;
                Ok(None)
            }
            SessionState::Busy => {
                *locked = SessionState::Busy;
                Err(SessionRegistryError::Busy)
            }
        }
    }

    fn resolve(&self, token: u64) -> Result<Arc<Mutex<SessionState<S>>>, SessionRegistryError> {
        let (index, generation) = decode_token(token)?;
        let slot = self.slots.get(index).ok_or(SessionRegistryError::Stale)?;
        if slot.generation != generation {
            return Err(SessionRegistryError::Stale);
        }
        Ok(slot.state.clone())
    }
}

pub(super) struct SessionGuard<S> {
    state: Arc<Mutex<SessionState<S>>>,
    value: Option<S>,
}

impl<S> SessionGuard<S> {
    pub(super) fn value_mut(&mut self) -> Option<&mut S> {
        self.value.as_mut()
    }
}

impl<S> Drop for SessionGuard<S> {
    fn drop(&mut self) {
        let Some(value) = self.value.take() else {
            return;
        };
        let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
        if matches!(*state, SessionState::Busy) {
            *state = SessionState::Open(value);
        }
    }
}

pub(super) struct SessionCloseGuard<S> {
    state: Arc<Mutex<SessionState<S>>>,
    value: Option<S>,
    committed: bool,
}

impl<S> SessionCloseGuard<S> {
    pub(super) fn value_mut(&mut self) -> Option<&mut S> {
        self.value.as_mut()
    }

    pub(super) fn commit(mut self) {
        let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
        if matches!(*state, SessionState::Busy) {
            *state = SessionState::Closed;
        }
        self.value.take();
        self.committed = true;
    }
}

impl<S> Drop for SessionCloseGuard<S> {
    fn drop(&mut self) {
        if self.committed {
            return;
        }
        let Some(value) = self.value.take() else {
            return;
        };
        let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
        if matches!(*state, SessionState::Busy) {
            *state = SessionState::Open(value);
        }
    }
}

fn encode_token(index: usize, generation: u32) -> Result<u64, SessionRegistryError> {
    let slot = u32::try_from(index)
        .ok()
        .and_then(|index| index.checked_add(1))
        .ok_or(SessionRegistryError::Exhausted)?;
    Ok((u64::from(generation) << 32) | u64::from(slot))
}

fn decode_token(token: u64) -> Result<(usize, u32), SessionRegistryError> {
    let generation = (token >> 32) as u32;
    let slot = token as u32;
    if generation == 0 || slot == 0 {
        return Err(SessionRegistryError::Stale);
    }
    Ok(((slot - 1) as usize, generation))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn tokens_are_nonzero_and_reuse_increments_generation() {
        let mut registry = SessionRegistry::default();
        let first = registry.insert(10).expect("insert first");
        assert_ne!(first, 0);
        registry
            .begin_close(first)
            .expect("begin close")
            .expect("open session")
            .commit();
        assert!(
            registry
                .begin_close(first)
                .expect("closed lookup")
                .is_none()
        );

        let second = registry.insert(20).expect("reuse slot");
        assert_ne!(second, first);
        assert!(matches!(
            registry.acquire(first),
            Err(SessionRegistryError::Stale)
        ));
        assert_eq!(
            *registry
                .acquire(second)
                .expect("new token")
                .value_mut()
                .expect("open value"),
            20
        );
    }

    #[test]
    fn guard_returns_the_session_after_unwind() {
        let mut registry = SessionRegistry::default();
        let token = registry.insert(String::from("open")).expect("insert");
        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            let mut session = registry.acquire(token).expect("acquire");
            session
                .value_mut()
                .expect("open value")
                .push_str("-mutated");
            panic!("fixture panic");
        }));
        assert!(result.is_err());
        assert_eq!(
            registry
                .acquire(token)
                .expect("reacquire")
                .value_mut()
                .expect("open value"),
            "open-mutated"
        );
    }

    #[test]
    fn concurrent_use_is_reported_as_busy() {
        let mut registry = SessionRegistry::default();
        let token = registry.insert(1).expect("insert");
        let _guard = registry.acquire(token).expect("first acquire");
        assert!(matches!(
            registry.acquire(token),
            Err(SessionRegistryError::Busy)
        ));
        assert_eq!(
            registry.begin_close(token).map(|guard| guard.is_some()),
            Err(SessionRegistryError::Busy)
        );
    }

    #[test]
    fn uncommitted_close_restores_the_session_for_retry() {
        let mut registry = SessionRegistry::default();
        let token = registry.insert(String::from("open")).expect("insert");
        {
            let mut closing = registry
                .begin_close(token)
                .expect("begin close")
                .expect("open session");
            closing
                .value_mut()
                .expect("closing value")
                .push_str("-first-attempt");
        }
        let mut retry = registry
            .begin_close(token)
            .expect("retry close")
            .expect("restored session");
        assert_eq!(
            retry.value_mut().expect("retry value"),
            "open-first-attempt"
        );
        retry.commit();
        assert!(
            registry
                .begin_close(token)
                .expect("closed lookup")
                .is_none()
        );
    }
}