wiwi/
clock_timer.rs

1pub extern crate chrono;
2extern crate tokio;
3
4use crate::prelude::*;
5use chrono::{ DateTime, Local, TimeDelta, TimeZone };
6use std::future::Future;
7use tokio::time::sleep;
8
9/// An interval tracking clock. Takes a start time, an end time or a run duration,
10/// and an interval. Calls to [`tick`][ClockTimer::tick] will return only if
11/// the current time is at or past the time of the next interval, waiting so
12/// that it is before returning. It yields timing information when returning.
13/// If this falls behind time for some reason, the ticks will be yielded with
14/// the time information at when it was supposed to yield, until catching up.
15pub struct ClockTimer {
16	/// The time the next tick will trigger
17	///
18	/// In a newly created clock timer, this is the starting time
19	next_tick: DateTime<Local>,
20
21	/// How often this clock timer will yield ticks
22	interval: TimeDelta,
23
24	/// How much time has elapsed since the first tick
25	///
26	/// More precisely, this tracks how much time is between the first
27	/// tick, and the next tick if there is one. Otherwise, the value in this
28	/// field is meaningless.
29	elapsed: TimeDelta,
30
31	/// How much time is remaining
32	///
33	/// More precisely, this tracks how much time is remaining after the time in
34	/// [`next_tick`](ClockTimer::next_tick)
35	remaining: TimeDelta
36}
37
38/// Timing information for one tick
39pub struct Tick {
40	/// The time of this tick (or, if this tick was delayed, what time this tick
41	/// was scheduled to be yielded at)
42	this_tick: DateTime<Local>,
43
44	/// The duration from the first tick to this tick (scheduled time),
45	/// ie. the time the clock timer has been running
46	elapsed: TimeDelta,
47
48	/// The duration from this tick to the last tick (scheduled time),
49	/// ie. the remaining time the clock timer will run before stopping
50	remaining: TimeDelta,
51
52	/// Whether or not this tick has been delayed
53	///
54	/// Note: We have not properly tested this (except in the
55	/// [april fools prank](https://www.fimfiction.net/story/553695/) that this
56	/// was built for of course heh), and we suspect this value is always `true`
57	/// no matter if it was _actually_ delayed by the definition of what you'd
58	/// expect. You might expect this to be `true` if previous task took too long
59	/// or something, ie. this was called delayed because of the application
60	/// logic itself, rather than little OS scheduling runtime things, ie. OS
61	/// thread scheduling, tokio task scheduling, syncronisation stuff, etc etc.
62	/// We expect this to always be `true`, because tokio will not wake up and
63	/// poll again a task until the time has passed, and never before, and if
64	/// there's any tiny bit of delay introduced anywhere detectable by the time
65	/// APIs, be it from OS thread syncronisation, or tokio syncronisation, or
66	/// the arithmetic and time putting the task away to sleep by the async
67	/// runtime, or something, which, based on how these things work, this will
68	/// likely always happen and make ths `true`.
69	///
70	/// ...whew ramble
71	delayed: bool
72}
73
74impl ClockTimer {
75	/// Gets a [`ClockTimer`] builder
76	#[inline]
77	pub fn builder() -> builder2::Builder {
78		builder2::Builder::new()
79	}
80
81	/// Runs the next tick and returns timing information for it, if this
82	/// interval is not finished already.
83	#[inline]
84	pub async fn tick(&mut self) -> Option<Tick> {
85		if self.remaining < TimeDelta::zero() { return None }
86
87		let mut tick = Tick {
88			this_tick: self.next_tick,
89			elapsed: self.elapsed,
90			remaining: self.remaining,
91			delayed: false
92		};
93
94		self.next_tick += self.interval;
95		self.elapsed += self.interval;
96		self.remaining -= self.interval;
97
98		let delta = tick.this_tick - Local::now();
99
100		// TODO: rethink delayed detection?
101		// because it is highly likely that due to various factors out of our
102		// control (eg. OS scheduling, tokio runtime scheduling, work stealing,
103		// syncronisation stuff, etc etc), we won't get polled until technically
104		// after our scheduled time, leading this to always be true? tests needed,
105		// and this delay is in the order of milliseconds, or maybe even micros/nanos
106		if delta <= TimeDelta::zero() {
107			// highly unlikely, but if delta somehow manages to hit exactly 0,
108			// we consider it on time. Maybe we should say like, if now is
109			// within 1ms after the set tick time? dunno (see above todo comment)
110			if delta < TimeDelta::zero() { tick.delayed = true }
111			return Some(tick)
112		}
113
114		// we checked above and returned if `delta` is lte zero,
115		// so this won't panic
116		sleep(delta.to_std().unwrap()).await;
117		Some(tick)
118	}
119
120	/// Convenience function, equivalent to running a `while let Some(tick)`
121	/// loop. When awaited on, the closure provided will be called every tick.
122	/// This consumes self and runs it to completion.
123	#[inline]
124	pub async fn run_to_end<F, Fu>(mut self, mut f: F)
125	where
126		F: FnMut(Tick) -> Fu,
127		Fu: Future<Output = ()>
128	{
129		while let Some(tick) = self.tick().await {
130			f(tick).await;
131		}
132	}
133}
134
135impl Tick {
136	/// Get time of this tick
137	#[inline]
138	pub fn time(&self) -> DateTime<Local> {
139		self.this_tick
140	}
141
142	/// Get elapsed time since the start of this timer
143	#[inline]
144	pub fn elapsed(&self) -> TimeDelta {
145		self.elapsed
146	}
147
148	/// Get remaining runtime of this timer
149	#[inline]
150	pub fn remaining(&self) -> TimeDelta {
151		self.remaining
152	}
153
154	/// Get start time of this timer
155	#[inline]
156	pub fn start_time(&self) -> DateTime<Local> {
157		self.this_tick - self.elapsed
158	}
159
160	/// Get end time of this timer
161	#[inline]
162	pub fn end_time(&self) -> DateTime<Local> {
163		self.this_tick + self.remaining
164	}
165
166	/// Get total runtime of this timer, including elapsed
167	/// time and remaining time
168	#[inline]
169	pub fn total_runtime(&self) -> TimeDelta {
170		self.elapsed + self.remaining
171	}
172
173	/// Returns if this tick was delayed. This tick is considered delayed if
174	/// the tick function was called after the time of this tick had already past.
175	///
176	/// Note: does the same thing as [`past_due`][Self::past_due]
177	#[inline]
178	pub fn delayed(&self) -> bool {
179		self.delayed
180	}
181
182	/// Returns if this tick is past due. This tick is considered past due if
183	/// the tick function was called after the time of this tick had already past.
184	///
185	/// Note: does the same thing as [`delayed`][Self::delayed]
186	#[inline]
187	pub fn past_due(&self) -> bool {
188		self.delayed
189	}
190}
191
192/// [`ClockTimer`] builder structs
193pub mod builder {
194	use super::*;
195
196	/// Builder for [`ClockTimer`].
197	pub struct Builder {
198		/// Forcing users to use [`new`] because I dunno style or something, that
199		/// [`new`] call and this struct is just literally gonna get optimised
200		/// away to nothing
201		///
202		/// [`new`]: Builder::new
203		__private: ()
204	}
205
206	impl Builder {
207		/// New builder. You can also obtain a builder through [`ClockTimer::builder`]
208		// there is no default that makes sense here
209		#[expect(clippy::new_without_default, reason = "api design")]
210		#[inline]
211		pub fn new() -> Self {
212			// its gonna optimise away to be noop lol
213			// I think it provides a good API though,
214			Self { __private: () }
215		}
216
217		/// Sets the start date/time of the ClockTimer, or in other words, the
218		/// time of the first tick.
219		#[inline]
220		pub fn with_start_datetime<TZ: TimeZone>(self, datetime: DateTime<TZ>) -> BuilderWithStart {
221			let start = datetime.with_timezone(&Local);
222			BuilderWithStart { start }
223		}
224	}
225
226	/// Intermediate builder state struct, returned after calling a method on
227	/// [`Builder`]
228	///
229	/// Most likely you won't need to ever interact with this type directly.
230	/// You're probably looking for [`Builder`].
231	pub struct BuilderWithStart {
232		/// The provided start datetime
233		start: DateTime<Local>
234	}
235
236	impl BuilderWithStart {
237		/// Sets the end date/time of the ClockTimer. ClockTimer will run until
238		/// this time is _passed_. A tick _will_ be emitted if the last tick is equal
239		/// to the end time.
240		#[inline]
241		pub fn with_end_datetime<TZ: TimeZone>(self, datetime: DateTime<TZ>) -> BuilderWithEnd {
242			let Self { start } = self;
243			let end = datetime.with_timezone(&Local);
244			BuilderWithEnd { start, end }
245		}
246
247		/// Sets a duration to run this ClockTimer for. Internally, the end time
248		/// is calculated and stored based on start time and the provided duration.
249		#[inline]
250		pub fn with_duration(self, duration: TimeDelta) -> BuilderWithEnd {
251			let Self { start } = self;
252			let end = start + duration;
253			BuilderWithEnd { start, end }
254		}
255	}
256
257	/// Intermediate builder state struct, returned after calling a method on
258	/// [`BuilderWithStart`]
259	///
260	/// Most likely you won't need to ever interact with this type directly.
261	/// You're probably looking for [`Builder`].
262	pub struct BuilderWithEnd {
263		/// The provided start datetime (from prev stage of builder)
264		start: DateTime<Local>,
265
266		/// The end datetime, either provided or calculated
267		/// from a runtime duration
268		end: DateTime<Local>
269	}
270
271	impl BuilderWithEnd {
272		/// Sets interval to run at, or the time between ticks.
273		#[inline]
274		pub fn with_interval(self, interval: TimeDelta) -> BuilderWithInterval {
275			let Self { start, end } = self;
276			BuilderWithInterval { start, end, interval }
277		}
278	}
279
280	/// Intermediate builder state struct, returned after calling a method on
281	/// [`BuilderWithEnd`]
282	///
283	/// Most likely you won't need to ever interact with this type directly.
284	/// You're probably looking for [`Builder`].
285	pub struct BuilderWithInterval {
286		/// The provided start datetime (from prev stage of builder)
287		start: DateTime<Local>,
288
289		/// The end datetime, either provided or calculated from a runtime
290		/// duration (from prev stage of builder)
291		end: DateTime<Local>,
292
293		/// The provided trigger interval
294		interval: TimeDelta
295	}
296
297	impl BuilderWithInterval {
298		/// Builds and returns a [`ClockTimer`]
299		#[inline]
300		pub fn build(self) -> ClockTimer {
301			let Self { start: next_tick, end, interval } = self;
302			let elapsed = TimeDelta::zero();
303			let remaining = end - next_tick;
304
305			ClockTimer { next_tick, interval, elapsed, remaining }
306		}
307	}
308}
309
310pub mod builder2 {
311	use super::*;
312	use crate::builder::{ Init, Uninit };
313
314	#[repr(transparent)]
315	pub struct Builder<
316		Start = Uninit,
317		End = Uninit,
318		Interval = Uninit
319	> {
320		inner: BuilderInner,
321		__marker: PhantomData<(Start, End, Interval)>
322	}
323
324	struct BuilderInner {
325		start: MaybeUninit<DateTime<Local>>,
326		end: MaybeUninit<DateTime<Local>>,
327		interval: MaybeUninit<TimeDelta>
328	}
329
330	impl Builder {
331		#[expect(clippy::new_without_default, reason = "api design")]
332		#[inline]
333		pub fn new() -> Builder {
334			Builder {
335				inner: BuilderInner {
336					start: MaybeUninit::uninit(),
337					end: MaybeUninit::uninit(),
338					interval: MaybeUninit::uninit()
339				},
340				__marker: PhantomData
341			}
342		}
343	}
344
345	impl<End, Interval> Builder<Uninit, End, Interval> {
346		#[inline]
347		pub fn with_start_datetime<TZ: TimeZone>(mut self, datetime: DateTime<TZ>) -> Builder<Init, End, Interval> {
348			self.inner.start.write(datetime.with_timezone(&Local));
349			Builder { inner: self.inner, __marker: PhantomData }
350		}
351	}
352
353	impl<Start, Interval> Builder<Start, Uninit, Interval> {
354		#[inline]
355		pub fn with_end_datetime<TZ: TimeZone>(mut self, datetime: DateTime<TZ>) -> Builder<Start, Init, Interval> {
356			self.inner.end.write(datetime.with_timezone(&Local));
357			Builder { inner: self.inner, __marker: PhantomData }
358		}
359	}
360
361	impl<Interval> Builder<Init, Uninit, Interval> {
362		#[inline]
363		pub fn with_duration(mut self, duration: TimeDelta) -> Builder<Init, Init, Interval> {
364			// SAFETY: enforced by type system (typestate pattern)
365			let start = unsafe { self.inner.start.assume_init() };
366
367			self.inner.end.write(start + duration);
368			Builder { inner: self.inner, __marker: PhantomData }
369		}
370	}
371
372	impl<Start, End> Builder<Start, End, Uninit> {
373		#[inline]
374		pub fn with_interval(mut self, interval: TimeDelta) -> Builder<Start, End, Init> {
375			self.inner.interval.write(interval);
376			Builder { inner: self.inner, __marker: PhantomData }
377		}
378	}
379
380	impl Builder<Init, Init, Init> {
381		#[inline]
382		pub fn build(self) -> ClockTimer {
383			// SAFETY: enforced by type system (typestate pattern)
384			let start = unsafe { self.inner.start.assume_init() };
385			// SAFETY: enforced by type system (typestate pattern)
386			let end = unsafe { self.inner.end.assume_init() };
387			// SAFETY: enforced by type system (typestate pattern)
388			let interval = unsafe { self.inner.interval.assume_init() };
389
390			ClockTimer {
391				next_tick: start,
392				interval,
393				elapsed: TimeDelta::zero(),
394				remaining: end - start
395			}
396		}
397	}
398}