Lines 90.54% 67 / 74
Methods 81.81% 9 / 11
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 __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
32final 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}