Skip to main content

unlit3dx/
job.rs

1//! Jobs: work handed to another world.
2//!
3//! A [`Job`](crate::job::Job) is a boxed closure the target world runs on its
4//! own thread. The sender never waits for it: a queue that is full or stopped
5//! reports [`SendError`](crate::job::SendError) and the job comes back.
6//!
7//! A job never answers back directly. What one world learns from another is
8//! state, and state travels the other way: a subscription publishes it, and the
9//! caller reads it on its own tick. A caller that needs the value one job
10//! produces asks for it under a key — [`Pending`](crate::request::Pending)
11//! sends the job, remembers the key, and answers on the caller's next tick — or
12//! publishes that value itself, through the snapshot its subscription fills.
13
14use unlit_ecs::World;
15
16/// A unit of work for a world: a boxed closure that runs against it.
17///
18/// A job answers nothing: the queue is one way, and a value the caller
19/// wants back travels as state, published by the world that made it and
20/// read on the caller's own tick. A [`Pending`](crate::request::Pending) is
21/// that round trip: one job per request, and the key its answer comes back
22/// under.
23pub type Job = Box<dyn FnOnce(&mut World) + Send + 'static>;
24
25/// Why a job was not handed to a world.
26#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
27pub enum SendError {
28    /// The world's command queue is full; the job was not queued.
29    #[error("the world's command queue is full")]
30    Full,
31    /// The world has stopped; the job was not queued.
32    #[error("the world has stopped")]
33    Closed,
34}
35
36impl<T> From<async_channel::TrySendError<T>> for SendError {
37    fn from(error: async_channel::TrySendError<T>) -> Self {
38        match error {
39            async_channel::TrySendError::Full(_) => Self::Full,
40            async_channel::TrySendError::Closed(_) => Self::Closed,
41        }
42    }
43}
44
45/// A cloneable handle to a world's job queue.
46///
47/// A world is reached either through the [`WorldHandle`](crate::handle::WorldHandle)
48/// it was spawned with or through the [`WorldSender`](crate::world::WorldSender)
49/// its proxy entity carries; both hold one of these. Sends never block: a full
50/// or stopped queue reports [`SendError`] and the job is dropped.
51#[derive(Clone)]
52pub struct JobSender {
53    queue: async_channel::Sender<Job>,
54}
55
56impl JobSender {
57    /// Wrap a world's command channel.
58    #[must_use]
59    pub fn new(queue: async_channel::Sender<Job>) -> Self {
60        Self { queue }
61    }
62
63    /// Queue a job. Never blocks.
64    ///
65    /// An `Ok` means the job is in the queue; whether it runs is the world's
66    /// business. A world its owner has asked to stop keeps accepting jobs
67    /// until its driver's next turn, and drops them when it ends, so a caller
68    /// that must know the job ran reads the world's state instead.
69    pub fn send(&self, job: Job) -> Result<(), SendError> {
70        self.queue.try_send(job).map_err(SendError::from)
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    /// A world that stopped refuses the job and says why.
79    #[test]
80    fn sending_to_a_stopped_world_reports_closed() {
81        let (sender, receiver) = async_channel::unbounded();
82        let jobs = JobSender::new(sender);
83        drop(receiver);
84        assert_eq!(
85            jobs.send(Box::new(|_: &mut World| {})),
86            Err(SendError::Closed)
87        );
88    }
89}