Lines
90.54%
67 / 74
Functions and Methods
81.81%
9 / 11
Classes and Traits
0.00%
0 / 1
| Name | Lines | Functions and Methods | CRAP | Classes and Traits | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| Client | 90.54% | 67 / 74 | 81.81% | 9 / 11 | 37.10 | 0.00% | 0 / 1 | |||
| __construct | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| subscribe | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| character | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| run | 100.00% | 13 / 13 | 100.00% | 1 / 1 | 5 | |||||
| send | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| wait | 25.00% | 2 / 8 | 0.00% | 0 / 1 | 10.75 | |||||
| execute | 100.00% | 9 / 9 | 100.00% | 1 / 1 | 5 | |||||
| await | 100.00% | 19 / 19 | 100.00% | 1 / 1 | 8 | |||||
| listen | 100.00% | 13 / 13 | 100.00% | 1 / 1 | 5 | |||||
| close | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| hear | 85.71% | 6 / 7 | 0.00% | 0 / 1 | 4.05 | |||||
| 1 | <?php | |
| 2 | ||
| 3 | declare(strict_types=1); | |
| 4 | ||
| 5 | namespace Lorekeeper\Client; | |
| 6 | ||
| 7 | use Closure; | |
| 8 | use Lorekeeper\Client\Command\Command; | |
| 9 | use Lorekeeper\Client\Exception\PeerClosedException; | |
| 10 | use Lorekeeper\Client\Script\Await; | |
| 11 | use Lorekeeper\Client\Script\Disconnected; | |
| 12 | use Lorekeeper\Client\Script\Instruction; | |
| 13 | use Lorekeeper\Client\Script\Pause; | |
| 14 | use Lorekeeper\Client\Script\Script; | |
| 15 | use Lorekeeper\Client\Script\Send; | |
| 16 | use Lorekeeper\Client\Script\Stopped; | |
| 17 | use Lorekeeper\Client\Script\TimedOut; | |
| 18 | use Lorekeeper\Server\Event\Event; | |
| 19 | use Lorekeeper\Server\Event\Events; | |
| 20 | use Lorekeeper\Server\Message\Message; | |
| 21 | use Psl\Async; | |
| 22 | use Psl\Async\CancellationTokenInterface; | |
| 23 | use Psl\Async\Exception\CancelledException; | |
| 24 | use Psl\Async\NullCancellationToken; | |
| 25 | use Psl\DateTime\Duration; | |
| 26 | use Psl\DateTime\Timestamp; | |
| 27 | ||
| 28 | /** | |
| 29 | * @mago-expect lint:cyclomatic-complexity | |
| 30 | * @mago-expect lint:kan-defect | |
| 31 | */ | |
| 32 | final class Client | |
| 33 | { | |
| 34 | /** @var list<Closure(Event): void> */ | |
| 35 | private array $subscribers = []; | |
| 36 | ||
| 37 | public function __construct( | |
| 38 | private readonly Connection $connection, | |
| 39 | private readonly Character $me, | |
| 40 | ) {} | |
| 41 | ||
| 42 | /** @param Closure(Event): void $subscriber */ | |
| 43 | public function subscribe(Closure $subscriber): void | |
| 44 | { | |
| 45 | $this->subscribers[] = $subscriber; | |
| 46 | } | |
| 47 | ||
| 48 | public function character(): Character | |
| 49 | { | |
| 50 | return $this->me; | |
| 51 | } | |
| 52 | ||
| 53 | /** | |
| 54 | * @template TResult | |
| 55 | * | |
| 56 | * @param Script<TResult> $script | |
| 57 | * | |
| 58 | * @return TResult|Disconnected|Stopped | |
| 59 | */ | |
| 60 | public function run(Script $script, CancellationTokenInterface $stop = new NullCancellationToken()): mixed | |
| 61 | { | |
| 62 | $steps = $script->run($this->me); | |
| 63 | ||
| 64 | try { | |
| 65 | while ($steps->valid()) { | |
| 66 | $stop->throwIfCancelled(); | |
| 67 | $instruction = $steps->current(); | |
| 68 | $answer = $this->execute($instruction, $stop); | |
| 69 | ||
| 70 | $steps->send($answer); | |
| 71 | } | |
| 72 | } catch (PeerClosedException $closed) { | |
| 73 | return new Disconnected($closed->getMessage()); | |
| 74 | } catch (CancelledException $cancelled) { | |
| 75 | if (!$stop->isCancelled()) { | |
| 76 | throw $cancelled; | |
| 77 | } | |
| 78 | ||
| 79 | return new Stopped(); | |
| 80 | } | |
| 81 | ||
| 82 | return $steps->getReturn(); | |
| 83 | } | |
| 84 | ||
| 85 | /** @throws PeerClosedException */ | |
| 86 | public function send(Command $command): void | |
| 87 | { | |
| 88 | $this->connection->send($command); | |
| 89 | } | |
| 90 | ||
| 91 | public function wait( | |
| 92 | Duration $duration, | |
| 93 | CancellationTokenInterface $stop = new NullCancellationToken(), | |
| 94 | ): null|Disconnected|Stopped { | |
| 95 | try { | |
| 96 | $this->listen(new Pause($duration), $stop); | |
| 97 | } catch (PeerClosedException $closed) { | |
| 98 | return new Disconnected($closed->getMessage()); | |
| 99 | } catch (CancelledException $cancelled) { | |
| 100 | if (!$stop->isCancelled()) { | |
| 101 | throw $cancelled; | |
| 102 | } | |
| 103 | ||
| 104 | return new Stopped(); | |
| 105 | } | |
| 106 | ||
| 107 | return null; | |
| 108 | } | |
| 109 | ||
| 110 | /** @throws PeerClosedException */ | |
| 111 | private function execute(Instruction $instruction, CancellationTokenInterface $stop): null|Event|TimedOut | |
| 112 | { | |
| 113 | if ($instruction instanceof Pause) { | |
| 114 | $this->listen($instruction, $stop); | |
| 115 | ||
| 116 | return null; | |
| 117 | } | |
| 118 | ||
| 119 | if ($instruction instanceof Send) { | |
| 120 | $this->connection->send($instruction->command); | |
| 121 | ||
| 122 | return $instruction->await === null ? null : $this->await($instruction->await, $stop); | |
| 123 | } | |
| 124 | ||
| 125 | if ($instruction instanceof Await) { | |
| 126 | return $this->await($instruction, $stop); | |
| 127 | } | |
| 128 | ||
| 129 | return null; | |
| 130 | } | |
| 131 | ||
| 132 | /** @throws PeerClosedException */ | |
| 133 | private function await(Await $await, CancellationTokenInterface $stop): Event|TimedOut | |
| 134 | { | |
| 135 | $deadline = Timestamp::monotonic()->plus($await->timeout); | |
| 136 | ||
| 137 | while (true) { | |
| 138 | $left = $deadline->since(Timestamp::monotonic()); | |
| 139 | if (!$left->isPositive()) { | |
| 140 | return new TimedOut($await); | |
| 141 | } | |
| 142 | ||
| 143 | try { | |
| 144 | $message = $this->connection->receive( | |
| 145 | new Async\LinkedCancellationToken(new Async\TimeoutCancellationToken($left), $stop), | |
| 146 | ); | |
| 147 | } catch (CancelledException $cancelled) { | |
| 148 | if ($stop->isCancelled()) { | |
| 149 | throw $cancelled; | |
| 150 | } | |
| 151 | ||
| 152 | return new TimedOut($await); | |
| 153 | } | |
| 154 | ||
| 155 | if ($message->isNone()) { | |
| 156 | continue; | |
| 157 | } | |
| 158 | ||
| 159 | $this->hear($message->unwrap()); | |
| 160 | ||
| 161 | foreach ($await->events as $event) { | |
| 162 | /** @mago-expect analysis:possibly-static-access-on-interface */ | |
| 163 | $heard = $event::from($message->unwrap()); | |
| 164 | if ($heard->isSome()) { | |
| 165 | return $heard->unwrap(); | |
| 166 | } | |
| 167 | } | |
| 168 | } | |
| 169 | } | |
| 170 | ||
| 171 | /** @throws PeerClosedException */ | |
| 172 | private function listen(Pause $pause, CancellationTokenInterface $stop): void | |
| 173 | { | |
| 174 | $deadline = Timestamp::monotonic()->plus($pause->duration); | |
| 175 | ||
| 176 | while (true) { | |
| 177 | $left = $deadline->since(Timestamp::monotonic()); | |
| 178 | if (!$left->isPositive()) { | |
| 179 | return; | |
| 180 | } | |
| 181 | ||
| 182 | try { | |
| 183 | $message = $this->connection->receive( | |
| 184 | new Async\LinkedCancellationToken(new Async\TimeoutCancellationToken($left), $stop), | |
| 185 | ); | |
| 186 | } catch (CancelledException $cancelled) { | |
| 187 | if ($stop->isCancelled()) { | |
| 188 | throw $cancelled; | |
| 189 | } | |
| 190 | ||
| 191 | return; | |
| 192 | } | |
| 193 | ||
| 194 | $message->apply($this->hear(...)); | |
| 195 | } | |
| 196 | } | |
| 197 | ||
| 198 | public function close(): void | |
| 199 | { | |
| 200 | $this->connection->close(); | |
| 201 | } | |
| 202 | ||
| 203 | private function hear(Message $message): void | |
| 204 | { | |
| 205 | $events = Events::recognize($message); | |
| 206 | ||
| 207 | foreach ($events as $event) { | |
| 208 | if (!$event instanceof Event) { | |
| 209 | continue; | |
| 210 | } | |
| 211 | ||
| 212 | $this->me->hear($event); | |
| 213 | foreach ($this->subscribers as $subscriber) { | |
| 214 | $subscriber($event); | |
| 215 | } | |
| 216 | } | |
| 217 | } | |
| 218 | } |