mirror of
https://github.com/danog/loop.git
synced 2024-11-26 11:54:51 +01:00
First commit
This commit is contained in:
commit
81a524f4d2
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
.vscode
|
||||
build
|
||||
composer.lock
|
||||
phpunit.xml
|
||||
vendor
|
||||
.php_cs.cache
|
||||
coverage
|
13
.php_cs.dist
Normal file
13
.php_cs.dist
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
$config = new Amp\CodeStyle\Config();
|
||||
$config->getFinder()
|
||||
->in(__DIR__ . '/examples')
|
||||
->in(__DIR__ . '/lib')
|
||||
->in(__DIR__ . '/test');
|
||||
|
||||
$cacheDir = getenv('TRAVIS') ? getenv('HOME') . '/.php-cs-fixer' : __DIR__;
|
||||
|
||||
$config->setCacheFile($cacheDir . '/.php_cs.cache');
|
||||
|
||||
return $config;
|
40
.travis.yml
Normal file
40
.travis.yml
Normal file
@ -0,0 +1,40 @@
|
||||
sudo: false
|
||||
|
||||
language: php
|
||||
|
||||
php:
|
||||
- 7.1
|
||||
- 7.2
|
||||
- 7.3
|
||||
- 7.4
|
||||
- nightly
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- php: nightly
|
||||
fast_finish: true
|
||||
|
||||
env:
|
||||
- AMP_DEBUG=true
|
||||
|
||||
before_install:
|
||||
- phpenv config-rm xdebug.ini || echo "No xdebug config."
|
||||
|
||||
install:
|
||||
- composer update -n --prefer-dist
|
||||
- wget https://github.com/php-coveralls/php-coveralls/releases/download/v1.0.2/coveralls.phar
|
||||
- chmod +x coveralls.phar
|
||||
|
||||
script:
|
||||
- vendor/bin/phpunit --coverage-text --coverage-clover build/logs/clover.xml
|
||||
- PHP_CS_FIXER_IGNORE_ENV=1 php vendor/bin/php-cs-fixer --diff --dry-run -v fix
|
||||
- vendor/bin/psalm.phar
|
||||
|
||||
after_script:
|
||||
- ./coveralls.phar -v
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.composer/cache
|
||||
- $HOME/.php-cs-fixer
|
||||
- $HOME/.local
|
7
LICENSE
Normal file
7
LICENSE
Normal file
@ -0,0 +1,7 @@
|
||||
Copyright 2019-2020 Daniil Gentili
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
48
composer.json
Normal file
48
composer.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "danog/loop",
|
||||
"description": "Loop abstraction for AMPHP.",
|
||||
"keywords": [
|
||||
"asynchronous",
|
||||
"async",
|
||||
"concurrent",
|
||||
"multi-threading",
|
||||
"multi-processing"
|
||||
],
|
||||
"homepage": "https://github.com/danog/loop",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Daniil Gentili",
|
||||
"email": "daniil@daniil.it"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.1",
|
||||
"amphp/amp": "^2"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^7 | ^8 | ^9",
|
||||
"amphp/phpunit-util": "^1.3",
|
||||
"amphp/php-cs-fixer-config": "dev-master",
|
||||
"vimeo/psalm": "dev-master"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"danog\\Loop\\": "lib"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"danog\\Loop\\Test\\": "test"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"check": [
|
||||
"@cs",
|
||||
"@test"
|
||||
],
|
||||
"cs": "php-cs-fixer fix -v --diff --dry-run",
|
||||
"cs-fix": "php-cs-fixer fix -v --diff",
|
||||
"test": "@php -dzend.assertions=1 -dassert.exception=1 ./vendor/bin/phpunit --coverage-text"
|
||||
}
|
||||
}
|
129
lib/Generic/GenericLoop.php
Normal file
129
lib/Generic/GenericLoop.php
Normal file
@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Generic loop.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/licenses/MIT MIT
|
||||
*/
|
||||
|
||||
namespace danog\Loop\Generic;
|
||||
|
||||
use Amp\Promise;
|
||||
use danog\Loop\Impl\ResumableSignalLoop;
|
||||
|
||||
/**
|
||||
* Generic loop, runs single callable.
|
||||
*
|
||||
* The return value of the callable can be:
|
||||
* * A number - the loop will be paused for the specified number of seconds
|
||||
* * GenericLoop::STOP - The loop will stop
|
||||
* * GenericLoop::PAUSE - The loop will pause forever (or until loop is `resumed()`
|
||||
* from outside the loop)
|
||||
* * GenericLoop::CONTINUE - Return this if you want to rerun the loop immediately
|
||||
*
|
||||
* If the callable does not return anything,
|
||||
* the loop will behave is if GenericLoop::PAUSE was returned.
|
||||
*
|
||||
* The loop can be stopped from the outside by signaling `true`.
|
||||
*
|
||||
* @template T as int|float|null
|
||||
* @template TGenerator as \Generator<mixed,Promise|array<array-key,Promise>,mixed,Promise<T>|T>
|
||||
* @template TPromise as Promise<T>
|
||||
*
|
||||
* @template TCallable as T|TPromise|TGenerator
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
*/
|
||||
class GenericLoop extends ResumableSignalLoop
|
||||
{
|
||||
/**
|
||||
* Stop the loop.
|
||||
*/
|
||||
const STOP = -1;
|
||||
/**
|
||||
* Pause the loop.
|
||||
*/
|
||||
const PAUSE = null;
|
||||
/**
|
||||
* Rerun the loop.
|
||||
*/
|
||||
const CONTINUE = 0;
|
||||
/**
|
||||
* Callable.
|
||||
*
|
||||
* @var callable
|
||||
*
|
||||
* @psalm-var callable():TCallable
|
||||
*/
|
||||
protected $callable;
|
||||
/**
|
||||
* Loop name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param callable $callable Callable to run
|
||||
* @param string $name Loop name
|
||||
*
|
||||
* @psalm-param callable():TCallable $callable Callable to run
|
||||
*/
|
||||
public function __construct(callable $callable, string $name)
|
||||
{
|
||||
$this->callable = $callable;
|
||||
$this->name = $name;
|
||||
}
|
||||
/**
|
||||
* Loop implementation.
|
||||
*
|
||||
* @return \Generator
|
||||
*/
|
||||
public function loop(): \Generator
|
||||
{
|
||||
$callable = $this->callable;
|
||||
while (true) {
|
||||
/** @psalm-var TCallable */
|
||||
$timeout = $callable();
|
||||
if ($timeout instanceof \Generator) {
|
||||
/** @psalm-var TGenerator */
|
||||
$timeout = yield from $timeout;
|
||||
} elseif ($timeout instanceof Promise) {
|
||||
/** @psalm-var TPromise */
|
||||
$timeout = yield $timeout;
|
||||
}
|
||||
if ($timeout === self::PAUSE) {
|
||||
$this->reportPause(0);
|
||||
} elseif ($timeout > 0) {
|
||||
/** @psalm-suppress MixedArgument */
|
||||
$this->reportPause($timeout);
|
||||
}
|
||||
/** @psalm-suppress MixedArgument */
|
||||
if ($timeout === self::STOP || yield $this->waitSignal($this->pause($timeout))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Report pause, can be overriden for logging.
|
||||
*
|
||||
* @param integer $timeout Pause duration, 0 = forever
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function reportPause(int $timeout): void
|
||||
{
|
||||
}
|
||||
/**
|
||||
* Get loop name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
83
lib/Generic/PeriodicLoop.php
Normal file
83
lib/Generic/PeriodicLoop.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
/**
|
||||
* Periodic loop.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/licenses/MIT MIT
|
||||
*/
|
||||
|
||||
namespace danog\Loop\Generic;
|
||||
|
||||
use danog\Loop\Impl\ResumableSignalLoop;
|
||||
|
||||
/**
|
||||
* Periodic loop.
|
||||
*
|
||||
* Runs a callback at a periodic interval.
|
||||
*
|
||||
* The loop can be stopped from the outside or
|
||||
* from the inside by signaling or returning `true`.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
*/
|
||||
class PeriodicLoop extends ResumableSignalLoop
|
||||
{
|
||||
/**
|
||||
* Callback.
|
||||
*
|
||||
* @var callable
|
||||
*/
|
||||
private $callback;
|
||||
/**
|
||||
* Loop name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private string $name;
|
||||
/**
|
||||
* Loop interval.
|
||||
*
|
||||
* @var float|int
|
||||
*/
|
||||
private $interval;
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param callable $callback Callback to call
|
||||
* @param string $name Loop name
|
||||
* @param int|float $interval Loop interval
|
||||
*/
|
||||
public function __construct(callable $callback, string $name, $interval)
|
||||
{
|
||||
$this->callback = $callback;
|
||||
$this->name = $name;
|
||||
$this->interval = $interval;
|
||||
}
|
||||
/**
|
||||
* Loop implementation.
|
||||
*
|
||||
* @return \Generator
|
||||
*/
|
||||
public function loop(): \Generator
|
||||
{
|
||||
$callback = $this->callback;
|
||||
while (true) {
|
||||
/** @psalm-suppress MixedAssignment */
|
||||
$result = yield $this->waitSignal($this->pause($this->interval));
|
||||
if ($result) {
|
||||
return;
|
||||
}
|
||||
yield $callback();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get name of the loop.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
23
lib/Impl/Loop.php
Normal file
23
lib/Impl/Loop.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
/**
|
||||
* Loop class.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/licenses/MIT MIT
|
||||
*/
|
||||
|
||||
namespace danog\Loop\Impl;
|
||||
|
||||
use danog\Loop\LoopInterface;
|
||||
use danog\Loop\Traits\Loop as TraitsLoop;
|
||||
|
||||
/**
|
||||
* Loop abstract class.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
*/
|
||||
abstract class Loop implements LoopInterface
|
||||
{
|
||||
use TraitsLoop;
|
||||
}
|
23
lib/Impl/ResumableLoop.php
Normal file
23
lib/Impl/ResumableLoop.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
/**
|
||||
* Resumable loop class.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/licenses/MIT MIT
|
||||
*/
|
||||
|
||||
namespace danog\Loop\Impl;
|
||||
|
||||
use danog\Loop\ResumableLoopInterface;
|
||||
use danog\Loop\Traits\ResumableLoop as TraitsResumableLoop;
|
||||
|
||||
/**
|
||||
* Resumable loop abstract class.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
*/
|
||||
abstract class ResumableLoop implements ResumableLoopInterface
|
||||
{
|
||||
use TraitsResumableLoop;
|
||||
}
|
26
lib/Impl/ResumableSignalLoop.php
Normal file
26
lib/Impl/ResumableSignalLoop.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
* Resumable signal loop class.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/licenses/MIT MIT
|
||||
*/
|
||||
|
||||
namespace danog\Loop\Impl;
|
||||
|
||||
use danog\Loop\ResumableLoopInterface;
|
||||
use danog\Loop\SignalLoopInterface;
|
||||
use danog\Loop\Traits\ResumableLoop;
|
||||
use danog\Loop\Traits\SignalLoop;
|
||||
|
||||
/**
|
||||
* Resumable signal loop abstract class.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
*/
|
||||
abstract class ResumableSignalLoop implements ResumableLoopInterface, SignalLoopInterface
|
||||
{
|
||||
use ResumableLoop;
|
||||
use SignalLoop;
|
||||
}
|
25
lib/Impl/SignalLoop.php
Normal file
25
lib/Impl/SignalLoop.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* Signal loop class.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/licenses/MIT MIT
|
||||
*/
|
||||
|
||||
namespace danog\Loop\Impl;
|
||||
|
||||
use danog\Loop\SignalLoopInterface;
|
||||
use danog\Loop\Traits\Loop;
|
||||
use danog\Loop\Traits\SignalLoop as TraitsSignalLoop;
|
||||
|
||||
/**
|
||||
* Signal loop abstract class.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
*/
|
||||
abstract class SignalLoop implements SignalLoopInterface
|
||||
{
|
||||
use Loop;
|
||||
use TraitsSignalLoop;
|
||||
}
|
48
lib/LoopInterface.php
Normal file
48
lib/LoopInterface.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Loop interface.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/licenses/MIT MIT
|
||||
*/
|
||||
|
||||
namespace danog\Loop;
|
||||
|
||||
use Amp\Promise;
|
||||
|
||||
/**
|
||||
* Loop interface.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
*/
|
||||
interface LoopInterface
|
||||
{
|
||||
/**
|
||||
* Start the loop.
|
||||
*
|
||||
* Returns false if the loop is already running.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function start(): bool;
|
||||
/**
|
||||
* The actual loop function.
|
||||
*
|
||||
* @return \Generator
|
||||
*/
|
||||
public function loop(): \Generator;
|
||||
/**
|
||||
* Get name of the loop.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string;
|
||||
/**
|
||||
* Check whether loop is running.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isRunning(): bool;
|
||||
}
|
36
lib/ResumableLoopInterface.php
Normal file
36
lib/ResumableLoopInterface.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Resumable loop interface.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/licenses/MIT MIT
|
||||
*/
|
||||
|
||||
namespace danog\Loop;
|
||||
|
||||
use Amp\Promise;
|
||||
|
||||
/**
|
||||
* Resumable loop interface.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
*/
|
||||
interface ResumableLoopInterface extends LoopInterface
|
||||
{
|
||||
/**
|
||||
* Pause the loop.
|
||||
*
|
||||
* @param int|float|null $time For how long to pause the loop, if null will pause forever (until resume is called from outside of the loop)
|
||||
*
|
||||
* @return Promise Resolved when the loop is resumed
|
||||
*/
|
||||
public function pause($time = null): Promise;
|
||||
/**
|
||||
* Resume the loop.
|
||||
*
|
||||
* @return Promise Resolved when the loop is paused again
|
||||
*/
|
||||
public function resume(): Promise;
|
||||
}
|
45
lib/SignalLoopInterface.php
Normal file
45
lib/SignalLoopInterface.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Signal loop interface.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/licenses/MIT MIT
|
||||
*/
|
||||
|
||||
namespace danog\Loop;
|
||||
|
||||
use Amp\Promise;
|
||||
|
||||
/**
|
||||
* Signal loop interface.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
*/
|
||||
interface SignalLoopInterface extends LoopInterface
|
||||
{
|
||||
/**
|
||||
* Send a signal to the the loop.
|
||||
*
|
||||
* @param \Throwable|mixed $data Signal to send
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function signal($data): void;
|
||||
/**
|
||||
* Resolve the promise or return|throw the signal.
|
||||
*
|
||||
* @param Promise|\Generator $promise The original promise or generator
|
||||
*
|
||||
* @return Promise
|
||||
*
|
||||
* @template T
|
||||
*
|
||||
* @psalm-param Promise<T>|\Generator<mixed,Promise|array<array-key,
|
||||
* Promise>,mixed,Promise<T>|T> $promise The original promise or generator
|
||||
*
|
||||
* @psalm-return Promise<T|mixed>
|
||||
*/
|
||||
public function waitSignal($promise): Promise;
|
||||
}
|
80
lib/Traits/Loop.php
Normal file
80
lib/Traits/Loop.php
Normal file
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/**
|
||||
* Loop helper trait.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/licenses/MIT MIT
|
||||
*/
|
||||
|
||||
namespace danog\Loop\Traits;
|
||||
|
||||
use Amp\Promise;
|
||||
|
||||
use function Amp\asyncCall;
|
||||
|
||||
/**
|
||||
* Loop helper trait.
|
||||
*
|
||||
* Wraps the asynchronous generator methods with asynchronous promise-based methods
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
*/
|
||||
trait Loop
|
||||
{
|
||||
/**
|
||||
* Whether the loop was started.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $started = false;
|
||||
/**
|
||||
* Start the loop.
|
||||
*
|
||||
* Returns false if the loop is already running.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function start(): bool
|
||||
{
|
||||
if ($this->started) {
|
||||
return false;
|
||||
}
|
||||
asyncCall(function (): \Generator {
|
||||
$this->startedLoop();
|
||||
try {
|
||||
yield from $this->loop();
|
||||
} finally {
|
||||
$this->exitedLoop();
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Signal that loop has exIited.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function exitedLoop(): void
|
||||
{
|
||||
$this->started = false;
|
||||
}
|
||||
/**
|
||||
* Signal that loop has started.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function startedLoop(): void
|
||||
{
|
||||
$this->started = true;
|
||||
}
|
||||
/**
|
||||
* Check whether loop is running.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isRunning(): bool
|
||||
{
|
||||
return $this->started;
|
||||
}
|
||||
}
|
135
lib/Traits/ResumableLoop.php
Normal file
135
lib/Traits/ResumableLoop.php
Normal file
@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Loop helper trait.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/licenses/MIT MIT
|
||||
*/
|
||||
|
||||
namespace danog\Loop\Traits;
|
||||
|
||||
use Amp\Deferred;
|
||||
use Amp\Loop as AmpLoop;
|
||||
use Amp\Promise;
|
||||
use Amp\Success;
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* Resumable loop helper trait.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
*/
|
||||
trait ResumableLoop
|
||||
{
|
||||
use Loop {
|
||||
exitedLoop as parentExitedLoop;
|
||||
}
|
||||
/**
|
||||
* Resume deferred.
|
||||
*
|
||||
* @var ?Deferred
|
||||
*/
|
||||
private $resume;
|
||||
/**
|
||||
* Pause deferred.
|
||||
*
|
||||
* @var ?Deferred
|
||||
*/
|
||||
private $pause;
|
||||
/**
|
||||
* Resume timer ID.
|
||||
*
|
||||
* @var ?string
|
||||
*/
|
||||
private $resumeTimer;
|
||||
/**
|
||||
* Pause the loop.
|
||||
*
|
||||
* @param int|float|null $time For how long to pause the loop, if null will pause forever (until resume is called from outside of the loop)
|
||||
*
|
||||
* @return Promise Resolved when the loop is resumed
|
||||
*/
|
||||
public function pause($time = null): Promise
|
||||
{
|
||||
if (!\is_null($time)) {
|
||||
if ($time <= 0) {
|
||||
return new Success(0);
|
||||
}
|
||||
if ($this->resumeTimer) {
|
||||
AmpLoop::cancel($this->resumeTimer);
|
||||
$this->resumeTimer = null;
|
||||
}
|
||||
$this->resumeTimer = AmpLoop::delay((int) ($time * 1000), \Closure::fromCallable([$this, 'resumeInternal']));
|
||||
}
|
||||
$this->resume = new Deferred();
|
||||
$pause = $this->pause;
|
||||
$this->pause = new Deferred();
|
||||
if ($pause) {
|
||||
/**
|
||||
* @psalm-suppress InvalidArgument
|
||||
*/
|
||||
AmpLoop::defer([$pause, 'resolve']);
|
||||
}
|
||||
return $this->resume->promise();
|
||||
}
|
||||
/**
|
||||
* Resume the loop.
|
||||
*
|
||||
* @return Promise Resolved when the loop is paused again
|
||||
*/
|
||||
public function resume(): Promise
|
||||
{
|
||||
$this->resumeInternal();
|
||||
if (!$this->pause) {
|
||||
$this->pause = new Deferred;
|
||||
}
|
||||
return $this->pause->promise();
|
||||
}
|
||||
/**
|
||||
* Defer resuming the loop.
|
||||
*
|
||||
* @return Promise Resolved when the loop is paused again
|
||||
*/
|
||||
public function resumeDefer(): Promise
|
||||
{
|
||||
AmpLoop::defer(Closure::fromCallable([$this, 'resumeInternal']));
|
||||
if (!$this->pause) {
|
||||
$this->pause = new Deferred;
|
||||
}
|
||||
return $this->pause->promise();
|
||||
}
|
||||
/**
|
||||
* Internal resume function.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function resumeInternal(): void
|
||||
{
|
||||
if ($this->resumeTimer) {
|
||||
$storedWatcherId = $this->resumeTimer;
|
||||
AmpLoop::cancel($storedWatcherId);
|
||||
$this->resumeTimer = null;
|
||||
}
|
||||
if ($this->resume) {
|
||||
$resume = $this->resume;
|
||||
$this->resume = null;
|
||||
$resume->resolve();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signal that loop has exIited.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function exitedLoop(): void
|
||||
{
|
||||
$this->parentExitedLoop();
|
||||
if ($this->resumeTimer) {
|
||||
AmpLoop::cancel($this->resumeTimer);
|
||||
$this->resumeTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
81
lib/Traits/SignalLoop.php
Normal file
81
lib/Traits/SignalLoop.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Loop helper trait.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/licenses/MIT MIT
|
||||
*/
|
||||
|
||||
namespace danog\Loop\Traits;
|
||||
|
||||
use Amp\Coroutine;
|
||||
use Amp\Deferred;
|
||||
use Amp\Promise;
|
||||
|
||||
/**
|
||||
* Signal loop helper trait.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
*/
|
||||
trait SignalLoop
|
||||
{
|
||||
/**
|
||||
* Signal deferred.
|
||||
*
|
||||
* @var ?Deferred
|
||||
*/
|
||||
private $signalDeferred;
|
||||
/**
|
||||
* Send signal to loop.
|
||||
*
|
||||
* @param mixed|\Throwable $what Data to signal
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function signal($what): void
|
||||
{
|
||||
if ($this->signalDeferred) {
|
||||
$deferred = $this->signalDeferred;
|
||||
$this->signalDeferred = null;
|
||||
if ($what instanceof \Throwable) {
|
||||
$deferred->fail($what);
|
||||
} else {
|
||||
$deferred->resolve($what);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Resolve the promise or return|throw the signal.
|
||||
*
|
||||
* @param Promise|\Generator $promise The original promise or generator
|
||||
*
|
||||
* @return Promise
|
||||
*
|
||||
* @template T
|
||||
*
|
||||
* @psalm-param Promise<T>|\Generator<mixed,Promise|array<array-key,
|
||||
* Promise>,mixed,Promise<T>|T> $promise The original promise or generator
|
||||
*
|
||||
* @psalm-return Promise<T|mixed>
|
||||
*/
|
||||
public function waitSignal($promise): Promise
|
||||
{
|
||||
if ($promise instanceof \Generator) {
|
||||
$promise = new Coroutine($promise);
|
||||
}
|
||||
$this->signalDeferred = new Deferred();
|
||||
$combinedPromise = $this->signalDeferred->promise();
|
||||
$promise->onResolve(
|
||||
function () use ($promise) {
|
||||
if ($this->signalDeferred !== null) {
|
||||
$deferred = $this->signalDeferred;
|
||||
$this->signalDeferred = null;
|
||||
$deferred->resolve($promise);
|
||||
}
|
||||
}
|
||||
);
|
||||
return $combinedPromise;
|
||||
}
|
||||
}
|
28
phpunit.xml.dist
Normal file
28
phpunit.xml.dist
Normal file
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd"
|
||||
backupGlobals="false"
|
||||
backupStaticAttributes="false"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
processIsolation="false"
|
||||
stopOnFailure="false"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Amp Concurrent">
|
||||
<directory>test</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<filter>
|
||||
<whitelist>
|
||||
<directory suffix=".php">lib</directory>
|
||||
</whitelist>
|
||||
</filter>
|
||||
<logging>
|
||||
<log type="coverage-html" target="build/coverage"/>
|
||||
</logging>
|
||||
</phpunit>
|
15
psalm.xml
Normal file
15
psalm.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0"?>
|
||||
<psalm
|
||||
errorLevel="1"
|
||||
resolveFromConfigFile="true"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="https://getpsalm.org/schema/config"
|
||||
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
|
||||
>
|
||||
<projectFiles>
|
||||
<directory name="lib" />
|
||||
<ignoreFiles>
|
||||
<directory name="vendor" />
|
||||
</ignoreFiles>
|
||||
</projectFiles>
|
||||
</psalm>
|
46
test/BasicInterface.php
Normal file
46
test/BasicInterface.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Basic loop test interface.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/licenses/MIT MIT
|
||||
*/
|
||||
|
||||
namespace danog\Loop\Test;
|
||||
|
||||
use danog\Loop\LoopInterface;
|
||||
|
||||
/**
|
||||
* Basic loop test interface.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
*/
|
||||
interface BasicInterface extends LoopInterface
|
||||
{
|
||||
/**
|
||||
* Check whether the loop inited.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function inited(): bool;
|
||||
/**
|
||||
* Check whether the loop ran.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function ran(): bool;
|
||||
/**
|
||||
* Get start counter.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function startCounter(): int;
|
||||
/**
|
||||
* Get end counter.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function endCounter(): int;
|
||||
}
|
87
test/LoopTest.php
Normal file
87
test/LoopTest.php
Normal file
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
/**
|
||||
* Loop test.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/licenses/MIT MIT
|
||||
*/
|
||||
|
||||
namespace danog\Loop\Test;
|
||||
|
||||
use Amp\PHPUnit\AsyncTestCase;
|
||||
use danog\Loop\Impl\Loop;
|
||||
use danog\Loop\Impl\ResumableLoop;
|
||||
use danog\Loop\Impl\ResumableSignalLoop;
|
||||
use danog\Loop\Impl\SignalLoop;
|
||||
use danog\Loop\Test\Traits\Basic;
|
||||
use danog\Loop\Test\Traits\BasicResumable;
|
||||
|
||||
use function Amp\delay;
|
||||
|
||||
class LoopTest extends AsyncTestCase
|
||||
{
|
||||
const LOOP_NAME = 'PONY';
|
||||
/**
|
||||
* Test basic loop.
|
||||
*
|
||||
* @param BasicInterface $loop Loop
|
||||
*
|
||||
* @return \Generator
|
||||
*
|
||||
* @dataProvider provideBasic
|
||||
*/
|
||||
public function testLoop(BasicInterface $loop): \Generator
|
||||
{
|
||||
$this->assertEquals("$loop", self::LOOP_NAME);
|
||||
|
||||
$this->assertTrue($loop->start());
|
||||
$this->assertFalse($loop->start());
|
||||
|
||||
$this->assertTrue($loop->inited());
|
||||
|
||||
$this->assertFalse($loop->ran());
|
||||
$this->assertTrue($loop->isRunning());
|
||||
|
||||
$this->assertEquals($loop->startCounter(), 1);
|
||||
$this->assertEquals($loop->endCounter(), 0);
|
||||
|
||||
yield delay(110);
|
||||
|
||||
$this->assertTrue($loop->ran());
|
||||
$this->assertFalse($loop->isRunning());
|
||||
|
||||
$this->assertEquals($loop->startCounter(), 1);
|
||||
$this->assertEquals($loop->endCounter(), 1);
|
||||
}
|
||||
/**
|
||||
* Provide loop implementations.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function provideBasic(): array
|
||||
{
|
||||
return [
|
||||
[new class() extends Loop implements BasicInterface {
|
||||
use Basic;
|
||||
}],
|
||||
[new class() extends SignalLoop implements BasicInterface {
|
||||
use Basic;
|
||||
}],
|
||||
[new class() extends ResumableLoop implements BasicInterface {
|
||||
use Basic;
|
||||
}],
|
||||
[new class() extends ResumableSignalLoop implements BasicInterface {
|
||||
use Basic;
|
||||
}],
|
||||
|
||||
|
||||
[new class() extends ResumableLoop implements BasicInterface {
|
||||
use BasicResumable;
|
||||
}],
|
||||
[new class() extends ResumableSignalLoop implements BasicInterface {
|
||||
use BasicResumable;
|
||||
}],
|
||||
];
|
||||
}
|
||||
}
|
121
test/Traits/Basic.php
Normal file
121
test/Traits/Basic.php
Normal file
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
/**
|
||||
* Loop test trait.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/licenses/MIT MIT
|
||||
*/
|
||||
|
||||
namespace danog\Loop\Test\Traits;
|
||||
|
||||
use danog\Loop\Test\LoopTest;
|
||||
use Generator;
|
||||
|
||||
use function Amp\delay;
|
||||
|
||||
trait Basic
|
||||
{
|
||||
/**
|
||||
* Check whether the loop started.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $startCounter = 0;
|
||||
/**
|
||||
* Check whether the loop ended.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $endCounter = 0;
|
||||
/**
|
||||
* Check whether the loop inited.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $inited = false;
|
||||
/**
|
||||
* Check whether the loop ran.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $ran = false;
|
||||
/**
|
||||
* Check whether the loop inited.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function inited(): bool
|
||||
{
|
||||
return $this->inited;
|
||||
}
|
||||
/**
|
||||
* Check whether the loop ran.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function ran(): bool
|
||||
{
|
||||
return $this->ran;
|
||||
}
|
||||
/**
|
||||
* Loop implementation.
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function loop(): Generator
|
||||
{
|
||||
$this->inited = true;
|
||||
yield delay(100);
|
||||
$this->ran = true;
|
||||
}
|
||||
/**
|
||||
* Get loop name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return LoopTest::LOOP_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signal that loop started.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function startedLoop(): void
|
||||
{
|
||||
$this->startCounter++;
|
||||
parent::startedLoop();
|
||||
}
|
||||
/**
|
||||
* Signal that loop ended.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function exitedLoop(): void
|
||||
{
|
||||
$this->endCounter++;
|
||||
parent::exitedLoop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get start counter.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function startCounter(): int
|
||||
{
|
||||
return $this->startCounter;
|
||||
}
|
||||
/**
|
||||
* Get end counter.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function endCounter(): int
|
||||
{
|
||||
return $this->endCounter;
|
||||
}
|
||||
}
|
29
test/Traits/BasicResumable.php
Normal file
29
test/Traits/BasicResumable.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* Resumable test trait.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/licenses/MIT MIT
|
||||
*/
|
||||
|
||||
namespace danog\Loop\Test\Traits;
|
||||
|
||||
use Generator;
|
||||
|
||||
use function Amp\delay;
|
||||
|
||||
trait BasicResumable
|
||||
{
|
||||
use Basic;
|
||||
/**
|
||||
* Loop implementation.
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function loop(): Generator
|
||||
{
|
||||
yield $this->pause(0.1);
|
||||
$this->ran = true;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user