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}