Skip to main content

unlit3dx/
channel.rs

1//! Subscriptions: what a world reports to the others, and how they read it.
2//!
3//! A subscription is a job that installs a collector in another world. That
4//! world runs the collector once per tick, after the state it reads has been
5//! updated, and the collector hands back what it found. Two kinds of thing can
6//! come back, and they differ in whether history matters:
7//!
8//! - A **snapshot** keeps the newest value only. Publishing replaces the
9//!   slot, so a reader that skips a tick misses history, never the present.
10//!   That is the shape state wants: poses, a playback position, the size a UI
11//!   is laid out at.
12//! - A **stream** keeps every value, once per reader. The collector appends
13//!   what it found to each reader's queue, and the reader folds that queue
14//!   into its own tick with [`StreamReader::pump`](unlit3d::relay::StreamReader::pump). That is the shape events
15//!   want: input, collisions, a sequence ending. A reader that ticks slower
16//!   than the producer still sees every value, in order.
17//!
18//! A third shape is a question, and it is not here: [`Pending`](crate::request::Pending)
19//! sends one job per request and folds each answer back under the key the
20//! caller asked with. That is the shape of something built on request rather
21//! than observed: an entity, a set of assets, a body spawned for one ball.
22//!
23//! Nothing is collected unasked: a world runs the collectors of its
24//! subscriptions only while someone holds a reader, and a dropped reader is
25//! pruned on the next tick. Both kinds are installed with a job, so every
26//! world offers them the same way, built-in or not, and the reading side
27//! never blocks.
28
29use unlit_ecs::World;
30use unlit_ecs::{Entity, bundle};
31
32use crate::job::{JobSender, SendError};
33
34pub use unlit3d::relay::{Snapshot, SnapshotReader, Stream, StreamReader};
35
36impl JobSender {
37    /// Watch a snapshot of this world.
38    ///
39    /// `collect` runs on the world's own thread once per tick, after its state
40    /// has been updated, and whatever it returns is published to the reader.
41    /// Returning [`None`] leaves the newest snapshot in place, which is how a
42    /// collector stays quiet while nothing has changed.
43    ///
44    /// The subscription is a job: it is refused with the queue's error when
45    /// the queue is full or the world has stopped, and the returned reader
46    /// then never sees a snapshot.
47    ///
48    /// # Panics
49    ///
50    /// A collector that publishes into its own world — by calling
51    /// [`publish_collected`] — panics on the borrow it takes of the
52    /// subscription list. A collector only reads the world.
53    pub fn watch<T, F>(&self, collect: F) -> Result<SnapshotReader<T>, SendError>
54    where
55        T: Send + Sync + 'static,
56        F: FnMut(&World) -> Option<T> + Send + 'static,
57    {
58        let snapshot = Snapshot::new();
59        let reader = snapshot.reader();
60        self.send(Box::new(move |world: &mut World| {
61            let entry: Box<dyn Entry> = Box::new(SnapshotCollector { collect, snapshot });
62            install(world, entry);
63        }))?;
64        Ok(reader)
65    }
66
67    /// Subscribe to a stream of this world.
68    ///
69    /// `collect` runs on the world's own thread once per tick and appends
70    /// whatever it found to `out`, which is reused across ticks. Every value
71    /// reaches every reader's queue; the reader drains its queue in its own
72    /// tick with [`StreamReader::pump`].
73    ///
74    /// The subscription is a job: it is refused with the queue's error when
75    /// the queue is full or the world has stopped, and the returned reader
76    /// then never sees a value.
77    ///
78    /// # Panics
79    ///
80    /// A collector that publishes into its own world — by calling
81    /// [`publish_collected`] — panics on the borrow it takes of the
82    /// subscription list. A collector only reads the world.
83    pub fn subscribe<T, F>(&self, collect: F) -> Result<StreamReader<T>, SendError>
84    where
85        T: Clone + Send + 'static,
86        F: FnMut(&World, &mut Vec<T>) + Send + 'static,
87    {
88        let stream = Stream::new();
89        let reader = stream.subscribe();
90        self.send(Box::new(move |world: &mut World| {
91            let entry: Box<dyn Entry> = Box::new(StreamCollector {
92                collect,
93                stream,
94                buffer: Vec::new(),
95            });
96            install(world, entry);
97        }))?;
98        Ok(reader)
99    }
100}
101
102/// Run every subscription the world carries, publishing what each collects.
103///
104/// Returns how many subscriptions are still alive, so a world can skip the
105/// work its collectors would have done when there are none. A reader that has
106/// been dropped is pruned here: the world stops collecting for it on the next
107/// tick.
108///
109/// A runner that drives a world — its thread, the host stepping it, or the
110/// host draining a sub-world — calls this itself at the end of every turn,
111/// after the state a collector reads has been updated, so a tick body
112/// publishes nothing by hand. That is the runner's contract rather than a
113/// guard: a host that drives a world of its own calls this once per turn, and
114/// calling it twice would run every collector twice.
115pub fn publish_collected(world: &World) -> usize {
116    let mut snapshot_outlet_rows = world.root_scope().query::<&mut SnapshotOutlet>();
117
118    let Some(outlet) = snapshot_outlet_rows.iter().next() else {
119        return 0;
120    };
121    outlet.entries.retain_mut(|entry| entry.publish(world));
122    outlet.entries.len()
123}
124
125/// One subscription inside a world: what to collect and where to put it.
126trait Entry {
127    /// Collect and publish one tick's worth; answers whether a reader is alive.
128    fn publish(&mut self, world: &World) -> bool;
129}
130
131/// A subscription that keeps the newest value its collector returns.
132struct SnapshotCollector<T, F> {
133    collect: F,
134    snapshot: Snapshot<T>,
135}
136
137impl<T, F> Entry for SnapshotCollector<T, F>
138where
139    T: Send + Sync + 'static,
140    F: FnMut(&World) -> Option<T> + 'static,
141{
142    fn publish(&mut self, world: &World) -> bool {
143        // Asked before the collector runs: a reader that is gone must not
144        // cost another collection.
145        if !self.snapshot.has_readers() {
146            return false;
147        }
148        if let Some(value) = (self.collect)(world) {
149            self.snapshot.publish(value);
150        }
151        true
152    }
153}
154
155/// A subscription that queues everything its collector appends.
156struct StreamCollector<T, F> {
157    collect: F,
158    stream: Stream<T>,
159    /// Reused across ticks so a collector never allocates its result.
160    buffer: Vec<T>,
161}
162
163impl<T, F> Entry for StreamCollector<T, F>
164where
165    T: Clone + Send + 'static,
166    F: FnMut(&World, &mut Vec<T>) + 'static,
167{
168    fn publish(&mut self, world: &World) -> bool {
169        // Asked before the collector runs: a reader that is gone must not
170        // cost another collection.
171        if !self.stream.has_readers() {
172            return false;
173        }
174        self.buffer.clear();
175        (self.collect)(world, &mut self.buffer);
176        self.stream.publish_all(self.buffer.drain(..)) > 0
177    }
178}
179
180/// Every subscription a world carries, as one component.
181///
182/// The first subscription to arrive creates it; it lives as long as the world
183/// does, and an entry is pruned once its reader is dropped.
184struct SnapshotOutlet {
185    entries: Vec<Box<dyn Entry>>,
186}
187
188/// Add one subscription to `world`, creating the outlet on first use.
189fn install(world: &mut World, entry: Box<dyn Entry>) {
190    let existing = world
191        .root_scope()
192        .query::<(&SnapshotOutlet, Entity)>()
193        .iter()
194        .next()
195        .map(|(_snapshot_outlet, entity)| entity);
196    match existing {
197        Some(entity) => {
198            let mut outlet = world
199                .root_scope()
200                .query_one::<&mut SnapshotOutlet>(entity)
201                .expect("the subscription outlet exists");
202            outlet.get().entries.push(entry);
203        }
204        None => {
205            world.root_scope_mut().spawn(bundle![SnapshotOutlet {
206                entries: vec![entry],
207            },]);
208        }
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use std::sync::Arc;
215    use std::sync::atomic::{AtomicU32, Ordering};
216
217    use super::*;
218
219    /// Install a collector into a fresh world, returning the reader it fills.
220    fn subscribe_in(
221        world: &mut World,
222        collect: impl FnMut(&World) -> Option<u32> + 'static,
223    ) -> SnapshotReader<u32> {
224        let snapshot = Snapshot::new();
225        let reader = snapshot.reader();
226        install(world, Box::new(SnapshotCollector { collect, snapshot }));
227        reader
228    }
229
230    /// A world with no subscription has nothing to run.
231    #[test]
232    fn a_world_without_subscriptions_publishes_nothing() {
233        assert_eq!(publish_collected(&World::new()), 0);
234    }
235
236    /// Each tick publishes what the collector returns, newest wins.
237    #[test]
238    fn a_subscription_publishes_what_its_collector_returns() {
239        let ticks = Arc::new(AtomicU32::new(0));
240        let counted = Arc::clone(&ticks);
241        let mut world = World::new();
242        let reader = subscribe_in(&mut world, move |_| {
243            Some(counted.fetch_add(1, Ordering::SeqCst))
244        });
245
246        assert_eq!(publish_collected(&world), 1);
247        assert_eq!(publish_collected(&world), 1);
248        assert_eq!(*reader.latest().expect("a snapshot"), 1);
249        assert_eq!(ticks.load(Ordering::SeqCst), 2);
250    }
251
252    /// A collector that answers nothing leaves the newest snapshot in place.
253    #[test]
254    fn a_collector_that_returns_nothing_leaves_the_newest() {
255        let mut first = true;
256        let mut world = World::new();
257        let reader = subscribe_in(&mut world, move |_| {
258            let value = if first { Some(1) } else { None };
259            first = false;
260            value
261        });
262
263        publish_collected(&world);
264        publish_collected(&world);
265        assert_eq!(*reader.latest().expect("the first snapshot"), 1);
266    }
267
268    /// Dropping the reader prunes the subscription on the next tick.
269    #[test]
270    fn dropping_the_reader_prunes_the_subscription() {
271        let mut world = World::new();
272        let reader = subscribe_in(&mut world, |_| Some(1));
273        assert_eq!(publish_collected(&world), 1);
274        drop(reader);
275        assert_eq!(publish_collected(&world), 0);
276    }
277
278    /// A snapshot already read stays valid and unchanged while later ones are
279    /// published: the producer swaps the slot, it does not write into what a
280    /// reader holds.
281    #[test]
282    fn a_read_snapshot_survives_later_publications() {
283        let mut tick = 0;
284        let mut world = World::new();
285        let reader = subscribe_in(&mut world, move |_| {
286            tick += 1;
287            Some(tick)
288        });
289
290        publish_collected(&world);
291        let first = reader.latest().expect("the first snapshot");
292        assert_eq!(*first, 1);
293
294        publish_collected(&world);
295        let second = reader.latest().expect("the second snapshot");
296        assert_eq!(*second, 2);
297        assert_eq!(*first, 1, "the held snapshot did not change under it");
298    }
299
300    /// Every value a stream collector appends reaches every reader, and each
301    /// reader folds its own queue on its own tick.
302    #[test]
303    fn a_stream_subscription_delivers_every_value() {
304        let mut tick = 0;
305        let mut world = World::new();
306        let stream = Stream::new();
307        let reader = stream.subscribe();
308        install(
309            &mut world,
310            Box::new(StreamCollector {
311                collect: move |_world: &World, out: &mut Vec<u32>| {
312                    tick += 1;
313                    out.extend([tick, tick * 10]);
314                },
315                stream,
316                buffer: Vec::new(),
317            }),
318        );
319
320        publish_collected(&world);
321        publish_collected(&world);
322        reader.pump();
323        assert_eq!(
324            reader.with_batch(|batch| batch.to_vec()),
325            vec![1, 10, 2, 20]
326        );
327
328        // A second pump with nothing new replaces the batch rather than
329        // repeating the events of the previous tick.
330        reader.pump();
331        assert!(reader.with_batch(|batch| batch.to_vec()).is_empty());
332    }
333}