Lines 100.00% 23 / 23
Methods 100.00% 10 / 10
Classes 100.00% 1 / 1
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 occursAtAll 100.00% 1 / 1 100.00% 1 / 1 1
 start 100.00% 2 / 2 100.00% 1 / 1 1
 dueAt 100.00% 1 / 1 100.00% 1 / 1 1
 isDue 100.00% 1 / 1 100.00% 1 / 1 2
 delayUntilDue 100.00% 5 / 5 100.00% 1 / 1 3
 advance 100.00% 8 / 8 100.00% 1 / 1 4
 isRecurring 100.00% 1 / 1 100.00% 1 / 1 1
 describe 100.00% 1 / 1 100.00% 1 / 1 1
 findNextOccurrenceAfter 100.00% 2 / 2 100.00% 1 / 1 2
12final class WallTrigger implements Trigger
13{
14    private const int MAX_COUNTED_MISSED_OCCURRENCES = 100;
15
16    // Timers sleep through NTP steps and host suspends; re-reading the wall clock catches them.
17    private const int WALL_CLOCK_RECHECK_SECONDS = 60;
18
19    private readonly Instant $armedAt;
20
21    private ?Instant $due = null;
22
23    public function __construct(
24        private readonly WallClockSchedule $schedule,
25        private readonly Clock $clock,
26    ) {
27        $this->armedAt = $clock->getNow();
28    }
29
30    public function occursAtAll(): bool
31    {
32        return $this->findNextOccurrenceAfter($this->armedAt) !== null;
33    }
34
35    public function start(): bool
36    {
37        $this->due = $this->findNextOccurrenceAfter($this->armedAt);
38
39        return $this->due !== null;
40    }
41
42    public function dueAt(): ?Instant
43    {
44        return $this->due;
45    }
46
47    public function isDue(): bool
48    {
49        return $this->due !== null && !$this->clock->getNow()->isBefore($this->due);
50    }
51
52    public function delayUntilDue(): ?Duration
53    {
54        if ($this->due === null) {
55            return null;
56        }
57
58        $delay = $this->due->elapsedSince($this->clock->getNow());
59        $recheckInterval = Duration::seconds(self::WALL_CLOCK_RECHECK_SECONDS);
60
61        return $delay->isLongerThan($recheckInterval) ? $recheckInterval : $delay;
62    }
63
64    public function advance(): int
65    {
66        $now = $this->clock->getNow();
67        $due = $this->due ?? $now;
68        $this->due = $this->findNextOccurrenceAfter($now);
69
70        $missed = 0;
71        $cursor = $due;
72
73        while ($missed < self::MAX_COUNTED_MISSED_OCCURRENCES && ($cursor = $this->findNextOccurrenceAfter($cursor)) !== null && !$cursor->isAfter($now)) {
74            ++$missed;
75        }
76
77        return $missed;
78    }
79
80    public function isRecurring(): bool
81    {
82        return $this->schedule->isRecurring();
83    }
84
85    public function describe(): string
86    {
87        return $this->schedule->describe();
88    }
89
90    private function findNextOccurrenceAfter(Instant $moment): ?Instant
91    {
92        $next = $this->schedule->findNextOccurrenceAfter($moment->toDateTime($this->clock->getTimeZone()));
93
94        return $next === null ? null : Instant::fromDateTime($next);
95    }
96}