Lines
100.00%
35 / 35
Methods
100.00%
8 / 8
Classes
100.00%
1 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| __construct | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| startTimer | 100.00% | 7 / 7 | 100.00% | 1 / 1 | 1 | ||
| waitFor | 100.00% | 6 / 6 | 100.00% | 1 / 1 | 2 | ||
| timeout | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 1 | ||
| delay | 100.00% | 8 / 8 | 100.00% | 1 / 1 | 2 | ||
| countPendingTimers | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| findEarliestDueBy | 100.00% | 6 / 6 | 100.00% | 1 / 1 | 5 | ||
| forget | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| 20 | final class ManualTimers implements Timers, Deadlines | |
| 21 | { | |
| 22 | /** @var array<int, ManualTimer> */ | |
| 23 | private array $timers = []; | |
| 24 | ||
| 25 | public readonly HandlerFibers $handlerFibers; | |
| 26 | ||
| 27 | public function __construct(public readonly VirtualClock $clock = new VirtualClock()) | |
| 28 | { | |
| 29 | $this->handlerFibers = new HandlerFibers(); | |
| 30 | } | |
| 31 | ||
| 32 | public function startTimer(Duration $delay, Closure $callback): TimerHandle | |
| 33 | { | |
| 34 | $timer = new ManualTimer( | |
| 35 | $this->clock->getMonotonicTime()->plus($delay), | |
| 36 | $callback, | |
| 37 | $this->forget(...), | |
| 38 | ); | |
| 39 | ||
| 40 | $this->timers[spl_object_id($timer)] = $timer; | |
| 41 | ||
| 42 | return $timer; | |
| 43 | } | |
| 44 | ||
| 45 | /** | |
| 46 | * @template T | |
| 47 | * @param Closure(PendingWait<T>): (Closure(): void) $arm | |
| 48 | * @return T|null | |
| 49 | */ | |
| 50 | public function waitFor(Closure $arm, ?Duration $timeout = null): mixed | |
| 51 | { | |
| 52 | /** @var DeferredWait<T> $wait */ | |
| 53 | $wait = new DeferredWait(); | |
| 54 | $teardown = $arm($wait); | |
| 55 | $timeoutTimer = $timeout === null ? null : $this->startTimer($timeout, $wait->expire(...)); | |
| 56 | ||
| 57 | try { | |
| 58 | return $wait->awaitOutcome(); | |
| 59 | } finally { | |
| 60 | $timeoutTimer?->cancel(); | |
| 61 | $teardown(); | |
| 62 | } | |
| 63 | } | |
| 64 | ||
| 65 | public function timeout(Duration $limit): Cancellation | |
| 66 | { | |
| 67 | $deadline = new DeferredCancellation(); | |
| 68 | $timer = $this->startTimer($limit, static function () use ($deadline): void { | |
| 69 | $deadline->cancel(new TimeoutException()); | |
| 70 | }); | |
| 71 | ||
| 72 | return new ManualTimeout($deadline->getCancellation(), $timer); | |
| 73 | } | |
| 74 | ||
| 75 | public function delay(Duration $wait, ?Cancellation $cancellation = null): void | |
| 76 | { | |
| 77 | $cancellation?->throwIfRequested(); | |
| 78 | ||
| 79 | $target = $this->clock->getMonotonicTime()->plus($wait); | |
| 80 | ||
| 81 | while (($due = $this->findEarliestDueBy($target)) !== null) { | |
| 82 | $this->clock->moveTo($due->dueAt); | |
| 83 | $due->fire(); | |
| 84 | // Fibers the timer resumed run before the next timer is due, so the timers they start count. | |
| 85 | EventLoopTicks::settle(); | |
| 86 | $cancellation?->throwIfRequested(); | |
| 87 | } | |
| 88 | ||
| 89 | $this->clock->moveTo($target); | |
| 90 | } | |
| 91 | ||
| 92 | public function countPendingTimers(): int | |
| 93 | { | |
| 94 | return \count($this->timers); | |
| 95 | } | |
| 96 | ||
| 97 | private function findEarliestDueBy(MonotonicTime $target): ?ManualTimer | |
| 98 | { | |
| 99 | $earliest = null; | |
| 100 | ||
| 101 | foreach ($this->timers as $timer) { | |
| 102 | if ($timer->dueAt->isAfter($target)) { | |
| 103 | continue; | |
| 104 | } | |
| 105 | ||
| 106 | if ($earliest === null || $timer->dueAt->isBefore($earliest->dueAt)) { | |
| 107 | $earliest = $timer; | |
| 108 | } | |
| 109 | } | |
| 110 | ||
| 111 | return $earliest; | |
| 112 | } | |
| 113 | ||
| 114 | private function forget(ManualTimer $timer): void | |
| 115 | { | |
| 116 | unset($this->timers[spl_object_id($timer)]); | |
| 117 | } | |
| 118 | } |