* @copyright 2016-2020 Daniil Gentili * @license https://opensource.org/licenses/MIT MIT */ namespace danog\Loop; use AssertionError; use Revolt\EventLoop; use Stringable; /** * Generic loop, runs single callable. * * @author Daniil Gentili */ abstract class Loop implements Stringable { /** * Stop the loop. */ public const STOP = -1.0; /** * Pause the loop. */ public const PAUSE = null; /** * Rerun the loop. */ public const CONTINUE = 0.0; /** * Whether the loop is running. */ private bool $running = false; /** * Resume timer ID. */ private ?string $resumeTimer = null; /** * Resume deferred ID. */ private ?string $resumeImmediate = null; /** * Report pause, can be overriden for logging. * * @param float $timeout Pause duration, 0 = forever */ protected function reportPause(float $timeout): void { } /** * Start the loop. * * Returns false if the loop is already running. */ public function start(): bool { if ($this->running) { return false; } $this->running = true; if (!$this->resume()) { throw new AssertionError("Could not resume!"); } $this->startedLoop(); return true; } /** * Stops loop. * * Returns false if the loop is not running. */ public function stop(): bool { if (!$this->running) { return false; } $this->running = false; if ($this->resumeTimer) { $storedWatcherId = $this->resumeTimer; EventLoop::cancel($storedWatcherId); $this->resumeTimer = null; } if ($this->resumeImmediate) { $storedWatcherId = $this->resumeImmediate; EventLoop::cancel($storedWatcherId); $this->resumeImmediate = null; } if ($this->paused) { $this->exitedLoop(); } return true; } abstract protected function loop(): ?float; private bool $paused = true; private function loopInternal(): void { if (!$this->running) { throw new AssertionError("Already running!"); } if (!$this->paused) { throw new AssertionError("Already paused!"); } $this->paused = false; try { $timeout = $this->loop(); } catch (\Throwable $e) { $this->exitedLoopInternal(); throw $e; } /** @var bool $this->running */ if (!$this->running) { $this->exitedLoopInternal(); return; } if ($timeout === self::STOP) { $this->exitedLoopInternal(); return; } $this->paused = true; if ($timeout === self::PAUSE) { $this->reportPause(0.0); } else { if (!$this->resumeImmediate) { if ($this->resumeTimer !== null) { throw new AssertionError("Already have a resume timer!"); } $this->resumeTimer = EventLoop::delay($timeout, function (): void { $this->resumeTimer = null; $this->loopInternal(); }); } $this->reportPause($timeout); } } private function exitedLoopInternal(): void { $this->running = false; $this->paused = true; if ($this->resumeTimer !== null) { throw new AssertionError("Already have a resume timer!"); } if ($this->resumeTimer !== null) { throw new AssertionError("Already have a resume immediate timer!"); } $this->exitedLoop(); } /** * Signal that loop was started. */ protected function startedLoop(): void { } /** * Signal that loop has exited. */ protected function exitedLoop(): void { } /** * Check whether loop is running. */ public function isRunning(): bool { return $this->running; } /** * Check whether loop is paused (different from isRunning, a loop may be running but paused). */ public function isPaused(): bool { return $this->paused; } /** * Resume the loop. * * @return bool Returns false if the loop is not paused. */ public function resume(): bool { if (!$this->resumeImmediate && $this->running && $this->paused) { if ($this->resumeTimer) { $timer = $this->resumeTimer; $this->resumeTimer = null; EventLoop::cancel($timer); } $this->resumeImmediate = EventLoop::defer(function (): void { $this->resumeImmediate = null; $this->loopInternal(); }); return true; } return false; } }