Skip to main content

nexus_rt/
timer.rs

1//! Timer driver for nexus-rt.
2//!
3//! Integrates [`nexus_timer::Wheel`] as a driver following the
4//! [`Installer`]/[`Plugin`](crate::Plugin) pattern. Handlers access the
5//! timer wheel directly via `ResMut<Wheel<S>>` during dispatch — no
6//! command queues, no side-channel communication.
7//!
8//! # Architecture
9//!
10//! - [`TimerInstaller`] is the installer — consumed at setup, registers the
11//!   wheel into [`WorldBuilder`] and returns a [`TimerPoller`].
12//! - [`TimerPoller`] is the poll-time handle. `poll(world, now)` drains
13//!   expired timers and fires their handlers.
14//! - Handlers reschedule themselves directly via `ResMut<Wheel<S>>`.
15//!
16//! # Timing
17//!
18//! The timer wheel records an **epoch** (`Instant`) at construction time
19//! (inside [`TimerInstaller::install`]). All deadlines are converted to
20//! integer ticks relative to this epoch:
21//!
22//! ```text
23//! ticks = (deadline - epoch).as_nanos() / tick_ns
24//! ```
25//!
26//! - **Default tick resolution**: 1ms (configurable via [`WheelBuilder::tick_duration`]).
27//! - **Instants before the epoch** saturate to tick 0 (fire immediately).
28//! - **Instants beyond the wheel's range** are clamped to the highest
29//!   level's last slot (they fire eventually, not exactly on time).
30//! - **Deadlines in the past** at poll time fire immediately — no "missed
31//!   timer" error.
32//!
33//! The epoch is captured as `Instant::now()` during `install()`. This
34//! means the wheel's zero point is the moment the driver is installed,
35//! which is fine for monotonic deadlines derived from the same clock.
36//!
37//! # Examples
38//!
39//! ```ignore
40//! use std::time::{Duration, Instant};
41//! use nexus_rt::{WorldBuilder, ResMut, IntoHandler, Handler, WheelBuilder};
42//! use nexus_rt::timer::{TimerInstaller, TimerPoller, TimerWheel};
43//!
44//! fn on_timeout(mut state: ResMut<bool>, _poll_time: Instant) {
45//!     *state = true;
46//! }
47//!
48//! let mut builder = WorldBuilder::new();
49//! builder.register::<bool>(false);
50//! let wheel = WheelBuilder::default().unbounded(64).build(Instant::now());
51//! let mut timer: TimerPoller = builder.install_driver(
52//!     TimerInstaller::new(wheel),
53//! );
54//! let mut world = builder.build();
55//!
56//! // Schedule a one-shot timer
57//! let handler = on_timeout.into_handler(world.registry());
58//! world.resource_mut::<TimerWheel>().schedule_forget(
59//!     Instant::now() + Duration::from_millis(100),
60//!     Box::new(handler),
61//! );
62//!
63//! // In the poll loop:
64//! // timer.poll(&mut world, Instant::now());
65//! ```
66
67use std::marker::PhantomData;
68use std::ops::DerefMut;
69use std::time::{Duration, Instant};
70
71use nexus_timer::store::SlabStore;
72
73// Re-export types that users need from nexus-timer
74pub use nexus_timer::{
75    BoundedWheel, BoundedWheelBuilder, Full, TimerHandle, UnboundedWheelBuilder, Wheel,
76    WheelBuilder, WheelEntry,
77};
78
79// Resource impls for timer wheel types registered by the timer driver.
80// TimerWheel has its own `unsafe impl Send` (the wheel owns the slab
81// exclusively, no RawSlots escape). We don't require S: Send here —
82// the wheel's Send impl handles it.
83impl<T: Send + 'static, S: nexus_timer::store::SlabStore<Item = WheelEntry<T>> + 'static>
84    crate::world::Resource for nexus_timer::TimerWheel<T, S>
85{
86}
87
88use crate::Handler;
89use crate::driver::Installer;
90use crate::world::{ResourceId, World, WorldBuilder};
91
92/// Type alias for a timer wheel using boxed handlers (heap-allocated).
93///
94/// `Box<dyn Handler<Instant>>` — each timer entry is a type-erased handler
95/// that receives the poll timestamp as its event.
96pub type TimerWheel = Wheel<Box<dyn Handler<Instant>>>;
97
98/// Type alias for a bounded timer wheel using boxed handlers (heap-allocated).
99///
100/// Fixed-capacity — `try_schedule` returns `Err(Full)` when the wheel is full.
101pub type BoundedTimerWheel = BoundedWheel<Box<dyn Handler<Instant>>>;
102
103/// Type alias for a timer wheel using inline handler storage.
104///
105/// B256 = 256-byte inline buffer. Panics if a handler doesn't fit.
106/// Realistic timer callbacks (0-2 resources + context) are 24-96 bytes
107/// (ResourceId is pointer-sized: 8 bytes per resource param, plus 16
108/// bytes base overhead, plus context size). B256 provides comfortable
109/// headroom without a cache-line penalty over B128 (SIMD memcpy).
110#[cfg(feature = "smartptr")]
111pub type InlineTimerWheel = Wheel<crate::FlatVirtual<Instant, nexus_smartptr::B256>>;
112
113/// Type alias for a timer wheel using inline storage with heap fallback.
114#[cfg(feature = "smartptr")]
115pub type FlexTimerWheel = Wheel<crate::FlexVirtual<Instant, nexus_smartptr::B256>>;
116
117/// Type alias for a bounded timer wheel using inline handler storage.
118#[cfg(feature = "smartptr")]
119pub type BoundedInlineTimerWheel = BoundedWheel<crate::FlatVirtual<Instant, nexus_smartptr::B256>>;
120
121/// Type alias for a bounded timer wheel using inline storage with heap fallback.
122#[cfg(feature = "smartptr")]
123pub type BoundedFlexTimerWheel = BoundedWheel<crate::FlexVirtual<Instant, nexus_smartptr::B256>>;
124
125/// Configuration trait for generic timer code.
126///
127/// ZST annotation type that bundles the handler storage type with a
128/// wrapping function. Library code parameterized over `C: TimerConfig`
129/// can schedule, cancel, and wrap handlers without knowing the concrete
130/// storage strategy.
131///
132/// # Example
133///
134/// ```ignore
135/// use std::time::Instant;
136/// use nexus_rt::timer::{BoxedTimers, TimerConfig};
137/// use nexus_rt::{Handler, World};
138/// use nexus_timer::Wheel;
139///
140/// fn schedule_heartbeat<C: TimerConfig>(
141///     world: &mut World,
142///     handler: impl Handler<Instant> + 'static,
143///     deadline: Instant,
144/// ) {
145///     world.resource_mut::<Wheel<C::Storage>>()
146///         .schedule_forget(deadline, C::wrap(handler));
147/// }
148/// ```
149pub trait TimerConfig: Send + 'static {
150    /// The handler storage type (e.g. `Box<dyn Handler<Instant>>`).
151    type Storage: DerefMut<Target = dyn Handler<Instant>> + Send + 'static;
152
153    /// Wrap a concrete handler into the storage type.
154    fn wrap(handler: impl Handler<Instant> + 'static) -> Self::Storage;
155}
156
157/// Boxed timer configuration — heap-allocates each handler.
158///
159/// This is the default and most flexible option. Zero-overhead for
160/// `Option<Box<T>>` due to niche optimization.
161pub struct BoxedTimers;
162
163impl TimerConfig for BoxedTimers {
164    type Storage = Box<dyn Handler<Instant>>;
165
166    fn wrap(handler: impl Handler<Instant> + 'static) -> Self::Storage {
167        Box::new(handler)
168    }
169}
170
171/// Inline timer configuration — stores handlers in a fixed-size buffer.
172///
173/// Panics if a handler exceeds the buffer size (256 bytes).
174/// Realistic timer callbacks (0-2 resources + context) are 24-96 bytes.
175#[cfg(feature = "smartptr")]
176pub struct InlineTimers;
177
178#[cfg(feature = "smartptr")]
179impl TimerConfig for InlineTimers {
180    type Storage = crate::FlatVirtual<Instant, nexus_smartptr::B256>;
181
182    fn wrap(handler: impl Handler<Instant> + 'static) -> Self::Storage {
183        let ptr: *const dyn Handler<Instant> = &handler;
184        // SAFETY: ptr's metadata (vtable) corresponds to handler's concrete type.
185        unsafe { nexus_smartptr::Flat::new_raw(handler, ptr) }
186    }
187}
188
189/// Flex timer configuration — inline with heap fallback.
190///
191/// Stores inline if the handler fits in 256 bytes, otherwise
192/// heap-allocates. No panics.
193#[cfg(feature = "smartptr")]
194pub struct FlexTimers;
195
196#[cfg(feature = "smartptr")]
197impl TimerConfig for FlexTimers {
198    type Storage = crate::FlexVirtual<Instant, nexus_smartptr::B256>;
199
200    fn wrap(handler: impl Handler<Instant> + 'static) -> Self::Storage {
201        let ptr: *const dyn Handler<Instant> = &handler;
202        // SAFETY: ptr's metadata (vtable) corresponds to handler's concrete type.
203        unsafe { nexus_smartptr::Flex::new_raw(handler, ptr) }
204    }
205}
206
207/// Timer driver installer — takes a pre-built [`TimerWheel`](nexus_timer::TimerWheel).
208///
209/// Build the wheel via [`WheelBuilder`], then hand it to the installer.
210/// The installer registers it into the [`World`] and returns a
211/// [`TimerPoller`] for poll-time use.
212///
213/// # Examples
214///
215/// ```ignore
216/// use std::time::{Duration, Instant};
217/// use nexus_rt::{TimerInstaller, TimerPoller, BoundedTimerPoller, WheelBuilder};
218///
219/// // Unbounded — slab grows as needed, scheduling never fails
220/// let wheel = WheelBuilder::default().unbounded(64).build(Instant::now());
221/// let timer: TimerPoller = wb.install_driver(TimerInstaller::new(wheel));
222///
223/// // Bounded — fixed capacity, try_schedule returns Err(Full) when full
224/// let wheel = WheelBuilder::default().bounded(1024).build(Instant::now());
225/// let timer: BoundedTimerPoller = wb.install_driver(TimerInstaller::new(wheel));
226///
227/// // Custom tick resolution for microsecond-precision timers
228/// let wheel = WheelBuilder::default()
229///     .tick_duration(Duration::from_micros(100))
230///     .unbounded(256)
231///     .build(Instant::now());
232/// let timer: TimerPoller = wb.install_driver(TimerInstaller::new(wheel));
233/// ```
234pub struct TimerInstaller<
235    S: 'static = Box<dyn Handler<Instant>>,
236    Store: SlabStore<Item = WheelEntry<S>> = nexus_timer::store::UnboundedSlab<WheelEntry<S>>,
237> {
238    wheel: nexus_timer::TimerWheel<S, Store>,
239}
240
241impl<S: 'static, Store: SlabStore<Item = WheelEntry<S>>> TimerInstaller<S, Store> {
242    /// Creates a timer installer from a pre-built wheel.
243    ///
244    /// Build the wheel via [`WheelBuilder`], then pass it here.
245    pub fn new(wheel: nexus_timer::TimerWheel<S, Store>) -> Self {
246        TimerInstaller { wheel }
247    }
248}
249
250impl<S, Store> Installer for TimerInstaller<S, Store>
251where
252    S: Send + 'static,
253    Store: SlabStore<Item = WheelEntry<S>> + 'static,
254{
255    type Poller = TimerPoller<S, Store>;
256
257    fn install(self, world: &mut WorldBuilder) -> TimerPoller<S, Store> {
258        let wheel_id = world.register(self.wheel);
259        TimerPoller {
260            wheel_id,
261            buf: Vec::new(),
262            _marker: PhantomData,
263        }
264    }
265}
266
267/// Type alias for a bounded timer installer.
268pub type BoundedTimerInstaller<S = Box<dyn Handler<Instant>>> =
269    TimerInstaller<S, nexus_timer::store::BoundedSlab<WheelEntry<S>>>;
270
271/// Timer driver poller — generic over handler storage and slab store.
272///
273/// Returned by [`TimerInstaller::install`]. Holds a pre-resolved
274/// [`ResourceId`] for the wheel and a reusable drain buffer.
275///
276/// `Store` is the slab backend — determines which [`TimerWheel<S, Store>`]
277/// resource to look up in the [`World`]. Defaults to the unbounded slab.
278pub struct TimerPoller<
279    S = Box<dyn Handler<Instant>>,
280    Store = nexus_timer::store::UnboundedSlab<WheelEntry<S>>,
281> {
282    wheel_id: ResourceId,
283    buf: Vec<S>,
284    _marker: PhantomData<fn() -> Store>,
285}
286
287/// Type alias for a bounded timer poller.
288pub type BoundedTimerPoller<S = Box<dyn Handler<Instant>>> =
289    TimerPoller<S, nexus_timer::store::BoundedSlab<WheelEntry<S>>>;
290
291impl<S, Store> std::fmt::Debug for TimerPoller<S, Store> {
292    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293        f.debug_struct("TimerPoller")
294            .field("wheel_id", &self.wheel_id)
295            .field("buf_len", &self.buf.len())
296            .finish()
297    }
298}
299
300impl<S, Store> TimerPoller<S, Store>
301where
302    S: DerefMut + Send + 'static,
303    S::Target: Handler<Instant>,
304    Store: SlabStore<Item = WheelEntry<S>> + 'static,
305{
306    /// Poll expired timers — drain from wheel, fire each handler, done.
307    ///
308    /// Each handler receives `now` as its event. Handlers that need to
309    /// reschedule themselves do so directly via the wheel resource.
310    ///
311    /// Returns the number of timers fired.
312    pub fn poll(&mut self, world: &mut World, now: Instant) -> usize {
313        // SAFETY: wheel_id was produced by install() on the same builder.
314        // Type matches TimerWheel<S, Store>. No aliases — we have &mut World.
315        let wheel = unsafe { world.get_mut::<nexus_timer::TimerWheel<S, Store>>(self.wheel_id) };
316        wheel.poll(now, &mut self.buf);
317        let fired = self.buf.len();
318
319        for mut handler in self.buf.drain(..) {
320            world.next_sequence();
321            handler.deref_mut().run(world, now);
322        }
323
324        fired
325    }
326
327    /// Earliest deadline in the wheel.
328    pub fn next_deadline(&self, world: &World) -> Option<Instant> {
329        // SAFETY: wheel_id from install(). Type matches. &World = shared access.
330        let wheel = unsafe { world.get::<nexus_timer::TimerWheel<S, Store>>(self.wheel_id) };
331        wheel.next_deadline()
332    }
333
334    /// Number of active timers.
335    pub fn len(&self, world: &World) -> usize {
336        // SAFETY: wheel_id from install(). Type matches. &World = shared access.
337        let wheel = unsafe { world.get::<nexus_timer::TimerWheel<S, Store>>(self.wheel_id) };
338        wheel.len()
339    }
340
341    /// Whether the wheel is empty.
342    pub fn is_empty(&self, world: &World) -> bool {
343        // SAFETY: wheel_id from install(). Type matches. &World = shared access.
344        let wheel = unsafe { world.get::<nexus_timer::TimerWheel<S, Store>>(self.wheel_id) };
345        wheel.is_empty()
346    }
347}
348
349// =============================================================================
350// Periodic
351// =============================================================================
352
353/// Periodic timer wrapper — automatically reschedules after each firing.
354///
355/// Generic over the concrete handler type `H` — no nesting, no type erasure
356/// overhead. When stored in a wheel, the `Periodic<H, C, Store>` is wrapped
357/// in `C::Storage` (e.g. `Box<dyn Handler<Instant>>`) once at the outermost
358/// level. The inner handler `H` is stored directly, not wrapped.
359///
360/// This means `Periodic<H>` is `size_of::<H>() + size_of::<Duration>()` plus
361/// a small marker — compact enough to fit in inline storage (`FlatVirtual`)
362/// alongside typical handlers.
363///
364/// # Scheduling
365///
366/// [`schedule_forget`](nexus_timer::TimerWheel::schedule_forget) is used for
367/// rescheduling. On bounded wheels, this panics if the slab is at capacity.
368/// This is a capacity planning error — size your wheel for peak concurrent
369/// timers including periodic overhead. See the [`store`](nexus_timer::store)
370/// module documentation for the OOM-as-panic rationale.
371///
372/// # Cancellation
373///
374/// If the periodic timer is cancelled (via [`cancel`](nexus_timer::TimerWheel::cancel))
375/// or dropped during shutdown, the inner handler is dropped normally — no leak.
376///
377/// # Example
378///
379/// ```ignore
380/// use std::time::{Duration, Instant};
381/// use nexus_rt::{IntoHandler, ResMut};
382/// use nexus_rt::timer::{Periodic, TimerWheel};
383///
384/// fn heartbeat(mut counter: ResMut<u64>, _now: Instant) {
385///     *counter += 1;
386/// }
387///
388/// let handler = heartbeat.into_handler(world.registry());
389/// let periodic = Periodic::new(handler, Duration::from_millis(100));
390/// world.resource_mut::<TimerWheel>()
391///     .schedule_forget(Instant::now(), Box::new(periodic));
392/// ```
393pub struct Periodic<
394    H,
395    C: TimerConfig = BoxedTimers,
396    Store: SlabStore<Item = WheelEntry<C::Storage>> = nexus_timer::store::UnboundedSlab<
397        WheelEntry<Box<dyn Handler<Instant>>>,
398    >,
399> {
400    inner: Option<H>,
401    interval: Duration,
402    #[allow(clippy::type_complexity)]
403    _marker: PhantomData<(fn() -> C, fn() -> Store)>,
404}
405
406impl<H, C: TimerConfig, Store: SlabStore<Item = WheelEntry<C::Storage>>> std::fmt::Debug
407    for Periodic<H, C, Store>
408{
409    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
410        f.debug_struct("Periodic")
411            .field("has_inner", &self.inner.is_some())
412            .field("interval", &self.interval)
413            .finish()
414    }
415}
416
417impl<H, C: TimerConfig, Store: SlabStore<Item = WheelEntry<C::Storage>>> Periodic<H, C, Store> {
418    /// Create a periodic wrapper around a handler.
419    ///
420    /// `C` and `Store` determine how the handler is stored in the wheel
421    /// and which wheel resource to look up on reschedule. Defaults are
422    /// `BoxedTimers` + `UnboundedSlab` — override via type annotation or
423    /// turbofish for inline/bounded configurations.
424    ///
425    /// # Example
426    ///
427    /// ```ignore
428    /// // Boxed + unbounded (defaults)
429    /// let p = Periodic::new(handler, Duration::from_millis(100));
430    ///
431    /// // Inline + unbounded (Store must be specified when C changes)
432    /// use nexus_timer::store::UnboundedSlab;
433    /// let p: Periodic<_, InlineTimers, UnboundedSlab<_>> =
434    ///     Periodic::new(handler, Duration::from_millis(100));
435    /// ```
436    pub fn new(handler: H, interval: Duration) -> Self {
437        Periodic {
438            inner: Some(handler),
439            interval,
440            _marker: PhantomData,
441        }
442    }
443
444    /// Returns the repetition interval.
445    pub fn interval(&self) -> Duration {
446        self.interval
447    }
448
449    /// Unwrap the inner handler, if present.
450    ///
451    /// Returns `None` only during the transient state inside
452    /// `Handler::run` (after fire, before reschedule).
453    pub fn into_inner(self) -> Option<H> {
454        self.inner
455    }
456}
457
458impl<H, C, Store> Handler<Instant> for Periodic<H, C, Store>
459where
460    H: Handler<Instant> + 'static,
461    C: TimerConfig,
462    Store: SlabStore<Item = WheelEntry<C::Storage>> + 'static,
463{
464    fn run(&mut self, world: &mut World, now: Instant) {
465        let mut inner = self
466            .inner
467            .take()
468            .expect("periodic handler already consumed");
469
470        // Fire the inner handler.
471        inner.run(world, now);
472
473        // Reconstruct and reschedule. The new Periodic<H, C, Store> is
474        // wrapped in C::Storage and placed back into the wheel. No nesting —
475        // H is the concrete handler, not C::Storage.
476        let next: Periodic<H, C, Store> = Periodic {
477            inner: Some(inner),
478            interval: self.interval,
479            _marker: PhantomData,
480        };
481        let deadline = now + self.interval;
482        let wheel = world.resource_mut::<nexus_timer::TimerWheel<C::Storage, Store>>();
483        wheel.schedule_forget(deadline, C::wrap(next));
484    }
485
486    fn name(&self) -> &'static str {
487        self.inner
488            .as_ref()
489            .map_or("<periodic:consumed>", |inner| inner.name())
490    }
491}
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496    use crate::{IntoCallback, IntoHandler, RegistryRef, ResMut, WorldBuilder};
497    use std::time::Duration;
498
499    #[test]
500    fn install_registers_wheel() {
501        let mut builder = WorldBuilder::new();
502        let _handle: TimerPoller =
503            builder.install_driver(TimerInstaller::new(Wheel::unbounded(64, Instant::now())));
504        let world = builder.build();
505        assert!(world.contains::<TimerWheel>());
506    }
507
508    #[test]
509    fn poll_empty_returns_zero() {
510        let mut builder = WorldBuilder::new();
511        let mut handle: TimerPoller =
512            builder.install_driver(TimerInstaller::new(Wheel::unbounded(64, Instant::now())));
513        let mut world = builder.build();
514        assert_eq!(handle.poll(&mut world, Instant::now()), 0);
515    }
516
517    #[test]
518    fn one_shot_fires() {
519        let mut builder = WorldBuilder::new();
520        builder.register::<bool>(false);
521        let mut timer: TimerPoller =
522            builder.install_driver(TimerInstaller::new(Wheel::unbounded(64, Instant::now())));
523        let mut world = builder.build();
524
525        fn on_timeout(mut flag: ResMut<bool>, _now: Instant) {
526            *flag = true;
527        }
528
529        let handler = on_timeout.into_handler(world.registry());
530        let now = Instant::now();
531        world
532            .resource_mut::<TimerWheel>()
533            .schedule_forget(now, Box::new(handler));
534
535        assert!(!*world.resource::<bool>());
536        let fired = timer.poll(&mut world, now);
537        assert_eq!(fired, 1);
538        assert!(*world.resource::<bool>());
539    }
540
541    #[test]
542    fn expired_timer_fires_accumulated() {
543        let mut builder = WorldBuilder::new();
544        builder.register::<u64>(0);
545        let mut timer: TimerPoller =
546            builder.install_driver(TimerInstaller::new(Wheel::unbounded(64, Instant::now())));
547        let mut world = builder.build();
548
549        fn inc(mut counter: ResMut<u64>, _now: Instant) {
550            *counter += 1;
551        }
552
553        let now = Instant::now();
554        let past = now - Duration::from_millis(10);
555
556        for _ in 0..3 {
557            let h = inc.into_handler(world.registry());
558            world
559                .resource_mut::<TimerWheel>()
560                .schedule_forget(past, Box::new(h));
561        }
562
563        let fired = timer.poll(&mut world, now);
564        assert_eq!(fired, 3);
565        assert_eq!(*world.resource::<u64>(), 3);
566    }
567
568    #[test]
569    fn future_timer_does_not_fire() {
570        let mut builder = WorldBuilder::new();
571        builder.register::<bool>(false);
572        let mut timer: TimerPoller =
573            builder.install_driver(TimerInstaller::new(Wheel::unbounded(64, Instant::now())));
574        let mut world = builder.build();
575
576        fn on_timeout(mut flag: ResMut<bool>, _now: Instant) {
577            *flag = true;
578        }
579
580        let now = Instant::now();
581        let future = now + Duration::from_secs(60);
582        let h = on_timeout.into_handler(world.registry());
583        world
584            .resource_mut::<TimerWheel>()
585            .schedule_forget(future, Box::new(h));
586
587        let fired = timer.poll(&mut world, now);
588        assert_eq!(fired, 0);
589        assert!(!*world.resource::<bool>());
590    }
591
592    #[test]
593    fn next_deadline_reports_earliest() {
594        let mut builder = WorldBuilder::new();
595        let timer: TimerPoller =
596            builder.install_driver(TimerInstaller::new(Wheel::unbounded(64, Instant::now())));
597        let mut world = builder.build();
598
599        let now = Instant::now();
600        let early = now + Duration::from_millis(50);
601        let late = now + Duration::from_millis(200);
602
603        fn noop(_now: Instant) {}
604
605        let h1 = noop.into_handler(world.registry());
606        let h2 = noop.into_handler(world.registry());
607        world
608            .resource_mut::<TimerWheel>()
609            .schedule_forget(late, Box::new(h1));
610        world
611            .resource_mut::<TimerWheel>()
612            .schedule_forget(early, Box::new(h2));
613
614        let deadline = timer.next_deadline(&world);
615        assert!(deadline.is_some());
616        // Deadline should be <= early (timer wheel rounds to tick granularity)
617        assert!(deadline.unwrap() <= early + Duration::from_millis(1));
618    }
619
620    #[test]
621    fn len_tracks_active_timers() {
622        let mut builder = WorldBuilder::new();
623        let mut timer: TimerPoller =
624            builder.install_driver(TimerInstaller::new(Wheel::unbounded(64, Instant::now())));
625        let mut world = builder.build();
626
627        assert_eq!(timer.len(&world), 0);
628        assert!(timer.is_empty(&world));
629
630        let now = Instant::now();
631        fn noop(_now: Instant) {}
632
633        let h = noop.into_handler(world.registry());
634        world
635            .resource_mut::<TimerWheel>()
636            .schedule_forget(now, Box::new(h));
637
638        assert_eq!(timer.len(&world), 1);
639        assert!(!timer.is_empty(&world));
640
641        timer.poll(&mut world, now);
642        assert_eq!(timer.len(&world), 0);
643    }
644
645    #[test]
646    fn self_rescheduling_callback() {
647        let mut builder = WorldBuilder::new();
648        builder.register::<u64>(0);
649        let mut timer: TimerPoller =
650            builder.install_driver(TimerInstaller::new(Wheel::unbounded(64, Instant::now())));
651        let mut world = builder.build();
652
653        fn periodic(
654            ctx: &mut Duration,
655            mut counter: ResMut<u64>,
656            mut wheel: ResMut<TimerWheel>,
657            reg: RegistryRef,
658            now: Instant,
659        ) {
660            *counter += 1;
661            if *counter < 3 {
662                let interval = *ctx;
663                let next = periodic.into_callback(interval, &reg);
664                wheel.schedule_forget(now + interval, Box::new(next));
665            }
666        }
667
668        let now = Instant::now();
669        let interval = Duration::from_millis(1);
670        let cb = periodic.into_callback(interval, world.registry());
671        world
672            .resource_mut::<TimerWheel>()
673            .schedule_forget(now, Box::new(cb));
674
675        // Fire first
676        timer.poll(&mut world, now);
677        assert_eq!(*world.resource::<u64>(), 1);
678
679        // Fire second (rescheduled)
680        timer.poll(&mut world, now + interval);
681        assert_eq!(*world.resource::<u64>(), 2);
682
683        // Fire third (rescheduled again, but won't reschedule since counter >= 3)
684        timer.poll(&mut world, now + interval * 2);
685        assert_eq!(*world.resource::<u64>(), 3);
686
687        // No more timers
688        assert!(timer.is_empty(&world));
689    }
690
691    #[test]
692    fn cancellable_timer() {
693        let mut builder = WorldBuilder::new();
694        builder.register::<bool>(false);
695        let mut timer: TimerPoller =
696            builder.install_driver(TimerInstaller::new(Wheel::unbounded(64, Instant::now())));
697        let mut world = builder.build();
698
699        fn on_fire(mut flag: ResMut<bool>, _now: Instant) {
700            *flag = true;
701        }
702
703        let now = Instant::now();
704        let deadline = now + Duration::from_millis(100);
705        let h = on_fire.into_handler(world.registry());
706        let cancel_handle = world
707            .resource_mut::<TimerWheel>()
708            .schedule(deadline, Box::new(h));
709
710        // Cancel before firing
711        let cancelled = world.resource_mut::<TimerWheel>().cancel(cancel_handle);
712        assert!(cancelled.is_some());
713
714        // Poll — nothing fires
715        let fired = timer.poll(&mut world, deadline);
716        assert_eq!(fired, 0);
717        assert!(!*world.resource::<bool>());
718    }
719
720    #[test]
721    fn poll_advances_sequence() {
722        let mut builder = WorldBuilder::new();
723        builder.register::<u64>(0);
724        let mut timer: TimerPoller =
725            builder.install_driver(TimerInstaller::new(Wheel::unbounded(64, Instant::now())));
726        let mut world = builder.build();
727
728        fn inc(mut counter: ResMut<u64>, _now: Instant) {
729            *counter += 1;
730        }
731
732        let now = Instant::now();
733        let h1 = inc.into_handler(world.registry());
734        let h2 = inc.into_handler(world.registry());
735        world
736            .resource_mut::<TimerWheel>()
737            .schedule_forget(now, Box::new(h1));
738        world
739            .resource_mut::<TimerWheel>()
740            .schedule_forget(now, Box::new(h2));
741
742        let seq_before = world.current_sequence();
743        timer.poll(&mut world, now);
744        // Two handlers fired, two next_sequence calls
745        assert_eq!(world.current_sequence().0, seq_before.0 + 2);
746    }
747
748    #[test]
749    fn reschedule_timer() {
750        let mut builder = WorldBuilder::new();
751        builder.register::<u64>(0);
752        let mut timer: TimerPoller =
753            builder.install_driver(TimerInstaller::new(Wheel::unbounded(64, Instant::now())));
754        let mut world = builder.build();
755
756        fn on_fire(mut counter: ResMut<u64>, _now: Instant) {
757            *counter += 1;
758        }
759
760        let now = Instant::now();
761        let h = on_fire.into_handler(world.registry());
762        let handle = world
763            .resource_mut::<TimerWheel>()
764            .schedule(now + Duration::from_millis(100), Box::new(h));
765
766        // Reschedule to earlier
767        let handle = world
768            .resource_mut::<TimerWheel>()
769            .reschedule(handle, now + Duration::from_millis(50));
770
771        // Should NOT fire at 40ms
772        let fired = timer.poll(&mut world, now + Duration::from_millis(40));
773        assert_eq!(fired, 0);
774        assert_eq!(*world.resource::<u64>(), 0);
775
776        // Should fire at 55ms
777        let fired = timer.poll(&mut world, now + Duration::from_millis(55));
778        assert_eq!(fired, 1);
779        assert_eq!(*world.resource::<u64>(), 1);
780
781        // Clean up zombie handle
782        world.resource_mut::<TimerWheel>().cancel(handle);
783    }
784
785    #[test]
786    fn periodic_fires_repeatedly() {
787        let mut builder = WorldBuilder::new();
788        builder.register::<u64>(0);
789        let mut timer: TimerPoller =
790            builder.install_driver(TimerInstaller::new(Wheel::unbounded(64, Instant::now())));
791        let mut world = builder.build();
792
793        fn tick(mut counter: ResMut<u64>, _now: Instant) {
794            *counter += 1;
795        }
796
797        let now = Instant::now();
798        let interval = Duration::from_millis(10);
799        let handler = tick.into_handler(world.registry());
800        let periodic: Periodic<_> = Periodic::new(handler, interval);
801        world
802            .resource_mut::<TimerWheel>()
803            .schedule_forget(now, Box::new(periodic));
804
805        // First firing
806        timer.poll(&mut world, now);
807        assert_eq!(*world.resource::<u64>(), 1);
808
809        // Second firing (rescheduled to now + 10ms)
810        timer.poll(&mut world, now + interval);
811        assert_eq!(*world.resource::<u64>(), 2);
812
813        // Third firing (rescheduled to now + 20ms)
814        timer.poll(&mut world, now + interval * 2);
815        assert_eq!(*world.resource::<u64>(), 3);
816
817        // Still active — periodic never stops on its own
818        assert!(!timer.is_empty(&world));
819    }
820
821    #[test]
822    fn periodic_cancel_drops_inner() {
823        let mut builder = WorldBuilder::new();
824        builder.register::<bool>(false);
825        let mut timer: TimerPoller =
826            builder.install_driver(TimerInstaller::new(Wheel::unbounded(64, Instant::now())));
827        let mut world = builder.build();
828
829        fn on_fire(mut flag: ResMut<bool>, _now: Instant) {
830            *flag = true;
831        }
832
833        let now = Instant::now();
834        let handler = on_fire.into_handler(world.registry());
835        let periodic: Periodic<_> = Periodic::new(handler, Duration::from_millis(50));
836        let handle = world
837            .resource_mut::<TimerWheel>()
838            .schedule(now + Duration::from_millis(50), Box::new(periodic));
839
840        // Cancel before it fires
841        let cancelled = world.resource_mut::<TimerWheel>().cancel(handle);
842        assert!(cancelled.is_some());
843
844        // Poll — nothing fires
845        let fired = timer.poll(&mut world, now + Duration::from_millis(100));
846        assert_eq!(fired, 0);
847        assert!(!*world.resource::<bool>());
848    }
849
850    #[test]
851    fn periodic_into_inner_recovers_handler() {
852        let mut builder = WorldBuilder::new();
853        let _timer: TimerPoller =
854            builder.install_driver(TimerInstaller::new(Wheel::unbounded(64, Instant::now())));
855        let world = builder.build();
856
857        fn noop(_now: Instant) {}
858
859        let handler = noop.into_handler(world.registry());
860        let periodic: Periodic<_> = Periodic::new(handler, Duration::from_millis(10));
861        assert!(periodic.into_inner().is_some());
862    }
863
864    // -- Bounded wheel tests --------------------------------------------------
865
866    #[test]
867    fn bounded_install_registers_wheel() {
868        let mut builder = WorldBuilder::new();
869        let wheel = BoundedTimerWheel::bounded(64, Instant::now());
870        let _handle: BoundedTimerPoller = builder.install_driver(TimerInstaller::new(wheel));
871        let world = builder.build();
872        assert!(world.contains::<BoundedTimerWheel>());
873    }
874
875    #[test]
876    fn bounded_one_shot_fires() {
877        let mut builder = WorldBuilder::new();
878        builder.register::<bool>(false);
879        let wheel = BoundedTimerWheel::bounded(64, Instant::now());
880        let mut timer: BoundedTimerPoller = builder.install_driver(TimerInstaller::new(wheel));
881        let mut world = builder.build();
882
883        fn on_timeout(mut flag: ResMut<bool>, _now: Instant) {
884            *flag = true;
885        }
886
887        let handler = on_timeout.into_handler(world.registry());
888        let now = Instant::now();
889        world
890            .resource_mut::<BoundedTimerWheel>()
891            .try_schedule_forget(now, Box::new(handler))
892            .expect("should not be full");
893
894        assert!(!*world.resource::<bool>());
895        let fired = timer.poll(&mut world, now);
896        assert_eq!(fired, 1);
897        assert!(*world.resource::<bool>());
898    }
899
900    #[test]
901    fn bounded_cancel_and_query() {
902        let mut builder = WorldBuilder::new();
903        let wheel = BoundedTimerWheel::bounded(64, Instant::now());
904        let mut timer: BoundedTimerPoller = builder.install_driver(TimerInstaller::new(wheel));
905        let mut world = builder.build();
906
907        fn noop(_now: Instant) {}
908
909        let now = Instant::now();
910        let h = noop.into_handler(world.registry());
911        let handle = world
912            .resource_mut::<BoundedTimerWheel>()
913            .try_schedule(now + Duration::from_millis(100), Box::new(h))
914            .expect("should not be full");
915
916        assert_eq!(timer.len(&world), 1);
917        assert!(!timer.is_empty(&world));
918        assert!(timer.next_deadline(&world).is_some());
919
920        let cancelled = world.resource_mut::<BoundedTimerWheel>().cancel(handle);
921        assert!(cancelled.is_some());
922
923        let fired = timer.poll(&mut world, now + Duration::from_millis(200));
924        assert_eq!(fired, 0);
925        assert_eq!(timer.len(&world), 0);
926    }
927
928    #[test]
929    fn bounded_full_returns_error() {
930        let mut builder = WorldBuilder::new();
931        let wheel = BoundedTimerWheel::bounded(1, Instant::now());
932        let _timer: BoundedTimerPoller = builder.install_driver(TimerInstaller::new(wheel));
933        let mut world = builder.build();
934
935        fn noop(_now: Instant) {}
936
937        let now = Instant::now();
938        let h1 = noop.into_handler(world.registry());
939        world
940            .resource_mut::<BoundedTimerWheel>()
941            .try_schedule_forget(now + Duration::from_secs(60), Box::new(h1))
942            .expect("first should succeed");
943
944        let h2 = noop.into_handler(world.registry());
945        let result = world
946            .resource_mut::<BoundedTimerWheel>()
947            .try_schedule_forget(now + Duration::from_secs(60), Box::new(h2));
948        assert!(result.is_err());
949    }
950
951    // -- Builder configuration coverage ---------------------------------------
952
953    /// Helper: install, schedule, poll, assert fired. Proves the full
954    /// path works end-to-end for a given installer configuration.
955    fn assert_unbounded_fires(installer: TimerInstaller) {
956        let mut builder = WorldBuilder::new();
957        builder.register::<bool>(false);
958        let mut timer: TimerPoller = builder.install_driver(installer);
959        let mut world = builder.build();
960
961        fn on_fire(mut flag: ResMut<bool>, _now: Instant) {
962            *flag = true;
963        }
964
965        let now = Instant::now();
966        let h = on_fire.into_handler(world.registry());
967        world
968            .resource_mut::<TimerWheel>()
969            .schedule_forget(now, Box::new(h));
970
971        let fired = timer.poll(&mut world, now);
972        assert_eq!(fired, 1);
973        assert!(*world.resource::<bool>());
974    }
975
976    fn assert_bounded_fires(installer: BoundedTimerInstaller) {
977        let mut builder = WorldBuilder::new();
978        builder.register::<bool>(false);
979        let mut timer: BoundedTimerPoller = builder.install_driver(installer);
980        let mut world = builder.build();
981
982        fn on_fire(mut flag: ResMut<bool>, _now: Instant) {
983            *flag = true;
984        }
985
986        let now = Instant::now();
987        let h = on_fire.into_handler(world.registry());
988        world
989            .resource_mut::<BoundedTimerWheel>()
990            .try_schedule_forget(now, Box::new(h))
991            .expect("should not be full");
992
993        let fired = timer.poll(&mut world, now);
994        assert_eq!(fired, 1);
995        assert!(*world.resource::<bool>());
996    }
997
998    // -- Unbounded constructors -----------------------------------------------
999
1000    #[test]
1001    fn cfg_unbounded_default() {
1002        let now = Instant::now();
1003        assert_unbounded_fires(TimerInstaller::new(Wheel::unbounded(64, now)));
1004    }
1005
1006    #[test]
1007    fn cfg_unbounded_chunk_capacity() {
1008        let now = Instant::now();
1009        assert_unbounded_fires(TimerInstaller::new(Wheel::unbounded(256, now)));
1010    }
1011
1012    #[test]
1013    fn cfg_unbounded_tick_duration() {
1014        let now = Instant::now();
1015        assert_unbounded_fires(TimerInstaller::new(
1016            WheelBuilder::default()
1017                .tick_duration(Duration::from_micros(100))
1018                .unbounded(64)
1019                .build(now),
1020        ));
1021    }
1022
1023    #[test]
1024    fn cfg_unbounded_slots_per_level() {
1025        let now = Instant::now();
1026        assert_unbounded_fires(TimerInstaller::new(
1027            WheelBuilder::default()
1028                .slots_per_level(32)
1029                .unbounded(64)
1030                .build(now),
1031        ));
1032    }
1033
1034    #[test]
1035    fn cfg_unbounded_clk_shift() {
1036        let now = Instant::now();
1037        assert_unbounded_fires(TimerInstaller::new(
1038            WheelBuilder::default()
1039                .clk_shift(2)
1040                .unbounded(64)
1041                .build(now),
1042        ));
1043    }
1044
1045    #[test]
1046    fn cfg_unbounded_num_levels() {
1047        let now = Instant::now();
1048        assert_unbounded_fires(TimerInstaller::new(
1049            WheelBuilder::default()
1050                .num_levels(4)
1051                .unbounded(64)
1052                .build(now),
1053        ));
1054    }
1055
1056    #[test]
1057    fn cfg_unbounded_full_chain() {
1058        let now = Instant::now();
1059        assert_unbounded_fires(TimerInstaller::new(
1060            WheelBuilder::default()
1061                .tick_duration(Duration::from_micros(500))
1062                .slots_per_level(32)
1063                .clk_shift(2)
1064                .num_levels(5)
1065                .unbounded(128)
1066                .build(now),
1067        ));
1068    }
1069
1070    // -- Bounded constructors -------------------------------------------------
1071
1072    #[test]
1073    fn cfg_bounded_default() {
1074        let now = Instant::now();
1075        assert_bounded_fires(TimerInstaller::new(
1076            WheelBuilder::default().bounded(64).build(now),
1077        ));
1078    }
1079
1080    #[test]
1081    fn cfg_bounded_tick_duration() {
1082        let now = Instant::now();
1083        assert_bounded_fires(TimerInstaller::new(
1084            WheelBuilder::default()
1085                .tick_duration(Duration::from_micros(100))
1086                .bounded(64)
1087                .build(now),
1088        ));
1089    }
1090
1091    #[test]
1092    fn cfg_bounded_slots_per_level() {
1093        let now = Instant::now();
1094        assert_bounded_fires(TimerInstaller::new(
1095            WheelBuilder::default()
1096                .slots_per_level(32)
1097                .bounded(64)
1098                .build(now),
1099        ));
1100    }
1101
1102    #[test]
1103    fn cfg_bounded_clk_shift() {
1104        let now = Instant::now();
1105        assert_bounded_fires(TimerInstaller::new(
1106            WheelBuilder::default().clk_shift(2).bounded(64).build(now),
1107        ));
1108    }
1109
1110    #[test]
1111    fn cfg_bounded_num_levels() {
1112        let now = Instant::now();
1113        assert_bounded_fires(TimerInstaller::new(
1114            WheelBuilder::default().num_levels(4).bounded(64).build(now),
1115        ));
1116    }
1117
1118    #[test]
1119    fn cfg_bounded_full_chain() {
1120        let now = Instant::now();
1121        assert_bounded_fires(TimerInstaller::new(
1122            WheelBuilder::default()
1123                .tick_duration(Duration::from_micros(500))
1124                .slots_per_level(32)
1125                .clk_shift(2)
1126                .num_levels(5)
1127                .bounded(128)
1128                .build(now),
1129        ));
1130    }
1131
1132    // -- Different handler storage types --------------------------------------
1133
1134    #[cfg(feature = "smartptr")]
1135    mod storage_tests {
1136        use super::*;
1137
1138        #[test]
1139        fn unbounded_inline_storage() {
1140            let mut builder = WorldBuilder::new();
1141            builder.register::<bool>(false);
1142            let wheel = InlineTimerWheel::unbounded(64, Instant::now());
1143            let mut timer = builder.install_driver(TimerInstaller::new(wheel));
1144            let mut world = builder.build();
1145
1146            fn on_fire(mut flag: ResMut<bool>, _now: Instant) {
1147                *flag = true;
1148            }
1149
1150            let now = Instant::now();
1151            let h = on_fire.into_handler(world.registry());
1152            let ptr: *const dyn Handler<Instant> = &h;
1153            // SAFETY: ptr metadata from h's concrete type
1154            let storage = unsafe { nexus_smartptr::Flat::new_raw(h, ptr) };
1155            world
1156                .resource_mut::<InlineTimerWheel>()
1157                .schedule_forget(now, storage);
1158
1159            let fired = timer.poll(&mut world, now);
1160            assert_eq!(fired, 1);
1161            assert!(*world.resource::<bool>());
1162        }
1163
1164        #[test]
1165        fn unbounded_flex_storage() {
1166            let mut builder = WorldBuilder::new();
1167            builder.register::<bool>(false);
1168            let wheel = FlexTimerWheel::unbounded(64, Instant::now());
1169            let mut timer = builder.install_driver(TimerInstaller::new(wheel));
1170            let mut world = builder.build();
1171
1172            fn on_fire(mut flag: ResMut<bool>, _now: Instant) {
1173                *flag = true;
1174            }
1175
1176            let now = Instant::now();
1177            let h = on_fire.into_handler(world.registry());
1178            let ptr: *const dyn Handler<Instant> = &h;
1179            // SAFETY: ptr metadata from h's concrete type
1180            let storage = unsafe { nexus_smartptr::Flex::new_raw(h, ptr) };
1181            world
1182                .resource_mut::<FlexTimerWheel>()
1183                .schedule_forget(now, storage);
1184
1185            let fired = timer.poll(&mut world, now);
1186            assert_eq!(fired, 1);
1187            assert!(*world.resource::<bool>());
1188        }
1189
1190        #[test]
1191        fn bounded_inline_storage() {
1192            let mut builder = WorldBuilder::new();
1193            builder.register::<bool>(false);
1194            let wheel = BoundedInlineTimerWheel::bounded(64, Instant::now());
1195            let mut timer = builder.install_driver(TimerInstaller::new(wheel));
1196            let mut world = builder.build();
1197
1198            fn on_fire(mut flag: ResMut<bool>, _now: Instant) {
1199                *flag = true;
1200            }
1201
1202            let now = Instant::now();
1203            let h = on_fire.into_handler(world.registry());
1204            let ptr: *const dyn Handler<Instant> = &h;
1205            // SAFETY: ptr metadata from h's concrete type
1206            let storage = unsafe { nexus_smartptr::Flat::new_raw(h, ptr) };
1207            world
1208                .resource_mut::<BoundedInlineTimerWheel>()
1209                .try_schedule_forget(now, storage)
1210                .expect("should not be full");
1211
1212            let fired = timer.poll(&mut world, now);
1213            assert_eq!(fired, 1);
1214            assert!(*world.resource::<bool>());
1215        }
1216
1217        #[test]
1218        fn bounded_flex_storage() {
1219            let mut builder = WorldBuilder::new();
1220            builder.register::<bool>(false);
1221            let wheel = BoundedFlexTimerWheel::bounded(64, Instant::now());
1222            let mut timer = builder.install_driver(TimerInstaller::new(wheel));
1223            let mut world = builder.build();
1224
1225            fn on_fire(mut flag: ResMut<bool>, _now: Instant) {
1226                *flag = true;
1227            }
1228
1229            let now = Instant::now();
1230            let h = on_fire.into_handler(world.registry());
1231            let ptr: *const dyn Handler<Instant> = &h;
1232            // SAFETY: ptr metadata from h's concrete type
1233            let storage = unsafe { nexus_smartptr::Flex::new_raw(h, ptr) };
1234            world
1235                .resource_mut::<BoundedFlexTimerWheel>()
1236                .try_schedule_forget(now, storage)
1237                .expect("should not be full");
1238
1239            let fired = timer.poll(&mut world, now);
1240            assert_eq!(fired, 1);
1241            assert!(*world.resource::<bool>());
1242        }
1243
1244        #[test]
1245        fn periodic_inline_fires_repeatedly() {
1246            let mut builder = WorldBuilder::new();
1247            builder.register::<u64>(0);
1248            let wheel = InlineTimerWheel::unbounded(64, Instant::now());
1249            let mut timer = builder.install_driver(TimerInstaller::new(wheel));
1250            let mut world = builder.build();
1251
1252            fn tick(mut counter: ResMut<u64>, _now: Instant) {
1253                *counter += 1;
1254            }
1255
1256            let now = Instant::now();
1257            let interval = Duration::from_millis(10);
1258            let handler = tick.into_handler(world.registry());
1259            let periodic: Periodic<_, InlineTimers, nexus_timer::store::UnboundedSlab<_>> =
1260                Periodic::new(handler, interval);
1261            world
1262                .resource_mut::<InlineTimerWheel>()
1263                .schedule_forget(now, InlineTimers::wrap(periodic));
1264
1265            // First firing
1266            timer.poll(&mut world, now);
1267            assert_eq!(*world.resource::<u64>(), 1);
1268
1269            // Second firing (rescheduled to now + 10ms)
1270            timer.poll(&mut world, now + interval);
1271            assert_eq!(*world.resource::<u64>(), 2);
1272
1273            // Third firing (rescheduled to now + 20ms)
1274            timer.poll(&mut world, now + interval * 2);
1275            assert_eq!(*world.resource::<u64>(), 3);
1276
1277            // Still active — periodic never stops on its own
1278            assert!(!timer.is_empty(&world));
1279        }
1280
1281        #[test]
1282        fn periodic_flex_fires_repeatedly() {
1283            let mut builder = WorldBuilder::new();
1284            builder.register::<u64>(0);
1285            let wheel = FlexTimerWheel::unbounded(64, Instant::now());
1286            let mut timer = builder.install_driver(TimerInstaller::new(wheel));
1287            let mut world = builder.build();
1288
1289            fn tick(mut counter: ResMut<u64>, _now: Instant) {
1290                *counter += 1;
1291            }
1292
1293            let now = Instant::now();
1294            let interval = Duration::from_millis(10);
1295            let handler = tick.into_handler(world.registry());
1296            let periodic: Periodic<_, FlexTimers, nexus_timer::store::UnboundedSlab<_>> =
1297                Periodic::new(handler, interval);
1298            world
1299                .resource_mut::<FlexTimerWheel>()
1300                .schedule_forget(now, FlexTimers::wrap(periodic));
1301
1302            timer.poll(&mut world, now);
1303            assert_eq!(*world.resource::<u64>(), 1);
1304
1305            timer.poll(&mut world, now + interval);
1306            assert_eq!(*world.resource::<u64>(), 2);
1307
1308            timer.poll(&mut world, now + interval * 2);
1309            assert_eq!(*world.resource::<u64>(), 3);
1310
1311            assert!(!timer.is_empty(&world));
1312        }
1313    }
1314}