├── LICENSE ├── composer.json ├── src ├── FiberLoop.php └── RejectedException.php └── stubs ├── Fiber.php ├── FiberError.php ├── FiberExit.php └── ReflectionFiber.php /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2020 Aaron Piotrowski 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.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trowski/react-fiber", 3 | "description": "ReactPHP + ext-fiber", 4 | "keywords": [ 5 | "async", 6 | "asynchronous", 7 | "concurrency", 8 | "promise", 9 | "awaitable", 10 | "future", 11 | "non-blocking", 12 | "event", 13 | "event-loop" 14 | ], 15 | "license": "MIT", 16 | "authors": [ 17 | { 18 | "name": "Aaron Piotrowski", 19 | "email": "aaron@trowski.com" 20 | } 21 | ], 22 | "require": { 23 | "php": ">=8", 24 | "ext-fiber": "*", 25 | "react/event-loop": "^1", 26 | "react/promise": "^2.2" 27 | }, 28 | "require-dev": { 29 | "phpunit/phpunit": "^9", 30 | "react/http": "^1.1", 31 | "react/promise-stream": "^1.2" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Trowski\\ReactFiber\\": "src" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "Trowski\\ReactFiber\\Test\\": "test" 41 | } 42 | }, 43 | "scripts": { 44 | "test": "@php -dzend.assertions=1 -dassert.exception=1 ./vendor/bin/phpunit", 45 | "code-style": "@php ./vendor/bin/php-cs-fixer fix" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/FiberLoop.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 23 | } 24 | 25 | public function addReadStream($stream, $listener): void 26 | { 27 | $this->loop->addReadStream($stream, $listener); 28 | } 29 | 30 | public function addWriteStream($stream, $listener): void 31 | { 32 | $this->loop->addWriteStream($stream, $listener); 33 | } 34 | 35 | public function removeReadStream($stream): void 36 | { 37 | $this->loop->removeReadStream($stream); 38 | } 39 | 40 | public function removeWriteStream($stream): void 41 | { 42 | $this->loop->removeWriteStream($stream); 43 | } 44 | 45 | public function addTimer($interval, $callback): TimerInterface 46 | { 47 | return $this->loop->addTimer($interval, $callback); 48 | } 49 | 50 | public function addPeriodicTimer($interval, $callback): TimerInterface 51 | { 52 | return $this->loop->addPeriodicTimer($interval, $callback); 53 | } 54 | 55 | public function cancelTimer(TimerInterface $timer): void 56 | { 57 | $this->loop->cancelTimer($timer); 58 | } 59 | 60 | public function futureTick($listener): void 61 | { 62 | $this->loop->futureTick($listener); 63 | } 64 | 65 | public function addSignal($signal, $listener): void 66 | { 67 | $this->loop->addSignal($signal, $listener); 68 | } 69 | 70 | public function removeSignal($signal, $listener): void 71 | { 72 | $this->loop->removeSignal($signal, $listener); 73 | } 74 | 75 | public function run(): void 76 | { 77 | $this->loop->run(); 78 | } 79 | 80 | public function stop(): void 81 | { 82 | $this->loop->stop(); 83 | } 84 | 85 | /** 86 | * @template TValue 87 | * 88 | * @param PromiseInterface $promise 89 | * 90 | * @psalm-param PromiseInterface $promise 91 | * 92 | * @return mixed 93 | * 94 | * @psalm-return TValue 95 | * 96 | * @throws \Throwable 97 | */ 98 | public function await(PromiseInterface $promise): mixed 99 | { 100 | $fiber = \Fiber::getCurrent(); 101 | $method = $promise instanceof ExtendedPromiseInterface ? 'done' : 'then'; 102 | 103 | $resolved = false; 104 | 105 | if ($fiber === null) { 106 | // Awaiting from {main}. 107 | if (!isset($this->fiber) || $this->fiber->isTerminated()) { 108 | $this->fiber = $loop = new \Fiber(fn() => $this->run()); 109 | // Run event loop to completion on shutdown. 110 | \register_shutdown_function(static function () use ($loop): void { 111 | if ($loop->isSuspended()) { 112 | $loop->resume(); 113 | } 114 | }); 115 | } 116 | 117 | $promise->{$method}( 118 | function (mixed $value) use (&$resolved): void { 119 | $resolved = true; 120 | $this->futureTick(static fn() => \Fiber::suspend(static fn() => $value)); 121 | }, 122 | function (mixed $reason) use (&$resolved): void { 123 | $resolved = true; 124 | $exception = $reason instanceof \Throwable ? $reason : new RejectedException($reason); 125 | $this->futureTick(static fn() => \Fiber::suspend(static fn() => throw $exception)); 126 | } 127 | ); 128 | 129 | $lambda = $this->fiber->isStarted() ? $this->fiber->resume() : $this->fiber->start(); 130 | 131 | if (!$resolved) { 132 | throw new \Error('Event loop suspended or exited without resolving the promise'); 133 | } 134 | 135 | return $lambda(); 136 | } 137 | 138 | if (isset($this->fiber) && $fiber === $this->fiber) { 139 | throw new \Error(\sprintf("Cannot call %s::%s() from a loop event handler callback", self::class, __METHOD__)); 140 | } 141 | 142 | $promise->{$method}( 143 | function (mixed $value) use (&$resolved, $fiber): void { 144 | $resolved = true; 145 | $this->futureTick(static fn() => $fiber->resume($value)); 146 | }, 147 | function (mixed $reason) use (&$resolved, $fiber): void { 148 | $resolved = true; 149 | $exception = $reason instanceof \Throwable ? $reason : new RejectedException($reason); 150 | $this->futureTick(static fn() => $fiber->throw($exception)); 151 | } 152 | ); 153 | 154 | try { 155 | $result = \Fiber::suspend(); 156 | } finally { 157 | if (!$resolved) { 158 | throw new \Error('Fiber resumed before the promise was resolved'); 159 | } 160 | } 161 | 162 | return $result; 163 | } 164 | 165 | 166 | /** 167 | * Create a new fiber (green-thread) using the given callback. The returned promise is 168 | * resolved with the return value of the callback once the fiber completes execution. 169 | * 170 | * @template TReturn 171 | * 172 | * @param callable $callback 173 | * @param mixed ...$args 174 | * 175 | * @psalm-param callable(mixed ...$args):TReturn $callback 176 | * 177 | * @return ExtendedPromiseInterface 178 | * 179 | * @psalm-return ExtendedPromiseInterface 180 | */ 181 | public function async(callable $callback, mixed ...$args): ExtendedPromiseInterface 182 | { 183 | return new Promise(function (callable $resolve, callable $reject) use ($callback, $args): void { 184 | $fiber = new \Fiber(function () use ($resolve, $reject, $callback, $args): void { 185 | try { 186 | $resolve($callback(...$args)); 187 | } catch (\Throwable $exception) { 188 | $reject($exception); 189 | } 190 | }); 191 | 192 | $this->futureTick(static fn() => $fiber->start()); 193 | }); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/RejectedException.php: -------------------------------------------------------------------------------- 1 | reason = $reason; 13 | } 14 | 15 | public function getReason(): mixed 16 | { 17 | return $this->reason; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /stubs/Fiber.php: -------------------------------------------------------------------------------- 1 |