├── .github └── workflows │ └── ci.yml ├── .php-cs-fixer.dist.php ├── LICENSE ├── composer-require-check.json ├── composer.json ├── etc └── Factory.php ├── examples ├── 1-timers.php └── 2-timers-factory.php ├── psalm.xml └── src ├── Internal ├── EventLoopAdapter.php ├── FiberAdapter.php └── Timer.php └── bootstrap.php /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | tests: 9 | strategy: 10 | matrix: 11 | include: 12 | - operating-system: 'ubuntu-latest' 13 | php-version: '8.1' 14 | 15 | name: PHP ${{ matrix.php-version }} ${{ matrix.job-description }} 16 | 17 | runs-on: ${{ matrix.operating-system }} 18 | 19 | steps: 20 | - name: Set git to use LF 21 | run: | 22 | git config --global core.autocrlf false 23 | git config --global core.eol lf 24 | 25 | - name: Checkout code 26 | uses: actions/checkout@v2 27 | 28 | - name: Install OS dependencies 29 | run: | 30 | sudo apt-get update 31 | sudo apt-get install libuv1-dev libevent-dev 32 | 33 | - name: Setup PHP 34 | uses: shivammathur/setup-php@v2 35 | with: 36 | php-version: ${{ matrix.php-version }} 37 | extensions: ev, event, uv-amphp/ext-uv@master 38 | 39 | - name: Get Composer cache directory 40 | id: composer-cache 41 | run: echo "::set-output name=dir::$(composer config cache-dir)" 42 | 43 | - name: Cache dependencies 44 | uses: actions/cache@v2 45 | with: 46 | path: ${{ steps.composer-cache.outputs.dir }} 47 | key: composer-${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.*') }}-${{ matrix.composer-flags }} 48 | restore-keys: | 49 | composer-${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.*') }}- 50 | composer-${{ runner.os }}-${{ matrix.php-version }}- 51 | composer-${{ runner.os }}- 52 | composer- 53 | 54 | - name: Install dependencies 55 | uses: nick-invision/retry@v2 56 | with: 57 | timeout_minutes: 5 58 | max_attempts: 5 59 | retry_wait_seconds: 30 60 | command: | 61 | composer update --optimize-autoloader --no-interaction --no-progress ${{ matrix.composer-flags }} 62 | composer info -D 63 | 64 | - name: Run tests 65 | run: vendor/bin/phpunit ${{ matrix.phpunit-flags }} 66 | 67 | - name: Run static analysis 68 | run: vendor/bin/psalm.phar 69 | 70 | - name: Run style fixer 71 | env: 72 | PHP_CS_FIXER_IGNORE_ENV: 1 73 | run: vendor/bin/php-cs-fixer --diff --dry-run -v fix 74 | if: runner.os != 'Windows' 75 | 76 | - name: Install composer-require-checker 77 | run: php -r 'file_put_contents("composer-require-checker.phar", file_get_contents("https://github.com/maglnet/ComposerRequireChecker/releases/download/3.7.0/composer-require-checker.phar"));' 78 | if: runner.os != 'Windows' && matrix.composer-require-checker-version != 'none' 79 | 80 | - name: Run composer-require-checker 81 | run: php composer-require-checker.phar check composer.json --config-file $PWD/composer-require-check.json 82 | if: runner.os != 'Windows' && matrix.composer-require-checker-version != 'none' 83 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true); 16 | $this->setLineEnding("\n"); 17 | 18 | $this->src = __DIR__ . '/src'; 19 | } 20 | 21 | public function getRules(): array 22 | { 23 | return [ 24 | "@PSR1" => true, 25 | "@PSR2" => true, 26 | "@PSR12" => true, 27 | "braces" => [ 28 | "allow_single_line_closure" => true, 29 | ], 30 | "array_syntax" => ["syntax" => "short"], 31 | "cast_spaces" => true, 32 | "combine_consecutive_unsets" => true, 33 | "function_to_constant" => true, 34 | "native_function_invocation" => [ 35 | 'include' => [ 36 | '@internal', 37 | 'pcntl_async_signals', 38 | 'pcntl_signal_dispatch', 39 | 'pcntl_signal', 40 | 'posix_kill', 41 | 'uv_loop_new', 42 | 'uv_poll_start', 43 | 'uv_poll_stop', 44 | 'uv_now', 45 | 'uv_run', 46 | 'uv_poll_init_socket', 47 | 'uv_timer_init', 48 | 'uv_timer_start', 49 | 'uv_timer_stop', 50 | 'uv_signal_init', 51 | 'uv_signal_start', 52 | 'uv_signal_stop', 53 | 'uv_update_time', 54 | 'uv_is_active', 55 | ], 56 | ], 57 | "multiline_whitespace_before_semicolons" => true, 58 | "no_unused_imports" => true, 59 | "no_useless_else" => true, 60 | "no_useless_return" => true, 61 | "no_whitespace_before_comma_in_array" => true, 62 | "no_whitespace_in_blank_line" => true, 63 | "non_printable_character" => true, 64 | "normalize_index_brace" => true, 65 | "ordered_imports" => ['imports_order' => ['class', 'const', 'function']], 66 | "php_unit_construct" => true, 67 | "php_unit_dedicate_assert" => true, 68 | "php_unit_fqcn_annotation" => true, 69 | "phpdoc_scalar" => ["types" => ['boolean', 'double', 'integer', 'real', 'str']], 70 | "phpdoc_summary" => true, 71 | "phpdoc_types" => ["groups" => ["simple", "meta"]], 72 | "psr_autoloading" => ['dir' => $this->src], 73 | "return_type_declaration" => ["space_before" => "none"], 74 | "short_scalar_cast" => true, 75 | "line_ending" => true, 76 | ]; 77 | } 78 | } 79 | 80 | $config = new Config; 81 | $config->getFinder() 82 | ->in(__DIR__ . '/examples') 83 | ->in(__DIR__ . '/src') 84 | ->in(__DIR__ . '/test'); 85 | 86 | $config->setCacheFile(__DIR__ . '/.php-cs-fixer.cache'); 87 | 88 | return $config; 89 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Revolt (Aaron Piotrowski, Cees-Jan Kiewiet, Niklas Keller, and contributors) 4 | Copyright (c) 2017-2022 amphp (Aaron Piotrowski, Niklas Keller, and contributors) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /composer-require-check.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol-whitelist": [ 3 | "null", 4 | "true", 5 | "false", 6 | "static", 7 | "self", 8 | "parent", 9 | "array", 10 | "string", 11 | "int", 12 | "float", 13 | "bool", 14 | "iterable", 15 | "callable", 16 | "mixed", 17 | "void", 18 | "object" 19 | ], 20 | "php-core-extensions": [ 21 | "Core", 22 | "date", 23 | "pcre", 24 | "Phar", 25 | "Reflection", 26 | "SPL", 27 | "standard", 28 | "hash" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "revolt/event-loop-adapter-react", 3 | "description": "Makes any ReactPHP based library run on top of the Revolt event loop.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Niklas Keller", 8 | "email": "me@kelunik.com" 9 | } 10 | ], 11 | "support": { 12 | "issues": "https://github.com/revoltphp/event-loop-adapter-react/issues" 13 | }, 14 | "require": { 15 | "php": ">=8.1", 16 | "react/async": "^4", 17 | "react/event-loop": "^1 || ^0.5", 18 | "revolt/event-loop": "^1 || ^0.2.4" 19 | }, 20 | "require-dev": { 21 | "friendsofphp/php-cs-fixer": "^3.0", 22 | "phpunit/phpunit": "^9.5.21", 23 | "psalm/phar": "^4.24" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Revolt\\EventLoop\\React\\": "src" 28 | }, 29 | "files": [ 30 | "src/bootstrap.php", 31 | "etc/Factory.php" 32 | ] 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "Revolt\\EventLoop\\React\\": "test", 37 | "React\\Tests\\Async\\": "vendor/react/async/tests", 38 | "React\\Tests\\EventLoop\\": "vendor/react/event-loop/tests" 39 | } 40 | }, 41 | "config": { 42 | "preferred-install": { 43 | "react/async": "source", 44 | "react/event-loop": "source" 45 | } 46 | }, 47 | "scripts": { 48 | "test": "@php -dzend.assertions=1 -dassert.exception=1 vendor/bin/phpunit", 49 | "code-style": "@php vendor/bin/php-cs-fixer fix" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /etc/Factory.php: -------------------------------------------------------------------------------- 1 | addTimer(0.8, function () { 14 | echo 'world!' . PHP_EOL; 15 | }); 16 | 17 | $loop->addTimer(0.3, function () { 18 | echo 'hello '; 19 | }); 20 | 21 | $loop->run(); 22 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/Internal/EventLoopAdapter.php: -------------------------------------------------------------------------------- 1 | driver = $driver ?? EventLoop::getDriver(); 35 | 36 | /** @psalm-suppress RedundantPropertyInitializationCheck */ 37 | self::$instances ??= new \WeakMap(); 38 | self::$instances[$this->driver] = $this; 39 | } 40 | 41 | public function addReadStream($stream, $listener): void 42 | { 43 | if (isset($this->readWatchers[(int) $stream])) { 44 | // Double watchers are silently ignored by ReactPHP 45 | return; 46 | } 47 | 48 | $watcher = $this->driver->onReadable($stream, static function () use ($stream, $listener) { 49 | $listener($stream); 50 | }); 51 | 52 | $this->readWatchers[(int) $stream] = $watcher; 53 | } 54 | 55 | public function addWriteStream($stream, $listener): void 56 | { 57 | if (isset($this->writeWatchers[(int) $stream])) { 58 | // Double watchers are silently ignored by ReactPHP 59 | return; 60 | } 61 | 62 | $watcher = $this->driver->onWritable($stream, static function () use ($stream, $listener) { 63 | $listener($stream); 64 | }); 65 | 66 | $this->writeWatchers[(int) $stream] = $watcher; 67 | } 68 | 69 | public function removeReadStream($stream): void 70 | { 71 | $key = (int) $stream; 72 | 73 | if (!isset($this->readWatchers[$key])) { 74 | return; 75 | } 76 | 77 | $this->driver->cancel($this->readWatchers[$key]); 78 | 79 | unset($this->readWatchers[$key]); 80 | } 81 | 82 | public function removeWriteStream($stream): void 83 | { 84 | $key = (int) $stream; 85 | 86 | if (!isset($this->writeWatchers[$key])) { 87 | return; 88 | } 89 | 90 | $this->driver->cancel($this->writeWatchers[$key]); 91 | 92 | unset($this->writeWatchers[$key]); 93 | } 94 | 95 | public function addTimer($interval, $callback): TimerInterface 96 | { 97 | $timer = new Timer($interval, $callback, false); 98 | 99 | $watcher = $this->driver->delay($timer->getInterval(), function () use ($timer, $callback) { 100 | $this->cancelTimer($timer); 101 | 102 | $callback($timer); 103 | }); 104 | 105 | $this->deferEnabling($watcher); 106 | $this->timers[\spl_object_hash($timer)] = $watcher; 107 | 108 | return $timer; 109 | } 110 | 111 | public function addPeriodicTimer($interval, $callback): TimerInterface 112 | { 113 | $timer = new Timer($interval, $callback, true); 114 | 115 | $watcher = $this->driver->repeat($timer->getInterval(), function () use ($timer, $callback) { 116 | $callback($timer); 117 | }); 118 | 119 | $this->deferEnabling($watcher); 120 | $this->timers[\spl_object_hash($timer)] = $watcher; 121 | 122 | return $timer; 123 | } 124 | 125 | public function cancelTimer(TimerInterface $timer): void 126 | { 127 | if (!isset($this->timers[\spl_object_hash($timer)])) { 128 | return; 129 | } 130 | 131 | $this->driver->cancel($this->timers[\spl_object_hash($timer)]); 132 | 133 | unset($this->timers[\spl_object_hash($timer)]); 134 | } 135 | 136 | public function futureTick($listener): void 137 | { 138 | $this->driver->defer(static function () use ($listener) { 139 | $listener(); 140 | }); 141 | } 142 | 143 | public function addSignal($signal, $listener): void 144 | { 145 | if (\in_array($listener, $this->signals[$signal] ?? [], true)) { 146 | return; 147 | } 148 | 149 | try { 150 | $watcherId = $this->driver->onSignal($signal, static function () use ($listener, $signal) { 151 | $listener($signal); 152 | }); 153 | 154 | $this->signals[$signal][$watcherId] = $listener; 155 | } catch (EventLoop\UnsupportedFeatureException) { 156 | throw new \BadMethodCallException("Signals aren't available in the current environment."); 157 | } 158 | } 159 | 160 | public function removeSignal($signal, $listener): void 161 | { 162 | if (!isset($this->signals[$signal])) { 163 | return; 164 | } 165 | 166 | $index = \array_search($listener, $this->signals[$signal], true); 167 | if ($index === false) { 168 | return; 169 | } 170 | 171 | $this->driver->cancel($index); 172 | 173 | unset($this->signals[$signal][$index]); 174 | if (empty($this->signals[$signal])) { 175 | unset($this->signals[$signal]); 176 | } 177 | } 178 | 179 | public function run(): void 180 | { 181 | $this->driver->run(); 182 | } 183 | 184 | public function stop(): void 185 | { 186 | $this->driver->stop(); 187 | } 188 | 189 | private function deferEnabling(string $watcherId): void 190 | { 191 | $this->driver->disable($watcherId); 192 | $this->driver->defer(function () use ($watcherId) { 193 | try { 194 | $this->driver->enable($watcherId); 195 | } catch (EventLoop\InvalidCallbackError) { 196 | // ignore 197 | } 198 | }); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/Internal/FiberAdapter.php: -------------------------------------------------------------------------------- 1 | suspension === null) { 16 | throw new \Error('Must call suspend() before calling resume()'); 17 | } 18 | 19 | $this->suspension->resume($value); 20 | 21 | // Note: resume() above is async in revolt, but sync in react, 22 | // so let's suspend here until the queued resumption above is executed. 23 | $suspension = EventLoop::getSuspension(); 24 | EventLoop::queue($suspension->resume(...)); 25 | $suspension->suspend(); 26 | } 27 | 28 | public function throw(\Throwable $throwable): void 29 | { 30 | if ($this->suspension === null) { 31 | throw new \Error('Must call suspend() before calling throw()'); 32 | } 33 | 34 | $this->suspension->throw($throwable); 35 | 36 | // Note: throw() above is async in revolt, but sync in react, 37 | // so let's suspend here until the queued throwing above is executed. 38 | $suspension = EventLoop::getSuspension(); 39 | EventLoop::queue($suspension->resume(...)); 40 | $suspension->suspend(); 41 | } 42 | 43 | public function suspend(): mixed 44 | { 45 | if ($this->suspension !== null) { 46 | throw new \Error('Must call resume() or throw() before calling suspend() again'); 47 | } 48 | 49 | $this->suspension = EventLoop::getSuspension(); 50 | 51 | return $this->suspension->suspend(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Internal/Timer.php: -------------------------------------------------------------------------------- 1 | interval = $interval; 24 | $this->callback = $callback; 25 | $this->periodic = $periodic; 26 | } 27 | 28 | public function getInterval(): float 29 | { 30 | return $this->interval; 31 | } 32 | 33 | public function getCallback(): callable 34 | { 35 | return $this->callback; 36 | } 37 | 38 | public function isPeriodic(): bool 39 | { 40 | return $this->periodic; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/bootstrap.php: -------------------------------------------------------------------------------- 1 | new FiberAdapter()); 18 | --------------------------------------------------------------------------------