Skip to main content

nexus_rt/
callback.rs

1//! Context-owning handler dispatch.
2//!
3//! [`Callback`] is the unified dispatch type in nexus-rt. Context-free
4//! handlers use `Callback<(), CtxFree<F>, P>` (via [`IntoHandler`](crate::IntoHandler));
5//! context-owning handlers use `Callback<C, F, P>` (via [`IntoCallback`]).
6//!
7//! The function convention for context-owning callbacks is
8//! `fn handler(ctx: &mut C, params: Res<T>..., event: E)` — context first,
9//! [`Param`]-resolved resources in the middle, event last.
10//!
11//! Same HRTB double-bound pattern, same macro generation, same ~2-cycle
12//! dispatch. Named functions only (same closure limitation as
13//! [`IntoHandler`](crate::IntoHandler)).
14//!
15//! # When to use Callback
16//!
17//! Use [`Callback`] over [`IntoHandler`](crate::IntoHandler) when each
18//! handler instance needs its own private state that isn't shared via
19//! [`World`](crate::World):
20//!
21//! - **Per-timer context** — each timer carries its own order ID, retry
22//!   count, or deadline metadata.
23//! - **Per-connection state** — each socket handler carries its own codec
24//!   state, read buffer, or session context.
25//! - **Protocol state machines** — each handler instance tracks its own
26//!   position in a protocol handshake or reconnection sequence.
27//!
28//! The `ctx` field is `pub`, so drivers can read or mutate it between
29//! dispatches (e.g. to update a deadline or check a counter).
30
31use crate::Handler;
32use crate::handler::Param;
33use crate::world::{Registry, World};
34
35// =============================================================================
36// Callback<C, F, Params>
37// =============================================================================
38
39/// Unified dispatch type. Stores per-callback context alongside
40/// pre-resolved resource access.
41///
42/// - Context-free handlers: `Callback<(), CtxFree<F>, P>` — created via
43///   [`IntoHandler::into_handler`](crate::IntoHandler::into_handler).
44/// - Context-owning handlers: `Callback<C, F, P>` — created via
45///   [`IntoCallback::into_callback`].
46///
47/// Both implement [`Handler<E>`].
48///
49/// # Examples
50///
51/// ```
52/// use nexus_rt::{WorldBuilder, ResMut, IntoCallback, Handler};
53///
54/// struct Ctx { count: u64 }
55///
56/// fn handler(ctx: &mut Ctx, mut val: ResMut<u64>, event: u32) {
57///     *val += event as u64;
58///     ctx.count += 1;
59/// }
60///
61/// let mut builder = WorldBuilder::new();
62/// builder.register::<u64>(0);
63/// let mut world = builder.build();
64///
65/// let mut cb = handler.into_callback(Ctx { count: 0 }, world.registry());
66/// cb.run(&mut world, 10u32);
67///
68/// assert_eq!(cb.ctx.count, 1);
69/// assert_eq!(*world.resource::<u64>(), 10);
70/// ```
71pub struct Callback<C, F, Params: Param> {
72    /// Per-callback owned state. Accessible outside dispatch.
73    pub ctx: C,
74    pub(crate) f: F,
75    pub(crate) state: Params::State,
76    pub(crate) name: &'static str,
77}
78
79// =============================================================================
80// IntoCallback
81// =============================================================================
82
83/// Converts a named function into a [`Callback`].
84///
85/// Identical to [`IntoHandler`](crate::IntoHandler) but injects `&mut C` as
86/// the first parameter. [`ResourceId`](crate::ResourceId)s resolved via
87/// `registry.id::<T>()` at call time — panics if any resource is not
88/// registered.
89///
90/// Use `IntoCallback` when each handler instance needs its own private
91/// state. For stateless handlers (or state shared via [`World`](crate::World)),
92/// prefer [`IntoHandler`](crate::IntoHandler).
93///
94/// # Named functions only
95///
96/// Closures do not work with `IntoCallback` due to Rust's HRTB inference
97/// limitations with GATs. Use named `fn` items instead.
98///
99/// # Examples
100///
101/// ```
102/// use nexus_rt::{WorldBuilder, ResMut, IntoCallback, Handler};
103///
104/// struct TimerCtx { order_id: u64, fires: u64 }
105///
106/// fn on_timeout(ctx: &mut TimerCtx, mut counter: ResMut<u64>, _event: ()) {
107///     ctx.fires += 1;
108///     *counter += ctx.order_id;
109/// }
110///
111/// let mut builder = WorldBuilder::new();
112/// builder.register::<u64>(0);
113/// let mut world = builder.build();
114///
115/// let mut cb = on_timeout.into_callback(
116///     TimerCtx { order_id: 42, fires: 0 },
117///     world.registry(),
118/// );
119/// cb.run(&mut world, ());
120///
121/// assert_eq!(cb.ctx.fires, 1);
122/// assert_eq!(*world.resource::<u64>(), 42);
123/// ```
124///
125/// # Panics
126///
127/// Panics if any [`Param`] resource is not registered in the
128/// [`Registry`](crate::Registry).
129pub trait IntoCallback<C, E, Params> {
130    /// The concrete Callback type produced.
131    type Callback: Handler<E>;
132
133    /// Convert this function + context into a Callback.
134    fn into_callback(self, ctx: C, registry: &Registry) -> Self::Callback;
135}
136
137// =============================================================================
138// Arity 0: fn(ctx: &mut C, E) — context + event only, no Param
139// =============================================================================
140
141impl<C: Send + 'static, E, F: FnMut(&mut C, E) + Send + 'static> IntoCallback<C, E, ()> for F {
142    type Callback = Callback<C, F, ()>;
143
144    fn into_callback(self, ctx: C, registry: &Registry) -> Self::Callback {
145        Callback {
146            ctx,
147            f: self,
148            state: <() as Param>::init(registry),
149            name: std::any::type_name::<F>(),
150        }
151    }
152}
153
154impl<C: Send + 'static, E, F: FnMut(&mut C, E) + Send + 'static> Handler<E> for Callback<C, F, ()> {
155    fn run(&mut self, _world: &mut World, event: E) {
156        (self.f)(&mut self.ctx, event);
157    }
158
159    fn name(&self) -> &'static str {
160        self.name
161    }
162}
163
164// =============================================================================
165// Macro-generated impls (arities 1-8)
166// =============================================================================
167
168macro_rules! impl_into_callback {
169    ($($P:ident),+) => {
170        impl<C: Send + 'static, E, F: Send + 'static, $($P: Param + 'static),+>
171            IntoCallback<C, E, ($($P,)+)> for F
172        where
173            for<'a> &'a mut F:
174                FnMut(&mut C, $($P,)+ E) +
175                FnMut(&mut C, $($P::Item<'a>,)+ E),
176        {
177            type Callback = Callback<C, F, ($($P,)+)>;
178
179            fn into_callback(self, ctx: C, registry: &Registry) -> Self::Callback {
180                let state = <($($P,)+) as Param>::init(registry);
181                {
182                    #[allow(non_snake_case)]
183                    let ($($P,)+) = &state;
184                    registry.check_access(&[
185                        $(
186                            (<$P as Param>::resource_id($P),
187                             std::any::type_name::<$P>()),
188                        )+
189                    ]);
190                }
191                Callback { ctx, f: self, state, name: std::any::type_name::<F>() }
192            }
193        }
194
195        impl<C: Send + 'static, E, F: Send + 'static, $($P: Param + 'static),+>
196            Handler<E> for Callback<C, F, ($($P,)+)>
197        where
198            for<'a> &'a mut F:
199                FnMut(&mut C, $($P,)+ E) +
200                FnMut(&mut C, $($P::Item<'a>,)+ E),
201        {
202            #[allow(non_snake_case)]
203            fn run(&mut self, world: &mut World, event: E) {
204                #[allow(clippy::too_many_arguments)]
205                fn call_inner<Ctx, $($P,)+ Ev>(
206                    mut f: impl FnMut(&mut Ctx, $($P,)+ Ev),
207                    ctx: &mut Ctx,
208                    $($P: $P,)+
209                    event: Ev,
210                ) {
211                    f(ctx, $($P,)+ event);
212                }
213
214                // SAFETY: state was produced by init() on the same registry
215                // that built this world. Single-threaded sequential dispatch
216                // ensures no mutable aliasing across params.
217                #[cfg(debug_assertions)]
218                world.clear_borrows();
219                let ($($P,)+) = unsafe {
220                    <($($P,)+) as Param>::fetch(world, &mut self.state)
221                };
222                call_inner(&mut self.f, &mut self.ctx, $($P,)+ event);
223            }
224
225            fn name(&self) -> &'static str {
226                self.name
227            }
228        }
229    };
230}
231
232// Reuse all_tuples — re-declared here since macros are module-scoped.
233macro_rules! all_tuples {
234    ($m:ident) => {
235        $m!(P0);
236        $m!(P0, P1);
237        $m!(P0, P1, P2);
238        $m!(P0, P1, P2, P3);
239        $m!(P0, P1, P2, P3, P4);
240        $m!(P0, P1, P2, P3, P4, P5);
241        $m!(P0, P1, P2, P3, P4, P5, P6);
242        $m!(P0, P1, P2, P3, P4, P5, P6, P7);
243    };
244}
245
246all_tuples!(impl_into_callback);
247
248// =============================================================================
249// Tests
250// =============================================================================
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use crate::{Local, Res, ResMut, WorldBuilder};
256
257    // -- Helper types ---------------------------------------------------------
258
259    struct TimerCtx {
260        order_id: u64,
261        call_count: u64,
262    }
263
264    struct OrderCache {
265        expired: Vec<u64>,
266    }
267
268    // -- Core dispatch --------------------------------------------------------
269
270    fn ctx_only_handler(ctx: &mut TimerCtx, _event: u32) {
271        ctx.call_count += 1;
272    }
273
274    #[test]
275    fn ctx_only_no_params() {
276        let mut world = WorldBuilder::new().build();
277        let mut cb = ctx_only_handler.into_callback(
278            TimerCtx {
279                order_id: 1,
280                call_count: 0,
281            },
282            world.registry_mut(),
283        );
284        cb.run(&mut world, 42u32);
285        assert_eq!(cb.ctx.call_count, 1);
286    }
287
288    fn ctx_one_res_handler(ctx: &mut TimerCtx, cache: Res<OrderCache>, _event: u32) {
289        ctx.call_count += cache.expired.len() as u64;
290    }
291
292    #[test]
293    fn ctx_one_res() {
294        let mut builder = WorldBuilder::new();
295        builder.register::<OrderCache>(OrderCache {
296            expired: vec![1, 2, 3],
297        });
298        let mut world = builder.build();
299
300        let mut cb = ctx_one_res_handler.into_callback(
301            TimerCtx {
302                order_id: 1,
303                call_count: 0,
304            },
305            world.registry_mut(),
306        );
307        cb.run(&mut world, 0u32);
308        assert_eq!(cb.ctx.call_count, 3);
309    }
310
311    fn ctx_one_res_mut_handler(ctx: &mut TimerCtx, mut cache: ResMut<OrderCache>, _event: u32) {
312        cache.expired.push(ctx.order_id);
313        ctx.call_count += 1;
314    }
315
316    #[test]
317    fn ctx_one_res_mut() {
318        let mut builder = WorldBuilder::new();
319        builder.register::<OrderCache>(OrderCache { expired: vec![] });
320        let mut world = builder.build();
321
322        let mut cb = ctx_one_res_mut_handler.into_callback(
323            TimerCtx {
324                order_id: 42,
325                call_count: 0,
326            },
327            world.registry_mut(),
328        );
329        cb.run(&mut world, 0u32);
330        assert_eq!(cb.ctx.call_count, 1);
331        assert_eq!(world.resource::<OrderCache>().expired, vec![42]);
332    }
333
334    fn ctx_two_params_handler(
335        ctx: &mut TimerCtx,
336        counter: Res<u64>,
337        mut cache: ResMut<OrderCache>,
338        _event: u32,
339    ) {
340        cache.expired.push(*counter);
341        ctx.call_count += 1;
342    }
343
344    #[test]
345    fn ctx_two_params() {
346        let mut builder = WorldBuilder::new();
347        builder.register::<u64>(99);
348        builder.register::<OrderCache>(OrderCache { expired: vec![] });
349        let mut world = builder.build();
350
351        let mut cb = ctx_two_params_handler.into_callback(
352            TimerCtx {
353                order_id: 0,
354                call_count: 0,
355            },
356            world.registry_mut(),
357        );
358        cb.run(&mut world, 0u32);
359        assert_eq!(cb.ctx.call_count, 1);
360        assert_eq!(world.resource::<OrderCache>().expired, vec![99]);
361    }
362
363    fn ctx_three_params_handler(
364        ctx: &mut TimerCtx,
365        a: Res<u64>,
366        b: Res<bool>,
367        mut c: ResMut<OrderCache>,
368        _event: u32,
369    ) {
370        if *b {
371            c.expired.push(*a);
372        }
373        ctx.call_count += 1;
374    }
375
376    #[test]
377    fn ctx_three_params() {
378        let mut builder = WorldBuilder::new();
379        builder.register::<u64>(7);
380        builder.register::<bool>(true);
381        builder.register::<OrderCache>(OrderCache { expired: vec![] });
382        let mut world = builder.build();
383
384        let mut cb = ctx_three_params_handler.into_callback(
385            TimerCtx {
386                order_id: 0,
387                call_count: 0,
388            },
389            world.registry_mut(),
390        );
391        cb.run(&mut world, 0u32);
392        assert_eq!(cb.ctx.call_count, 1);
393        assert_eq!(world.resource::<OrderCache>().expired, vec![7]);
394    }
395
396    // -- Context ownership ----------------------------------------------------
397
398    #[test]
399    fn ctx_mutated_persists() {
400        let mut world = WorldBuilder::new().build();
401        let mut cb = ctx_only_handler.into_callback(
402            TimerCtx {
403                order_id: 1,
404                call_count: 0,
405            },
406            world.registry_mut(),
407        );
408        cb.run(&mut world, 0u32);
409        cb.run(&mut world, 0u32);
410        cb.run(&mut world, 0u32);
411        assert_eq!(cb.ctx.call_count, 3);
412    }
413
414    #[test]
415    fn ctx_accessible_outside_dispatch() {
416        let mut world = WorldBuilder::new().build();
417        let mut cb = ctx_only_handler.into_callback(
418            TimerCtx {
419                order_id: 42,
420                call_count: 0,
421            },
422            world.registry_mut(),
423        );
424        assert_eq!(cb.ctx.order_id, 42);
425        assert_eq!(cb.ctx.call_count, 0);
426        cb.run(&mut world, 0u32);
427        assert_eq!(cb.ctx.call_count, 1);
428    }
429
430    #[test]
431    fn ctx_mutated_outside_dispatch() {
432        let mut world = WorldBuilder::new().build();
433        let mut cb = ctx_only_handler.into_callback(
434            TimerCtx {
435                order_id: 1,
436                call_count: 0,
437            },
438            world.registry_mut(),
439        );
440        cb.ctx.order_id = 99;
441        cb.run(&mut world, 0u32);
442        assert_eq!(cb.ctx.order_id, 99);
443        assert_eq!(cb.ctx.call_count, 1);
444    }
445
446    // -- Safety validation ----------------------------------------------------
447
448    #[test]
449    #[should_panic(expected = "not registered")]
450    fn panics_on_missing_resource() {
451        let mut world = WorldBuilder::new().build();
452
453        fn needs_cache(_ctx: &mut TimerCtx, _cache: Res<OrderCache>, _e: u32) {}
454
455        let _cb = needs_cache.into_callback(
456            TimerCtx {
457                order_id: 0,
458                call_count: 0,
459            },
460            world.registry_mut(),
461        );
462    }
463
464    #[test]
465    #[should_panic(expected = "not registered")]
466    fn panics_on_second_missing() {
467        let mut builder = WorldBuilder::new();
468        builder.register::<u64>(0);
469        let mut world = builder.build();
470
471        fn needs_two(_ctx: &mut TimerCtx, _a: Res<u64>, _b: Res<OrderCache>, _e: u32) {}
472
473        let _cb = needs_two.into_callback(
474            TimerCtx {
475                order_id: 0,
476                call_count: 0,
477            },
478            world.registry_mut(),
479        );
480    }
481
482    // -- Access conflict detection --------------------------------------------
483
484    #[test]
485    #[should_panic(expected = "conflicting access")]
486    fn callback_duplicate_access_panics() {
487        let mut builder = WorldBuilder::new();
488        builder.register::<u64>(0);
489        let mut world = builder.build();
490
491        fn bad(_ctx: &mut u64, a: Res<u64>, b: ResMut<u64>, _e: ()) {
492            let _ = (*a, &*b);
493        }
494
495        let _cb = bad.into_callback(0u64, world.registry_mut());
496    }
497
498    // -- Change detection -----------------------------------------------------
499
500    fn stamps_writer(_ctx: &mut u64, mut val: ResMut<u64>, _e: ()) {
501        *val = 99;
502    }
503
504    #[test]
505    fn mut_stamps_changed_at() {
506        let mut builder = WorldBuilder::new();
507        builder.register::<u64>(0);
508        let mut world = builder.build();
509
510        let mut cb = stamps_writer.into_callback(0u64, world.registry_mut());
511
512        world.next_sequence(); // tick=1
513        let id = world.id::<u64>();
514        unsafe {
515            assert_eq!(world.changed_at(id), crate::Sequence(0));
516        }
517
518        cb.run(&mut world, ());
519        unsafe {
520            assert_eq!(world.changed_at(id), crate::Sequence(1));
521        }
522    }
523
524    // -- Handler<E> interface -------------------------------------------------
525
526    #[test]
527    fn box_dyn_handler() {
528        let mut builder = WorldBuilder::new();
529        builder.register::<u64>(0);
530        let mut world = builder.build();
531
532        fn add_ctx(ctx: &mut u64, mut val: ResMut<u64>, event: u64) {
533            *val += event + *ctx;
534        }
535
536        let cb = add_ctx.into_callback(10u64, world.registry_mut());
537        let mut boxed: Box<dyn Handler<u64>> = Box::new(cb);
538        boxed.run(&mut world, 5u64);
539        // 0 + 5 + 10 = 15
540        assert_eq!(*world.resource::<u64>(), 15);
541    }
542
543    #[test]
544    fn callback_in_vec_dyn() {
545        let mut builder = WorldBuilder::new();
546        builder.register::<u64>(0);
547        let mut world = builder.build();
548
549        fn add(ctx: &mut u64, mut val: ResMut<u64>, _e: ()) {
550            *val += *ctx;
551        }
552        fn mul(ctx: &mut u64, mut val: ResMut<u64>, _e: ()) {
553            *val *= *ctx;
554        }
555
556        let cb_add = add.into_callback(3u64, world.registry_mut());
557        let cb_mul = mul.into_callback(2u64, world.registry_mut());
558
559        let mut handlers: Vec<Box<dyn Handler<()>>> = vec![Box::new(cb_add), Box::new(cb_mul)];
560
561        for h in handlers.iter_mut() {
562            h.run(&mut world, ());
563        }
564        // 0 + 3 = 3, then 3 * 2 = 6
565        assert_eq!(*world.resource::<u64>(), 6);
566    }
567
568    fn with_local(_ctx: &mut u64, mut local: Local<u64>, mut val: ResMut<u64>, _e: ()) {
569        *local += 1;
570        *val = *local;
571    }
572
573    #[test]
574    fn callback_with_local() {
575        let mut builder = WorldBuilder::new();
576        builder.register::<u64>(0);
577        let mut world = builder.build();
578
579        let mut cb = with_local.into_callback(0u64, world.registry_mut());
580        cb.run(&mut world, ());
581        cb.run(&mut world, ());
582        cb.run(&mut world, ());
583        assert_eq!(*world.resource::<u64>(), 3);
584    }
585
586    // -- Integration ----------------------------------------------------------
587
588    #[test]
589    fn callback_interop_with_handler() {
590        use crate::IntoHandler;
591
592        let mut builder = WorldBuilder::new();
593        builder.register::<u64>(0);
594        let mut world = builder.build();
595
596        fn sys_add(mut val: ResMut<u64>, event: u64) {
597            *val += event;
598        }
599        fn cb_mul(ctx: &mut u64, mut val: ResMut<u64>, _e: u64) {
600            *val *= *ctx;
601        }
602
603        let sys = sys_add.into_handler(world.registry_mut());
604        let cb = cb_mul.into_callback(3u64, world.registry_mut());
605
606        let mut handlers: Vec<Box<dyn Handler<u64>>> = vec![Box::new(sys), Box::new(cb)];
607
608        for h in handlers.iter_mut() {
609            h.run(&mut world, 5u64);
610        }
611        // 0 + 5 = 5, then 5 * 3 = 15
612        assert_eq!(*world.resource::<u64>(), 15);
613    }
614}