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}