Lines 88.88% 24 / 27
Functions and Methods 75.00% 6 / 8
Classes and Traits 0.00% 0 / 1
Name Lines Functions and Methods CRAP Classes and Traits
ExposedCommandSettlements 88.88% 24 / 27 75.00% 6 / 8 14.27 0.00% 0 / 1
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 openCommand 100.00% 1 / 1 100.00% 1 / 1 1
 settleThroughHandler 100.00% 8 / 8 100.00% 1 / 1 3
 acceptCommand 85.71% 6 / 7 0.00% 0 / 1 3.03
 rejectCommand 100.00% 3 / 3 100.00% 1 / 1 2
 refuseUnknownCommand 100.00% 1 / 1 100.00% 1 / 1 1
 takeOpenCommand 100.00% 3 / 3 100.00% 1 / 1 1
 sendAnswer 33.33% 1 / 3 0.00% 0 / 1 3.19
1<?php
2
3declare(strict_types=1);
4
5namespace Stewart\Runtime\Worker\Exposure;
6
7use Closure;
8use Psr\Log\LoggerInterface;
9use Stewart\Contracts\Exception\CommandException;
10use Stewart\Contracts\Exposure\Command\ExposedCommand;
11use Stewart\Runtime\Exception\TransportException;
12use Stewart\Runtime\Ipc\Message\ExposedCommandAnswered;
13use Stewart\Runtime\Ipc\Transport;
14use Throwable;
15use WeakMap;
16
17// Weak keys let a command an operator dropped vanish; Home Assistant times it out.
18final class ExposedCommandSettlements
19{
20    /** @var WeakMap<ExposedCommand, OpenExposedCommand> */
21    private WeakMap $openCommands;
22
23    public function __construct(
24        private readonly Transport $transport,
25        private readonly LoggerInterface $logger,
26    ) {
27        $this->openCommands = new WeakMap();
28    }
29
30    public function openCommand(string $commandId, ExposedCommand $command, ExposedHandle $handle): void
31    {
32        $this->openCommands[$command] = new OpenExposedCommand($commandId, $handle);
33    }
34
35    /**
36     * @template T of ExposedCommand
37     *
38     * @param T $command
39     * @param Closure(T): mixed $handler
40     * @throws Throwable
41     */
42    public function settleThroughHandler(ExposedCommand $command, Closure $handler): void
43    {
44        try {
45            $handler($command);
46        } catch (CommandException $e) {
47            $this->rejectCommand($command, $e->getMessage());
48
49            return;
50        } catch (Throwable $e) {
51            $this->rejectCommand($command, 'The app failed to handle the command.');
52
53            throw $e;
54        }
55
56        $this->acceptCommand($command);
57    }
58
59    public function acceptCommand(ExposedCommand $command): void
60    {
61        $open = $this->takeOpenCommand($command);
62
63        if ($open === null) {
64            return;
65        }
66
67        $requestedState = $command->getRequestedState();
68
69        if ($requestedState !== null) {
70            $open->handle->recordCommandedState($requestedState);
71        }
72
73        $this->sendAnswer(new ExposedCommandAnswered($open->commandId, true));
74    }
75
76    public function rejectCommand(ExposedCommand $command, string $reason): void
77    {
78        $open = $this->takeOpenCommand($command);
79
80        if ($open !== null) {
81            $this->sendAnswer(new ExposedCommandAnswered($open->commandId, false, $reason));
82        }
83    }
84
85    public function refuseUnknownCommand(string $commandId, string $reason): void
86    {
87        $this->sendAnswer(new ExposedCommandAnswered($commandId, false, $reason));
88    }
89
90    private function takeOpenCommand(ExposedCommand $command): ?OpenExposedCommand
91    {
92        $open = $this->openCommands[$command] ?? null;
93        unset($this->openCommands[$command]);
94
95        return $open;
96    }
97
98    private function sendAnswer(ExposedCommandAnswered $answer): void
99    {
100        try {
101            $this->transport->send($answer);
102        } catch (TransportException $e) {
103            $this->logger->debug('Could not answer an exposed entity command; the broker channel is closed', ['exception' => $e, 'command' => $answer->commandId]);
104        }
105    }
106}