use std::cell::RefCell;
use crate::Context;
use crate::cell::CellHandle;
pub trait TimelineSource {
fn tick(&mut self, now: u64) -> bool;
fn next_fire(&self) -> Option<u64>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ManualClock {
now: u64,
}
impl ManualClock {
pub fn new() -> Self {
Self { now: 0 }
}
pub fn now(&self) -> u64 {
self.now
}
pub fn advance(&mut self, now: u64) -> u64 {
self.now = self.now.max(now);
self.now
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TimerCore {
fire_at: u64,
fired: bool,
}
impl TimerCore {
pub fn new(fire_at: u64) -> Self {
Self {
fire_at,
fired: false,
}
}
pub fn fired(&self) -> bool {
self.fired
}
}
impl TimelineSource for TimerCore {
fn tick(&mut self, now: u64) -> bool {
if self.fired || now < self.fire_at {
return false;
}
self.fired = true;
true
}
fn next_fire(&self) -> Option<u64> {
if self.fired { None } else { Some(self.fire_at) }
}
}
pub struct TimerCell {
core: RefCell<TimerCore>,
fired: CellHandle<bool>,
}
impl TimerCell {
pub fn new(ctx: &Context, fire_at: u64) -> Self {
Self {
core: RefCell::new(TimerCore::new(fire_at)),
fired: ctx.cell(false),
}
}
pub fn tick(&self, ctx: &Context, now: u64) -> bool {
let edge = self.core.borrow_mut().tick(now);
if edge {
self.fired.set(ctx, true);
}
edge
}
pub fn has_fired(&self, ctx: &Context) -> bool {
self.fired.get(ctx)
}
pub fn value(&self, ctx: &Context) -> Option<()> {
if self.fired.get(ctx) { Some(()) } else { None }
}
pub fn fired_cell(&self) -> CellHandle<bool> {
self.fired
}
pub fn next_fire(&self) -> Option<u64> {
self.core.borrow().next_fire()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IntervalCore {
period: u64,
next: u64,
count: u64,
}
impl IntervalCore {
pub fn new(period: u64) -> Self {
let period = period.max(1);
Self {
period,
next: period,
count: 0,
}
}
pub fn count(&self) -> u64 {
self.count
}
fn fires_this_tick(&self, now: u64) -> u64 {
if now < self.next {
0
} else {
(now - self.next) / self.period + 1
}
}
}
impl TimelineSource for IntervalCore {
fn tick(&mut self, now: u64) -> bool {
let fires = self.fires_this_tick(now);
if fires == 0 {
return false;
}
self.count += fires;
self.next += fires * self.period;
true
}
fn next_fire(&self) -> Option<u64> {
Some(self.next)
}
}
pub struct IntervalCell {
core: RefCell<IntervalCore>,
count: CellHandle<u64>,
}
impl IntervalCell {
pub fn new(ctx: &Context, period: u64) -> Self {
Self {
core: RefCell::new(IntervalCore::new(period)),
count: ctx.cell(0u64),
}
}
pub fn tick(&self, ctx: &Context, now: u64) -> bool {
let edge = self.core.borrow_mut().tick(now);
if edge {
let c = self.core.borrow().count();
self.count.set(ctx, c);
}
edge
}
pub fn count(&self, ctx: &Context) -> u64 {
self.count.get(ctx)
}
pub fn count_cell(&self) -> CellHandle<u64> {
self.count
}
pub fn next_fire(&self) -> Option<u64> {
self.core.borrow().next_fire()
}
}
fn count_upto(n: u64, o: u64, cycle: u64) -> u64 {
if o == 0 {
n / cycle
} else if o <= n {
(n - o) / cycle + 1
} else {
0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CronCore {
cycle: u64,
offsets: Vec<u64>,
cursor: u64,
count: u64,
}
impl CronCore {
pub fn new(cycle: u64, offsets: impl IntoIterator<Item = u64>) -> Self {
let cycle = cycle.max(1);
let mut offsets: Vec<u64> = offsets.into_iter().map(|o| o % cycle).collect();
offsets.sort_unstable();
offsets.dedup();
Self {
cycle,
offsets,
cursor: 0,
count: 0,
}
}
pub fn count(&self) -> u64 {
self.count
}
fn matches_in(&self, lo: u64, hi: u64) -> u64 {
self.offsets
.iter()
.map(|&o| count_upto(hi, o, self.cycle) - count_upto(lo, o, self.cycle))
.sum()
}
}
impl TimelineSource for CronCore {
fn tick(&mut self, now: u64) -> bool {
if now <= self.cursor {
self.cursor = self.cursor.max(now);
return false;
}
let fires = self.matches_in(self.cursor, now);
self.cursor = now;
if fires == 0 {
return false;
}
self.count += fires;
true
}
fn next_fire(&self) -> Option<u64> {
if self.offsets.is_empty() {
return None;
}
let start = self.cursor + 1;
let base = start / self.cycle * self.cycle;
for cyc in 0..2u64 {
let block = base + cyc * self.cycle;
for &o in &self.offsets {
let cand = block + o;
if cand >= start {
return Some(cand);
}
}
}
None
}
}
pub struct CronCell {
core: RefCell<CronCore>,
count: CellHandle<u64>,
}
impl CronCell {
pub fn new(ctx: &Context, cycle: u64, offsets: impl IntoIterator<Item = u64>) -> Self {
Self {
core: RefCell::new(CronCore::new(cycle, offsets)),
count: ctx.cell(0u64),
}
}
pub fn tick(&self, ctx: &Context, now: u64) -> bool {
let edge = self.core.borrow_mut().tick(now);
if edge {
let c = self.core.borrow().count();
self.count.set(ctx, c);
}
edge
}
pub fn count(&self, ctx: &Context) -> u64 {
self.count.get(ctx)
}
pub fn count_cell(&self) -> CellHandle<u64> {
self.count
}
pub fn next_fire(&self) -> Option<u64> {
self.core.borrow().next_fire()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Deadlined<T> {
Live(T),
Expired(T),
}
impl<T> Deadlined<T> {
pub fn is_expired(&self) -> bool {
matches!(self, Deadlined::Expired(_))
}
pub fn value(&self) -> &T {
match self {
Deadlined::Live(v) | Deadlined::Expired(v) => v,
}
}
pub fn into_value(self) -> T {
match self {
Deadlined::Live(v) | Deadlined::Expired(v) => v,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DeadlineCore {
timer: TimerCore,
}
impl DeadlineCore {
pub fn new(deadline: u64) -> Self {
Self {
timer: TimerCore::new(deadline),
}
}
pub fn is_expired(&self) -> bool {
self.timer.fired()
}
}
impl TimelineSource for DeadlineCore {
fn tick(&mut self, now: u64) -> bool {
self.timer.tick(now)
}
fn next_fire(&self) -> Option<u64> {
self.timer.next_fire()
}
}
pub struct DeadlineCell<T> {
core: RefCell<DeadlineCore>,
value: T,
expired: CellHandle<bool>,
}
impl<T> DeadlineCell<T>
where
T: Clone + 'static,
{
pub fn new(ctx: &Context, value: T, deadline: u64) -> Self {
Self {
core: RefCell::new(DeadlineCore::new(deadline)),
value,
expired: ctx.cell(false),
}
}
pub fn tick(&self, ctx: &Context, now: u64) -> bool {
let edge = self.core.borrow_mut().tick(now);
if edge {
self.expired.set(ctx, true);
}
edge
}
pub fn state(&self, ctx: &Context) -> Deadlined<T> {
if self.expired.get(ctx) {
Deadlined::Expired(self.value.clone())
} else {
Deadlined::Live(self.value.clone())
}
}
pub fn is_expired(&self, ctx: &Context) -> bool {
self.expired.get(ctx)
}
pub fn expired_cell(&self) -> CellHandle<bool> {
self.expired
}
pub fn next_fire(&self) -> Option<u64> {
self.core.borrow().next_fire()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn timer_fires_once() {
let mut t = TimerCore::new(3);
assert!(!t.tick(1));
assert_eq!(t.next_fire(), Some(3));
assert!(t.tick(3));
assert!(!t.tick(5)); assert_eq!(t.next_fire(), None);
assert!(t.fired());
}
#[test]
fn timer_cell_edge_only_invalidation() {
let ctx = Context::new();
let timer = TimerCell::new(&ctx, 3);
let observed = {
let timer_fired = timer.fired_cell();
ctx.computed(move |c| timer_fired.get(c))
};
assert!(!observed.get(&ctx));
assert!(!timer.tick(&ctx, 1));
assert_eq!(timer.value(&ctx), None);
assert!(timer.tick(&ctx, 3));
assert_eq!(timer.value(&ctx), Some(()));
assert!(timer.has_fired(&ctx));
assert!(!timer.tick(&ctx, 9));
assert!(observed.get(&ctx));
}
#[test]
fn interval_counts_boundaries_including_jumps() {
let mut iv = IntervalCore::new(2);
assert!(!iv.tick(1));
assert_eq!(iv.count(), 0);
assert!(iv.tick(2));
assert_eq!(iv.count(), 1);
assert!(iv.tick(4));
assert_eq!(iv.count(), 2);
assert!(!iv.tick(5));
assert_eq!(iv.count(), 2);
assert!(iv.tick(8)); assert_eq!(iv.count(), 4);
assert_eq!(iv.next_fire(), Some(10));
}
#[test]
fn cron_fires_on_pattern() {
let mut c = CronCore::new(5, [0, 3]);
assert!(!c.tick(2));
assert_eq!(c.count(), 0);
assert_eq!(c.next_fire(), Some(3));
assert!(c.tick(3));
assert_eq!(c.count(), 1);
assert_eq!(c.next_fire(), Some(5));
assert!(c.tick(5));
assert_eq!(c.count(), 2);
assert!(c.tick(8));
assert_eq!(c.count(), 3);
assert!(c.tick(10));
assert_eq!(c.count(), 4);
assert_eq!(c.next_fire(), Some(13));
}
#[test]
fn deadline_expires_preserving_value() {
let ctx = Context::new();
let d = DeadlineCell::new(&ctx, "payload".to_string(), 4);
assert!(matches!(d.state(&ctx), Deadlined::Live(_)));
assert!(!d.tick(&ctx, 2));
assert!(d.tick(&ctx, 4));
match d.state(&ctx) {
Deadlined::Expired(v) => assert_eq!(v, "payload"),
_ => panic!("expected Expired"),
}
assert!(!d.tick(&ctx, 9)); assert!(d.is_expired(&ctx));
}
}