gled-ui 2.32.1

gled's egui interface, shared by the desktop app and the browser
Documentation
//! The interface's connection to the engine, whatever carries it: the
//! desktop app talks to its engine in the same process, the browser and a
//! remote desktop over a WebSocket. Actions and other input enqueued on the
//! interface's thread go through it, numbered, so the interface knows when
//! the engine applied its edits.

use std::{
    cell::{Cell, RefCell},
    rc::Rc,
    sync::Arc,
};

use gled_protocol::{
    action::route_actions_on_this_thread,
    message::{ClientMessage, ServerMessage, Subscriptions},
    state::{ClientInput, route_inputs_on_this_thread},
};
use web_time::{Duration, Instant};

/// How long without any message from a remote engine counts as an
/// interrupted connection. The engine sends the beat timing about 30 times
/// a second.
const SILENCE: Duration = Duration::from_secs(3);

/// How messages reach the engine and come back.
pub trait EngineLink {
    fn send(&mut self, message: ClientMessage);
    /// The messages that arrived since the last call.
    fn receive(&mut self) -> Vec<ServerMessage>;

    /// Whether the messages travel over a network, where the connection can
    /// be interrupted.
    fn is_remote(&self) -> bool {
        false
    }

    /// Whether the network connection is open.
    fn is_open(&self) -> bool {
        true
    }

    /// Where the engine runs, if the interface should say so while it
    /// connects.
    fn target(&self) -> Option<String> {
        None
    }

    /// Sends from any thread, without waiting for the interface's next frame,
    /// where the link can.
    fn background_sender(&self) -> Option<BackgroundSender> {
        None
    }
}

/// Sends a message to the engine from any thread.
pub type BackgroundSender = Arc<dyn Fn(ClientMessage) + Send + Sync>;

/// A connection to a remote engine that is interrupted.
#[derive(Debug, Clone, Copy)]
pub(crate) struct Interruption {
    /// How long ago the last message arrived.
    pub(crate) last_contact: Duration,
}

/// The interface's end of its link. Cheap to clone; all clones share the
/// link and the numbering.
#[derive(Clone)]
pub struct Connection {
    link: Rc<RefCell<Box<dyn EngineLink>>>,
    /// Number of the last action sent.
    sequence: Rc<Cell<u64>>,
    /// When the last message from the engine arrived.
    last_contact: Rc<Cell<Option<Instant>>>,
}

impl std::fmt::Debug for Connection {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Connection")
            .field("sequence", &self.sequence.get())
            .finish_non_exhaustive()
    }
}

impl Connection {
    /// Wraps `link` and makes actions and client inputs enqueued on the
    /// calling thread go through it. Call on the interface's thread.
    pub fn new(link: Box<dyn EngineLink>, subscriptions: Subscriptions) -> Self {
        let connection = Self {
            link: Rc::new(RefCell::new(link)),
            sequence: Rc::default(),
            last_contact: Rc::default(),
        };
        connection.send(ClientMessage::Subscribe(subscriptions));
        let actions = connection.clone();
        route_actions_on_this_thread(move |action| {
            let sequence = actions.sequence.get() + 1;
            actions.sequence.set(sequence);
            actions.send(ClientMessage::Input(ClientInput::Action {
                sequence,
                action,
            }));
        });
        let inputs = connection.clone();
        route_inputs_on_this_thread(move |input| inputs.send(ClientMessage::Input(input)));
        connection
    }

    pub(crate) fn send(&self, message: ClientMessage) {
        match self.link.try_borrow_mut() {
            Ok(mut link) => link.send(message),
            Err(_) => tracing::warn!("Dropped {message:?}: the link is busy"),
        }
    }

    pub(crate) fn receive(&self) -> Vec<ServerMessage> {
        let mut link = self.link.borrow_mut();
        let messages = link.receive();
        // A link that is not open only reports its own errors.
        if !messages.is_empty() && link.is_open() {
            self.last_contact.set(Some(Instant::now()));
        }
        messages
    }

    /// Whether the engine runs on another machine.
    pub(crate) fn is_remote(&self) -> bool {
        self.link.borrow().is_remote()
    }

    pub(crate) fn target(&self) -> Option<String> {
        self.link.borrow().target()
    }

    pub(crate) fn background_sender(&self) -> Option<BackgroundSender> {
        self.link.borrow().background_sender()
    }

    /// Whether the connection to a remote engine is interrupted: closed, or
    /// silent for a while. `None` while all is well and before the first
    /// contact, which the interface waits for anyway.
    pub(crate) fn interruption(&self) -> Option<Interruption> {
        let link = self.link.borrow();
        let last_contact = self.last_contact.get()?.elapsed();
        (link.is_remote() && (!link.is_open() || last_contact > SILENCE))
            .then_some(Interruption { last_contact })
    }

    /// The number of the last action sent.
    pub(crate) fn sent_sequence(&self) -> u64 {
        self.sequence.get()
    }

    /// Starts the numbering over, for a new session with the engine after
    /// the link reconnected.
    pub(crate) fn reset_sequence(&self) {
        self.sequence.set(0);
    }
}