From bf1de4f7f33877275aac8780819266de43661fbd Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 7 Mar 2026 15:24:08 +0100 Subject: [PATCH 01/65] remove current implementation --- .gitattributes | 3 - .github/workflows/ci.yml | 67 -- .gitignore | 2 - fixtures/client.php | 47 -- fixtures/eternal-server.php | 17 - fixtures/long-client-multi-message.php | 43 -- fixtures/long-client.php | 33 - fixtures/self-closing-client.php | 33 - fixtures/server.php | 21 - phpunit.xml.dist | 23 - src/Client.php | 29 - src/Client/Unix.php | 56 -- src/Continuation.php | 133 ---- src/Exception/DomainException.php | 8 - src/Exception/Exception.php | 8 - src/Exception/LogicException.php | 8 - src/Exception/MessageContentTooLong.php | 8 - src/Factory.php | 30 - src/IPC.php | 30 - src/IPC/Unix.php | 145 ---- src/Message.php | 17 - src/Message/ConnectionClose.php | 42 - src/Message/ConnectionCloseOk.php | 42 - src/Message/ConnectionStart.php | 42 - src/Message/ConnectionStartOk.php | 42 - src/Message/Generic.php | 50 -- src/Message/Heartbeat.php | 42 - src/Message/MessageReceived.php | 42 - src/Process.php | 34 - src/Process/Name.php | 49 -- src/Process/Unix.php | 264 ------- src/Protocol.php | 20 - src/Protocol/Binary.php | 102 --- src/Server.php | 19 - src/Server/ClientLifecycle.php | 164 ---- src/Server/ClientLifecycle/State.php | 165 ---- src/Server/Connections.php | 176 ----- src/Server/Connections/Active.php | 55 -- src/Server/UnableToStart.php | 11 - src/Server/Unix.php | 140 ---- src/Server/Unix/Iteration.php | 251 ------ src/Server/Unix/State.php | 113 --- tests/Client/UnixTest.php | 144 ---- tests/FactoryTest.php | 26 - tests/FunctionalTest.php | 69 -- tests/IPC/UnixTest.php | 341 --------- tests/Message/ConnectionCloseOkTest.php | 41 - tests/Message/ConnectionCloseTest.php | 41 - tests/Message/ConnectionStartOkTest.php | 41 - tests/Message/ConnectionStartTest.php | 41 - tests/Message/GenericTest.php | 57 -- tests/Message/HeartbeatTest.php | 41 - tests/Message/MessageReceivedTest.php | 41 - tests/Process/NameTest.php | 41 - tests/Process/UnixTest.php | 969 ------------------------ tests/Protocol/BinaryTest.php | 91 --- tests/Server/ClientLifecycleTest.php | 776 ------------------- tests/Server/UnixTest.php | 362 --------- 58 files changed, 5748 deletions(-) delete mode 100644 fixtures/client.php delete mode 100644 fixtures/eternal-server.php delete mode 100644 fixtures/long-client-multi-message.php delete mode 100644 fixtures/long-client.php delete mode 100644 fixtures/self-closing-client.php delete mode 100644 fixtures/server.php delete mode 100644 phpunit.xml.dist delete mode 100644 src/Client.php delete mode 100644 src/Client/Unix.php delete mode 100644 src/Continuation.php delete mode 100644 src/Exception/DomainException.php delete mode 100644 src/Exception/Exception.php delete mode 100644 src/Exception/LogicException.php delete mode 100644 src/Exception/MessageContentTooLong.php delete mode 100644 src/Factory.php delete mode 100644 src/IPC.php delete mode 100644 src/IPC/Unix.php delete mode 100644 src/Message.php delete mode 100644 src/Message/ConnectionClose.php delete mode 100644 src/Message/ConnectionCloseOk.php delete mode 100644 src/Message/ConnectionStart.php delete mode 100644 src/Message/ConnectionStartOk.php delete mode 100644 src/Message/Generic.php delete mode 100644 src/Message/Heartbeat.php delete mode 100644 src/Message/MessageReceived.php delete mode 100644 src/Process.php delete mode 100644 src/Process/Name.php delete mode 100644 src/Process/Unix.php delete mode 100644 src/Protocol.php delete mode 100644 src/Protocol/Binary.php delete mode 100644 src/Server.php delete mode 100644 src/Server/ClientLifecycle.php delete mode 100644 src/Server/ClientLifecycle/State.php delete mode 100644 src/Server/Connections.php delete mode 100644 src/Server/Connections/Active.php delete mode 100644 src/Server/UnableToStart.php delete mode 100644 src/Server/Unix.php delete mode 100644 src/Server/Unix/Iteration.php delete mode 100644 src/Server/Unix/State.php delete mode 100644 tests/Client/UnixTest.php delete mode 100644 tests/FactoryTest.php delete mode 100644 tests/FunctionalTest.php delete mode 100644 tests/IPC/UnixTest.php delete mode 100644 tests/Message/ConnectionCloseOkTest.php delete mode 100644 tests/Message/ConnectionCloseTest.php delete mode 100644 tests/Message/ConnectionStartOkTest.php delete mode 100644 tests/Message/ConnectionStartTest.php delete mode 100644 tests/Message/GenericTest.php delete mode 100644 tests/Message/HeartbeatTest.php delete mode 100644 tests/Message/MessageReceivedTest.php delete mode 100644 tests/Process/NameTest.php delete mode 100644 tests/Process/UnixTest.php delete mode 100644 tests/Protocol/BinaryTest.php delete mode 100644 tests/Server/ClientLifecycleTest.php delete mode 100644 tests/Server/UnixTest.php diff --git a/.gitattributes b/.gitattributes index 7d503af..3a01b37 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,2 @@ /.gitattributes export-ignore /.gitignore export-ignore -/phpunit.xml.dist export-ignore -/fixtures export-ignore -/tests export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e7d05e8..766b235 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,70 +3,3 @@ name: CI on: [push] jobs: - phpunit: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macOS-latest] - php-version: ['8.2', '8.3'] - dependencies: ['lowest', 'highest'] - name: 'PHPUnit' - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - extensions: mbstring, intl - coverage: xdebug - - name: Composer - uses: "ramsey/composer-install@v2" - with: - dependency-versions: ${{ matrix.dependencies }} - - name: PHPUnit - run: vendor/bin/phpunit --coverage-clover=coverage.clover --stop-on-error - env: - CI: 'github' - - uses: codecov/codecov-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - psalm: - runs-on: ubuntu-latest - strategy: - matrix: - php-version: ['8.2', '8.3'] - dependencies: ['lowest', 'highest'] - name: 'Psalm' - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - extensions: mbstring, intl - - name: Composer - uses: "ramsey/composer-install@v2" - with: - dependency-versions: ${{ matrix.dependencies }} - - name: Psalm - run: vendor/bin/psalm --shepherd - cs: - runs-on: ubuntu-latest - strategy: - matrix: - php-version: ['8.2'] - name: 'CS' - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - extensions: mbstring, intl - - name: Composer - uses: "ramsey/composer-install@v2" - - name: CS - run: vendor/bin/php-cs-fixer fix --diff --dry-run diff --git a/.gitignore b/.gitignore index 3285dca..ff72e2d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,2 @@ /composer.lock /vendor -.phpunit.result.cache -.phpunit.cache diff --git a/fixtures/client.php b/fixtures/client.php deleted file mode 100644 index 99016e1..0000000 --- a/fixtures/client.php +++ /dev/null @@ -1,47 +0,0 @@ -wait(Name::of('server'))->match( - static fn($process) => $process, - static fn() => null, -); -$process->send(Sequence::of(new Message\Generic( - MediaType::of('text/plain'), - Str::of('hello world') -))); -$_ = $process - ->wait() - ->flatMap( - static fn($message) => $process - ->send(Sequence::of(new Message\Generic( - MediaType::of('text/plain'), - Str::of('stop') - ))) - ->map(static fn() => $message), - ) - ->flatMap( - static fn($message) => $process - ->wait() // wait for server termination - ->otherwise(static fn() => Maybe::just($message)), - ) - ->match( - static fn($message) => print($message->content()->toString()), - static fn() => null, - ); diff --git a/fixtures/eternal-server.php b/fixtures/eternal-server.php deleted file mode 100644 index 0b1ff87..0000000 --- a/fixtures/eternal-server.php +++ /dev/null @@ -1,17 +0,0 @@ -listen(Name::of('server'))(null, static function($message, $continuation) { - return $continuation; -}); diff --git a/fixtures/long-client-multi-message.php b/fixtures/long-client-multi-message.php deleted file mode 100644 index 1f1f190..0000000 --- a/fixtures/long-client-multi-message.php +++ /dev/null @@ -1,43 +0,0 @@ -wait(Name::of('server'))->match( - static fn($process) => $process, - static fn() => null, -); -$_ = $process - ->send(Sequence::of( - new Message\Generic( - MediaType::of('text/plain'), - Str::of('hello world'), - ), - new Message\Generic( - MediaType::of('text/plain'), - Str::of('second'), - ), - new Message\Generic( - MediaType::of('text/plain'), - Str::of('third'), - ), - )) - ->flatMap(static fn($process) => $process->wait()) - ->match( - static fn() => null, - static fn() => null, - ); diff --git a/fixtures/long-client.php b/fixtures/long-client.php deleted file mode 100644 index 05daa81..0000000 --- a/fixtures/long-client.php +++ /dev/null @@ -1,33 +0,0 @@ -wait(Name::of('server'))->match( - static fn($process) => $process, - static fn() => null, -); -$_ = $process - ->send(Sequence::of(new Message\Generic( - MediaType::of('text/plain'), - Str::of('hello world') - ))) - ->flatMap(static fn($process) => $process->wait()) - ->match( - static fn() => null, - static fn() => null, - ); diff --git a/fixtures/self-closing-client.php b/fixtures/self-closing-client.php deleted file mode 100644 index ce0402e..0000000 --- a/fixtures/self-closing-client.php +++ /dev/null @@ -1,33 +0,0 @@ -wait(Name::of('server'))->match( - static fn($process) => $process, - static fn() => null, -); -$_ = $process - ->send(Sequence::of(new Message\Generic( - MediaType::of('text/plain'), - Str::of('hello world') - ))) - ->flatMap(static fn($process) => $process->close()) - ->match( - static fn() => null, - static fn() => null, - ); diff --git a/fixtures/server.php b/fixtures/server.php deleted file mode 100644 index 033c0cb..0000000 --- a/fixtures/server.php +++ /dev/null @@ -1,21 +0,0 @@ -listen(Name::of('server'))(null, static function($message, $continuation, $carry) { - if ($message->content()->equals(Str::of('stop'))) { - return $continuation->stop($carry); - } - - return $continuation->respond($carry, $message); -}); diff --git a/phpunit.xml.dist b/phpunit.xml.dist deleted file mode 100644 index 37a893d..0000000 --- a/phpunit.xml.dist +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - ./tests - - - - - . - - - ./tests - ./vendor - ./fixtures - - - diff --git a/src/Client.php b/src/Client.php deleted file mode 100644 index 6552116..0000000 --- a/src/Client.php +++ /dev/null @@ -1,29 +0,0 @@ - Returns nothing when it fails to send the message - */ - public function send(Message $message): Maybe; - - /** - * @return Maybe - */ - public function read(): Maybe; - - /** - * Close the underlying connection - * - * @return Maybe Returns nothing when it fails to close properly - */ - public function close(): Maybe; -} diff --git a/src/Client/Unix.php b/src/Client/Unix.php deleted file mode 100644 index 988b3bb..0000000 --- a/src/Client/Unix.php +++ /dev/null @@ -1,56 +0,0 @@ -connection = $connection; - $this->protocol = $protocol; - } - - #[\Override] - public function send(Message $message): Maybe - { - if ($this->connection->closed()) { - /** @var Maybe */ - return Maybe::nothing(); - } - - /** @var Maybe */ - return $this - ->connection - ->write($this->protocol->encode($message)) - ->maybe() - ->map(fn() => $this); - } - - #[\Override] - public function read(): Maybe - { - /** @var Maybe */ - return $this - ->protocol - ->decode($this->connection) - ->map(fn($message) => [$this, $message]); - } - - #[\Override] - public function close(): Maybe - { - return $this->connection->close()->maybe(); - } -} diff --git a/src/Continuation.php b/src/Continuation.php deleted file mode 100644 index 7ac3e6a..0000000 --- a/src/Continuation.php +++ /dev/null @@ -1,133 +0,0 @@ -client = $client; - $this->carry = $carry; - $this->closed = $closed; - $this->response = $response; - $this->stop = $stop; - } - - /** - * @internal - * @template A - * - * @param A $carry - * - * @return self - */ - public static function start(Client $client, mixed $carry): self - { - return new self($client, $carry); - } - - /** - * @param T $carry - * - * @return self - */ - public function continue(mixed $carry): self - { - return new self($this->client, $carry); - } - - /** - * This will send the given message to the client - * - * @param T $carry - * - * @return self - */ - public function respond(mixed $carry, Message $message): self - { - return new self($this->client, $carry, response: $message); - } - - /** - * The client will be closed and then garbage collected - * - * @param T $carry - * - * @return self - */ - public function close(mixed $carry): self - { - return new self($this->client, $carry, closed: true); - } - - /** - * The server will be gracefully shutdown - * - * @param T $carry - * - * @return self - */ - public function stop(mixed $carry): self - { - return new self($this->client, $carry, stop: true); - } - - /** - * @internal - * @template A - * @template B - * @template C - * @template D - * - * @param callable(Client, Message, T): A $onResponse - * @param callable(Client, T): B $onClose - * @param callable(Client, T): C $onStop - * @param callable(Client, T): D $onContinue - * - * @return A|B|C|D - */ - public function match( - callable $onResponse, - callable $onClose, - callable $onStop, - callable $onContinue, - ): mixed { - if ($this->response instanceof Message) { - /** @psalm-suppress ImpureFunctionCall */ - return $onResponse($this->client, $this->response, $this->carry); - } - - if ($this->closed) { - /** @psalm-suppress ImpureFunctionCall */ - return $onClose($this->client, $this->carry); - } - - if ($this->stop) { - /** @psalm-suppress ImpureFunctionCall */ - return $onStop($this->client, $this->carry); - } - - /** @psalm-suppress ImpureFunctionCall */ - return $onContinue($this->client, $this->carry); - } -} diff --git a/src/Exception/DomainException.php b/src/Exception/DomainException.php deleted file mode 100644 index ec9c06e..0000000 --- a/src/Exception/DomainException.php +++ /dev/null @@ -1,8 +0,0 @@ -status()->tmp()->resolve(Path::of('innmind/ipc/')); - $heartbeat ??= new ElapsedPeriod(1000); // default to 1 second - - return new IPC\Unix( - $os->sockets(), - $os->filesystem()->mount($sockets), - $os->clock(), - $os->process(), - new Protocol\Binary, - $sockets, - $heartbeat, - ); - } -} diff --git a/src/IPC.php b/src/IPC.php deleted file mode 100644 index 0cc0ef8..0000000 --- a/src/IPC.php +++ /dev/null @@ -1,30 +0,0 @@ - All processes waiting for messages - */ - public function processes(): Set; - - /** - * @return Maybe - */ - public function get(Process\Name $name): Maybe; - public function exist(Process\Name $name): bool; - - /** - * @return Maybe - */ - public function wait(Process\Name $name, ?ElapsedPeriod $timeout = null): Maybe; - public function listen(Process\Name $self, ?ElapsedPeriod $timeout = null): Server; -} diff --git a/src/IPC/Unix.php b/src/IPC/Unix.php deleted file mode 100644 index 93bc6b9..0000000 --- a/src/IPC/Unix.php +++ /dev/null @@ -1,145 +0,0 @@ -directory()) { - throw new LogicException("Path must be a directory, got '{$path->toString()}'"); - } - - $this->sockets = $sockets; - $this->filesystem = $filesystem; - $this->clock = $clock; - $this->process = $process; - $this->protocol = $protocol; - $this->path = $path; - $this->heartbeat = $heartbeat; - } - - #[\Override] - public function processes(): Set - { - return $this - ->filesystem - ->root() - ->all() - ->map(static fn($file) => Process\Name::maybe($file->name()->toString())->match( - static fn($name) => $name, - static fn() => null, - )) - ->keep(Instance::of(Process\Name::class)) - ->toSet(); - } - - #[\Override] - public function get(Process\Name $name): Maybe - { - if (!$this->exist($name)) { - /** @var Maybe */ - return Maybe::nothing(); - } - - return Process\Unix::of( - $this->sockets, - $this->protocol, - $this->clock, - $this->addressOf($name->toString()), - $name, - $this->heartbeat, - ); - } - - #[\Override] - public function exist(Process\Name $name): bool - { - return $this->filesystem->contains(FileName::of("{$name->toString()}.sock")); - } - - #[\Override] - public function wait(Process\Name $name, ?ElapsedPeriod $timeout = null): Maybe - { - $start = $this->clock->now(); - - while (!$this->exist($name)) { - if ( - $timeout instanceof ElapsedPeriod && - $this->clock->now()->elapsedSince($start)->longerThan($timeout) - ) { - /** @var Maybe */ - return Maybe::nothing(); - } - - $this->process->halt(new Millisecond($this->heartbeat->milliseconds())); - } - - return $this->get($name); - } - - #[\Override] - public function listen(Process\Name $self, ?ElapsedPeriod $timeout = null): Server - { - return new Server\Unix( - $this->sockets, - $this->protocol, - $this->clock, - $this->process->signals(), - $this->addressOf($self->toString()), - $this->heartbeat, - $timeout, - ); - } - - private function addressOf(string $name): Address - { - return new Address( - $this->path->resolve(Path::of($name)), - ); - } -} diff --git a/src/Message.php b/src/Message.php deleted file mode 100644 index 7de043a..0000000 --- a/src/Message.php +++ /dev/null @@ -1,17 +0,0 @@ -mediaType = new MediaType('text', 'plain'); - $this->content = Str::of('innmind/ipc:connection.close'); - } - - #[\Override] - public function mediaType(): MediaType - { - return $this->mediaType; - } - - #[\Override] - public function content(): Str - { - return $this->content; - } - - #[\Override] - public function equals(Message $message): bool - { - return $this->mediaType->toString() === $message->mediaType()->toString() && - $this->content->toString() === $message->content()->toString(); - } -} diff --git a/src/Message/ConnectionCloseOk.php b/src/Message/ConnectionCloseOk.php deleted file mode 100644 index 60fc566..0000000 --- a/src/Message/ConnectionCloseOk.php +++ /dev/null @@ -1,42 +0,0 @@ -mediaType = new MediaType('text', 'plain'); - $this->content = Str::of('innmind/ipc:connection.close-ok'); - } - - #[\Override] - public function mediaType(): MediaType - { - return $this->mediaType; - } - - #[\Override] - public function content(): Str - { - return $this->content; - } - - #[\Override] - public function equals(Message $message): bool - { - return $this->mediaType->toString() === $message->mediaType()->toString() && - $this->content->toString() === $message->content()->toString(); - } -} diff --git a/src/Message/ConnectionStart.php b/src/Message/ConnectionStart.php deleted file mode 100644 index 03d12a0..0000000 --- a/src/Message/ConnectionStart.php +++ /dev/null @@ -1,42 +0,0 @@ -mediaType = new MediaType('text', 'plain'); - $this->content = Str::of('innmind/ipc:connection.start'); - } - - #[\Override] - public function mediaType(): MediaType - { - return $this->mediaType; - } - - #[\Override] - public function content(): Str - { - return $this->content; - } - - #[\Override] - public function equals(Message $message): bool - { - return $this->mediaType->toString() === $message->mediaType()->toString() && - $this->content->toString() === $message->content()->toString(); - } -} diff --git a/src/Message/ConnectionStartOk.php b/src/Message/ConnectionStartOk.php deleted file mode 100644 index e709a88..0000000 --- a/src/Message/ConnectionStartOk.php +++ /dev/null @@ -1,42 +0,0 @@ -mediaType = new MediaType('text', 'plain'); - $this->content = Str::of('innmind/ipc:connection.start-ok'); - } - - #[\Override] - public function mediaType(): MediaType - { - return $this->mediaType; - } - - #[\Override] - public function content(): Str - { - return $this->content; - } - - #[\Override] - public function equals(Message $message): bool - { - return $this->mediaType->toString() === $message->mediaType()->toString() && - $this->content->toString() === $message->content()->toString(); - } -} diff --git a/src/Message/Generic.php b/src/Message/Generic.php deleted file mode 100644 index 7063007..0000000 --- a/src/Message/Generic.php +++ /dev/null @@ -1,50 +0,0 @@ -mediaType = $mediaType; - $this->content = $content; - } - - public static function of(string $mediaType, string $content): self - { - return new self( - MediaType::of($mediaType), - Str::of($content), - ); - } - - #[\Override] - public function mediaType(): MediaType - { - return $this->mediaType; - } - - #[\Override] - public function content(): Str - { - return $this->content; - } - - #[\Override] - public function equals(Message $message): bool - { - return $this->mediaType->toString() === $message->mediaType()->toString() && - $this->content->toString() === $message->content()->toString(); - } -} diff --git a/src/Message/Heartbeat.php b/src/Message/Heartbeat.php deleted file mode 100644 index bec5435..0000000 --- a/src/Message/Heartbeat.php +++ /dev/null @@ -1,42 +0,0 @@ -mediaType = new MediaType('text', 'plain'); - $this->content = Str::of('innmind/ipc:heartbeat'); - } - - #[\Override] - public function mediaType(): MediaType - { - return $this->mediaType; - } - - #[\Override] - public function content(): Str - { - return $this->content; - } - - #[\Override] - public function equals(Message $message): bool - { - return $this->mediaType->toString() === $message->mediaType()->toString() && - $this->content->toString() === $message->content()->toString(); - } -} diff --git a/src/Message/MessageReceived.php b/src/Message/MessageReceived.php deleted file mode 100644 index 1f74bde..0000000 --- a/src/Message/MessageReceived.php +++ /dev/null @@ -1,42 +0,0 @@ -mediaType = new MediaType('text', 'plain'); - $this->content = Str::of('innmind/ipc:message.received'); - } - - #[\Override] - public function mediaType(): MediaType - { - return $this->mediaType; - } - - #[\Override] - public function content(): Str - { - return $this->content; - } - - #[\Override] - public function equals(Message $message): bool - { - return $this->mediaType->toString() === $message->mediaType()->toString() && - $this->content->toString() === $message->content()->toString(); - } -} diff --git a/src/Process.php b/src/Process.php deleted file mode 100644 index e1b0d11..0000000 --- a/src/Process.php +++ /dev/null @@ -1,34 +0,0 @@ - $messages - * - * @return Maybe Returns nothing when messages can't be sent - */ - public function send(Sequence $messages): Maybe; - - /** - * @return Maybe - */ - public function wait(?ElapsedPeriod $timeout = null): Maybe; - - /** - * @return Maybe Returns nothing when couldn't close the connection properly - */ - public function close(): Maybe; - public function closed(): bool; -} diff --git a/src/Process/Name.php b/src/Process/Name.php deleted file mode 100644 index e1ba950..0000000 --- a/src/Process/Name.php +++ /dev/null @@ -1,49 +0,0 @@ -value = $value; - } - - /** - * @param literal-string $value - * - * @throws DomainException - */ - public static function of(string $value): self - { - return self::maybe($value)->match( - static fn($self) => $self, - static fn() => throw new DomainException($value), - ); - } - - /** - * @return Maybe - */ - public static function maybe(string $value): Maybe - { - return Maybe::just($value) - ->map(Str::of(...)) - ->filter(static fn($value) => $value->matches('~^[a-zA-Z0-9-_]+$~')) - ->map(static fn($value) => new self($value->toString())); - } - - public function toString(): string - { - return $this->value; - } -} diff --git a/src/Process/Unix.php b/src/Process/Unix.php deleted file mode 100644 index b6014ad..0000000 --- a/src/Process/Unix.php +++ /dev/null @@ -1,264 +0,0 @@ -socket = $socket; - $this->watch = $watch; - $this->protocol = $protocol; - $this->clock = $clock; - $this->name = $name; - $this->lastReceivedData = $clock->now(); - } - - /** - * @return Maybe - */ - public static function of( - Sockets $sockets, - Protocol $protocol, - Clock $clock, - Address $address, - Name $name, - ElapsedPeriod $watchTimeout, - ): Maybe { - /** - * @psalm-suppress PossiblyInvalidArgument - * @var Maybe - */ - return $sockets - ->connectTo($address) - ->map(static fn($socket) => new self( - $socket->unwrap(), - $sockets->watch($watchTimeout)->forRead($socket->unwrap()), - $protocol, - $clock, - $name, - )) - ->flatMap(static fn($self) => $self->open()); - } - - #[\Override] - public function name(): Name - { - return $this->name; - } - - #[\Override] - public function send(Sequence $messages): Maybe - { - /** @var Maybe */ - return $messages->reduce( - Maybe::just($this), - self::maybeSendMessage(...), - ); - } - - #[\Override] - public function wait(?ElapsedPeriod $timeout = null): Maybe - { - do { - if ($this->closed()) { - /** @var Maybe */ - return $this - ->cut() - ->filter(static fn() => false); // never return anything - } - - /** @var Set */ - $toRead = ($this->watch)()->match( - static fn($ready) => $ready->toRead(), - static fn() => Set::of(), - ); - - $receivedData = $toRead->contains($this->socket); - - if (!$receivedData) { - $stop = $this - ->sendMessage(new Heartbeat) - ->filter(static fn($self) => !$self->timedout($timeout)) - ->match( - static fn() => false, - static fn() => true, - ); - - if ($stop) { - /** @var Maybe */ - return Maybe::nothing(); - } - } - } while (!$receivedData); - - $this->lastReceivedData = $this->clock->now(); - - return $this - ->protocol - ->decode($this->socket) - ->flatMap(function($message) use ($timeout) { - if ($message->equals(new Heartbeat)) { - return $this->wait($timeout); - } - - return Maybe::just($message); - }) - ->flatMap(function($message) { - if ($message->equals(new ConnectionClose)) { - /** @var Maybe */ - return $this - ->sendMessage(new ConnectionCloseOk) - ->flatMap(static fn($self) => $self->cut()) - ->filter(static fn() => false); // never return anything - } - - return Maybe::just($message); - }); - } - - #[\Override] - public function close(): Maybe - { - if ($this->closed()) { - return Maybe::just(new SideEffect); - } - - return $this - ->sendMessage(new ConnectionClose) - ->flatMap( - static fn($self) => $self - ->wait() - ->filter(static fn($message) => $message->equals(new ConnectionCloseOk)) - ->map(static fn() => $self), - ) - ->flatMap(static fn($self) => $self->cut()) - ->otherwise( - fn() => $this - ->cut() - ->filter(static fn() => false), // never return anything - ); - } - - #[\Override] - public function closed(): bool - { - return $this->closed || $this->socket->closed(); - } - - /** - * @return Maybe - */ - private function open(): Maybe - { - /** @var Maybe */ - return $this - ->wait() - ->filter(static fn($message) => $message->equals(new ConnectionStart)) - ->flatMap(fn() => $this->sendMessage(new ConnectionStartOk)); - } - - /** - * @return Maybe - */ - private function cut(): Maybe - { - $this->closed = true; - - return $this->socket->close()->maybe(); - } - - private function timedout(?ElapsedPeriod $timeout = null): bool - { - if ($timeout === null) { - return false; - } - - $iteration = $this->clock->now()->elapsedSince($this->lastReceivedData); - - return $iteration->longerThan($timeout); - } - - /** - * @return Maybe - */ - private function sendMessage(Message $message): Maybe - { - if ($this->closed()) { - /** @var Maybe */ - return Maybe::nothing(); - } - - /** @var Maybe */ - return $this - ->socket - ->write($this->protocol->encode($message)) - ->maybe() - ->map(fn() => $this); - } - - /** - * @param Maybe $maybe - * - * @return Maybe - */ - private static function maybeSendMessage(Maybe $maybe, Message $message): Maybe - { - return $maybe->flatMap( - static fn($self) => $self - ->sendMessage($message) - ->flatMap( - static fn($self) => $self - ->wait() // for message acknowledgement - ->filter(static fn($message) => $message->equals(new MessageReceived)) - ->map(static fn() => $self), - ), - ); - } -} diff --git a/src/Protocol.php b/src/Protocol.php deleted file mode 100644 index f45c425..0000000 --- a/src/Protocol.php +++ /dev/null @@ -1,20 +0,0 @@ - - */ - public function decode(Readable $stream): Maybe; -} diff --git a/src/Protocol/Binary.php b/src/Protocol/Binary.php deleted file mode 100644 index 7124475..0000000 --- a/src/Protocol/Binary.php +++ /dev/null @@ -1,102 +0,0 @@ -content()->toEncoding(Str\Encoding::ascii); - $mediaType = Str::of($message->mediaType()->toString())->toEncoding(Str\Encoding::ascii); - - if ($content->length() > 4_294_967_295) { // unsigned long integer - throw new MessageContentTooLong((string) $content->length()); - } - - return Str::of('%s%s%s%s%s', Str\Encoding::ascii)->sprintf( - \pack('n', $mediaType->length()), - $mediaType->toString(), - \pack('N', $content->length()), - $content->toString(), - \pack('C', $this->end()), - ); - } - - #[\Override] - public function decode(Readable $stream): Maybe - { - /** @var Maybe */ - return $stream - ->read(2) - ->filter(static fn($length) => !$length->empty()) - ->map(static function($length): int { - /** - * @psalm-suppress PossiblyInvalidArrayAccess Todo apply a predicate - * @var positive-int $mediaTypeLength - */ - [, $mediaTypeLength] = \unpack('n', $length->toString()); - - return $mediaTypeLength; - }) - ->flatMap(static fn($mediaTypeLength) => $stream->read($mediaTypeLength)) - ->map(static fn($mediaType) => $mediaType->toString()) - ->flatMap(static function($mediaType) use ($stream) { - return $stream - ->read(4) - ->map(static function($length): int { - /** - * @psalm-suppress PossiblyInvalidArrayAccess Todo apply a predicate - * @var positive-int $contentLength - */ - [, $contentLength] = \unpack('N', $length->toString()); - - return $contentLength; - }) - ->flatMap(static fn($contentLength) => $stream->read($contentLength)->map( - static fn($content) => [$mediaType, $contentLength, $content], - )); - }) - ->flatMap( - // verify the message end boundary is correct - fn($parsed) => $stream - ->read(1) - ->map(static function($end): mixed { - /** @psalm-suppress PossiblyInvalidArrayAccess Todo apply a predicate */ - [, $end] = \unpack('C', $end->toString()); - - return $end; - }) - ->filter(fn($end) => $end === $this->end()) - ->map(static fn() => $parsed), - ) - // verify the read content is of the length specified - ->filter(static fn($parsed) => $parsed[1] === $parsed[2]->toEncoding(Str\Encoding::ascii)->length()) - ->flatMap( - static fn($parsed) => MediaType::maybe($parsed[0])->map( - static fn($mediaType) => new Message\Generic( - $mediaType, - $parsed[2], - ), - ), - ); - } - - private function end(): int - { - return 0xCE; - } -} diff --git a/src/Server.php b/src/Server.php deleted file mode 100644 index 649f3c0..0000000 --- a/src/Server.php +++ /dev/null @@ -1,19 +0,0 @@ -, C): Continuation $listen - * - * @return Either - */ - public function __invoke(mixed $carry, callable $listen): Either; -} diff --git a/src/Server/ClientLifecycle.php b/src/Server/ClientLifecycle.php deleted file mode 100644 index 4c39378..0000000 --- a/src/Server/ClientLifecycle.php +++ /dev/null @@ -1,164 +0,0 @@ -clock = $clock; - $this->heartbeat = $heartbeat; - $this->client = $client; - $this->lastHeartbeat = $lastHeartbeat; - $this->state = $state; - } - - /** - * @return Maybe - */ - public static function of( - Client $client, - Clock $clock, - ElapsedPeriod $heartbeat, - ): Maybe { - return $client - ->send(new ConnectionStart) - ->map(static fn($client) => new self( - $clock, - $heartbeat, - $client, - $clock->now(), - State::pendingStartOk, - )); - } - - /** - * @template C - * - * @param callable(Message, Continuation, C): Continuation $notify - * @param C $carry - * - * @return Either> Left side of Either means the notify asked the server to stop - */ - public function notify(callable $notify, mixed $carry): Either - { - return $this - ->client - ->read() - ->either() - ->flatMap(fn($tuple) => $this->state->actUpon( - $tuple[0], - $tuple[1], - $notify, - $carry, - )) - ->map( - fn($either) => $either - ->map($this->update(...)) - ->leftMap($this->update(...)), - ); - } - - public function heartbeat(): self - { - $trigger = $this - ->clock - ->now() - ->elapsedSince($this->lastHeartbeat) - ->longerThan($this->heartbeat); - - if ($trigger) { - // do nothing when failling to send the message as it happens when - // the client has been forced closed (for example with a `kill -9` - // on the client process) - $client = $this->client->send(new Heartbeat)->match( - static fn($client) => $client, - fn() => $this->client, - ); - - return new self( - $this->clock, - $this->heartbeat, - $client, - $this->lastHeartbeat, - $this->state, - ); - } - - return $this; - } - - /** - * @return Maybe - */ - public function shutdown(): Maybe - { - return match ($this->state) { - State::pendingCloseOk => Maybe::just($this), - default => $this - ->client - ->send(new ConnectionClose) - ->map($this->pendingCloseOk(...)), - }; - } - - private function pendingCloseOk(Client $client): self - { - return new self( - $this->clock, - $this->heartbeat, - $client, - $this->lastHeartbeat, - State::pendingCloseOk, - ); - } - - /** - * @template C - * - * @param array{Client, State, C} $tuple - * - * @return array{self, C} - */ - private function update(array $tuple): array - { - return [new self( - $this->clock, - $this->heartbeat, - $tuple[0], - $this->clock->now(), - $tuple[1], - ), $tuple[2]]; - } -} diff --git a/src/Server/ClientLifecycle/State.php b/src/Server/ClientLifecycle/State.php deleted file mode 100644 index be74bc4..0000000 --- a/src/Server/ClientLifecycle/State.php +++ /dev/null @@ -1,165 +0,0 @@ -, C): Continuation $notify - * @param C $carry - * - * @return Either> Inner left side of Either means stop the server - */ - public function actUpon( - Client $client, - Message $message, - callable $notify, - mixed $carry, - ): Either { - /** @var Either> */ - return match ($this) { - self::pendingStartOk => Either::right(Either::right([ - $client, - $this->ackStartOk($message), - $carry, - ])), - self::awaitingMessage => $this->handleMessage( - $client, - $message, - $notify, - $carry, - ), - self::pendingCloseOk => $this->ackCloseOk($client, $message, $carry), - }; - } - - private function ackStartOk(Message $message): self - { - if ($message->equals(new ConnectionStartOk)) { - return self::awaitingMessage; - } - - return $this; - } - - /** - * @template C - * - * @param callable(Message, Continuation, C): Continuation $notify - * @param C $carry - * - * @return Either> - */ - private function handleMessage( - Client $client, - Message $message, - callable $notify, - mixed $carry, - ): Either { - if ($message->equals(new ConnectionClose)) { - /** @var Either> */ - return $client - ->send(new ConnectionCloseOk) - ->flatMap(static fn($client) => $client->close()) - ->either() - ->leftMap(static fn() => $carry) - ->flatMap(static fn() => Either::left($carry)); - } - - if ( - $message->equals(new ConnectionStart) || - $message->equals(new ConnectionStartOk) || - $message->equals(new ConnectionClose) || - $message->equals(new ConnectionCloseOk) || - $message->equals(new MessageReceived) || - $message->equals(new Heartbeat) - ) { - // never notify with a protocol message - /** @var Either> */ - return Either::right(Either::right([$client, $this, $carry])); - } - - /** @var Either> */ - return $client - ->send(new MessageReceived) - ->either() - ->leftMap(static fn() => $carry) - ->map(static fn($client) => $notify( - $message, - Continuation::start($client, $carry), - $carry, - )) - ->flatMap($this->determineNextState(...)); - } - - /** - * @template C - * - * @param C $carry - * - * @return Either> - */ - private function ackCloseOk( - Client $client, - Message $message, - mixed $carry, - ): Either { - if ($message->equals(new ConnectionCloseOk)) { - /** @var Either> */ - return $client - ->close() - ->either() - ->leftMap(static fn() => $carry) - ->flatMap(static fn() => Either::left($carry)); - } - - /** @var Either> */ - return Either::right(Either::right([$client, $this, $carry])); - } - - /** - * @template C - * - * @param Continuation $continuation - * - * @return Either> - */ - private function determineNextState(Continuation $continuation): Either - { - /** @var Either> */ - return $continuation->match( - fn($client, $message, $carry) => $client - ->send($message) - ->either() - ->map(fn($client) => Either::right([$client, $this, $carry])) - ->leftMap(static fn(): mixed => $carry), - static fn($client, $carry) => $client - ->send(new ConnectionClose) - ->either() - ->map(static fn($client) => Either::right([$client, self::pendingCloseOk, $carry])) - ->leftMap(static fn(): mixed => $carry), - fn($client, $carry) => Either::right(Either::left([$client, $this, $carry])), - fn($client, $carry) => Either::right(Either::right([$client, $this, $carry])), - ); - } -} diff --git a/src/Server/Connections.php b/src/Server/Connections.php deleted file mode 100644 index 27f0172..0000000 --- a/src/Server/Connections.php +++ /dev/null @@ -1,176 +0,0 @@ - */ - private Map $connections; - - /** - * @param Map $connections - */ - private function __construct( - Server $server, - Watch $watch, - Map $connections, - ) { - $this->server = $server; - $this->watch = $watch; - $this->connections = $connections; - } - - public static function start( - Watch $watch, - Server $server, - ): self { - /** @var Map */ - $connections = Map::of(); - - return new self( - $server, - $watch->forRead($server), - $connections, - ); - } - - /** - * @return Maybe - */ - public function watch(): Maybe - { - /** @psalm-suppress ArgumentTypeCoercion */ - return ($this->watch)()->map(fn($ready) => new Connections\Active( - $ready->toRead()->find(fn($socket) => $socket === $this->server), - $ready->toRead()->remove($this->server), - )); - } - - public function add( - Connection $connection, - ClientLifecycle $client, - ): self { - return new self( - $this->server, - $this->watch->forRead($connection), - ($this->connections)($connection, $client), - ); - } - - /** - * @param callable(Connection, ClientLifecycle): ClientLifecycle $map - */ - public function map(callable $map): self - { - return new self( - $this->server, - $this->watch, - $this->connections->map($map), - ); - } - - /** - * @param callable(Connection, ClientLifecycle): Map $map - */ - public function flatMap(callable $map): self - { - return new self( - $this->server, - $this->watch, - $this->connections->flatMap($map), - ); - } - - /** - * @template C - * - * @param callable(Message, Continuation, C): Continuation $listen - * @param C $carry - * - * @return Either Left side means the connections must be shutdown - */ - public function notify( - Connection $connection, - callable $listen, - mixed $carry, - ): Either { - return $this - ->connections - ->get($connection) - ->either() - ->flatMap(static fn($client) => $client->notify($listen, $carry)) - ->match( - fn($either) => $either - ->map(fn($tuple) => [new self( - $this->server, - $this->watch, - ($this->connections)($connection, $tuple[0]), - ), $tuple[1]]) - ->leftMap(fn($tuple) => [new self( - $this->server, - $this->watch, - ($this->connections)($connection, $tuple[0]), - ), $tuple[1]]), - fn($carry) => Either::right([new self( - $this->server, - $this->watch->unwatch($connection), - $this->connections->remove($connection), - ), $carry]), - ); - } - - /** - * @return Maybe - */ - public function close(): Maybe - { - return $this - ->connections - ->reduce( - Maybe::just(new SideEffect), - static fn(Maybe $maybe, $connection) => $maybe->flatMap( - static fn(): Maybe => $connection - ->close() - ->maybe(), - ), - ) - ->otherwise(fn() => $this->server->close()->maybe()) // force close the server even if a client could not be closed - ->flatMap(fn() => $this->server->close()->maybe()); - } - - /** - * @return Maybe Returns nothing when it was terminated - */ - public function terminate(): Maybe - { - if ($this->connections->empty()) { - /** @var Maybe */ - return $this - ->server - ->close() - ->maybe() - ->filter(static fn() => false); // in all cases the connections are no longer usable - } - - return Maybe::just($this); - } -} diff --git a/src/Server/Connections/Active.php b/src/Server/Connections/Active.php deleted file mode 100644 index e82f914..0000000 --- a/src/Server/Connections/Active.php +++ /dev/null @@ -1,55 +0,0 @@ - */ - private Maybe $server; - /** @var Set */ - private Set $clients; - - /** - * @param Maybe $server - * @param Set $clients - */ - public function __construct(Maybe $server, Set $clients) - { - $this->server = $server; - $this->clients = $clients; - } - - public static function none(): self - { - /** @var Maybe */ - $server = Maybe::nothing(); - - return new self($server, Set::of()); - } - - /** - * @return Maybe - */ - public function server(): Maybe - { - return $this->server; - } - - /** - * @return Set - */ - public function clients(): Set - { - return $this->clients; - } -} diff --git a/src/Server/UnableToStart.php b/src/Server/UnableToStart.php deleted file mode 100644 index 431016c..0000000 --- a/src/Server/UnableToStart.php +++ /dev/null @@ -1,11 +0,0 @@ -sockets = $sockets; - $this->protocol = $protocol; - $this->clock = $clock; - $this->signals = $signals; - $this->address = $address; - $this->heartbeat = $heartbeat; - $this->timeout = $timeout; - } - - /** - * @template C - * - * @param C $carry - * @param callable(Message, Continuation, C): Continuation $listen - * - * @return Either - */ - #[\Override] - public function __invoke(mixed $carry, callable $listen): Either - { - $iteration = $this - ->sockets - ->open($this->address) - ->map(fn($server) => Connections::start( - $this->sockets->watch($this->heartbeat), - $server->unwrap(), - )) - ->map(fn($connections) => Unix\Iteration::first( - $this->protocol, - $this->clock, - $connections, - $this->heartbeat, - $this->timeout, - $carry, - )) - ->match( - static fn($iteration) => $iteration, - static fn() => null, - ); - - if (\is_null($iteration)) { - return Either::left(new UnableToStart); - } - - $shutdown = static function() use (&$iteration): void { - /** @var Unix\Iteration $iteration */ - $iteration->startShutdown(); - }; - $this->registerSignals($shutdown); - - // we use a while loop instead of recursion to avoid too deep call stacks - // for long running servers - do { - try { - /** - * @psalm-suppress MixedMethodCall Due to the reference above for the shutdown - * @var Unix\Iteration|C - */ - $iteration = $iteration->next($listen)->match( - static fn(mixed $iteration): mixed => $iteration, - static fn(mixed $carry): mixed => $carry, - ); - } catch (\Throwable $e) { - $this->unregisterSignals($shutdown); - - throw $e; - } - } while ($iteration instanceof Unix\Iteration); - - $this->unregisterSignals($shutdown); - - return Either::right($iteration); - } - - /** - * @param callable(Signal, Info): void $shutdown - */ - private function registerSignals(callable $shutdown): void - { - $this->signals->listen(Signal::hangup, $shutdown); - $this->signals->listen(Signal::interrupt, $shutdown); - $this->signals->listen(Signal::abort, $shutdown); - $this->signals->listen(Signal::terminate, $shutdown); - $this->signals->listen(Signal::terminalStop, $shutdown); - $this->signals->listen(Signal::alarm, $shutdown); - } - - /** - * @param callable(Signal, Info): void $shutdown - */ - private function unregisterSignals(callable $shutdown): void - { - $this->signals->remove($shutdown); - } -} diff --git a/src/Server/Unix/Iteration.php b/src/Server/Unix/Iteration.php deleted file mode 100644 index 74deabc..0000000 --- a/src/Server/Unix/Iteration.php +++ /dev/null @@ -1,251 +0,0 @@ -protocol = $protocol; - $this->clock = $clock; - $this->connections = $connections; - $this->heartbeat = $heartbeat; - $this->timeout = $timeout; - $this->lastActivity = $lastActivity; - $this->state = $state; - $this->carry = $carry; - } - - /** - * @template T - * - * @param T $carry - */ - public static function first( - Protocol $protocol, - Clock $clock, - Connections $connections, - ElapsedPeriod $heartbeat, - ?ElapsedPeriod $timeout, - mixed $carry, - ): self { - return new self( - $protocol, - $clock, - $connections, - $heartbeat, - $timeout, - $clock->now(), - State::awaitingConnection, - $carry, - ); - } - - /** - * @param callable(Message, Continuation, C): Continuation $listen - * - * @return Either - */ - public function next(callable $listen): Either - { - return $this - ->state - ->watch($this->connections) - ->either() - ->flatMap(fn($active) => $this->act($active, $listen)); - } - - /** - * This method mutates the object instead of returning a new one because it - * is called when the process is signaled and the callback cannot mutate the - * variable `$iteration` in `Server\Unix` - */ - public function startShutdown(): void - { - $this->connections = $this->state->shutdown($this->connections); - $this->lastActivity = $this->clock->now(); - $this->state = State::shuttingDown; - } - - /** - * @param callable(Message, Continuation, C): Continuation $listen - * - * @return Either - */ - private function act(Active $active, callable $listen): Either - { - $connections = $this->state->acceptConnection( - $active->server(), - $this->connections, - $this->protocol, - $this->clock, - $this->heartbeat, - ); - $connections = $this->heartbeat($connections, $active->clients()); - - return $this - ->notify($active->clients(), $connections, $listen) - ->leftMap($this->shutdown(...)) - ->flatMap($this->monitorTimeout(...)) - ->map($this->monitorTermination(...)) - ->match( - fn($connections) => $connections->map(fn($tuple) => new self( - $this->protocol, - $this->clock, - $tuple[0], - $this->heartbeat, - $this->timeout, - match ($active->clients()->empty()) { - true => $this->lastActivity, - false => $this->clock->now(), - }, - $this->state, - $tuple[1], - )), - static fn($shuttingDown) => Either::right($shuttingDown), - ); - } - - /** - * @param Set $active - */ - private function heartbeat( - Connections $connections, - Set $active, - ): Connections { - // send heartbeat message for clients not found in the active sockets - return $connections->map( - static fn($connection, $client) => $active - ->find(static fn($active) => $active === $connection) - ->match( - static fn() => $client, - static fn() => $client->heartbeat(), - ), - ); - } - - /** - * @param Set $active - * @param callable(Message, Continuation, C): Continuation $listen - * - * @return Either Left side means the connections must be shutdown - */ - private function notify( - Set $active, - Connections $connections, - callable $listen, - ): Either { - /** - * Errors are due to the weak typing of the tuples - * @psalm-suppress MixedArgumentTypeCoercion - * @psalm-suppress MixedInferredReturnType - * @psalm-suppress MixedReturnStatement - * @psalm-suppress MixedMethodCall - * @var Either - */ - return $active->reduce( - Either::right([$connections, $this->carry]), - static fn(Either $either, $connection): Either => $either->flatMap( - static fn(array $tuple): Either => $tuple[0]->notify( - $connection, - $listen, - $tuple[1], - ), - ), - ); - } - - /** - * @param array{Connections, C} $tuple - */ - private function shutdown(array $tuple): self - { - return new self( - $this->protocol, - $this->clock, - $this->state->shutdown($tuple[0]), - $this->heartbeat, - $this->timeout, - $this->clock->now(), - State::shuttingDown, - $tuple[1], - ); - } - - /** - * @param array{Connections, C} $tuple - * - * @return Either - */ - private function monitorTimeout(array $tuple): Either - { - if (!$this->timeout) { - return Either::right($tuple); - } - - $iterationDuration = $this->clock->now()->elapsedSince($this->lastActivity); - - return match ($iterationDuration->longerThan($this->timeout)) { - true => Either::left($this->shutdown($tuple)), - false => Either::right($tuple), - }; - } - - /** - * @param array{Connections, C} $tuple - * - * @return Either - */ - private function monitorTermination(array $tuple): Either - { - return $this - ->state - ->terminate($tuple[0]) - ->either() - ->map(static fn($connections) => [$connections, $tuple[1]]) - ->leftMap(static fn() => $tuple[1]); - } -} diff --git a/src/Server/Unix/State.php b/src/Server/Unix/State.php deleted file mode 100644 index 7eca1b3..0000000 --- a/src/Server/Unix/State.php +++ /dev/null @@ -1,113 +0,0 @@ - - */ - public function watch(Connections $connections): Maybe - { - // When awaiting connections if we encounter an error when watch the - // sockets we return an empty Active as it may only be a hiccup or it - // happens when the process is signaled and we enter in shutting down - // mode. This allows to properly shutdown the sockets. - // However when shutting down if we encounter an error we directly close - // all the sockets (connections and server). We do this because it's - // either the process is crashing or an error due to a signal that mess - // with the function that watch the sockets so we simply give up and let - // the server stop - - /** @var Maybe */ - return match ($this) { - self::shuttingDown => $connections - ->watch() - ->otherwise( - static fn() => $connections - ->close() - ->filter(static fn() => false), // force to return nothing - ), - self::awaitingConnection => $connections - ->watch() - ->otherwise(static fn() => Maybe::just(Active::none())), - }; - } - - /** - * @param Maybe $server - */ - public function acceptConnection( - Maybe $server, - Connections $connections, - Protocol $protocol, - Clock $clock, - ElapsedPeriod $heartbeat, - ): Connections { - return match ($this) { - self::shuttingDown => $connections, - self::awaitingConnection => $server - ->flatMap(static fn($server) => $server->accept()) - ->flatMap( - static fn($connection) => ClientLifecycle::of( - new Client\Unix($connection, $protocol), - $clock, - $heartbeat, - )->map(static fn($lifecycle) => $connections->add( - $connection, - $lifecycle, - )), - ) - ->match( - static fn($connections) => $connections, - static fn() => $connections, - ), - }; - } - - public function shutdown(Connections $connections): Connections - { - /** @psalm-suppress InvalidArgument Due to the empty map */ - return match ($this) { - self::shuttingDown => $connections, - self::awaitingConnection => $connections->flatMap( - static fn($connection, $client) => $client->shutdown()->match( - static fn($client) => Map::of([$connection, $client]), // pendingCloseOk - static fn() => Map::of(), // can't shutdown properly, discard - ), - ), - }; - } - - /** - * @return Maybe - */ - public function terminate(Connections $connections): Maybe - { - return match ($this) { - self::awaitingConnection => Maybe::just($connections), - self::shuttingDown => $connections->terminate(), - }; - } -} diff --git a/tests/Client/UnixTest.php b/tests/Client/UnixTest.php deleted file mode 100644 index 1fb8d2c..0000000 --- a/tests/Client/UnixTest.php +++ /dev/null @@ -1,144 +0,0 @@ -assertInstanceOf( - Client::class, - new Unix( - $this->createMock(Connection::class), - $this->createMock(Protocol::class), - ), - ); - } - - public function testSend() - { - $client = new Unix( - $connection = $this->createMock(Connection::class), - $protocol = $this->createMock(Protocol::class), - ); - $message = $this->createMock(Message::class); - $connection - ->expects($this->once()) - ->method('write') - ->with(Str::of('watev')) - ->willReturn(Either::right($connection)); - $protocol - ->expects($this->once()) - ->method('encode') - ->with($message) - ->willReturn(Str::of('watev')); - - $this->assertSame($client, $client->send($message)->match( - static fn($client) => $client, - static fn() => null, - )); - } - - public function testClose() - { - $client = new Unix( - $connection = $this->createMock(Connection::class), - $this->createMock(Protocol::class), - ); - $connection - ->expects($this->once()) - ->method('close') - ->willReturn(Either::right($expected = new SideEffect)); - - $this->assertSame($expected, $client->close()->match( - static fn($sideEffect) => $sideEffect, - static fn() => null, - )); - } - - public function testDoesntSendOnceClosed() - { - $client = new Unix( - $connection = $this->createMock(Connection::class), - $this->createMock(Protocol::class), - ); - $connection - ->expects($this->once()) - ->method('closed') - ->willReturn(true); - $connection - ->expects($this->never()) - ->method('write'); - - $this->assertNull($client->send($this->createMock(Message::class))->match( - static fn($client) => $client, - static fn() => null, - )); - } - - public function testReturnNothingWhenCantSendMessageDueToSocketError() - { - $client = new Unix( - $connection = $this->createMock(Connection::class), - $protocol = $this->createMock(Protocol::class), - ); - $message = new Message\Generic( - MediaType::of('text/plain'), - Str::of('watev'), - ); - $protocol - ->expects($this->once()) - ->method('encode') - ->with($message) - ->willReturn(Str::of('watev')); - $connection - ->expects($this->once()) - ->method('write') - ->willReturn(Either::left(new FailedToWriteToStream)); - - $this->assertNull($client->send($message)->match( - static fn($client) => $client, - static fn() => null, - )); - } - - public function testReturnNothingWhenCantProperlyCloseDueToSocketError() - { - $client = new Unix( - $connection = $this->createMock(Connection::class), - $protocol = $this->createMock(Protocol::class), - ); - $protocol - ->expects($this->never()) - ->method('encode'); - $connection - ->expects($this->once()) - ->method('close') - ->willReturn(Either::left(new FailedToCloseStream)); - - $this->assertNull($client->close()->match( - static fn($sideEffect) => $sideEffect, - static fn() => null, - )); - } -} diff --git a/tests/FactoryTest.php b/tests/FactoryTest.php deleted file mode 100644 index 6e9298c..0000000 --- a/tests/FactoryTest.php +++ /dev/null @@ -1,26 +0,0 @@ -assertInstanceOf( - IPC::class, - Factory::build( - $this->createMock(OperatingSystem::class), - Path::of('/tmp/innmind/ipc/'), - ), - ); - } -} diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php deleted file mode 100644 index b3719f7..0000000 --- a/tests/FunctionalTest.php +++ /dev/null @@ -1,69 +0,0 @@ -status()->tmp()->toString().'/innmind/ipc/server.sock'); - $processes = $os->control()->processes(); - $processes->execute( - Command::background('php') - ->withArgument('fixtures/server.php') - ->withEnvironment('TMPDIR', $os->status()->tmp()->toString()) - ->withEnvironment('PATH', $_SERVER['PATH']), - ); - $process = $processes->execute( - Command::foreground('php') - ->withArgument('fixtures/client.php') - ->withEnvironment('TMPDIR', $os->status()->tmp()->toString()) - ->withEnvironment('PATH', $_SERVER['PATH']), - ); - $process->wait(); - $output = $process->output()->toString(); - - $this->assertSame('hello world', $output); - } - - public function testKillServer() - { - if (\getenv('CI')) { - $this->markTestSkipped(); - } - - $os = Factory::build(); - @\unlink($os->status()->tmp()->toString().'/innmind/ipc/server.sock'); - $processes = $os->control()->processes(); - $server = $processes->execute( - Command::foreground('php') - ->withArgument('fixtures/eternal-server.php') - ->withEnvironment('TMPDIR', $os->status()->tmp()->toString()) - ->withEnvironment('PATH', $_SERVER['PATH']), - ); - $os->process()->halt(new Second(1)); - $processes->kill( - $server->pid()->match( - static fn($pid) => $pid, - static fn() => null, - ), - Signal::interrupt, - ); - $this->assertTrue($server->wait()->match( - static fn() => true, - static fn() => false, - )); - - $this->assertFalse(\file_exists($os->status()->tmp()->toString().'/innmind/ipc/server.sock')); - } -} diff --git a/tests/IPC/UnixTest.php b/tests/IPC/UnixTest.php deleted file mode 100644 index bc71424..0000000 --- a/tests/IPC/UnixTest.php +++ /dev/null @@ -1,341 +0,0 @@ -assertInstanceOf( - IPC::class, - new Unix( - $this->createMock(Sockets::class), - $this->createMock(Adapter::class), - $this->createMock(Clock::class), - $this->createMock(CurrentProcess::class), - $this->createMock(Protocol::class), - Path::of('/tmp/somewhere/'), - new Timeout(1000), - ), - ); - } - - public function testThrowIfThePathDoesntRepresentADirectory() - { - $this->expectException(LogicException::class); - $this->expectExceptionMessage("Path must be a directory, got '/tmp/somewhere'"); - - new Unix( - $this->createMock(Sockets::class), - $this->createMock(Adapter::class), - $this->createMock(Clock::class), - $this->createMock(CurrentProcess::class), - $this->createMock(Protocol::class), - Path::of('/tmp/somewhere'), - new Timeout(1000), - ); - } - - public function testProcesses() - { - $ipc = new Unix( - $sockets = $this->createMock(Sockets::class), - $filesystem = Adapter\InMemory::emulateFilesystem(), - $this->createMock(Clock::class), - $this->createMock(CurrentProcess::class), - $protocol = $this->createMock(Protocol::class), - Path::of('/tmp/'), - new Timeout(1000), - ); - $filesystem->add(File::named('foo', File\Content::none())); - $filesystem->add(File::named('bar', File\Content::none())); - - $processes = $ipc->processes(); - - $this->assertInstanceOf(Set::class, $processes); - $this->assertCount(2, $processes); - $processes = $processes->toList(); - - $foo = \current($processes); - $this->assertSame('foo', $foo->toString()); - \next($processes); - $bar = \current($processes); - $this->assertSame('bar', $bar->toString()); - } - - public function testReturnNothingWhenGettingUnknownProcess() - { - $ipc = new Unix( - $this->createMock(Sockets::class), - $filesystem = $this->createMock(Adapter::class), - $this->createMock(Clock::class), - $this->createMock(CurrentProcess::class), - $this->createMock(Protocol::class), - Path::of('/tmp/somewhere/'), - new Timeout(1000), - ); - $filesystem - ->expects($this->once()) - ->method('contains') - ->with(FileName::of('foo.sock')) - ->willReturn(false); - - $this->assertNull($ipc->get(Name::of('foo'))->match( - static fn($foo) => $foo, - static fn() => null, - )); - } - - public function testGetProcess() - { - $ipc = new Unix( - $sockets = $this->createMock(Sockets::class), - $filesystem = $this->createMock(Adapter::class), - $this->createMock(Clock::class), - $this->createMock(CurrentProcess::class), - $protocol = $this->createMock(Protocol::class), - Path::of('/tmp/'), - $heartbeat = new Timeout(1000), - ); - $filesystem - ->expects($this->once()) - ->method('contains') - ->with(FileName::of('foo.sock')) - ->willReturn(true); - $sockets - ->expects($this->once()) - ->method('connectTo') - ->willReturn(Maybe::just($client = $this->createMock(Client::class))->map( - static fn($client) => IOClient::of( - static fn() => null, - $client, - ), - )); - $sockets - ->expects($this->once()) - ->method('watch') - ->with($heartbeat) - ->willReturn(Select::timeoutAfter($heartbeat)); - $resource = \tmpfile(); - $client - ->expects($this->any()) - ->method('resource') - ->willReturn($resource); - $client - ->expects($this->any()) - ->method('write') - ->willReturn(Either::right($client)); - $protocol - ->expects($this->once()) - ->method('encode') - ->willReturn(Str::of('welcome')); - $protocol - ->expects($this->once()) - ->method('decode') - ->willReturn(Maybe::just(new ConnectionStart)); - - $foo = $ipc->get(Name::of('foo'))->match( - static fn($foo) => $foo, - static fn() => null, - ); - - $this->assertInstanceOf(Process\Unix::class, $foo); - $this->assertSame('foo', $foo->name()->toString()); - } - - public function testExist() - { - $ipc = new Unix( - $this->createMock(Sockets::class), - $filesystem = $this->createMock(Adapter::class), - $this->createMock(Clock::class), - $this->createMock(CurrentProcess::class), - $this->createMock(Protocol::class), - Path::of('/tmp/'), - new Timeout(1000), - ); - $filesystem - ->expects($this->exactly(2)) - ->method('contains') - ->with(FileName::of('foo.sock')) - ->will($this->onConsecutiveCalls(true, false)); - - $this->assertTrue($ipc->exist(Name::of('foo'))); - $this->assertFalse($ipc->exist(Name::of('foo'))); - } - - public function testListen() - { - $ipc = new Unix( - $sockets = $this->createMock(Sockets::class), - $filesystem = $this->createMock(Adapter::class), - $this->createMock(Clock::class), - $this->createMock(CurrentProcess::class), - $this->createMock(Protocol::class), - Path::of('/tmp/'), - new Timeout(1000), - ); - - $server = $ipc->listen( - Name::of('bar'), - $this->createMock(ElapsedPeriod::class), - ); - - $this->assertInstanceOf(Server\Unix::class, $server); - } - - public function testWait() - { - $ipc = new Unix( - $sockets = $this->createMock(Sockets::class), - $filesystem = $this->createMock(Adapter::class), - $this->createMock(Clock::class), - $process = $this->createMock(CurrentProcess::class), - $protocol = $this->createMock(Protocol::class), - Path::of('/tmp/'), - $timeout = new Timeout(1000), - ); - $filesystem - ->expects($this->exactly(4)) - ->method('contains') - ->with(FileName::of('foo.sock')) - ->will($this->onConsecutiveCalls(false, false, true, true)); - $process - ->expects($this->exactly(2)) - ->method('halt') - ->with(new Millisecond(1000)); - $sockets - ->expects($this->once()) - ->method('connectTo') - ->willReturn(Maybe::just($socket = $this->createMock(Client::class))->map( - static fn($client) => IOClient::of( - static fn() => null, - $client, - ), - )); - $sockets - ->expects($this->once()) - ->method('watch') - ->with($timeout) - ->willReturn(Select::timeoutAfter($timeout)); - $resource = \tmpfile(); - $socket - ->expects($this->any()) - ->method('resource') - ->willReturn($resource); - $protocol - ->expects($this->once()) - ->method('decode') - ->with($socket) - ->willReturn(Maybe::just(new ConnectionStart)); - $protocol - ->expects($this->once()) - ->method('encode') - ->with(new ConnectionStartOk) - ->willReturn(Str::of('start-ok')); - $socket - ->method('write') - ->with(Str::of('start-ok')) - ->willReturn(Either::right($socket)); - - $this->assertInstanceOf(Process::class, $ipc->wait(Name::of('foo'))->match( - static fn($process) => $process, - static fn() => null, - )); - } - - public function testStopWaitingWhenTimeoutExceeded() - { - $ipc = new Unix( - $this->createMock(Sockets::class), - $filesystem = $this->createMock(Adapter::class), - $clock = $this->createMock(Clock::class), - $process = $this->createMock(CurrentProcess::class), - $this->createMock(Protocol::class), - Path::of('/tmp/'), - new Timeout(1000), - ); - $timeout = $this->createMock(ElapsedPeriod::class); - $filesystem - ->expects($this->any()) - ->method('contains') - ->with(FileName::of('foo.sock')) - ->willReturn(false); - $process - ->expects($this->once()) - ->method('halt'); - $clock - ->expects($this->exactly(3)) - ->method('now') - ->will($this->onConsecutiveCalls( - $start = $this->createMock(PointInTime::class), - $firstIteration = $this->createMock(PointInTime::class), - $secondIteration = $this->createMock(PointInTime::class), - )); - $firstIteration - ->expects($this->once()) - ->method('elapsedSince') - ->with($start) - ->willReturn($duration = $this->createMock(ElapsedPeriod::class)); - $duration - ->expects($this->once()) - ->method('longerThan') - ->with($timeout) - ->willReturn(false); - $secondIteration - ->expects($this->once()) - ->method('elapsedSince') - ->with($start) - ->willReturn($duration = $this->createMock(ElapsedPeriod::class)); - $duration - ->expects($this->once()) - ->method('longerThan') - ->with($timeout) - ->willReturn(true); - - $this->assertNull($ipc->wait(Name::of('foo'), $timeout)->match( - static fn($process) => $process, - static fn() => null, - )); - } -} diff --git a/tests/Message/ConnectionCloseOkTest.php b/tests/Message/ConnectionCloseOkTest.php deleted file mode 100644 index 0bc60df..0000000 --- a/tests/Message/ConnectionCloseOkTest.php +++ /dev/null @@ -1,41 +0,0 @@ -assertInstanceOf(Message::class, $message); - $this->assertSame('text/plain', $message->mediaType()->toString()); - $this->assertSame('innmind/ipc:connection.close-ok', $message->content()->toString()); - } - - public function testEquals() - { - $message = new ConnectionCloseOk; - $same = new Generic( - MediaType::of('text/plain'), - Str::of('innmind/ipc:connection.close-ok'), - ); - $different = new Generic( - MediaType::of('text/plain'), - Str::of('foo'), - ); - - $this->assertTrue($message->equals($same)); - $this->assertFalse($message->equals($different)); - } -} diff --git a/tests/Message/ConnectionCloseTest.php b/tests/Message/ConnectionCloseTest.php deleted file mode 100644 index 909c6c1..0000000 --- a/tests/Message/ConnectionCloseTest.php +++ /dev/null @@ -1,41 +0,0 @@ -assertInstanceOf(Message::class, $message); - $this->assertSame('text/plain', $message->mediaType()->toString()); - $this->assertSame('innmind/ipc:connection.close', $message->content()->toString()); - } - - public function testEquals() - { - $message = new ConnectionClose; - $same = new Generic( - MediaType::of('text/plain'), - Str::of('innmind/ipc:connection.close'), - ); - $different = new Generic( - MediaType::of('text/plain'), - Str::of('foo'), - ); - - $this->assertTrue($message->equals($same)); - $this->assertFalse($message->equals($different)); - } -} diff --git a/tests/Message/ConnectionStartOkTest.php b/tests/Message/ConnectionStartOkTest.php deleted file mode 100644 index 7417611..0000000 --- a/tests/Message/ConnectionStartOkTest.php +++ /dev/null @@ -1,41 +0,0 @@ -assertInstanceOf(Message::class, $message); - $this->assertSame('text/plain', $message->mediaType()->toString()); - $this->assertSame('innmind/ipc:connection.start-ok', $message->content()->toString()); - } - - public function testEquals() - { - $message = new ConnectionStartOk; - $same = new Generic( - MediaType::of('text/plain'), - Str::of('innmind/ipc:connection.start-ok'), - ); - $different = new Generic( - MediaType::of('text/plain'), - Str::of('foo'), - ); - - $this->assertTrue($message->equals($same)); - $this->assertFalse($message->equals($different)); - } -} diff --git a/tests/Message/ConnectionStartTest.php b/tests/Message/ConnectionStartTest.php deleted file mode 100644 index 3ef12de..0000000 --- a/tests/Message/ConnectionStartTest.php +++ /dev/null @@ -1,41 +0,0 @@ -assertInstanceOf(Message::class, $message); - $this->assertSame('text/plain', $message->mediaType()->toString()); - $this->assertSame('innmind/ipc:connection.start', $message->content()->toString()); - } - - public function testEquals() - { - $message = new ConnectionStart; - $same = new Generic( - MediaType::of('text/plain'), - Str::of('innmind/ipc:connection.start'), - ); - $different = new Generic( - MediaType::of('text/plain'), - Str::of('foo'), - ); - - $this->assertTrue($message->equals($same)); - $this->assertFalse($message->equals($different)); - } -} diff --git a/tests/Message/GenericTest.php b/tests/Message/GenericTest.php deleted file mode 100644 index d0865f3..0000000 --- a/tests/Message/GenericTest.php +++ /dev/null @@ -1,57 +0,0 @@ -assertInstanceOf(Message::class, $message); - $this->assertSame($mediaType, $message->mediaType()); - $this->assertSame($content, $message->content()); - } - - public function testOf() - { - $message = Generic::of('text/plain', 'foo'); - - $this->assertInstanceOf(Generic::class, $message); - $this->assertTrue($message->equals(new Generic( - MediaType::of('text/plain'), - Str::of('foo'), - ))); - } - - public function testEquals() - { - $message = new Generic( - MediaType::of('text/plain'), - Str::of('watev'), - ); - $same = new Generic( - MediaType::of('text/plain'), - Str::of('watev'), - ); - $different = new Generic( - MediaType::of('text/plain'), - Str::of('foo'), - ); - - $this->assertTrue($message->equals($same)); - $this->assertFalse($message->equals($different)); - } -} diff --git a/tests/Message/HeartbeatTest.php b/tests/Message/HeartbeatTest.php deleted file mode 100644 index 8d88de8..0000000 --- a/tests/Message/HeartbeatTest.php +++ /dev/null @@ -1,41 +0,0 @@ -assertInstanceOf(Message::class, $message); - $this->assertSame('text/plain', $message->mediaType()->toString()); - $this->assertSame('innmind/ipc:heartbeat', $message->content()->toString()); - } - - public function testEquals() - { - $message = new Heartbeat; - $same = new Generic( - MediaType::of('text/plain'), - Str::of('innmind/ipc:heartbeat'), - ); - $different = new Generic( - MediaType::of('text/plain'), - Str::of('foo'), - ); - - $this->assertTrue($message->equals($same)); - $this->assertFalse($message->equals($different)); - } -} diff --git a/tests/Message/MessageReceivedTest.php b/tests/Message/MessageReceivedTest.php deleted file mode 100644 index 14bd88e..0000000 --- a/tests/Message/MessageReceivedTest.php +++ /dev/null @@ -1,41 +0,0 @@ -assertInstanceOf(Message::class, $message); - $this->assertSame('text/plain', $message->mediaType()->toString()); - $this->assertSame('innmind/ipc:message.received', $message->content()->toString()); - } - - public function testEquals() - { - $message = new MessageReceived; - $same = new Generic( - MediaType::of('text/plain'), - Str::of('innmind/ipc:message.received'), - ); - $different = new Generic( - MediaType::of('text/plain'), - Str::of('foo'), - ); - - $this->assertTrue($message->equals($same)); - $this->assertFalse($message->equals($different)); - } -} diff --git a/tests/Process/NameTest.php b/tests/Process/NameTest.php deleted file mode 100644 index 7ecd0c9..0000000 --- a/tests/Process/NameTest.php +++ /dev/null @@ -1,41 +0,0 @@ -forAll(Set\Elements::of( - 'fooBar', - 'foo_bar', - 'foo-bar', - '42foo', - )) - ->then(function(string $string): void { - $this->assertSame($string, Name::of($string)->toString()); - }); - } - - public function testThrowWhenContainsInvalidCharacter() - { - $this->expectException(DomainException::class); - $this->expectExceptionMessage('foo.bar'); - - Name::of('foo.bar'); - } -} diff --git a/tests/Process/UnixTest.php b/tests/Process/UnixTest.php deleted file mode 100644 index 5d89e2f..0000000 --- a/tests/Process/UnixTest.php +++ /dev/null @@ -1,969 +0,0 @@ -createMock(Sockets::class); - $protocol = $this->createMock(Protocol::class); - $address = Address::of('/tmp/foo'); - $sockets - ->expects($this->once()) - ->method('connectTo') - ->with($address) - ->willReturn(Maybe::just($socket = $this->createMock(Client::class))->map( - static fn($client) => IOClient::of( - static fn() => null, - $client, - ), - )); - $sockets - ->expects($this->once()) - ->method('watch') - ->with($timeout) - ->willReturn(Select::timeoutAfter($timeout)); - $resource = \tmpfile(); - $socket - ->expects($this->any()) - ->method('resource') - ->willReturn($resource); - $protocol - ->expects($this->once()) - ->method('decode') - ->with($socket) - ->willReturn(Maybe::just(new ConnectionStart)); - $protocol - ->expects($this->once()) - ->method('encode') - ->with(new ConnectionStartOk) - ->willReturn(Str::of('start-ok')); - $socket - ->method('write') - ->with(Str::of('start-ok')) - ->willReturn(Either::right($socket)); - - $process = Unix::of( - $sockets, - $protocol, - $this->createMock(Clock::class), - $address, - $name = Name::of('foo'), - $timeout, - )->match( - static fn($process) => $process, - static fn() => null, - ); - - $this->assertInstanceOf(Process::class, $process); - $this->assertSame($name, $process->name()); - } - - public function testReturnNothingWhenFailedConnectionStart() - { - $timeout = new Timeout(1000); - $sockets = $this->createMock(Sockets::class); - $protocol = $this->createMock(Protocol::class); - $address = Address::of('/tmp/foo'); - $sockets - ->expects($this->once()) - ->method('connectTo') - ->with($address) - ->willReturn(Maybe::just($socket = $this->createMock(Client::class))->map( - static fn($client) => IOClient::of( - static fn() => null, - $client, - ), - )); - $sockets - ->expects($this->once()) - ->method('watch') - ->with($timeout) - ->willReturn(Select::timeoutAfter($timeout)); - $resource = \tmpfile(); - $socket - ->expects($this->any()) - ->method('resource') - ->willReturn($resource); - $protocol - ->expects($this->once()) - ->method('decode') - ->with($socket) - ->willReturn(Maybe::just($this->createMock(Message::class))); - $socket - ->method('write') - ->with(Str::of('start-ok')) - ->willReturn(Either::right($socket)); - - $process = Unix::of( - $sockets, - $protocol, - $this->createMock(Clock::class), - $address, - Name::of('foo'), - $timeout, - )->match( - static fn($process) => $process, - static fn() => null, - ); - - $this->assertNull($process); - } - - public function testSend() - { - $message = $this->createMock(Message::class); - - $timeout = new Timeout(1000); - $sockets = $this->createMock(Sockets::class); - $protocol = $this->createMock(Protocol::class); - $address = Address::of('/tmp/foo'); - $sockets - ->expects($this->once()) - ->method('connectTo') - ->with($address) - ->willReturn(Maybe::just($socket = $this->createMock(Client::class))->map( - static fn($client) => IOClient::of( - static fn() => null, - $client, - ), - )); - $sockets - ->expects($this->once()) - ->method('watch') - ->with($timeout) - ->willReturn(Select::timeoutAfter($timeout)); - $resource = \tmpfile(); - $socket - ->expects($this->any()) - ->method('resource') - ->willReturn($resource); - $protocol - ->expects($this->atLeast(1)) - ->method('decode') - ->with($socket) - ->will($this->onConsecutiveCalls( - Maybe::just(new ConnectionStart), - Maybe::just(new MessageReceived), - )); - $protocol - ->expects($matcher = $this->exactly(2)) - ->method('encode') - ->willReturnCallback(function($in) use ($matcher, $message) { - match ($matcher->numberOfInvocations()) { - 1 => $this->assertEquals(new ConnectionStartOk, $in), - 2 => $this->assertEquals($message, $in), - }; - - return match ($matcher->numberOfInvocations()) { - 1 => Str::of('start-ok'), - 2 => Str::of('message-to-send'), - }; - }); - $socket - ->expects($matcher = $this->exactly(2)) - ->method('write') - ->willReturnCallback(function($message) use ($matcher, $socket) { - match ($matcher->numberOfInvocations()) { - 1 => $this->assertEquals(Str::of('start-ok'), $message), - 2 => $this->assertEquals(Str::of('message-to-send'), $message), - }; - - return Either::right($socket); - }); - - $process = Unix::of( - $sockets, - $protocol, - $this->createMock(Clock::class), - $address, - $name = Name::of('foo'), - $timeout, - )->match( - static fn($process) => $process, - static fn() => null, - ); - - $this->assertSame($process, $process->send(Sequence::of($message))->match( - static fn($process) => $process, - static fn() => null, - )); - } - - public function testReturnNothingWhenErrorAtSent() - { - $message = $this->createMock(Message::class); - - $timeout = new Timeout(1000); - $sockets = $this->createMock(Sockets::class); - $protocol = $this->createMock(Protocol::class); - $address = Address::of('/tmp/foo'); - $sockets - ->expects($this->once()) - ->method('connectTo') - ->with($address) - ->willReturn(Maybe::just($socket = $this->createMock(Client::class))->map( - static fn($client) => IOClient::of( - static fn() => null, - $client, - ), - )); - $sockets - ->expects($this->once()) - ->method('watch') - ->with($timeout) - ->willReturn(Select::timeoutAfter($timeout)); - $resource = \tmpfile(); - $socket - ->expects($this->any()) - ->method('resource') - ->willReturn($resource); - $protocol - ->expects($this->once()) - ->method('decode') - ->with($socket) - ->willReturn(Maybe::just(new ConnectionStart)); - $protocol - ->expects($matcher = $this->exactly(2)) - ->method('encode') - ->willReturnCallback(function($in) use ($matcher, $message) { - match ($matcher->numberOfInvocations()) { - 1 => $this->assertEquals(new ConnectionStartOk, $in), - 2 => $this->assertEquals($message, $in), - }; - - return match ($matcher->numberOfInvocations()) { - 1 => Str::of('start-ok'), - 2 => Str::of('message content'), - }; - }); - $socket - ->expects($matcher = $this->exactly(2)) - ->method('write') - ->willReturnCallback(function($message) use ($matcher, $socket) { - match ($matcher->numberOfInvocations()) { - 1 => $this->assertEquals(Str::of('start-ok'), $message), - 2 => $this->assertEquals(Str::of('message content'), $message), - }; - - return match ($matcher->numberOfInvocations()) { - 1 => Either::right($socket), - 2 => Either::left(new FailedToWriteToStream), - }; - }); - - $process = Unix::of( - $sockets, - $protocol, - $this->createMock(Clock::class), - $address, - $name = Name::of('foo'), - $timeout, - )->match( - static fn($process) => $process, - static fn() => null, - ); - - $this->assertNull($process->send(Sequence::of($message))->match( - static fn($process) => $process, - static fn() => null, - )); - } - - public function testReturnNothingWhenTryingToSendOnClosedSocket() - { - $timeout = new Timeout(1000); - $sockets = $this->createMock(Sockets::class); - $protocol = $this->createMock(Protocol::class); - $address = Address::of('/tmp/foo'); - $sockets - ->expects($this->once()) - ->method('connectTo') - ->with($address) - ->willReturn(Maybe::just($socket = $this->createMock(Client::class))->map( - static fn($client) => IOClient::of( - static fn() => null, - $client, - ), - )); - $sockets - ->expects($this->once()) - ->method('watch') - ->with($timeout) - ->willReturn(Select::timeoutAfter($timeout)); - $resource = \tmpfile(); - $socket - ->expects($this->any()) - ->method('resource') - ->willReturn($resource); - $protocol - ->expects($this->once()) - ->method('decode') - ->with($socket) - ->willReturn(Maybe::just(new ConnectionStart)); - $protocol - ->expects($this->once()) - ->method('encode') - ->with(new ConnectionStartOk) - ->willReturn(Str::of('start-ok')); - $socket - ->expects($this->once()) - ->method('write') - ->with(Str::of('start-ok')) - ->willReturn(Either::right($socket)); - $socket - ->expects($this->exactly(3)) - ->method('closed') - ->will($this->onConsecutiveCalls( - false, - false, - true, - )); - - $process = Unix::of( - $sockets, - $protocol, - $this->createMock(Clock::class), - $address, - $name = Name::of('foo'), - $timeout, - )->match( - static fn($process) => $process, - static fn() => null, - ); - - $this->assertNull($process->send(Sequence::of($this->createMock(Message::class)))->match( - static fn($process) => $process, - static fn() => null, - )); - } - - public function testThrowWhenWaitingOnClosedSocket() - { - $timeout = new Timeout(1000); - $sockets = $this->createMock(Sockets::class); - $protocol = $this->createMock(Protocol::class); - $address = Address::of('/tmp/foo'); - $sockets - ->expects($this->once()) - ->method('connectTo') - ->with($address) - ->willReturn(Maybe::just($socket = $this->createMock(Client::class))->map( - static fn($client) => IOClient::of( - static fn() => null, - $client, - ), - )); - $sockets - ->expects($this->once()) - ->method('watch') - ->with($timeout) - ->willReturn(Select::timeoutAfter($timeout)); - $resource = \tmpfile(); - $socket - ->expects($this->any()) - ->method('resource') - ->willReturn($resource); - $protocol - ->expects($this->once()) - ->method('decode') - ->with($socket) - ->willReturn(Maybe::just(new ConnectionStart)); - $protocol - ->expects($this->once()) - ->method('encode') - ->with(new ConnectionStartOk) - ->willReturn(Str::of('start-ok')); - $socket - ->method('write') - ->with(Str::of('start-ok')) - ->willReturn(Either::right($socket)); - $socket - ->method('closed') - ->will($this->onConsecutiveCalls( - false, - false, - true, - )); - $socket - ->method('close') - ->willReturn(Either::right(new SideEffect)); - - $process = Unix::of( - $sockets, - $protocol, - $this->createMock(Clock::class), - $address, - $name = Name::of('foo'), - $timeout, - )->match( - static fn($process) => $process, - static fn() => null, - ); - - $this->assertNull($process->wait()->match( - static fn($message) => $message, - static fn() => null, - )); - $this->assertTrue($process->closed()); - } - - public function testWait() - { - $timeout = new Timeout(1000); - $sockets = $this->createMock(Sockets::class); - $protocol = $this->createMock(Protocol::class); - $address = Address::of('/tmp/foo'); - $sockets - ->expects($this->once()) - ->method('connectTo') - ->with($address) - ->willReturn(Maybe::just($socket = $this->createMock(Client::class))->map( - static fn($client) => IOClient::of( - static fn() => null, - $client, - ), - )); - $sockets - ->expects($this->once()) - ->method('watch') - ->with($timeout) - ->willReturn(Select::timeoutAfter($timeout)); - $resource = \tmpfile(); - $socket - ->expects($this->any()) - ->method('resource') - ->willReturn($resource); - $protocol - ->expects($this->once()) - ->method('encode') - ->with(new ConnectionStartOk) - ->willReturn(Str::of('start-ok')); - $protocol - ->expects($this->exactly(2)) - ->method('decode') - ->with($socket) - ->will($this->onConsecutiveCalls( - Maybe::just(new ConnectionStart), - Maybe::just($message = $this->createMock(Message::class)), - )); - $socket - ->method('write') - ->with(Str::of('start-ok')) - ->willReturn(Either::right($socket)); - - $process = Unix::of( - $sockets, - $protocol, - $this->createMock(Clock::class), - $address, - $name = Name::of('foo'), - $timeout, - )->match( - static fn($process) => $process, - static fn() => null, - ); - - $this->assertSame($message, $process->wait()->match( - static fn($message) => $message, - static fn() => null, - )); - } - - public function testDiscardHeartbeatWhenWaiting() - { - $timeout = new Timeout(1000); - $sockets = $this->createMock(Sockets::class); - $protocol = $this->createMock(Protocol::class); - $address = Address::of('/tmp/foo'); - $sockets - ->expects($this->once()) - ->method('connectTo') - ->with($address) - ->willReturn(Maybe::just($socket = $this->createMock(Client::class))->map( - static fn($client) => IOClient::of( - static fn() => null, - $client, - ), - )); - $sockets - ->expects($this->once()) - ->method('watch') - ->with($timeout) - ->willReturn(Select::timeoutAfter($timeout)); - $resource = \tmpfile(); - $socket - ->expects($this->any()) - ->method('resource') - ->willReturn($resource); - $protocol - ->expects($this->once()) - ->method('encode') - ->with(new ConnectionStartOk) - ->willReturn(Str::of('start-ok')); - $protocol - ->expects($this->exactly(3)) - ->method('decode') - ->with($socket) - ->will($this->onConsecutiveCalls( - Maybe::just(new ConnectionStart), - Maybe::just(new Heartbeat), - Maybe::just($message = $this->createMock(Message::class)), - )); - $socket - ->method('write') - ->with(Str::of('start-ok')) - ->willReturn(Either::right($socket)); - - $process = Unix::of( - $sockets, - $protocol, - $this->createMock(Clock::class), - $address, - $name = Name::of('foo'), - $timeout, - )->match( - static fn($process) => $process, - static fn() => null, - ); - - $this->assertSame($message, $process->wait()->match( - static fn($message) => $message, - static fn() => null, - )); - } - - public function testConfirmCloseWhenWaiting() - { - $timeout = new Timeout(1000); - $sockets = $this->createMock(Sockets::class); - $protocol = $this->createMock(Protocol::class); - $address = Address::of('/tmp/foo'); - $sockets - ->expects($this->once()) - ->method('connectTo') - ->with($address) - ->willReturn(Maybe::just($socket = $this->createMock(Client::class))->map( - static fn($client) => IOClient::of( - static fn() => null, - $client, - ), - )); - $sockets - ->expects($this->once()) - ->method('watch') - ->with($timeout) - ->willReturn(Select::timeoutAfter($timeout)); - $resource = \tmpfile(); - $socket - ->expects($this->any()) - ->method('resource') - ->willReturn($resource); - $protocol - ->expects($this->exactly(2)) - ->method('decode') - ->with($socket) - ->will($this->onConsecutiveCalls( - Maybe::just(new ConnectionStart), - Maybe::just(new ConnectionClose), - )); - $protocol - ->expects($matcher = $this->exactly(2)) - ->method('encode') - ->willReturnCallback(function($message) use ($matcher) { - match ($matcher->numberOfInvocations()) { - 1 => $this->assertEquals(new ConnectionStartOk, $message), - 2 => $this->assertEquals(new ConnectionCloseOk, $message), - }; - - return match ($matcher->numberOfInvocations()) { - 1 => Str::of('start-ok'), - 2 => Str::of('close-ok'), - }; - }); - $socket - ->expects($matcher = $this->exactly(2)) - ->method('write') - ->willReturnCallback(function($message) use ($matcher, $socket) { - match ($matcher->numberOfInvocations()) { - 1 => $this->assertEquals(Str::of('start-ok'), $message), - 2 => $this->assertEquals(Str::of('close-ok'), $message), - }; - - return Either::right($socket); - }); - $socket - ->method('close') - ->willReturn(Either::right(new SideEffect)); - - $process = Unix::of( - $sockets, - $protocol, - $this->createMock(Clock::class), - $address, - $name = Name::of('foo'), - $timeout, - )->match( - static fn($process) => $process, - static fn() => null, - ); - - $this->assertNull($process->wait()->match( - static fn($message) => $message, - static fn() => null, - )); - $this->assertTrue($process->closed()); - } - - public function testReturnNothingWhenNoMessageDecoded() - { - $timeout = new Timeout(1000); - $sockets = $this->createMock(Sockets::class); - $protocol = $this->createMock(Protocol::class); - $address = Address::of('/tmp/foo'); - $sockets - ->expects($this->once()) - ->method('connectTo') - ->with($address) - ->willReturn(Maybe::just($socket = $this->createMock(Client::class))->map( - static fn($client) => IOClient::of( - static fn() => null, - $client, - ), - )); - $sockets - ->expects($this->once()) - ->method('watch') - ->with($timeout) - ->willReturn(Select::timeoutAfter($timeout)); - $resource = \tmpfile(); - $socket - ->expects($this->any()) - ->method('resource') - ->willReturn($resource); - $protocol - ->expects($this->exactly(2)) - ->method('decode') - ->with($socket) - ->will($this->onConsecutiveCalls( - Maybe::just(new ConnectionStart), - Maybe::nothing(), - )); - $protocol - ->expects($this->once()) - ->method('encode') - ->with(new ConnectionStartOk) - ->willReturn(Str::of('start-ok')); - $socket - ->method('write') - ->with(Str::of('start-ok')) - ->willReturn(Either::right($socket)); - - $process = Unix::of( - $sockets, - $protocol, - $this->createMock(Clock::class), - $address, - $name = Name::of('foo'), - $timeout, - )->match( - static fn($process) => $process, - static fn() => null, - ); - - $this->assertNull($process->wait()->match( - static fn($message) => $message, - static fn() => null, - )); - } - - public function testClose() - { - $timeout = new Timeout(1000); - $sockets = $this->createMock(Sockets::class); - $protocol = $this->createMock(Protocol::class); - $address = Address::of('/tmp/foo'); - $sockets - ->expects($this->once()) - ->method('connectTo') - ->with($address) - ->willReturn(Maybe::just($socket = $this->createMock(Client::class))->map( - static fn($client) => IOClient::of( - static fn() => null, - $client, - ), - )); - $sockets - ->expects($this->once()) - ->method('watch') - ->with($timeout) - ->willReturn(Select::timeoutAfter($timeout)); - $resource = \tmpfile(); - $socket - ->expects($this->any()) - ->method('resource') - ->willReturn($resource); - $protocol - ->expects($this->exactly(2)) - ->method('decode') - ->with($socket) - ->will($this->onConsecutiveCalls( - Maybe::just(new ConnectionStart), - Maybe::just(new ConnectionCloseOk), - )); - $protocol - ->expects($matcher = $this->exactly(2)) - ->method('encode') - ->willReturnCallback(function($message) use ($matcher) { - match ($matcher->numberOfInvocations()) { - 1 => $this->assertEquals(new ConnectionStartOk, $message), - 2 => $this->assertEquals(new ConnectionClose, $message), - }; - - return match ($matcher->numberOfInvocations()) { - 1 => Str::of('start-ok'), - 2 => Str::of('close'), - }; - }); - $socket - ->expects($matcher = $this->exactly(2)) - ->method('write') - ->willReturnCallback(function($message) use ($matcher, $socket) { - match ($matcher->numberOfInvocations()) { - 1 => $this->assertEquals(Str::of('start-ok'), $message), - 2 => $this->assertEquals(Str::of('close'), $message), - }; - - return Either::right($socket); - }); - $socket - ->method('close') - ->willReturn(Either::right(new SideEffect)); - - $process = Unix::of( - $sockets, - $protocol, - $this->createMock(Clock::class), - $address, - $name = Name::of('foo'), - $timeout, - )->match( - static fn($process) => $process, - static fn() => null, - ); - - $this->assertInstanceOf(SideEffect::class, $process->close()->match( - static fn($sideEffect) => $sideEffect, - static fn() => null, - )); - $this->assertTrue($process->closed()); - } - - public function testThrowWhenInvalidCloseConfirmation() - { - $timeout = new Timeout(1000); - $sockets = $this->createMock(Sockets::class); - $protocol = $this->createMock(Protocol::class); - $address = Address::of('/tmp/foo'); - $sockets - ->expects($this->once()) - ->method('connectTo') - ->with($address) - ->willReturn(Maybe::just($socket = $this->createMock(Client::class))->map( - static fn($client) => IOClient::of( - static fn() => null, - $client, - ), - )); - $sockets - ->expects($this->once()) - ->method('watch') - ->with($timeout) - ->willReturn(Select::timeoutAfter($timeout)); - $resource = \tmpfile(); - $socket - ->expects($this->any()) - ->method('resource') - ->willReturn($resource); - $protocol - ->expects($this->exactly(2)) - ->method('decode') - ->with($socket) - ->will($this->onConsecutiveCalls( - Maybe::just(new ConnectionStart), - Maybe::just($this->createMock(Message::class)), - )); - $protocol - ->expects($matcher = $this->exactly(2)) - ->method('encode') - ->willReturnCallback(function($message) use ($matcher) { - match ($matcher->numberOfInvocations()) { - 1 => $this->assertEquals(new ConnectionStartOk, $message), - 2 => $this->assertEquals(new ConnectionClose, $message), - }; - - return match ($matcher->numberOfInvocations()) { - 1 => Str::of('start-ok'), - 2 => Str::of('close'), - }; - }); - $socket - ->expects($matcher = $this->exactly(2)) - ->method('write') - ->willReturnCallback(function($message) use ($matcher, $socket) { - match ($matcher->numberOfInvocations()) { - 1 => $this->assertEquals(Str::of('start-ok'), $message), - 2 => $this->assertEquals(Str::of('close'), $message), - }; - - return Either::right($socket); - }); - $socket - ->method('close') - ->willReturn(Either::right(new SideEffect)); - - $process = Unix::of( - $sockets, - $protocol, - $this->createMock(Clock::class), - $address, - $name = Name::of('foo'), - $timeout, - )->match( - static fn($process) => $process, - static fn() => null, - ); - - $this->assertNull($process->close()->match( - static fn($sideEffect) => $sideEffect, - static fn() => null, - )); - $this->assertTrue($process->closed()); - } - - public function testStopWaitingAfterTimeout() - { - $os = Factory::build(); - @\unlink($os->status()->tmp()->toString().'/innmind/ipc/server.sock'); - $processes = $os->control()->processes(); - $server = $processes->execute( - Command::foreground('php') - ->withArgument('fixtures/eternal-server.php') - ->withEnvironment('TMPDIR', $os->status()->tmp()->toString()) - ->withEnvironment('PATH', $_SERVER['PATH']), - ); - - $started = $server - ->output() - ->chunks() - ->find(static fn($pair) => $pair[0]->startsWith('Server ready!')); - - $this->assertTrue($started->match( - static fn() => true, - static fn() => false, - )); - - \sleep(1); - - $process = Unix::of( - $os->sockets(), - new Protocol\Binary, - $os->clock(), - Address::of($os->status()->tmp()->toString().'/innmind/ipc/server'), - $name = Name::of('server'), - new Timeout(1000), - )->match( - static fn($process) => $process, - static fn() => null, - ); - - $this->assertNotNull($process); - - try { - $this->assertNull($process->wait(new Timeout(100))->match( - static fn($message) => $message, - static fn() => null, - )); - } finally { - $processes->kill( - $server->pid()->match( - static fn($pid) => $pid, - static fn() => null, - ), - Signal::terminate, - ); - } - } - - public function testReturnNothingWhenSocketErrorWhenConnecting() - { - $sockets = $this->createMock(Sockets::class); - $protocol = $this->createMock(Protocol::class); - $address = Address::of('/tmp/foo'); - $sockets - ->expects($this->once()) - ->method('connectTo') - ->with($address) - ->willReturn(Maybe::nothing()); - - $process = Unix::of( - $sockets, - $protocol, - $this->createMock(Clock::class), - $address, - Name::of('foo'), - new Timeout(1000), - )->match( - static fn($process) => $process, - static fn() => null, - ); - - $this->assertNull($process); - } -} diff --git a/tests/Protocol/BinaryTest.php b/tests/Protocol/BinaryTest.php deleted file mode 100644 index a6227c3..0000000 --- a/tests/Protocol/BinaryTest.php +++ /dev/null @@ -1,91 +0,0 @@ -assertInstanceOf(Protocol::class, new Binary); - } - - public function testEncode() - { - $protocol = new Binary; - $message = new Message\Generic( - MediaType::of('application/json'), - Str::of('{"foo":"bar🙏"}'), - ); - - $binary = $protocol->encode($message); - - $this->assertInstanceOf(Str::class, $binary); - $this->assertSame( - \pack('n', 16).'application/json'.\pack('N', 17).'{"foo":"bar🙏"}'.\pack('C', 0xCE), - $binary->toString(), - ); - $this->assertSame('ASCII', $binary->encoding()->toString()); - } - - public function testDecode() - { - $protocol = new Binary; - $stream = Stream::ofContent(\pack('n', 16).'application/json'.\pack('N', 17).'{"foo":"bar🙏"}'.\pack('C', 0xCE).'baz'); - - $message = $protocol->decode($stream)->match( - static fn($message) => $message, - static fn() => null, - ); - - $this->assertInstanceOf(Message::class, $message); - $this->assertSame('application/json', $message->mediaType()->toString()); - $this->assertSame('{"foo":"bar🙏"}', $message->content()->toString()); - $this->assertSame('baz', $stream->read(3)->match( - static fn($chunk) => $chunk->toString(), - static fn() => null, - )); // to verify the protocol didn't read that part - } - - public function testReturnNothingWhenEmptyStream() - { - $protocol = new Binary; - - $this->assertNull($protocol->decode(Stream::ofContent(''))->match( - static fn($message) => $message, - static fn() => null, - )); - } - - public function testReturnNothingWhenMessageContentNotOfExceptedSize() - { - $protocol = new Binary; - $stream = Stream::ofContent(\pack('n', 16).'application/json'.\pack('N', 17).'{"foo":"bar🙏'.\pack('C', 0xCE).'baz'); - - $this->assertNull($protocol->decode($stream)->match( - static fn($message) => $message, - static fn() => null, - )); - } - - public function testReturnNothingWhenMessageNotEndedWithSpecialCharacter() - { - $protocol = new Binary; - $stream = Stream::ofContent(\pack('n', 16).'application/json'.\pack('N', 17).'{"foo":"bar🙏"}baz'); - - $this->assertNull($protocol->decode($stream)->match( - static fn($message) => $message, - static fn() => null, - )); - } -} diff --git a/tests/Server/ClientLifecycleTest.php b/tests/Server/ClientLifecycleTest.php deleted file mode 100644 index d6fc79a..0000000 --- a/tests/Server/ClientLifecycleTest.php +++ /dev/null @@ -1,776 +0,0 @@ -createMock(Client::class); - $client - ->expects($this->once()) - ->method('send') - ->with(new ConnectionStart) - ->willReturn(Maybe::just($client)); - $clock = $this->createMock(Clock::class); - $heartbeat = new Timeout(1000); - - $lifecycle = ClientLifecycle::of($client, $clock, $heartbeat)->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ); - - $this->assertInstanceOf(ClientLifecycle::class, $lifecycle); - } - - public function testDoNotSendHeartbeatWhenFewerThanHeartbeatPeriod() - { - $client = $this->createMock(Client::class); - $client - ->expects($this->once()) - ->method('send') - ->with(new ConnectionStart) - ->willReturn(Maybe::just($client)); - $clock = $this->createMock(Clock::class); - $heartbeat = new Timeout(1000); - $clock - ->expects($this->exactly(2)) - ->method('now') - ->will($this->onConsecutiveCalls( - $start = $this->createMock(PointInTime::class), - $now = $this->createMock(PointInTime::class), - )); - $now - ->expects($this->once()) - ->method('elapsedSince') - ->with($start) - ->willReturn($period = $this->createMock(ElapsedPeriod::class)); - $period - ->expects($this->once()) - ->method('longerThan') - ->with($heartbeat) - ->willReturn(false); - - $lifecycle = ClientLifecycle::of($client, $clock, $heartbeat)->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ); - - $lifecycle = $lifecycle->heartbeat(); - $this->assertInstanceOf(ClientLifecycle::class, $lifecycle); - } - - public function testSilenceFailureToHeartbeatClient() - { - $client = $this->createMock(Client::class); - $client - ->expects($matcher = $this->exactly(2)) - ->method('send') - ->willReturnCallback(function($message) use ($matcher, $client) { - match ($matcher->numberOfInvocations()) { - 1 => $this->assertEquals(new ConnectionStart, $message), - 2 => $this->assertEquals(new Heartbeat, $message), - }; - - return match ($matcher->numberOfInvocations()) { - 1 => Maybe::just($client), - 2 => Maybe::nothing(), - }; - }); - $clock = $this->createMock(Clock::class); - $heartbeat = new Timeout(1000); - $clock - ->expects($this->exactly(2)) - ->method('now') - ->will($this->onConsecutiveCalls( - $start = $this->createMock(PointInTime::class), - $now = $this->createMock(PointInTime::class), - )); - $now - ->expects($this->once()) - ->method('elapsedSince') - ->with($start) - ->willReturn($period = $this->createMock(ElapsedPeriod::class)); - $period - ->expects($this->once()) - ->method('longerThan') - ->with($heartbeat) - ->willReturn(true); - - $lifecycle = ClientLifecycle::of($client, $clock, $heartbeat)->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ); - - $lifecycle = $lifecycle->heartbeat(); - $this->assertInstanceOf(ClientLifecycle::class, $lifecycle); - } - - public function testSendHeartbeatWhenLongerThanHeartbeatPeriod() - { - $client = $this->createMock(Client::class); - $client - ->expects($matcher = $this->exactly(2)) - ->method('send') - ->willReturnCallback(function($message) use ($matcher, $client) { - match ($matcher->numberOfInvocations()) { - 1 => $this->assertEquals(new ConnectionStart, $message), - 2 => $this->assertEquals(new Heartbeat, $message), - }; - - return Maybe::just($client); - }); - $clock = $this->createMock(Clock::class); - $heartbeat = new Timeout(1000); - $clock - ->expects($this->exactly(2)) - ->method('now') - ->will($this->onConsecutiveCalls( - $start = $this->createMock(PointInTime::class), - $now = $this->createMock(PointInTime::class), - )); - $now - ->expects($this->once()) - ->method('elapsedSince') - ->with($start) - ->willReturn($period = $this->createMock(ElapsedPeriod::class)); - $period - ->expects($this->once()) - ->method('longerThan') - ->with($heartbeat) - ->willReturn(true); - - $lifecycle = ClientLifecycle::of($client, $clock, $heartbeat)->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ); - - $lifecycle = $lifecycle->heartbeat(); - $this->assertInstanceOf(ClientLifecycle::class, $lifecycle); - } - - public function testConsiderGarbageWhenReadingButNoMessage() - { - $client = $this->createMock(Client::class); - $client - ->expects($this->once()) - ->method('send') - ->with(new ConnectionStart) - ->willReturn(Maybe::just($client)); - $client - ->expects($this->once()) - ->method('read') - ->willReturn(Maybe::nothing()); - $clock = $this->createMock(Clock::class); - $heartbeat = new Timeout(1000); - - $lifecycle = ClientLifecycle::of($client, $clock, $heartbeat)->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ); - $called = false; - - [$lifecycle] = $lifecycle->notify(static function() use (&$called) { - $called = true; - }, null)->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ); - $this->assertNull($lifecycle); - $this->assertFalse($called); - } - - public function testDoNotNotifyWhenNotReceivedMesageOk() - { - $client = $this->createMock(Client::class); - $client - ->expects($this->once()) - ->method('send') - ->with(new ConnectionStart) - ->willReturn(Maybe::just($client)); - $client - ->expects($this->once()) - ->method('read') - ->willReturn(Maybe::just([$client, $this->createMock(Message::class)])); - $clock = $this->createMock(Clock::class); - $heartbeat = new Timeout(1000); - - $lifecycle = ClientLifecycle::of($client, $clock, $heartbeat)->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ); - $called = false; - - [$lifecycle2] = $lifecycle->notify(static function() use (&$called) { - $called = true; - }, null)->match( - static fn($either) => $either->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ), - static fn() => null, - ); - $this->assertEquals($lifecycle2, $lifecycle); - $this->assertFalse($called); - } - - public function testDoNotNotifyWhenHeartbeatMessage() - { - $client = $this->createMock(Client::class); - $client - ->expects($this->once()) - ->method('send') - ->with(new ConnectionStart) - ->willReturn(Maybe::just($client)); - $client - ->expects($this->once()) - ->method('read') - ->willReturn(Maybe::just([$client, new Heartbeat])); - $clock = $this->createMock(Clock::class); - $heartbeat = new Timeout(1000); - - $lifecycle = ClientLifecycle::of($client, $clock, $heartbeat)->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ); - $called = false; - - [$lifecycle2] = $lifecycle->notify(static function() use (&$called) { - $called = true; - }, null)->match( - static fn($either) => $either->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ), - static fn() => null, - ); - $this->assertEquals($lifecycle2, $lifecycle); - $this->assertFalse($called); - } - - public function testConfirmConnectionClose() - { - $client = $this->createMock(Client::class); - $client - ->expects($matcher = $this->exactly(2)) - ->method('send') - ->willReturnCallback(function($message) use ($matcher, $client) { - match ($matcher->numberOfInvocations()) { - 1 => $this->assertEquals(new ConnectionStart, $message), - 2 => $this->assertEquals(new ConnectionCloseOk, $message), - }; - - return Maybe::just($client); - }); - $client - ->expects($this->exactly(2)) - ->method('read') - ->will($this->onConsecutiveCalls( - Maybe::just([$client, new ConnectionStartOk]), - Maybe::just([$client, new ConnectionClose]), - )); - $client - ->expects($this->once()) - ->method('close') - ->willReturn(Maybe::just(new SideEffect)); - $clock = $this->createMock(Clock::class); - $heartbeat = new Timeout(1000); - - $lifecycle = ClientLifecycle::of($client, $clock, $heartbeat)->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ); - $called = false; - $callback = static function($_, $continuation) use (&$called) { - return $continuation->continue(true); - }; - - [$lifecycle] = $lifecycle->notify($callback, null)->match( - static fn($either) => $either->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ), - static fn() => null, - ); // connection start - $this->assertInstanceOf(ClientLifecycle::class, $lifecycle); - [$lifecycle] = $lifecycle->notify($callback, null)->match( - static fn($either) => $either, - static fn() => null, - ); // connection close - $this->assertNull($lifecycle); - $this->assertFalse($called); - } - - public function testNotify() - { - $client = $this->createMock(Client::class); - $client - ->expects($matcher = $this->exactly(3)) - ->method('send') - ->willReturnCallback(function($message) use ($matcher, $client) { - match ($matcher->numberOfInvocations()) { - 1 => $this->assertEquals(new ConnectionStart, $message), - 2 => $this->assertEquals(new MessageReceived, $message), - 3 => $this->assertEquals(new MessageReceived, $message), - }; - - return Maybe::just($client); - }); - $client - ->expects($this->exactly(3)) - ->method('read') - ->will($this->onConsecutiveCalls( - Maybe::just([$client, new ConnectionStartOk]), - Maybe::just([$client, $message = $this->createMock(Message::class)]), - Maybe::just([$client, $message]), - )); - $clock = $this->createMock(Clock::class); - $heartbeat = new Timeout(1000); - - $lifecycle = ClientLifecycle::of($client, $clock, $heartbeat)->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ); - $called = 0; - $callback = function($a, $b) use (&$called, $message) { - ++$called; - $this->assertSame($message, $a); - $this->assertInstanceOf(Continuation::class, $b); - - return $b; - }; - - [$lifecycle] = $lifecycle->notify($callback, null)->match( - static fn($either) => $either->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ), - static fn() => null, - ); // connection start - $this->assertInstanceOf(ClientLifecycle::class, $lifecycle); - [$lifecycle] = $lifecycle->notify($callback, null)->match( - static fn($either) => $either->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ), - static fn() => null, - ); // message 1 - $this->assertInstanceOf(ClientLifecycle::class, $lifecycle); - [$lifecycle] = $lifecycle->notify($callback, null)->match( - static fn($either) => $either->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ), - static fn() => null, - ); // message 2 - $this->assertInstanceOf(ClientLifecycle::class, $lifecycle); - $this->assertSame(2, $called); - } - - public function testDoNotNotifyWhenPendingCloseOkButNoConfirmation() - { - $client = $this->createMock(Client::class); - $client - ->expects($matcher = $this->exactly(3)) - ->method('send') - ->willReturnCallback(function($message) use ($matcher, $client) { - match ($matcher->numberOfInvocations()) { - 1 => $this->assertEquals(new ConnectionStart, $message), - 2 => $this->assertEquals(new MessageReceived, $message), - 3 => $this->assertEquals(new ConnectionClose, $message), - }; - - return Maybe::just($client); - }); - $client - ->expects($this->exactly(3)) - ->method('read') - ->will($this->onConsecutiveCalls( - Maybe::just([$client, new ConnectionStartOk]), - Maybe::just([$client, $this->createMock(Message::class)]), - Maybe::just([$client, $this->createMock(Message::class)]), - )); - $clock = $this->createMock(Clock::class); - $heartbeat = new Timeout(1000); - - $lifecycle = ClientLifecycle::of($client, $clock, $heartbeat)->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ); - $callback = static function($_, $continuation, $called) { - return $continuation->close(++$called); - }; - - [$lifecycle, $called] = $lifecycle->notify($callback, 0)->match( - static fn($either) => $either->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ), - static fn() => null, - ); // connection start - $this->assertInstanceOf(ClientLifecycle::class, $lifecycle); - [$lifecycle, $called] = $lifecycle->notify($callback, $called)->match( - static fn($either) => $either->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ), - static fn() => null, - ); // message 1 - $this->assertInstanceOf(ClientLifecycle::class, $lifecycle); - [$lifecycle, $called] = $lifecycle->notify($callback, $called)->match( - static fn($either) => $either->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ), - static fn() => null, - ); // message 2 - $this->assertInstanceOf(ClientLifecycle::class, $lifecycle); - $this->assertSame(1, $called); - } - - public function testCloseConfirmation() - { - $client = $this->createMock(Client::class); - $client - ->expects($this->once()) - ->method('close') - ->willReturn(Maybe::just(new SideEffect)); - $client - ->expects($matcher = $this->exactly(3)) - ->method('send') - ->willReturnCallback(function($message) use ($matcher, $client) { - match ($matcher->numberOfInvocations()) { - 1 => $this->assertEquals(new ConnectionStart, $message), - 2 => $this->assertEquals(new MessageReceived, $message), - 3 => $this->assertEquals(new ConnectionClose, $message), - }; - - return Maybe::just($client); - }); - $client - ->expects($this->exactly(3)) - ->method('read') - ->will($this->onConsecutiveCalls( - Maybe::just([$client, new ConnectionStartOk]), - Maybe::just([$client, $this->createMock(Message::class)]), - Maybe::just([$client, new ConnectionCloseOk]), - )); - $clock = $this->createMock(Clock::class); - $heartbeat = new Timeout(1000); - - $lifecycle = ClientLifecycle::of($client, $clock, $heartbeat)->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ); - $callback = static function($_, $continuation, $called) { - return $continuation->close(++$called); - }; - - [$lifecycle, $called] = $lifecycle->notify($callback, 0)->match( - static fn($either) => $either->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ), - static fn() => null, - ); // connection start - $this->assertInstanceOf(ClientLifecycle::class, $lifecycle); - [$lifecycle, $called] = $lifecycle->notify($callback, $called)->match( - static fn($either) => $either->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ), - static fn() => null, - ); // message 1 - $this->assertInstanceOf(ClientLifecycle::class, $lifecycle); - [$lifecycle] = $lifecycle->notify($callback, $called)->match( - static fn($either) => $either, - static fn() => null, - ); // connection close ok - $this->assertNull($lifecycle); - $this->assertSame(1, $called); - } - - public function testCloseConfirmationEvenWhenError() - { - $client = $this->createMock(Client::class); - $client - ->expects($matcher = $this->exactly(3)) - ->method('send') - ->willReturnCallback(function($message) use ($matcher, $client) { - match ($matcher->numberOfInvocations()) { - 1 => $this->assertEquals(new ConnectionStart, $message), - 2 => $this->assertEquals(new MessageReceived, $message), - 3 => $this->assertEquals(new ConnectionClose, $message), - }; - - return Maybe::just($client); - }); - $client - ->expects($this->exactly(3)) - ->method('read') - ->will($this->onConsecutiveCalls( - Maybe::just([$client, new ConnectionStartOk]), - Maybe::just([$client, $this->createMock(Message::class)]), - Maybe::just([$client, new ConnectionCloseOk]), - )); - $client - ->expects($this->once()) - ->method('close') - ->willReturn(Maybe::nothing()); - $clock = $this->createMock(Clock::class); - $heartbeat = new Timeout(1000); - - $lifecycle = ClientLifecycle::of($client, $clock, $heartbeat)->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ); - $callback = static function($_, $continuation, $called) { - return $continuation->close(++$called); - }; - - [$lifecycle, $called] = $lifecycle->notify($callback, 0)->match( - static fn($either) => $either->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ), - static fn() => null, - ); // connection start - $this->assertInstanceOf(ClientLifecycle::class, $lifecycle); - [$lifecycle, $called] = $lifecycle->notify($callback, $called)->match( - static fn($either) => $either->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ), - static fn() => null, - ); // message 1 - $this->assertInstanceOf(ClientLifecycle::class, $lifecycle); - [$lifecycle] = $lifecycle->notify($callback, $called)->match( - static fn($either) => $either, - static fn() => null, - ); // connection close ok - $this->assertNull($lifecycle); - $this->assertSame(1, $called); - } - - /** - * @dataProvider protocolMessages - */ - public function testNeverNotifyProtocolMessages($message) - { - $client = $this->createMock(Client::class); - $client - ->expects($matcher = $this->exactly(2)) - ->method('send') - ->willReturnCallback(function($message) use ($matcher, $client) { - match ($matcher->numberOfInvocations()) { - 1 => $this->assertEquals(new ConnectionStart, $message), - 2 => $this->assertEquals(new MessageReceived, $message), - }; - - return Maybe::just($client); - }); - $client - ->expects($this->exactly(3)) - ->method('read') - ->will($this->onConsecutiveCalls( - Maybe::just([$client, new ConnectionStartOk]), - Maybe::just([$client, $this->createMock(Message::class)]), - Maybe::just([$client, $message]), - )); - $clock = $this->createMock(Clock::class); - $heartbeat = new Timeout(1000); - - $lifecycle = ClientLifecycle::of($client, $clock, $heartbeat)->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ); - $callback = static function($a, $b, $called) { - return $b->continue(++$called); - }; - - [$lifecycle, $called] = $lifecycle->notify($callback, 0)->match( - static fn($either) => $either->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ), - static fn() => null, - ); // connection start - $this->assertInstanceOf(ClientLifecycle::class, $lifecycle); - [$lifecycle, $called] = $lifecycle->notify($callback, $called)->match( - static fn($either) => $either->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ), - static fn() => null, - ); // message - $this->assertInstanceOf(ClientLifecycle::class, $lifecycle); - [$lifecycle, $called] = $lifecycle->notify($callback, $called)->match( - static fn($either) => $either->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ), - static fn() => null, - ); // protocol message - $this->assertInstanceOf(ClientLifecycle::class, $lifecycle); - $this->assertSame(1, $called); - } - - public function testShutdown() - { - $client = $this->createMock(Client::class); - $client - ->expects($this->once()) - ->method('close') - ->willReturn(Maybe::just(new SideEffect)); - $client - ->expects($matcher = $this->exactly(2)) - ->method('send') - ->willReturnCallback(function($message) use ($matcher, $client) { - match ($matcher->numberOfInvocations()) { - 1 => $this->assertEquals(new ConnectionStart, $message), - 2 => $this->assertEquals(new ConnectionClose, $message), - }; - - return Maybe::just($client); - }); - $client - ->expects($this->once()) - ->method('read') - ->willReturn(Maybe::just([$client, new ConnectionCloseOk])); - $clock = $this->createMock(Clock::class); - $heartbeat = new Timeout(1000); - - $lifecycle = ClientLifecycle::of($client, $clock, $heartbeat)->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ); - $called = false; - - $lifecycle = $lifecycle->shutdown()->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ); - $this->assertInstanceOf(ClientLifecycle::class, $lifecycle); - $lifecycle = $lifecycle->notify(static function() use (&$called) { - $called = true; - }, null)->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ); - $this->assertFalse($called); - $this->assertNull($lifecycle); - } - - public function testShutdownEvenWhenErrorWhenClosingConnection() - { - $client = $this->createMock(Client::class); - $client - ->expects($this->once()) - ->method('close') - ->willReturn(Maybe::nothing()); - $client - ->expects($matcher = $this->exactly(2)) - ->method('send') - ->willReturnCallback(function($message) use ($matcher, $client) { - match ($matcher->numberOfInvocations()) { - 1 => $this->assertEquals(new ConnectionStart, $message), - 2 => $this->assertEquals(new ConnectionClose, $message), - }; - - return Maybe::just($client); - }); - $client - ->expects($this->once()) - ->method('read') - ->willReturn(Maybe::just([$client, new ConnectionCloseOk])); - $clock = $this->createMock(Clock::class); - $heartbeat = new Timeout(1000); - - $lifecycle = ClientLifecycle::of($client, $clock, $heartbeat)->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ); - $called = false; - - $lifecycle = $lifecycle->shutdown()->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ); - $this->assertInstanceOf(ClientLifecycle::class, $lifecycle); - $lifecycle = $lifecycle->notify(static function() use (&$called) { - $called = true; - }, null)->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ); - $this->assertFalse($called); - $this->assertNull($lifecycle); - } - - public function testConsiderToBeGarbageCollectedWhenFailToClose() - { - $client = $this->createMock(Client::class); - $client - ->expects($matcher = $this->exactly(2)) - ->method('send') - ->willReturnCallback(function($message) use ($matcher, $client) { - match ($matcher->numberOfInvocations()) { - 1 => $this->assertEquals(new ConnectionStart, $message), - 2 => $this->assertEquals(new ConnectionClose, $message), - }; - - return match ($matcher->numberOfInvocations()) { - 1 => Maybe::just($client), - 2 => Maybe::nothing(), - }; - }); - $clock = $this->createMock(Clock::class); - $heartbeat = new Timeout(1000); - - $lifecycle = ClientLifecycle::of($client, $clock, $heartbeat)->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - ); - - $this->assertNull($lifecycle->shutdown()->match( - static fn($lifecycle) => $lifecycle, - static fn() => null, - )); - } - - public static function protocolMessages(): array - { - return [ - [new ConnectionStart], - [new ConnectionStartOk], - [new ConnectionCloseOk], - [new Heartbeat], - [new MessageReceived], - ]; - } -} diff --git a/tests/Server/UnixTest.php b/tests/Server/UnixTest.php deleted file mode 100644 index 53497e1..0000000 --- a/tests/Server/UnixTest.php +++ /dev/null @@ -1,362 +0,0 @@ -assertInstanceOf( - Server::class, - new Unix( - $this->createMock(Sockets::class), - $this->createMock(Protocol::class), - $this->createMock(Clock::class), - $this->createMock(Signals::class), - Address::of('/tmp/foo.sock'), - new Timeout(1000), - ), - ); - } - - public function testFailWhenCantOpenTheServer() - { - $receive = new Unix( - $sockets = $this->createMock(Sockets::class), - $this->createMock(Protocol::class), - $this->createMock(Clock::class), - $this->createMock(Signals::class), - Address::of('/tmp/foo.sock'), - new Timeout(1000), - ); - $sockets - ->expects($this->once()) - ->method('open') - ->willReturn(Maybe::nothing()); - - $e = $receive(null, static function($_, $continuation) { - return $continuation; - })->match( - static fn() => null, - static fn($e) => $e, - ); - - $this->assertInstanceOf(Server\UnableToStart::class, $e); - } - - public function testStopWhenNoActivityInGivenPeriod() - { - $os = Factory::build(); - @\unlink($os->status()->tmp()->toString().'/innmind/ipc/server.sock'); - - $listen = new Unix( - $os->sockets(), - new Protocol\Binary, - $os->clock(), - $os->process()->signals(), - Address::of($os->status()->tmp()->toString().'/innmind/ipc/server'), - new Timeout(100), - new Timeout(1000), - ); - - $this->assertNull($listen(null, static function($_, $continuation) { - return $continuation; - })->match( - static fn() => null, - static fn($e) => $e, - )); - } - - public function testInstallSignalsHandlerOnlyWhenStartingTheServer() - { - $signals = $this->createMock(Signals::class); - - $server = new Unix( - $sockets = $this->createMock(Sockets::class), - $this->createMock(Protocol::class), - $this->createMock(Clock::class), - $signals, - $address = Address::of('/tmp/foo.sock'), - $heartbeat = new Timeout(10), - $this->createMock(ElapsedPeriod::class), - ); - $sockets - ->expects($this->any()) - ->method('open') - ->willReturn(ServerSocket\Unix::recoverable($address)->map( - static fn($server) => IOServer::of( - static fn() => null, - $server, - ), - )); - $sockets - ->expects($this->any()) - ->method('watch') - ->with($heartbeat) - ->willReturn($watch = $this->createMock(Watch::class)); - $watch - ->expects($this->any()) - ->method('forRead') - ->will($this->returnSelf()); - $watch - ->expects($this->any()) - ->method('__invoke') - ->will($this->throwException($expected = new \Exception)); - - $signals - ->expects($matcher = $this->exactly(12)) - ->method('listen') - ->willReturnCallback(function($signal, $listen) use ($matcher) { - match ($matcher->numberOfInvocations()) { - 1 => $this->assertSame(Signal::hangup, $signal), - 2 => $this->assertSame(Signal::interrupt, $signal), - 3 => $this->assertSame(Signal::abort, $signal), - 4 => $this->assertSame(Signal::terminate, $signal), - 5 => $this->assertSame(Signal::terminalStop, $signal), - 6 => $this->assertSame(Signal::alarm, $signal), - 7 => $this->assertSame(Signal::hangup, $signal), - 8 => $this->assertSame(Signal::interrupt, $signal), - 9 => $this->assertSame(Signal::abort, $signal), - 10 => $this->assertSame(Signal::terminate, $signal), - 11 => $this->assertSame(Signal::terminalStop, $signal), - 12 => $this->assertSame(Signal::alarm, $signal), - }; - - $listen(); - }); - - try { - $server(null, static function($_, $continuation) { - return $continuation->stop(null); - }); - } catch (\Exception $e) { - $this->assertSame($expected, $e); - } - - try { - // check signals are not registered twice - $server(null, static function($_, $continuation) { - return $continuation->stop(null); - }); - } catch (\Exception $e) { - $this->assertSame($expected, $e); - } - } - - public function testShutdownProcess() - { - $os = Factory::build(); - @\unlink($os->status()->tmp()->toString().'/innmind/ipc/server.sock'); - $processes = $os->control()->processes(); - $server = $processes->execute( - Command::foreground('php') - ->withArgument('fixtures/long-client.php') - ->withEnvironment('TMPDIR', $os->status()->tmp()->toString()) - ->withEnvironment('PATH', $_SERVER['PATH']), - ); - - $listen = new Unix( - $os->sockets(), - new Protocol\Binary, - $os->clock(), - $os->process()->signals(), - Address::of($os->status()->tmp()->toString().'/innmind/ipc/server'), - new Timeout(100), - new Timeout(10000), - ); - - $this->assertNull($listen(null, static function($_, $continuation) { - return $continuation->stop(null); - })->match( - static fn() => null, - static fn($e) => $e, - )); - } - - public function testClientClose() - { - $os = Factory::build(); - @\unlink($os->status()->tmp()->toString().'/innmind/ipc/server.sock'); - $processes = $os->control()->processes(); - $client = $processes->execute( - Command::foreground('php') - ->withArgument('fixtures/long-client.php') - ->withEnvironment('TMPDIR', $os->status()->tmp()->toString()) - ->withEnvironment('PATH', $_SERVER['PATH']), - ); - - $listen = new Unix( - $os->sockets(), - new Protocol\Binary, - $os->clock(), - $os->process()->signals(), - Address::of($os->status()->tmp()->toString().'/innmind/ipc/server'), - new Timeout(100), - new Timeout(3000), - ); - - $this->assertNull($listen(null, static function($message, $continuation) { - return $continuation->close(null); - })->match( - static fn() => null, - static fn($e) => $e, - )); - } - - public function testBidirectionalHeartbeat() - { - $os = Factory::build(); - @\unlink($os->status()->tmp()->toString().'/innmind/ipc/server.sock'); - $processes = $os->control()->processes(); - $processes->execute( - Command::foreground('php') - ->withArgument('fixtures/long-client.php') - ->withEnvironment('TMPDIR', $os->status()->tmp()->toString()) - ->withEnvironment('PATH', $_SERVER['PATH']), - ); - - $listen = new Unix( - $os->sockets(), - new Protocol\Binary, - $os->clock(), - $os->process()->signals(), - Address::of($os->status()->tmp()->toString().'/innmind/ipc/server'), - new Timeout(100), - new Timeout(3000), - ); - - $this->assertNull($listen(null, static function($_, $continuation) { - return $continuation; - })->match( - static fn() => null, - static fn($e) => $e, - )); - // only test coverage can show that heartbeat messages are sent - } - - public function testEmergencyShutdown() - { - $os = Factory::build(); - @\unlink($os->status()->tmp()->toString().'/innmind/ipc/server.sock'); - $processes = $os->control()->processes(); - $processes->execute( - Command::foreground('php') - ->withArgument('fixtures/long-client.php') - ->withEnvironment('TMPDIR', $os->status()->tmp()->toString()) - ->withEnvironment('PATH', $_SERVER['PATH']), - ); - - $listen = new Unix( - $os->sockets(), - new Protocol\Binary, - $os->clock(), - $os->process()->signals(), - Address::of($os->status()->tmp()->toString().'/innmind/ipc/server'), - new Timeout(100), - new Timeout(3000), - ); - - $this->expectException(\Exception::class); - - $listen(null, static function() { - throw new \Exception; - }); - // only test coverage can show that show that connections are closed on - // user exception - } - - public function testRespondToClientClose() - { - $os = Factory::build(); - @\unlink($os->status()->tmp()->toString().'/innmind/ipc/server.sock'); - $processes = $os->control()->processes(); - $client = $processes->execute( - Command::foreground('php') - ->withArgument('fixtures/self-closing-client.php') - ->withEnvironment('TMPDIR', $os->status()->tmp()->toString()) - ->withEnvironment('PATH', $_SERVER['PATH']), - ); - - $listen = new Unix( - $os->sockets(), - new Protocol\Binary, - $os->clock(), - $os->process()->signals(), - Address::of($os->status()->tmp()->toString().'/innmind/ipc/server'), - new Timeout(100), - new Timeout(3000), - ); - - $this->assertNull($listen(null, static function($_, $continuation) { - return $continuation; - })->match( - static fn() => null, - static fn($e) => $e, - )); - $client->wait(); - $this->assertSame('', $client->output()->toString()); - } - - public function testCarriedValueIsReturnedWhenStopped() - { - $os = Factory::build(); - @\unlink($os->status()->tmp()->toString().'/innmind/ipc/server.sock'); - $processes = $os->control()->processes(); - $processes->execute( - Command::foreground('php') - ->withArgument('fixtures/long-client-multi-message.php') - ->withEnvironment('TMPDIR', $os->status()->tmp()->toString()) - ->withEnvironment('PATH', $_SERVER['PATH']), - ); - - $listen = new Unix( - $os->sockets(), - new Protocol\Binary, - $os->clock(), - $os->process()->signals(), - Address::of($os->status()->tmp()->toString().'/innmind/ipc/server'), - new Timeout(100), - new Timeout(3000), - ); - - $carry = $listen(0, static function($_, $continuation, $carry) { - if ($carry === 2) { - return $continuation->stop($carry + 1); - } - - return $continuation->continue($carry + 1); - })->match( - static fn($carry) => $carry, - static fn() => null, - ); - - $this->assertSame(3, $carry); - } -} From 64b8682eca839d873bb7f90464226e15e3512e92 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 7 Mar 2026 15:26:12 +0100 Subject: [PATCH 02/65] setup project --- .gitattributes | 1 + .github/workflows/ci.yml | 11 ++++++++++- .github/workflows/extensive.yml | 12 ++++++++++++ .github/workflows/release.yml | 11 +++++++++++ .php-cs-fixer.dist.php | 2 +- CHANGELOG.md | 5 +++++ README.md | 2 +- blackbox.php | 28 ++++++++++++++++++++++++++++ composer.json | 19 ++++--------------- 9 files changed, 73 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/extensive.yml create mode 100644 .github/workflows/release.yml create mode 100644 blackbox.php diff --git a/.gitattributes b/.gitattributes index 3a01b37..b8ac0d9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ /.gitattributes export-ignore /.gitignore export-ignore +/proofs/ export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 766b235..2f3eecb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,14 @@ name: CI -on: [push] +on: [push, pull_request] jobs: + blackbox: + uses: innmind/github-workflows/.github/workflows/black-box-matrix.yml@main + coverage: + uses: innmind/github-workflows/.github/workflows/coverage-matrix.yml@main + secrets: inherit + psalm: + uses: innmind/github-workflows/.github/workflows/psalm-matrix.yml@main + cs: + uses: innmind/github-workflows/.github/workflows/cs.yml@main diff --git a/.github/workflows/extensive.yml b/.github/workflows/extensive.yml new file mode 100644 index 0000000..257f139 --- /dev/null +++ b/.github/workflows/extensive.yml @@ -0,0 +1,12 @@ +name: Extensive CI + +on: + push: + tags: + - '*' + paths: + - '.github/workflows/extensive.yml' + +jobs: + blackbox: + uses: innmind/github-workflows/.github/workflows/extensive.yml@main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b25ad8a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,11 @@ +name: Create release + +on: + push: + tags: + - '*' + +jobs: + release: + uses: innmind/github-workflows/.github/workflows/release.yml@main + secrets: inherit diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 1431d66..021fd34 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -1,6 +1,6 @@ map(static fn($app) => match (\getenv('BLACKBOX_ENV')) { + 'coverage' => $app + ->codeCoverage( + CodeCoverage::of( + __DIR__.'/src/', + __DIR__.'/proofs/', + ) + ->dumpTo('coverage.clover') + ->enableWhen(true), + ) + ->scenariiPerProof(1), + 'extensive' => $app->scenariiPerProof(1_000), + default => $app, + }) + ->tryToProve(Load::everythingIn(__DIR__.'/proofs/')) + ->exit(); diff --git a/composer.json b/composer.json index d8dcde3..cfc652c 100644 --- a/composer.json +++ b/composer.json @@ -15,28 +15,17 @@ "issues": "http://github.com/Innmind/IPC/issues" }, "require": { - "php": "~8.2", - "innmind/immutable": "~4.15|~5.0", - "innmind/operating-system": "~5.0", - "innmind/url": "~4.0", - "innmind/media-type": "~2.0", - "innmind/socket": "~6.0", - "innmind/server-control": "~5.0" + "php": "~8.4", + "innmind/foundation": "~2.1" }, "autoload": { "psr-4": { "Innmind\\IPC\\": "src/" } }, - "autoload-dev": { - "psr-4": { - "Tests\\Innmind\\IPC\\": "tests/" - } - }, "require-dev": { - "phpunit/phpunit": "~10.2", - "innmind/static-analysis": "^1.2.1", - "innmind/black-box": "~5.5", + "innmind/static-analysis": "~1.3", + "innmind/black-box": "~6.5", "innmind/coding-standard": "~2.0" } } From 06b21d82f2d79864cd48cf8648e277f0abc1ebdc Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 7 Mar 2026 16:31:54 +0100 Subject: [PATCH 03/65] lay out the new api --- src/Continuation.php | 27 ++++++++++++++++++++ src/IPC.php | 44 ++++++++++++++++++++++++++++++++ src/Message.php | 29 +++++++++++++++++++++ src/Process.php | 44 ++++++++++++++++++++++++++++++++ src/Process/Name.php | 50 ++++++++++++++++++++++++++++++++++++ src/Server.php | 60 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 254 insertions(+) create mode 100644 src/Continuation.php create mode 100644 src/IPC.php create mode 100644 src/Message.php create mode 100644 src/Process.php create mode 100644 src/Process/Name.php create mode 100644 src/Server.php diff --git a/src/Continuation.php b/src/Continuation.php new file mode 100644 index 0000000..81a41b3 --- /dev/null +++ b/src/Continuation.php @@ -0,0 +1,27 @@ + + */ + public static function new(mixed $carry): self + { + return new self($carry); + } +} diff --git a/src/IPC.php b/src/IPC.php new file mode 100644 index 0000000..7823a95 --- /dev/null +++ b/src/IPC.php @@ -0,0 +1,44 @@ + + */ + public function processes(): Sequence + { + return Sequence::of(); + } + + /** + * @return Attempt + */ + public function connectTo( + Process\Name $name, + ?Period $timeout = null, + ): Attempt { + return Attempt::error(new \Exception); + } + + /** + * @return Server + */ + public function serve(Process\Name $name): Server + { + return Server::of(); + } +} diff --git a/src/Message.php b/src/Message.php new file mode 100644 index 0000000..1edcff9 --- /dev/null +++ b/src/Message.php @@ -0,0 +1,29 @@ + $messages + * + * @return Attempt + */ + public function send(Sequence $messages): Attempt + { + return Attempt::result(SideEffect::identity); + } + + /** + * @return Attempt + */ + public function wait(?Period $timeout = null): Attempt + { + return Attempt::error(new \RuntimeException); + } + + /** + * @return Attempt + */ + public function close(): Attempt + { + return Attempt::result(SideEffect::identity); + } +} diff --git a/src/Process/Name.php b/src/Process/Name.php new file mode 100644 index 0000000..0e54ef9 --- /dev/null +++ b/src/Process/Name.php @@ -0,0 +1,50 @@ +unwrap(); + } + + /** + * @return Attempt + */ + public static function attempt(string $value): Attempt + { + if (!Str::of($value)->matches('~^[a-zA-Z0-9-_]+$~')) { + return Attempt::error(new \DomainException($value)); + } + + /** @psalm-suppress ArgumentTypeCoercion */ + return Attempt::result(new self($value)); + } + + /** + * @return non-empty-string + */ + public function toString(): string + { + return $this->value; + } +} diff --git a/src/Server.php b/src/Server.php new file mode 100644 index 0000000..52a344f --- /dev/null +++ b/src/Server.php @@ -0,0 +1,60 @@ + + */ + public static function of(): self + { + return new self(SideEffect::identity); + } + + /** + * @psalm-mutation-free + * @template U + * + * @param U $carry + * + * @return self + */ + public function sink(mixed $carry): self + { + return new self($carry); + } + + // todo differentiate a reducer for the server loop (aka the scheduler sink) + // and reducer that will operate on each connection + // the server loop must transform the initial carry as an initial carry + // dedicated for the connection reducer + // and there should be a way to fold the returned carry from all connections + // to a single value that will in the end be returned by the server + + /** + * @param callable(Message, Continuation, T): Continuation $listen + * + * @return Attempt + */ + public function with(callable $listen): Attempt + { + return Attempt::result($this->carry); + } +} From 55428adc1dc59016d38dbdd02d58c651f120ba99 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 7 Mar 2026 16:33:14 +0100 Subject: [PATCH 04/65] add dummy test --- proofs/proof.php | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 proofs/proof.php diff --git a/proofs/proof.php b/proofs/proof.php new file mode 100644 index 0000000..187082d --- /dev/null +++ b/proofs/proof.php @@ -0,0 +1,9 @@ + $assert->true(true), + ); +}; From fa819e40048bc3f049429b95d3e518d61ed85ec0 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 8 Mar 2026 11:08:39 +0100 Subject: [PATCH 05/65] implement protocol messages --- src/Message.php | 67 ++++++++++++++++++++++++++++++++-- src/Message/Generic.php | 28 ++++++++++++++ src/Message/Implementation.php | 13 +++++++ src/Message/Protocol.php | 42 +++++++++++++++++++++ 4 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 src/Message/Generic.php create mode 100644 src/Message/Implementation.php create mode 100644 src/Message/Protocol.php diff --git a/src/Message.php b/src/Message.php index 1edcff9..7efa9d7 100644 --- a/src/Message.php +++ b/src/Message.php @@ -3,27 +3,86 @@ namespace Innmind\IPC; +use Innmind\IPC\Message\{ + Implementation, + Protocol, + Generic, +}; use Innmind\MediaType\MediaType; use Innmind\Immutable\Str; final class Message { - private function __construct() + private function __construct( + private Implementation $implementation, + ) { + } + + public static function of(MediaType $mediaType, Str $content): self + { + return new self(new Generic($mediaType, $content)); + } + + public static function connectionStart(): self + { + return new self(Protocol::connectionStart); + } + + public static function connectionStartOk(): self + { + return new self(Protocol::connectionStartOk); + } + + public static function connectionClose(): self + { + return new self(Protocol::connectionClose); + } + + public static function connectionCloseOk(): self + { + return new self(Protocol::connectionCloseOk); + } + + public static function heartbeat(): self + { + return new self(Protocol::heartbeat); + } + + public static function ack(): self { + return new self(Protocol::ack); } public function mediaType(): MediaType { - return MediaType::null(); + return $this->implementation->mediaType(); } public function content(): Str { - return Str::of(''); + return $this->implementation->content(); } public function equals(self $message): bool { - return false; + $self = $this->implementation; + $other = $message->implementation; + + if ($self instanceof Protocol && $other instanceof Protocol) { + return $self === $other; + } + + if ($other instanceof Protocol) { + // This is to avoid reading the message content when checking if the + // message is a protocol one. If it's a protocol one $self must be + // correctly decoded as Message\Protocol by the protocol decoder. + return false; + } + + if ($self->mediaType()->toString() !== $other->mediaType()->toString()) { + return false; + } + + return $self->content()->equals($other->content()); } } diff --git a/src/Message/Generic.php b/src/Message/Generic.php new file mode 100644 index 0000000..5056eb9 --- /dev/null +++ b/src/Message/Generic.php @@ -0,0 +1,28 @@ +mediaType; + } + + #[\Override] + public function content(): Str + { + return $this->content; + } +} diff --git a/src/Message/Implementation.php b/src/Message/Implementation.php new file mode 100644 index 0000000..2fdd557 --- /dev/null +++ b/src/Message/Implementation.php @@ -0,0 +1,13 @@ + 'innmind/ipc:connection.start', + self::connectionStartOk => 'innmind/ipc:connection.start-ok', + self::connectionClose => 'innmind/ipc:connection.close', + self::connectionCloseOk => 'innmind/ipc:connection.close-ok', + self::heartbeat => 'innmind/ipc:heartbeat', + self::ack => 'innmind/ipc:ack', + }); + } +} From d42a257f2a79a63ecc16022d29693055f767efdd Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 8 Mar 2026 11:34:29 +0100 Subject: [PATCH 06/65] add protocol --- src/Message/Protocol.php | 24 ++++++++ src/Protocol.php | 129 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 src/Protocol.php diff --git a/src/Message/Protocol.php b/src/Message/Protocol.php index 0413b6e..23c7a1f 100644 --- a/src/Message/Protocol.php +++ b/src/Message/Protocol.php @@ -3,6 +3,7 @@ namespace Innmind\IPC\Message; +use Innmind\IPC\Message; use Innmind\MediaType\{ MediaType, TopLevel, @@ -18,6 +19,17 @@ enum Protocol implements Implementation case heartbeat; case ack; + public static function parse(Str $content): ?self + { + foreach (self::cases() as $case) { + if ($case->content()->equals($content)) { + return $case; + } + } + + return null; + } + #[\Override] public function mediaType(): MediaType { @@ -39,4 +51,16 @@ public function content(): Str self::ack => 'innmind/ipc:ack', }); } + + public function message(): Message + { + return match ($this) { + self::connectionStart => Message::connectionStart(), + self::connectionStartOk => Message::connectionStartOk(), + self::connectionClose => Message::connectionClose(), + self::connectionCloseOk => Message::connectionCloseOk(), + self::heartbeat => Message::heartbeat(), + self::ack => Message::ack(), + }; + } } diff --git a/src/Protocol.php b/src/Protocol.php new file mode 100644 index 0000000..f17f565 --- /dev/null +++ b/src/Protocol.php @@ -0,0 +1,129 @@ + $frame + */ + private function __construct( + private Frame $frame, + ) { + } + + public static function binary(): self + { + $frame = Frame::chunk(2) + ->strict() + ->map(static function($length) { + /** + * @psalm-suppress PossiblyInvalidArrayAccess Todo apply a predicate + * @var int<1, max> $mediaTypeLength + */ + [, $mediaTypeLength] = \unpack('n', $length->toString()); + + return $mediaTypeLength; + }) + ->flatMap( + static fn($mediaTypeLength) => Frame::chunk($mediaTypeLength)->strict(), + ) + ->map(static fn($mediaType) => $mediaType->toString()) + ->flatMap( + static fn($mediaType) => Frame::chunk(4) + ->strict() + ->map(static function($length): int { + /** + * @psalm-suppress PossiblyInvalidArrayAccess Todo apply a predicate + * @var int<0, max> $contentLength + */ + [, $contentLength] = \unpack('N', $length->toString()); + + return $contentLength; + }) + ->flatMap(static fn($contentLength) => match($contentLength) { + 0 => Frame::just([$mediaType, Str::of('')]), + default => Frame::chunk($contentLength) + ->strict() + ->map(static fn($content) => [ + $mediaType, + $content, + ]), + }), + ) + ->flatMap( + // verify the message end boundary is correct + static fn($parsed) => Frame::chunk(1) + ->strict() + ->map(static function($end): mixed { + /** @psalm-suppress PossiblyInvalidArrayAccess Todo apply a predicate */ + [, $end] = \unpack('C', $end->toString()); + + return $end; + }) + ->filter(static fn($end) => $end === self::end()) + ->map(static fn() => $parsed), + ) + ->flatMap( + static fn($parsed) => match ($protocol = Message\Protocol::parse($parsed[1])) { + null => Frame::maybe( + MediaType::maybe($parsed[0])->map( + static fn($mediaType) => Message::of( + $mediaType, + $parsed[1], + ), + ), + ), + default => Frame::just($protocol->message()), + }, + ); + + return new self($frame); + } + + /** + * @return Attempt + */ + public function encode(Message $message): Attempt + { + $content = $message->content()->toEncoding(Str\Encoding::ascii); + $mediaType = Str::of($message->mediaType()->toString())->toEncoding(Str\Encoding::ascii); + $length = $content->length(); + + if ($length > 4_294_967_295) { // unsigned long integer + return Attempt::error(new \RuntimeException(\sprintf( + 'Message being sent is too long (length %s)', + $length, + ))); + } + + return Attempt::result(Str::of('%s%s%s%s%s', Str\Encoding::ascii)->sprintf( + \pack('n', $mediaType->length()), + $mediaType->toString(), + \pack('N', $content->length()), + $content->toString(), + \pack('C', self::end()), + )); + } + + /** + * @return Frame + */ + public function frame(): Frame + { + return $this->frame; + } + + private static function end(): int + { + return 0xCE; + } +} From 5d3b02d4f6fc4c06d6596093c083d436b06ca017 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 8 Mar 2026 11:52:12 +0100 Subject: [PATCH 07/65] implement process logic to send/receive messages --- src/Process.php | 82 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 6 deletions(-) diff --git a/src/Process.php b/src/Process.php index 5ee26c3..b8d25ca 100644 --- a/src/Process.php +++ b/src/Process.php @@ -3,7 +3,12 @@ namespace Innmind\IPC; -use Innmind\Time\Period; +use Innmind\IO\Sockets\Clients\Client; +use Innmind\Time\{ + Clock, + Point, + Period, +}; use Innmind\Immutable\{ Sequence, Attempt, @@ -12,8 +17,11 @@ final class Process { - private function __construct() - { + private function __construct( + private Client $socket, + private Protocol $protocol, + private Clock $clock, + ) { } /** @@ -23,7 +31,20 @@ private function __construct() */ public function send(Sequence $messages): Attempt { - return Attempt::result(SideEffect::identity); + return $messages + ->sink(SideEffect::identity) + ->attempt( + fn($_, $message) => $this + ->protocol + ->encode($message) + ->map(Sequence::of(...)) + ->flatMap($this->socket->sink(...)) + ->flatMap(fn() => $this->wait()) + ->flatMap(static fn($message) => match ($message->equals(Message::ack())) { + true => Attempt::result(SideEffect::identity), + false => Attempt::error(new \RuntimeException('Was expecting a message acknowledgement')), + }), + ); } /** @@ -31,7 +52,7 @@ public function send(Sequence $messages): Attempt */ public function wait(?Period $timeout = null): Attempt { - return Attempt::error(new \RuntimeException); + return $this->doWait($this->clock->now(), $timeout); } /** @@ -39,6 +60,55 @@ public function wait(?Period $timeout = null): Attempt */ public function close(): Attempt { - return Attempt::result(SideEffect::identity); + return $this->socket->close(); + } + + /** + * @return Attempt + */ + private function doWait( + Point $start, + ?Period $timeout = null, + ): Attempt { + $heartbeat = $this + ->protocol + ->encode(Message::heartbeat()) + ->unwrap(); + + return $this + ->socket + ->heartbeatWith(static fn() => Sequence::of($heartbeat)) + ->abortWhen(function() use ($start, $timeout) { + if (\is_null($timeout)) { + return false; + } + + return $this + ->clock + ->now() + ->elapsedSince($start) + ->longerThan($timeout->asElapsedPeriod()); + }) + ->frames($this->protocol->frame()) + ->one() + ->flatMap(function($message) use ($start, $timeout) { + if ($message->equals(Message::heartbeat())) { + return $this->doWait($start, $timeout); + } + + return Attempt::result($message); + }) + ->flatMap(function($message) { + if ($message->equals(Message::connectionClose())) { + return $this + ->send(Sequence::of(Message::connectionCloseOk())) + ->flatMap(fn() => $this->close()) + ->flatMap(static fn() => Attempt::error(new \RuntimeException( + 'Connection closed by the server', + ))); + } + + return Attempt::result($message); + }); } } From 1f4cc117e43c662cf37c3971e7ef38e1a320e9a7 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 8 Mar 2026 12:25:13 +0100 Subject: [PATCH 08/65] implement logic to connect to a process --- src/IPC.php | 98 ++++++++++++++++++++++++++++++++++++++++++++++--- src/Process.php | 38 ++++++++++++++++++- 2 files changed, 130 insertions(+), 6 deletions(-) diff --git a/src/IPC.php b/src/IPC.php index 7823a95..4fcc65a 100644 --- a/src/IPC.php +++ b/src/IPC.php @@ -3,7 +3,20 @@ namespace Innmind\IPC; -use Innmind\Time\Period; +use Innmind\OperatingSystem\{ + Sockets, + CurrentProcess, +}; +use Innmind\Filesystem\{ + Adapter, + Name as FileName, +}; +use Innmind\IO\Sockets\Unix\Address; +use Innmind\Time\{ + Clock, + Period, +}; +use Innmind\Url\Path; use Innmind\Immutable\{ Attempt, Sequence, @@ -12,8 +25,38 @@ final class IPC { - private function __construct() - { + private function __construct( + private Sockets $sockets, + private Adapter $filesystem, + private Clock $clock, + private CurrentProcess $process, + private Protocol $protocol, + private Path $path, + private Period $heartbeat, + ) { + } + + public static function of( + Sockets $sockets, + Adapter $filesystem, + Clock $clock, + CurrentProcess $process, + Path $path, + Period $heartbeat, + ): self { + if (!$path->directory()) { + throw new \LogicException('The path must represent a directory'); + } + + return new self( + $sockets, + $filesystem, + $clock, + $process, + Protocol::binary(), + $path, + $heartbeat, + ); } /** @@ -21,7 +64,15 @@ private function __construct() */ public function processes(): Sequence { - return Sequence::of(); + return $this + ->filesystem + ->root() + ->all() + ->flatMap( + static fn($file) => Process\Name::attempt($file->name()->toString()) + ->maybe() + ->toSequence(), + ); } /** @@ -31,7 +82,37 @@ public function connectTo( Process\Name $name, ?Period $timeout = null, ): Attempt { - return Attempt::error(new \Exception); + $file = FileName::of($name->toString()); + $start = $this->clock->now(); + + return Sequence::lazy(function() use ($file) { + while (!$this->filesystem->contains($file)) { + yield $this->clock->now(); + } + }) + ->map( + fn($now) => $this + ->process + ->halt($this->heartbeat) + ->map(static fn() => $now->elapsedSince($start)), + ) + ->sink(SideEffect::identity) + ->attempt(fn($_, $halted) => $halted->flatMap( + static fn($elapsed) => match ($timeout) { + null => Attempt::result($_), + default => match ($elapsed->longerThan($timeout->asElapsedPeriod())) { + true => Attempt::error(new \RuntimeException('Timeout')), + false => Attempt::result($_), + }, + }, + )) + ->flatMap(fn() => Process::of( + $this->sockets, + $this->protocol, + $this->clock, + $this->addressOf($name), + $this->heartbeat, + )); } /** @@ -41,4 +122,11 @@ public function serve(Process\Name $name): Server { return Server::of(); } + + private function addressOf(Process\Name $name): Address + { + return Address::of( + $this->path->resolve(Path::of($name->toString())), + ); + } } diff --git a/src/Process.php b/src/Process.php index b8d25ca..524eac2 100644 --- a/src/Process.php +++ b/src/Process.php @@ -3,7 +3,11 @@ namespace Innmind\IPC; -use Innmind\IO\Sockets\Clients\Client; +use Innmind\OperatingSystem\Sockets; +use Innmind\IO\Sockets\{ + Clients\Client, + Unix\Address, +}; use Innmind\Time\{ Clock, Point, @@ -24,6 +28,38 @@ private function __construct( ) { } + /** + * @return Attempt + */ + public static function of( + Sockets $sockets, + Protocol $protocol, + Clock $clock, + Address $address, + Period $timeout, + ): Attempt { + return $sockets + ->connectTo($address) + ->map(static fn($client) => new self( + $client->timeoutAfter($timeout), + $protocol, + $clock, + )) + ->flatMap( + static fn($self) => $self + ->wait() + ->flatMap(static fn($message) => match ($message->equals(Message::connectionStart())) { + true => Attempt::result($self), + false => Attempt::error(new \RuntimeException('Connection handshake failure')), + }), + ) + ->flatMap( + static fn($self) => $self + ->send(Sequence::of(Message::connectionStartOk())) + ->map(static fn() => $self), + ); + } + /** * @param Sequence $messages * From 9d2c7d57a83afcf1c2454bb0708485d8792105f2 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 8 Mar 2026 13:58:08 +0100 Subject: [PATCH 09/65] CS --- src/IPC.php | 2 +- src/Protocol.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/IPC.php b/src/IPC.php index 4fcc65a..6e9ab4b 100644 --- a/src/IPC.php +++ b/src/IPC.php @@ -97,7 +97,7 @@ public function connectTo( ->map(static fn() => $now->elapsedSince($start)), ) ->sink(SideEffect::identity) - ->attempt(fn($_, $halted) => $halted->flatMap( + ->attempt(static fn($_, $halted) => $halted->flatMap( static fn($elapsed) => match ($timeout) { null => Attempt::result($_), default => match ($elapsed->longerThan($timeout->asElapsedPeriod())) { diff --git a/src/Protocol.php b/src/Protocol.php index f17f565..d369f3e 100644 --- a/src/Protocol.php +++ b/src/Protocol.php @@ -49,7 +49,7 @@ public static function binary(): self return $contentLength; }) - ->flatMap(static fn($contentLength) => match($contentLength) { + ->flatMap(static fn($contentLength) => match ($contentLength) { 0 => Frame::just([$mediaType, Str::of('')]), default => Frame::chunk($contentLength) ->strict() From 6a871ceb2a36b7a40e1d3ad86b9c5b1dda3ea333 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 8 Mar 2026 14:07:18 +0100 Subject: [PATCH 10/65] require the whole OS to be injected in the IPC --- src/IPC.php | 47 +++++++++++++++++------------------------------ 1 file changed, 17 insertions(+), 30 deletions(-) diff --git a/src/IPC.php b/src/IPC.php index 6e9ab4b..905f470 100644 --- a/src/IPC.php +++ b/src/IPC.php @@ -3,19 +3,13 @@ namespace Innmind\IPC; -use Innmind\OperatingSystem\{ - Sockets, - CurrentProcess, -}; +use Innmind\OperatingSystem\OperatingSystem; use Innmind\Filesystem\{ Adapter, Name as FileName, }; use Innmind\IO\Sockets\Unix\Address; -use Innmind\Time\{ - Clock, - Period, -}; +use Innmind\Time\Period; use Innmind\Url\Path; use Innmind\Immutable\{ Attempt, @@ -26,10 +20,8 @@ final class IPC { private function __construct( - private Sockets $sockets, + private OperatingSystem $os, private Adapter $filesystem, - private Clock $clock, - private CurrentProcess $process, private Protocol $protocol, private Path $path, private Period $heartbeat, @@ -37,25 +29,19 @@ private function __construct( } public static function of( - Sockets $sockets, - Adapter $filesystem, - Clock $clock, - CurrentProcess $process, + OperatingSystem $os, Path $path, - Period $heartbeat, + ?Period $heartbeat = null, ): self { - if (!$path->directory()) { - throw new \LogicException('The path must represent a directory'); - } - return new self( - $sockets, - $filesystem, - $clock, - $process, + $os, + $os + ->filesystem() + ->mount($path) + ->unwrap(), Protocol::binary(), $path, - $heartbeat, + $heartbeat ?? Period::second(1), ); } @@ -83,16 +69,17 @@ public function connectTo( ?Period $timeout = null, ): Attempt { $file = FileName::of($name->toString()); - $start = $this->clock->now(); + $start = $this->os->clock()->now(); return Sequence::lazy(function() use ($file) { while (!$this->filesystem->contains($file)) { - yield $this->clock->now(); + yield $this->os->clock()->now(); } }) ->map( fn($now) => $this - ->process + ->os + ->process() ->halt($this->heartbeat) ->map(static fn() => $now->elapsedSince($start)), ) @@ -107,9 +94,9 @@ public function connectTo( }, )) ->flatMap(fn() => Process::of( - $this->sockets, + $this->os->sockets(), $this->protocol, - $this->clock, + $this->os->clock(), $this->addressOf($name), $this->heartbeat, )); From b611e48b28616235ff2ca5eb57c36197190fbfb3 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 8 Mar 2026 15:19:30 +0100 Subject: [PATCH 11/65] implement server accepting connections and waiting for messages --- src/IPC.php | 7 ++- src/Server.php | 45 +++++++++++++--- src/Server/Client.php | 109 +++++++++++++++++++++++++++++++++++++++ src/Server/Instance.php | 84 ++++++++++++++++++++++++++++++ src/Server/Unstarted.php | 40 ++++++++++++++ 5 files changed, 277 insertions(+), 8 deletions(-) create mode 100644 src/Server/Client.php create mode 100644 src/Server/Instance.php create mode 100644 src/Server/Unstarted.php diff --git a/src/IPC.php b/src/IPC.php index 905f470..718c766 100644 --- a/src/IPC.php +++ b/src/IPC.php @@ -107,7 +107,12 @@ public function connectTo( */ public function serve(Process\Name $name): Server { - return Server::of(); + return Server::of( + $this->os, + $this->protocol, + $this->addressOf($name), + $this->heartbeat, + ); } private function addressOf(Process\Name $name): Address diff --git a/src/Server.php b/src/Server.php index 52a344f..b7cca46 100644 --- a/src/Server.php +++ b/src/Server.php @@ -3,6 +3,10 @@ namespace Innmind\IPC; +use Innmind\Async\Scheduler; +use Innmind\OperatingSystem\OperatingSystem; +use Innmind\IO\Sockets\Unix\Address; +use Innmind\Time\Period; use Innmind\Immutable\{ Attempt, SideEffect, @@ -16,16 +20,31 @@ final class Server /** * @param T $carry */ - private function __construct(private mixed $carry) - { + private function __construct( + private OperatingSystem $os, + private Protocol $protocol, + private Address $address, + private Period $timeout, + private mixed $carry, + ) { } /** * @return self */ - public static function of(): self - { - return new self(SideEffect::identity); + public static function of( + OperatingSystem $os, + Protocol $protocol, + Address $address, + Period $timeout, + ): self { + return new self( + $os, + $protocol, + $address, + $timeout, + SideEffect::identity, + ); } /** @@ -38,7 +57,13 @@ public static function of(): self */ public function sink(mixed $carry): self { - return new self($carry); + return new self( + $this->os, + $this->protocol, + $this->address, + $this->timeout, + $carry, + ); } // todo differentiate a reducer for the server loop (aka the scheduler sink) @@ -55,6 +80,12 @@ public function sink(mixed $carry): self */ public function with(callable $listen): Attempt { - return Attempt::result($this->carry); + return Scheduler::of($this->os) + ->sink(Attempt::result($this->carry)) + ->with(Server\Instance::of( + $this->protocol, + $this->address, + $this->timeout, + )); } } diff --git a/src/Server/Client.php b/src/Server/Client.php new file mode 100644 index 0000000..8cf67b6 --- /dev/null +++ b/src/Server/Client.php @@ -0,0 +1,109 @@ + + */ + public function __invoke(OperatingSystem $os): Attempt + { + $abort = false; + + $signaled = $os + ->process() + ->signals() + ->listen(Signal::terminate, static function() use (&$abort) { + $abort = true; + }); + $frame = $this->protocol->frame(); + // unwrapping is safe as it's internal messages + $heartbeat = $this->protocol->encode(Message::heartbeat())->unwrap(); + $ack = $this->protocol->encode(Message::ack())->unwrap(); + + // Use an infinite sequence to iteractively wait for a message to arrive + // If the received one is a heartbeat we return a side effect, meaning + // we restart the loop. Otherwise we send an acknowledgement to the + // client before handling the message. + // Since we heartbeat when waiting for a message the ->one() call will + // always return a message unless there's a network error. This means + // that by default ->one() will wait forever. + // And since we use the sink pattern on the sequence, everything will + // stop as soon any part of the system returns an error. + return Sequence::lazy(static function() use ($signaled) { + yield $signaled; + + while (true) { + yield SideEffect::identity; + } + }) + ->sink(SideEffect::identity) + ->attempt( + fn($_, $val) => match (true) { + $val instanceof Attempt => $val, + default => $this + ->client + ->heartbeatWith(static fn() => Sequence::of($heartbeat)) + ->abortWhen(static function() use (&$abort) { + return $abort; + }) + ->frames($frame) + ->one() + ->flatMap( + fn($message) => match ($message->equals(Message::heartbeat())) { + true => Attempt::result(SideEffect::identity), + false => $this + ->client + ->sink(Sequence::of($ack)) + ->flatMap(fn() => $this->handle($message)), + }, + ), + }, + ) + ->recover( + fn($e) => $this + ->client + ->close() + ->match( // make sure to return the original error + static fn() => Attempt::error($e), + static fn() => Attempt::error($e), + ), + ); + } + + public static function of( + Socket $client, + Protocol $protocol, + ): self { + return new self($client, $protocol); + } + + /** + * @return Attempt + */ + private function handle(Message $message): Attempt + { + return Attempt::result(SideEffect::identity); // todo + } +} diff --git a/src/Server/Instance.php b/src/Server/Instance.php new file mode 100644 index 0000000..1990e3d --- /dev/null +++ b/src/Server/Instance.php @@ -0,0 +1,84 @@ + $carry + * @param Continuation> $continuation + * + * @return Continuation> + */ + public function __invoke( + Attempt $carry, + OperatingSystem $os, + Continuation $continuation, + ): Continuation { + if ($this->server instanceof Unstarted) { + return ($this->server)($os) + ->map(function($server) { + $this->server = $server; + + return $server; + }) + ->match( + static fn() => $continuation, + static fn($e) => $continuation + ->carryWith(Attempt::error($e)) + ->finish(), + ); + } + + return $this + ->server + ->accept() + ->map(fn($socket) => Client::of( + $socket->timeoutAfter($this->timeout), + $this->protocol, + )) + ->map(Sequence::of(...)) + ->match( + static fn($clients) => $continuation->schedule($clients), + static fn() => $continuation, // restart the loop when no new client within the timeout + ); + } + + public static function of( + Protocol $protocol, + Address $address, + Period $timeout, + ): self { + return new self( + Unstarted::of( + $address, + $timeout, + ), + $protocol, + $timeout, + ); + } +} diff --git a/src/Server/Unstarted.php b/src/Server/Unstarted.php new file mode 100644 index 0000000..c44bbac --- /dev/null +++ b/src/Server/Unstarted.php @@ -0,0 +1,40 @@ + + */ + public function __invoke(OperatingSystem $os): Attempt + { + return $os + ->sockets() + ->open($this->address) + ->map(fn($server) => $server->timeoutAfter($this->timeout)); + } + + public static function of( + Address $address, + Period $timeout, + ): self { + return new self($address, $timeout); + } +} From fb95116ad2bff03d9ac90511b3c303bf909217aa Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 8 Mar 2026 15:56:12 +0100 Subject: [PATCH 12/65] lay out the system to combine clients results --- src/Monoid.php | 30 +++++++++++++++++++++++++ src/Server.php | 16 +++++++------ src/Server/Client.php | 50 +++++++++++++++++++++++++++++++---------- src/Server/Instance.php | 31 +++++++++++++++++++++++-- 4 files changed, 106 insertions(+), 21 deletions(-) create mode 100644 src/Monoid.php diff --git a/src/Monoid.php b/src/Monoid.php new file mode 100644 index 0000000..65846fe --- /dev/null +++ b/src/Monoid.php @@ -0,0 +1,30 @@ + + */ +enum Monoid implements Monoid_ +{ + case sideEffect; + + #[\Override] + public function identity(): SideEffect + { + return SideEffect::identity; + } + + #[\Override] + public function combine(mixed $a, mixed $b): SideEffect + { + return SideEffect::identity; + } +} diff --git a/src/Server.php b/src/Server.php index b7cca46..905ab94 100644 --- a/src/Server.php +++ b/src/Server.php @@ -10,6 +10,7 @@ use Innmind\Immutable\{ Attempt, SideEffect, + Monoid as Monoid_, }; /** @@ -18,14 +19,14 @@ final class Server { /** - * @param T $carry + * @param Monoid_ $monoid */ private function __construct( private OperatingSystem $os, private Protocol $protocol, private Address $address, private Period $timeout, - private mixed $carry, + private Monoid_ $monoid, ) { } @@ -43,7 +44,7 @@ public static function of( $protocol, $address, $timeout, - SideEffect::identity, + Monoid::sideEffect, ); } @@ -51,18 +52,18 @@ public static function of( * @psalm-mutation-free * @template U * - * @param U $carry + * @param Monoid_ $carry * * @return self */ - public function sink(mixed $carry): self + public function sink(Monoid_ $monoid): self { return new self( $this->os, $this->protocol, $this->address, $this->timeout, - $carry, + $monoid, ); } @@ -81,11 +82,12 @@ public function sink(mixed $carry): self public function with(callable $listen): Attempt { return Scheduler::of($this->os) - ->sink(Attempt::result($this->carry)) + ->sink(Attempt::result($this->monoid->identity())) ->with(Server\Instance::of( $this->protocol, $this->address, $this->timeout, + $this->monoid, )); } } diff --git a/src/Server/Client.php b/src/Server/Client.php index 8cf67b6..e091afb 100644 --- a/src/Server/Client.php +++ b/src/Server/Client.php @@ -14,29 +14,39 @@ Sequence, Attempt, SideEffect, + Monoid, }; +/** + * @template T + */ final class Client { + /** + * @param Monoid $monoid + */ private function __construct( private Socket $client, private Protocol $protocol, + private Monoid $monoid, ) { } /** - * @return Attempt + * @return Attempt */ public function __invoke(OperatingSystem $os): Attempt { $abort = false; + $identity = $this->monoid->identity(); $signaled = $os ->process() ->signals() ->listen(Signal::terminate, static function() use (&$abort) { $abort = true; - }); + }) + ->map(static fn() => $identity); $frame = $this->protocol->frame(); // unwrapping is safe as it's internal messages $heartbeat = $this->protocol->encode(Message::heartbeat())->unwrap(); @@ -51,16 +61,16 @@ public function __invoke(OperatingSystem $os): Attempt // that by default ->one() will wait forever. // And since we use the sink pattern on the sequence, everything will // stop as soon any part of the system returns an error. - return Sequence::lazy(static function() use ($signaled) { + return Sequence::lazy(static function() use ($signaled, $identity) { yield $signaled; while (true) { - yield SideEffect::identity; + yield $identity; } }) - ->sink(SideEffect::identity) + ->sink($identity) ->attempt( - fn($_, $val) => match (true) { + fn($identity, $val) => match (true) { $val instanceof Attempt => $val, default => $this ->client @@ -72,11 +82,17 @@ public function __invoke(OperatingSystem $os): Attempt ->one() ->flatMap( fn($message) => match ($message->equals(Message::heartbeat())) { - true => Attempt::result(SideEffect::identity), + true => Attempt::result($identity), false => $this ->client ->sink(Sequence::of($ack)) - ->flatMap(fn() => $this->handle($message)), + ->flatMap( + /** @psalm-suppress MixedArgument Don't know why it loses the type */ + fn() => $this->handle( + $identity, + $message, + ), + ), }, ), }, @@ -92,18 +108,28 @@ public function __invoke(OperatingSystem $os): Attempt ); } + /** + * @template A + * + * @param Monoid $monoid + * + * @return self + */ public static function of( Socket $client, Protocol $protocol, + Monoid $monoid, ): self { - return new self($client, $protocol); + return new self($client, $protocol, $monoid); } /** - * @return Attempt + * @param T $identity + * + * @return Attempt */ - private function handle(Message $message): Attempt + private function handle(mixed $identity, Message $message): Attempt { - return Attempt::result(SideEffect::identity); // todo + return Attempt::result($identity); // todo } } diff --git a/src/Server/Instance.php b/src/Server/Instance.php index 1990e3d..5d43737 100644 --- a/src/Server/Instance.php +++ b/src/Server/Instance.php @@ -14,20 +14,26 @@ use Innmind\Immutable\{ Attempt, Sequence, + Monoid, }; +/** + * @template T + */ final class Instance { + /** + * @param Monoid $monoid + */ private function __construct( private Server|Unstarted $server, private Protocol $protocol, private Period $timeout, + private Monoid $monoid, ) { } /** - * @template T - * * @param Attempt $carry * @param Continuation> $continuation * @@ -53,12 +59,24 @@ public function __invoke( ); } + /** @var Sequence */ + $all = Sequence::of(); + /** @var Sequence> */ + $results = $continuation->results(); + $carry = $results + ->prepend(Sequence::of($carry)) + ->sink($all) + ->attempt(static fn($all, $result) => $result->map($all)) + ->map(fn($results) => $results->fold($this->monoid)); + $continuation = $continuation->carryWith($carry); + return $this ->server ->accept() ->map(fn($socket) => Client::of( $socket->timeoutAfter($this->timeout), $this->protocol, + $this->monoid, )) ->map(Sequence::of(...)) ->match( @@ -67,10 +85,18 @@ public function __invoke( ); } + /** + * @template A + * + * @param Monoid $monoid + * + * @return self + */ public static function of( Protocol $protocol, Address $address, Period $timeout, + Monoid $monoid, ): self { return new self( Unstarted::of( @@ -79,6 +105,7 @@ public static function of( ), $protocol, $timeout, + $monoid, ); } } From 1d58b0958257a10db58cfe18a37d598b52031dba Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 8 Mar 2026 16:41:27 +0100 Subject: [PATCH 13/65] allow to stop the server --- src/Server.php | 35 ++++++++++++++ src/Server/Continuation.php | 75 ++++++++++++++++++++++++++++++ src/Server/Continuation/Next.php | 10 ++++ src/Server/Instance.php | 79 ++++++++++++++++++++++++-------- 4 files changed, 180 insertions(+), 19 deletions(-) create mode 100644 src/Server/Continuation.php create mode 100644 src/Server/Continuation/Next.php diff --git a/src/Server.php b/src/Server.php index 905ab94..3e25218 100644 --- a/src/Server.php +++ b/src/Server.php @@ -20,6 +20,7 @@ final class Server { /** * @param Monoid_ $monoid + * @param \Closure(T, Server\Continuation): Server\Continuation $monitor */ private function __construct( private OperatingSystem $os, @@ -27,6 +28,7 @@ private function __construct( private Address $address, private Period $timeout, private Monoid_ $monoid, + private \Closure $monitor, ) { } @@ -45,6 +47,7 @@ public static function of( $address, $timeout, Monoid::sideEffect, + self::defaultMonitor(Monoid::sideEffect), ); } @@ -64,6 +67,24 @@ public function sink(Monoid_ $monoid): self $this->address, $this->timeout, $monoid, + self::defaultMonitor($monoid), + ); + } + + /** + * @param callable(T, Server\Continuation): Server\Continuation $monitor + * + * @return self + */ + public function monitor(callable $monitor): self + { + return new self( + $this->os, + $this->protocol, + $this->address, + $this->timeout, + $this->monoid, + \Closure::fromCallable($monitor), ); } @@ -88,6 +109,20 @@ public function with(callable $listen): Attempt $this->address, $this->timeout, $this->monoid, + $this->monitor, )); } + + /** + * @psalm-pure + * @template A + * + * @param Monoid_ $monoid + * + * @return \Closure(A, Server\Continuation): Server\Continuation + */ + private static function defaultMonitor(Monoid_ $monoid): \Closure + { + return static fn($_, Server\Continuation $continuation) => $continuation; + } } diff --git a/src/Server/Continuation.php b/src/Server/Continuation.php new file mode 100644 index 0000000..ed1dc74 --- /dev/null +++ b/src/Server/Continuation.php @@ -0,0 +1,75 @@ + + */ + public static function new(mixed $carry): self + { + return new self( + Next::continue, + $carry, + ); + } + + /** + * @param T $carry + * + * @return self + */ + public function carryWith(mixed $carry): self + { + return new self($this->next, $carry); + } + + /** + * @return self + */ + public function finish(): self + { + return new self(Next::finish, $this->carry); + } + + /** + * @template R + * + * @param callable(T): R $continue + * @param callable(T): R $finish + * + * @return R + */ + public function match( + callable $continue, + callable $finish, + ): mixed { + /** @psalm-suppress ImpureFunctionCall */ + return match ($this->next) { + Next::continue => $continue($this->carry), + Next::finish => $finish($this->carry), + }; + } +} diff --git a/src/Server/Continuation/Next.php b/src/Server/Continuation/Next.php new file mode 100644 index 0000000..695e001 --- /dev/null +++ b/src/Server/Continuation/Next.php @@ -0,0 +1,10 @@ + $monoid + * @param \Closure(T, Continuation): Continuation $monitor */ private function __construct( private Server|Unstarted $server, private Protocol $protocol, private Period $timeout, private Monoid $monoid, + private \Closure $monitor, ) { } /** * @param Attempt $carry - * @param Continuation> $continuation + * @param Scope\Continuation> $continuation * - * @return Continuation> + * @return Scope\Continuation> */ public function __invoke( Attempt $carry, OperatingSystem $os, - Continuation $continuation, - ): Continuation { + Scope\Continuation $continuation, + ): Scope\Continuation { if ($this->server instanceof Unstarted) { return ($this->server)($os) ->map(function($server) { @@ -63,32 +65,35 @@ public function __invoke( $all = Sequence::of(); /** @var Sequence> */ $results = $continuation->results(); + /** @var Attempt */ $carry = $results ->prepend(Sequence::of($carry)) ->sink($all) ->attempt(static fn($all, $result) => $result->map($all)) ->map(fn($results) => $results->fold($this->monoid)); - $continuation = $continuation->carryWith($carry); - return $this - ->server - ->accept() - ->map(fn($socket) => Client::of( - $socket->timeoutAfter($this->timeout), - $this->protocol, - $this->monoid, - )) - ->map(Sequence::of(...)) - ->match( - static fn($clients) => $continuation->schedule($clients), - static fn() => $continuation, // restart the loop when no new client within the timeout - ); + /** @psalm-suppress MixedArgument Don't know why it loses the type */ + return $carry->match( + fn($carry) => ($this->monitor)($carry, Continuation::new($carry))->match( + fn($carry) => $this->listen( + $carry, + $continuation, + ), + static fn($carry) => $continuation + ->carryWith(Attempt::result($carry)) + ->finish(), + ), + static fn() => $continuation + ->carryWith($carry) + ->terminate(), + ); } /** * @template A * * @param Monoid $monoid + * @param \Closure(A, Continuation): Continuation $monitor * * @return self */ @@ -97,6 +102,7 @@ public static function of( Address $address, Period $timeout, Monoid $monoid, + \Closure $monitor, ): self { return new self( Unstarted::of( @@ -106,6 +112,41 @@ public static function of( $protocol, $timeout, $monoid, + $monitor, ); } + + /** + * @param T $carry + * @param Scope\Continuation> $continuation + * + * @return Scope\Continuation> + */ + private function listen( + mixed $carry, + Scope\Continuation $continuation, + ): Scope\Continuation { + if ($this->server instanceof Unstarted) { + return $continuation + ->carryWith(Attempt::error(new \LogicException('Unstarted server'))) + ->terminate(); + } + + return $this + ->server + ->accept() + ->map(fn($socket) => Client::of( + $socket->timeoutAfter($this->timeout), + $this->protocol, + $this->monoid, + )) + ->map(Sequence::of(...)) + ->match( + static fn($clients) => $continuation + ->carryWith(Attempt::result($carry)) + ->schedule($clients), + static fn() => $continuation // restart the loop when no new client within the timeout + ->carryWith(Attempt::result($carry)), + ); + } } From e938dad26a1378c405ee7e7cb3a0e24465613b10 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 8 Mar 2026 17:12:20 +0100 Subject: [PATCH 14/65] allow for a client to respond to received messages --- src/Continuation.php | 80 +++++++++++++++++++++++++++++++++++++- src/Continuation/Next.php | 10 +++++ src/Server.php | 1 + src/Server/Client.php | 62 ++++++++++++++++++++++++----- src/Server/Client/Stop.php | 16 ++++++++ src/Server/Instance.php | 12 +++++- 6 files changed, 170 insertions(+), 11 deletions(-) create mode 100644 src/Continuation/Next.php create mode 100644 src/Server/Client/Stop.php diff --git a/src/Continuation.php b/src/Continuation.php index 81a41b3..657ccc1 100644 --- a/src/Continuation.php +++ b/src/Continuation.php @@ -3,17 +3,28 @@ namespace Innmind\IPC; +use Innmind\IPC\Continuation\Next; +use Innmind\Immutable\Sequence; + /** + * @psalm-immutable * @template T */ final class Continuation { + /** + * @param Sequence $messages + * @param T $carry + */ private function __construct( + private Next $next, + private Sequence $messages, private mixed $carry, ) { } /** + * @psalm-pure * @template A * * @param A $carry @@ -22,6 +33,73 @@ private function __construct( */ public static function new(mixed $carry): self { - return new self($carry); + return new self( + Next::continue, + Sequence::of(), + $carry, + ); + } + + /** + * @param T $carry + * + * @return self + */ + public function carryWith(mixed $carry): self + { + return new self( + $this->next, + $this->messages, + $carry, + ); + } + + /** + * @param Message|Sequence $messages + * + * @return self + */ + public function respond(Message|Sequence $messages): self + { + if ($messages instanceof Message) { + $messages = Sequence::of($messages); + } + + return new self( + $this->next, + $messages->prepend($this->messages), // to keep the user provided lazyness + $this->carry, + ); + } + + /** + * @return self + */ + public function finish(): self + { + return new self( + Next::finish, + $this->messages, + $this->carry, + ); + } + + /** + * @template R + * + * @param callable(T, Sequence): R $continue + * @param callable(T, Sequence): R $finish + * + * @return R + */ + public function match( + callable $continue, + callable $finish, + ): mixed { + /** @psalm-suppress ImpureFunctionCall */ + return match ($this->next) { + Next::continue => $continue($this->carry, $this->messages), + Next::finish => $finish($this->carry, $this->messages), + }; } } diff --git a/src/Continuation/Next.php b/src/Continuation/Next.php new file mode 100644 index 0000000..658cfcd --- /dev/null +++ b/src/Continuation/Next.php @@ -0,0 +1,10 @@ +timeout, $this->monoid, $this->monitor, + \Closure::fromCallable($listen), )); } diff --git a/src/Server/Client.php b/src/Server/Client.php index e091afb..640e748 100644 --- a/src/Server/Client.php +++ b/src/Server/Client.php @@ -6,6 +6,8 @@ use Innmind\IPC\{ Protocol, Message, + Continuation, + Server\Client\Stop, }; use Innmind\OperatingSystem\OperatingSystem; use Innmind\Signals\Signal; @@ -24,11 +26,13 @@ final class Client { /** * @param Monoid $monoid + * @param \Closure(Message, Continuation, T): Continuation $listen */ private function __construct( private Socket $client, private Protocol $protocol, private Monoid $monoid, + private \Closure $listen, ) { } @@ -98,13 +102,22 @@ public function __invoke(OperatingSystem $os): Attempt }, ) ->recover( - fn($e) => $this - ->client - ->close() - ->match( // make sure to return the original error - static fn() => Attempt::error($e), - static fn() => Attempt::error($e), - ), + fn($e) => match (true) { + $e instanceof Stop => $this + ->client + ->close() + ->match( // make sure to keep the user provided value + static fn() => Attempt::result($e->unwrap()), + static fn() => Attempt::result($e->unwrap()), + ), + default => $this + ->client + ->close() + ->match( // make sure to return the original error + static fn() => Attempt::error($e), + static fn() => Attempt::error($e), + ), + }, ); } @@ -112,6 +125,7 @@ public function __invoke(OperatingSystem $os): Attempt * @template A * * @param Monoid $monoid + * @param \Closure(Message, Continuation, A): Continuation $listen * * @return self */ @@ -119,8 +133,9 @@ public static function of( Socket $client, Protocol $protocol, Monoid $monoid, + \Closure $listen, ): self { - return new self($client, $protocol, $monoid); + return new self($client, $protocol, $monoid, $listen); } /** @@ -130,6 +145,35 @@ public static function of( */ private function handle(mixed $identity, Message $message): Attempt { - return Attempt::result($identity); // todo + /** @psalm-suppress MixedArgument Don't know why it loses the type */ + return ($this->listen)($message, Continuation::new($identity), $identity)->match( + fn($carry, $messages) => $this->respond($carry, $messages), + fn($carry, $messages) => $this + ->respond($carry, $messages) + ->flatMap(static fn($carry) => Attempt::error(new Stop($carry))), + ); + } + + /** + * @param T $carry + * @param Sequence $messages + * + * @return Attempt + */ + private function respond( + mixed $carry, + Sequence $messages, + ): Attempt { + return $messages + ->sink($carry) + ->attempt( + fn($carry, $message) => $this + ->protocol + ->encode($message) + ->map(Sequence::of(...)) + ->flatMap($this->client->sink(...)) + // todo wait for acks + ->map(static fn(): mixed => $carry), + ); } } diff --git a/src/Server/Client/Stop.php b/src/Server/Client/Stop.php new file mode 100644 index 0000000..219a455 --- /dev/null +++ b/src/Server/Client/Stop.php @@ -0,0 +1,16 @@ +value; + } +} diff --git a/src/Server/Instance.php b/src/Server/Instance.php index 7e25e70..b6ed6c0 100644 --- a/src/Server/Instance.php +++ b/src/Server/Instance.php @@ -3,7 +3,11 @@ namespace Innmind\IPC\Server; -use Innmind\IPC\Protocol; +use Innmind\IPC\{ + Protocol, + Continuation as Continuation_, + Message, +}; use Innmind\Async\Scope; use Innmind\OperatingSystem\OperatingSystem; use Innmind\IO\Sockets\{ @@ -25,6 +29,7 @@ final class Instance /** * @param Monoid $monoid * @param \Closure(T, Continuation): Continuation $monitor + * @param \Closure(Message, Continuation_, T): Continuation_ $listen */ private function __construct( private Server|Unstarted $server, @@ -32,6 +37,7 @@ private function __construct( private Period $timeout, private Monoid $monoid, private \Closure $monitor, + private \Closure $listen, ) { } @@ -94,6 +100,7 @@ public function __invoke( * * @param Monoid $monoid * @param \Closure(A, Continuation): Continuation $monitor + * @param \Closure(Message, Continuation_, A): Continuation_ $listen * * @return self */ @@ -103,6 +110,7 @@ public static function of( Period $timeout, Monoid $monoid, \Closure $monitor, + \Closure $listen, ): self { return new self( Unstarted::of( @@ -113,6 +121,7 @@ public static function of( $timeout, $monoid, $monitor, + $listen, ); } @@ -139,6 +148,7 @@ private function listen( $socket->timeoutAfter($this->timeout), $this->protocol, $this->monoid, + $this->listen, )) ->map(Sequence::of(...)) ->match( From 6577a67dfdc4c3cf4672b001341737cc5654d6e6 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 8 Mar 2026 17:16:49 +0100 Subject: [PATCH 15/65] use a property to keep track if a client must be aborted --- src/Server/Client.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Server/Client.php b/src/Server/Client.php index 640e748..ac4a6b4 100644 --- a/src/Server/Client.php +++ b/src/Server/Client.php @@ -33,6 +33,7 @@ private function __construct( private Protocol $protocol, private Monoid $monoid, private \Closure $listen, + private bool $abort = false, ) { } @@ -41,14 +42,13 @@ private function __construct( */ public function __invoke(OperatingSystem $os): Attempt { - $abort = false; $identity = $this->monoid->identity(); $signaled = $os ->process() ->signals() - ->listen(Signal::terminate, static function() use (&$abort) { - $abort = true; + ->listen(Signal::terminate, function() { + $this->abort = true; }) ->map(static fn() => $identity); $frame = $this->protocol->frame(); @@ -79,9 +79,7 @@ public function __invoke(OperatingSystem $os): Attempt default => $this ->client ->heartbeatWith(static fn() => Sequence::of($heartbeat)) - ->abortWhen(static function() use (&$abort) { - return $abort; - }) + ->abortWhen(fn() => $this->abort) ->frames($frame) ->one() ->flatMap( From 1b17d0471ee4ccb4d00153569e6a2d847d7fe590 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 8 Mar 2026 17:22:56 +0100 Subject: [PATCH 16/65] wait for acknowledgements of sent messages --- src/Server/Client.php | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Server/Client.php b/src/Server/Client.php index ac4a6b4..13a76fb 100644 --- a/src/Server/Client.php +++ b/src/Server/Client.php @@ -162,6 +162,11 @@ private function respond( mixed $carry, Sequence $messages, ): Attempt { + $frame = $this->protocol->frame(); + // unwrapping is safe as it's internal messages + $heartbeat = $this->protocol->encode(Message::heartbeat())->unwrap(); + $ack = $this->protocol->encode(Message::ack())->unwrap(); + return $messages ->sink($carry) ->attempt( @@ -170,8 +175,18 @@ private function respond( ->encode($message) ->map(Sequence::of(...)) ->flatMap($this->client->sink(...)) - // todo wait for acks - ->map(static fn(): mixed => $carry), + ->flatMap( + fn() => $this + ->client + ->heartbeatWith(static fn() => Sequence::of($heartbeat)) + ->abortWhen(fn() => $this->abort) + ->frames($frame) + ->one(), + ) + ->flatMap(static fn($message) => match ($message->equals(Message::ack())) { + true => Attempt::result($carry), + false => Attempt::error(new \RuntimeException('Was expecting a message acknowledgement')), + }), ); } } From be58bf9046dd6ac16640ce87f837c2e7e903e13f Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 8 Mar 2026 17:38:36 +0100 Subject: [PATCH 17/65] centralize the way to wait for messages --- src/Server/Client.php | 67 +++++++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 25 deletions(-) diff --git a/src/Server/Client.php b/src/Server/Client.php index 13a76fb..d204c79 100644 --- a/src/Server/Client.php +++ b/src/Server/Client.php @@ -77,25 +77,18 @@ public function __invoke(OperatingSystem $os): Attempt fn($identity, $val) => match (true) { $val instanceof Attempt => $val, default => $this - ->client - ->heartbeatWith(static fn() => Sequence::of($heartbeat)) - ->abortWhen(fn() => $this->abort) - ->frames($frame) - ->one() + ->wait() ->flatMap( - fn($message) => match ($message->equals(Message::heartbeat())) { - true => Attempt::result($identity), - false => $this - ->client - ->sink(Sequence::of($ack)) - ->flatMap( - /** @psalm-suppress MixedArgument Don't know why it loses the type */ - fn() => $this->handle( - $identity, - $message, - ), + fn($message) => $this + ->client + ->sink(Sequence::of($ack)) + ->flatMap( + /** @psalm-suppress MixedArgument Don't know why it loses the type */ + fn() => $this->handle( + $identity, + $message, ), - }, + ), ), }, ) @@ -175,18 +168,42 @@ private function respond( ->encode($message) ->map(Sequence::of(...)) ->flatMap($this->client->sink(...)) - ->flatMap( - fn() => $this - ->client - ->heartbeatWith(static fn() => Sequence::of($heartbeat)) - ->abortWhen(fn() => $this->abort) - ->frames($frame) - ->one(), - ) + ->flatMap(fn() => $this->wait()) ->flatMap(static fn($message) => match ($message->equals(Message::ack())) { true => Attempt::result($carry), false => Attempt::error(new \RuntimeException('Was expecting a message acknowledgement')), }), ); } + + /** + * @return Attempt + */ + private function wait(): Attempt + { + // unwrapping is safe as it's internal messages + $heartbeat = $this->protocol->encode(Message::heartbeat())->unwrap(); + + // This is to avoid recursion. Otherwise for processes that wait for a + // long time it may reach the maximum call stack. + // todo find a more elegant way + do { + $result = $this + ->client + ->heartbeatWith(static fn() => Sequence::of($heartbeat)) + ->abortWhen(fn() => $this->abort) + ->frames($this->protocol->frame()) + ->one() + ->match( + static fn($message) => $message, + static fn($e) => $e, + ); + + if ($result instanceof \Throwable) { + return Attempt::error($result); + } + } while ($result->equals(Message::heartbeat())); + + return Attempt::result($result); + } } From f679cfff51fd75379ca12682e2bfe76950268a9b Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 8 Mar 2026 18:02:28 +0100 Subject: [PATCH 18/65] extract logic to wait/send messages --- src/Pipe.php | 118 ++++++++++++++++++++++++++++++++++++++++++++++++ src/Process.php | 82 ++++++--------------------------- 2 files changed, 131 insertions(+), 69 deletions(-) create mode 100644 src/Pipe.php diff --git a/src/Pipe.php b/src/Pipe.php new file mode 100644 index 0000000..3b4bfff --- /dev/null +++ b/src/Pipe.php @@ -0,0 +1,118 @@ + + */ + public function wait( + callable $abort, + ?Period $timeout = null + ): Attempt { + $start = $this->clock->now(); + // It's safe to unwrap as it's an internal message that never fails + $heartbeat = $this + ->protocol + ->encode(Message::heartbeat()) + ->unwrap(); + + do { + $result = $this + ->socket + ->heartbeatWith(static fn() => Sequence::of($heartbeat)) + ->abortWhen(function() use ($abort, $start, $timeout) { + if ($abort()) { + return true; + } + + if (\is_null($timeout)) { + return false; + } + + return $this + ->clock + ->now() + ->elapsedSince($start) + ->longerThan($timeout->asElapsedPeriod()); + }) + ->frames($this->protocol->frame()) + ->one() + ->match( + static fn($message) => $message, + static fn($e) => $e, + ); + + if ($result instanceof \Throwable) { + return Attempt::error($result); + } + } while ($result->equals(Message::heartbeat())); + + if ($result->equals(Message::connectionClose())) { + return $this + ->protocol + ->encode(Message::connectionCloseOk()) + ->map(Sequence::of(...)) + ->flatMap($this->socket->sink(...)) + ->flatMap(fn() => $this->socket->close()) + ->flatMap(static fn() => Attempt::error(new \RuntimeException( + 'Connection closed by the other side', + ))); + } + + return Attempt::result($result); + } + + /** + * @param Sequence $messages + * + * @return Attempt + */ + public function send(Sequence $messages): Attempt + { + return $messages + ->sink(SideEffect::identity) + ->attempt( + fn($_, $message) => $this + ->protocol + ->encode($message) + ->map(Sequence::of(...)) + ->flatMap($this->socket->sink(...)) + ->flatMap(fn() => $this->wait(static fn() => false)) + ->flatMap(static fn($message) => match ($message->equals(Message::ack())) { + true => Attempt::result(SideEffect::identity), + false => Attempt::error(new \RuntimeException('Was expecting a message acknowledgement')), + }), + ); + } +} diff --git a/src/Process.php b/src/Process.php index 524eac2..dc0decb 100644 --- a/src/Process.php +++ b/src/Process.php @@ -23,8 +23,7 @@ final class Process { private function __construct( private Client $socket, - private Protocol $protocol, - private Clock $clock, + private Pipe $pipe, ) { } @@ -40,10 +39,14 @@ public static function of( ): Attempt { return $sockets ->connectTo($address) + ->map(static fn($client) => $client->timeoutAfter($timeout)) ->map(static fn($client) => new self( - $client->timeoutAfter($timeout), - $protocol, - $clock, + $client, + Pipe::of( + $client, + $protocol, + $clock, + ), )) ->flatMap( static fn($self) => $self @@ -67,20 +70,7 @@ public static function of( */ public function send(Sequence $messages): Attempt { - return $messages - ->sink(SideEffect::identity) - ->attempt( - fn($_, $message) => $this - ->protocol - ->encode($message) - ->map(Sequence::of(...)) - ->flatMap($this->socket->sink(...)) - ->flatMap(fn() => $this->wait()) - ->flatMap(static fn($message) => match ($message->equals(Message::ack())) { - true => Attempt::result(SideEffect::identity), - false => Attempt::error(new \RuntimeException('Was expecting a message acknowledgement')), - }), - ); + return $this->pipe->send($messages); } /** @@ -88,7 +78,10 @@ public function send(Sequence $messages): Attempt */ public function wait(?Period $timeout = null): Attempt { - return $this->doWait($this->clock->now(), $timeout); + return $this->pipe->wait( + static fn() => false, // todo handle signals ? + $timeout, + ); } /** @@ -98,53 +91,4 @@ public function close(): Attempt { return $this->socket->close(); } - - /** - * @return Attempt - */ - private function doWait( - Point $start, - ?Period $timeout = null, - ): Attempt { - $heartbeat = $this - ->protocol - ->encode(Message::heartbeat()) - ->unwrap(); - - return $this - ->socket - ->heartbeatWith(static fn() => Sequence::of($heartbeat)) - ->abortWhen(function() use ($start, $timeout) { - if (\is_null($timeout)) { - return false; - } - - return $this - ->clock - ->now() - ->elapsedSince($start) - ->longerThan($timeout->asElapsedPeriod()); - }) - ->frames($this->protocol->frame()) - ->one() - ->flatMap(function($message) use ($start, $timeout) { - if ($message->equals(Message::heartbeat())) { - return $this->doWait($start, $timeout); - } - - return Attempt::result($message); - }) - ->flatMap(function($message) { - if ($message->equals(Message::connectionClose())) { - return $this - ->send(Sequence::of(Message::connectionCloseOk())) - ->flatMap(fn() => $this->close()) - ->flatMap(static fn() => Attempt::error(new \RuntimeException( - 'Connection closed by the server', - ))); - } - - return Attempt::result($message); - }); - } } From 7d90f671fc0d3650d6c16b9488ffa613749a6d15 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 8 Mar 2026 18:08:31 +0100 Subject: [PATCH 19/65] reuse the pipe for waiting/sending messages from the server side --- src/Server/Client.php | 100 ++++++++++-------------------------------- 1 file changed, 22 insertions(+), 78 deletions(-) diff --git a/src/Server/Client.php b/src/Server/Client.php index d204c79..d77551c 100644 --- a/src/Server/Client.php +++ b/src/Server/Client.php @@ -8,6 +8,7 @@ Message, Continuation, Server\Client\Stop, + Pipe, }; use Innmind\OperatingSystem\OperatingSystem; use Innmind\Signals\Signal; @@ -42,6 +43,11 @@ private function __construct( */ public function __invoke(OperatingSystem $os): Attempt { + $pipe = Pipe::of( + $this->client, + $this->protocol, + $os->clock(), + ); $identity = $this->monoid->identity(); $signaled = $os @@ -51,10 +57,6 @@ public function __invoke(OperatingSystem $os): Attempt $this->abort = true; }) ->map(static fn() => $identity); - $frame = $this->protocol->frame(); - // unwrapping is safe as it's internal messages - $heartbeat = $this->protocol->encode(Message::heartbeat())->unwrap(); - $ack = $this->protocol->encode(Message::ack())->unwrap(); // Use an infinite sequence to iteractively wait for a message to arrive // If the received one is a heartbeat we return a side effect, meaning @@ -76,15 +78,15 @@ public function __invoke(OperatingSystem $os): Attempt ->attempt( fn($identity, $val) => match (true) { $val instanceof Attempt => $val, - default => $this - ->wait() + default => $pipe + ->wait(fn() => $this->abort) ->flatMap( - fn($message) => $this - ->client - ->sink(Sequence::of($ack)) + fn($message) => $pipe + ->send(Sequence::of(Message::ack())) ->flatMap( /** @psalm-suppress MixedArgument Don't know why it loses the type */ fn() => $this->handle( + $pipe, $identity, $message, ), @@ -134,76 +136,18 @@ public static function of( * * @return Attempt */ - private function handle(mixed $identity, Message $message): Attempt - { - /** @psalm-suppress MixedArgument Don't know why it loses the type */ + private function handle( + Pipe $pipe, + mixed $identity, + Message $message + ): Attempt { return ($this->listen)($message, Continuation::new($identity), $identity)->match( - fn($carry, $messages) => $this->respond($carry, $messages), - fn($carry, $messages) => $this - ->respond($carry, $messages) - ->flatMap(static fn($carry) => Attempt::error(new Stop($carry))), + static fn($carry, $messages) => $pipe + ->send($messages) + ->map(static fn(): mixed => $carry), + static fn($carry, $messages) => $pipe + ->send($messages) + ->flatMap(static fn() => Attempt::error(new Stop($carry))), ); } - - /** - * @param T $carry - * @param Sequence $messages - * - * @return Attempt - */ - private function respond( - mixed $carry, - Sequence $messages, - ): Attempt { - $frame = $this->protocol->frame(); - // unwrapping is safe as it's internal messages - $heartbeat = $this->protocol->encode(Message::heartbeat())->unwrap(); - $ack = $this->protocol->encode(Message::ack())->unwrap(); - - return $messages - ->sink($carry) - ->attempt( - fn($carry, $message) => $this - ->protocol - ->encode($message) - ->map(Sequence::of(...)) - ->flatMap($this->client->sink(...)) - ->flatMap(fn() => $this->wait()) - ->flatMap(static fn($message) => match ($message->equals(Message::ack())) { - true => Attempt::result($carry), - false => Attempt::error(new \RuntimeException('Was expecting a message acknowledgement')), - }), - ); - } - - /** - * @return Attempt - */ - private function wait(): Attempt - { - // unwrapping is safe as it's internal messages - $heartbeat = $this->protocol->encode(Message::heartbeat())->unwrap(); - - // This is to avoid recursion. Otherwise for processes that wait for a - // long time it may reach the maximum call stack. - // todo find a more elegant way - do { - $result = $this - ->client - ->heartbeatWith(static fn() => Sequence::of($heartbeat)) - ->abortWhen(fn() => $this->abort) - ->frames($this->protocol->frame()) - ->one() - ->match( - static fn($message) => $message, - static fn($e) => $e, - ); - - if ($result instanceof \Throwable) { - return Attempt::error($result); - } - } while ($result->equals(Message::heartbeat())); - - return Attempt::result($result); - } } From dd4e56b294eef3ee94084b61ba5183dc66332b86 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 8 Mar 2026 18:15:34 +0100 Subject: [PATCH 20/65] implement connection handshake on server side --- src/Server/Client.php | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Server/Client.php b/src/Server/Client.php index d77551c..1adf25d 100644 --- a/src/Server/Client.php +++ b/src/Server/Client.php @@ -50,13 +50,22 @@ public function __invoke(OperatingSystem $os): Attempt ); $identity = $this->monoid->identity(); - $signaled = $os + $handshaked = $os ->process() ->signals() ->listen(Signal::terminate, function() { $this->abort = true; }) - ->map(static fn() => $identity); + ->flatMap(static fn() => $pipe->send( + Sequence::of(Message::connectionStart()), + )) + ->flatMap(fn() => $pipe->wait( + fn() => $this->abort, + )) + ->flatMap(static fn($message) => match ($message->equals(Message::connectionStartOk())) { + true => Attempt::result($identity), + false => Attempt::error(new \RuntimeException('Connection handshake failure')), + }); // Use an infinite sequence to iteractively wait for a message to arrive // If the received one is a heartbeat we return a side effect, meaning @@ -67,8 +76,8 @@ public function __invoke(OperatingSystem $os): Attempt // that by default ->one() will wait forever. // And since we use the sink pattern on the sequence, everything will // stop as soon any part of the system returns an error. - return Sequence::lazy(static function() use ($signaled, $identity) { - yield $signaled; + return Sequence::lazy(static function() use ($handshaked, $identity) { + yield $handshaked; while (true) { yield $identity; From 5ec7ca5b55c6a302b695d0dcccda560a8a187d81 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 8 Mar 2026 18:36:11 +0100 Subject: [PATCH 21/65] fix docblock --- src/Server.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Server.php b/src/Server.php index d2a7cf5..af040dd 100644 --- a/src/Server.php +++ b/src/Server.php @@ -55,7 +55,7 @@ public static function of( * @psalm-mutation-free * @template U * - * @param Monoid_ $carry + * @param Monoid_ $monoid * * @return self */ From b5ec4f3bba8cd0dcf3f6d23843f0c380bdf3aa9c Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 8 Mar 2026 18:36:27 +0100 Subject: [PATCH 22/65] CS --- src/Pipe.php | 2 +- src/Process.php | 1 - src/Server/Client.php | 3 +-- src/Server/Unstarted.php | 1 - 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Pipe.php b/src/Pipe.php index 3b4bfff..b0efaa2 100644 --- a/src/Pipe.php +++ b/src/Pipe.php @@ -38,7 +38,7 @@ public static function of( */ public function wait( callable $abort, - ?Period $timeout = null + ?Period $timeout = null, ): Attempt { $start = $this->clock->now(); // It's safe to unwrap as it's an internal message that never fails diff --git a/src/Process.php b/src/Process.php index dc0decb..ac43548 100644 --- a/src/Process.php +++ b/src/Process.php @@ -10,7 +10,6 @@ }; use Innmind\Time\{ Clock, - Point, Period, }; use Innmind\Immutable\{ diff --git a/src/Server/Client.php b/src/Server/Client.php index 1adf25d..91ccae2 100644 --- a/src/Server/Client.php +++ b/src/Server/Client.php @@ -16,7 +16,6 @@ use Innmind\Immutable\{ Sequence, Attempt, - SideEffect, Monoid, }; @@ -148,7 +147,7 @@ public static function of( private function handle( Pipe $pipe, mixed $identity, - Message $message + Message $message, ): Attempt { return ($this->listen)($message, Continuation::new($identity), $identity)->match( static fn($carry, $messages) => $pipe diff --git a/src/Server/Unstarted.php b/src/Server/Unstarted.php index c44bbac..d4e15a1 100644 --- a/src/Server/Unstarted.php +++ b/src/Server/Unstarted.php @@ -3,7 +3,6 @@ namespace Innmind\IPC\Server; -use Innmind\IPC\Protocol; use Innmind\OperatingSystem\OperatingSystem; use Innmind\IO\Sockets\{ Servers\Server, From ffa4a6c222fcef0160ae14600e9d39b24f8f1175 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 10:51:00 +0100 Subject: [PATCH 23/65] use a dedicated object to know if the stream must be aborted due to signals --- src/Abort.php | 27 +++++++++++++++++++++++++++ src/Server/Client.php | 32 +++++++++++++++++++++++++------- 2 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 src/Abort.php diff --git a/src/Abort.php b/src/Abort.php new file mode 100644 index 0000000..b2c1d4e --- /dev/null +++ b/src/Abort.php @@ -0,0 +1,27 @@ +value; + } + + public static function new(): self + { + return new self; + } + + public function enable(): void + { + $this->value = true; + } +} diff --git a/src/Server/Client.php b/src/Server/Client.php index 91ccae2..d65cc8c 100644 --- a/src/Server/Client.php +++ b/src/Server/Client.php @@ -9,6 +9,7 @@ Continuation, Server\Client\Stop, Pipe, + Abort, }; use Innmind\OperatingSystem\OperatingSystem; use Innmind\Signals\Signal; @@ -33,7 +34,7 @@ private function __construct( private Protocol $protocol, private Monoid $monoid, private \Closure $listen, - private bool $abort = false, + private Abort $abort, ) { } @@ -48,18 +49,17 @@ public function __invoke(OperatingSystem $os): Attempt $os->clock(), ); $identity = $this->monoid->identity(); + $abort = $this->abort->enable(...); $handshaked = $os ->process() ->signals() - ->listen(Signal::terminate, function() { - $this->abort = true; - }) + ->listen(Signal::terminate, $abort) ->flatMap(static fn() => $pipe->send( Sequence::of(Message::connectionStart()), )) ->flatMap(fn() => $pipe->wait( - fn() => $this->abort, + $this->abort, )) ->flatMap(static fn($message) => match ($message->equals(Message::connectionStartOk())) { true => Attempt::result($identity), @@ -87,7 +87,7 @@ public function __invoke(OperatingSystem $os): Attempt fn($identity, $val) => match (true) { $val instanceof Attempt => $val, default => $pipe - ->wait(fn() => $this->abort) + ->wait($this->abort) ->flatMap( fn($message) => $pipe ->send(Sequence::of(Message::ack())) @@ -119,6 +119,18 @@ public function __invoke(OperatingSystem $os): Attempt static fn() => Attempt::error($e), ), }, + ) + ->eitherWay( + static fn($value) => $os + ->process() + ->signals() + ->remove($abort) + ->map(static fn(): mixed => $value), + static fn($e) => $os + ->process() + ->signals() + ->remove($abort) + ->flatMap(static fn() => Attempt::error($e)), ); } @@ -136,7 +148,13 @@ public static function of( Monoid $monoid, \Closure $listen, ): self { - return new self($client, $protocol, $monoid, $listen); + return new self( + $client, + $protocol, + $monoid, + $listen, + Abort::new(), + ); } /** From 212e946d0a19fda613e3d2369e3a24438a2550e7 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 10:51:19 +0100 Subject: [PATCH 24/65] avoid capturing $this --- src/Server/Client.php | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Server/Client.php b/src/Server/Client.php index d65cc8c..30d31bf 100644 --- a/src/Server/Client.php +++ b/src/Server/Client.php @@ -49,18 +49,17 @@ public function __invoke(OperatingSystem $os): Attempt $os->clock(), ); $identity = $this->monoid->identity(); - $abort = $this->abort->enable(...); + $abort = $this->abort; + $enable = $this->abort->enable(...); $handshaked = $os ->process() ->signals() - ->listen(Signal::terminate, $abort) + ->listen(Signal::terminate, $enable) ->flatMap(static fn() => $pipe->send( Sequence::of(Message::connectionStart()), )) - ->flatMap(fn() => $pipe->wait( - $this->abort, - )) + ->flatMap(static fn() => $pipe->wait($abort)) ->flatMap(static fn($message) => match ($message->equals(Message::connectionStartOk())) { true => Attempt::result($identity), false => Attempt::error(new \RuntimeException('Connection handshake failure')), @@ -87,7 +86,7 @@ public function __invoke(OperatingSystem $os): Attempt fn($identity, $val) => match (true) { $val instanceof Attempt => $val, default => $pipe - ->wait($this->abort) + ->wait($abort) ->flatMap( fn($message) => $pipe ->send(Sequence::of(Message::ack())) @@ -124,12 +123,12 @@ public function __invoke(OperatingSystem $os): Attempt static fn($value) => $os ->process() ->signals() - ->remove($abort) + ->remove($enable) ->map(static fn(): mixed => $value), static fn($e) => $os ->process() ->signals() - ->remove($abort) + ->remove($enable) ->flatMap(static fn() => Attempt::error($e)), ); } From 4111a1dd4e3898d7382aa587298eac708719ba03 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 10:53:12 +0100 Subject: [PATCH 25/65] avoid capturing $this --- src/Server/Client.php | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Server/Client.php b/src/Server/Client.php index 30d31bf..fa7b39d 100644 --- a/src/Server/Client.php +++ b/src/Server/Client.php @@ -51,6 +51,7 @@ public function __invoke(OperatingSystem $os): Attempt $identity = $this->monoid->identity(); $abort = $this->abort; $enable = $this->abort->enable(...); + $listen = $this->listen; $handshaked = $os ->process() @@ -83,16 +84,17 @@ public function __invoke(OperatingSystem $os): Attempt }) ->sink($identity) ->attempt( - fn($identity, $val) => match (true) { + static fn($identity, $val) => match (true) { $val instanceof Attempt => $val, default => $pipe ->wait($abort) ->flatMap( - fn($message) => $pipe + static fn($message) => $pipe ->send(Sequence::of(Message::ack())) ->flatMap( /** @psalm-suppress MixedArgument Don't know why it loses the type */ - fn() => $this->handle( + static fn() => self::handle( + $listen, $pipe, $identity, $message, @@ -157,16 +159,20 @@ public static function of( } /** - * @param T $identity + * @template A * - * @return Attempt + * @param \Closure(Message, Continuation, A): Continuation $listen + * @param A $identity + * + * @return Attempt */ - private function handle( + private static function handle( + \Closure $listen, Pipe $pipe, mixed $identity, Message $message, ): Attempt { - return ($this->listen)($message, Continuation::new($identity), $identity)->match( + return $listen($message, Continuation::new($identity), $identity)->match( static fn($carry, $messages) => $pipe ->send($messages) ->map(static fn(): mixed => $carry), From b8d785cf122d8a2f17eae364846dafcb84b25974 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 10:55:25 +0100 Subject: [PATCH 26/65] rename Abort::new() to ::disabled() --- src/Abort.php | 2 +- src/Server/Client.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Abort.php b/src/Abort.php index b2c1d4e..ae6868b 100644 --- a/src/Abort.php +++ b/src/Abort.php @@ -15,7 +15,7 @@ public function __invoke(): bool return $this->value; } - public static function new(): self + public static function disabled(): self { return new self; } diff --git a/src/Server/Client.php b/src/Server/Client.php index fa7b39d..f115b25 100644 --- a/src/Server/Client.php +++ b/src/Server/Client.php @@ -154,7 +154,7 @@ public static function of( $protocol, $monoid, $listen, - Abort::new(), + Abort::disabled(), ); } From fc535b279d992ecbfaa5f2b4aca491d2fd5217a0 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 11:01:55 +0100 Subject: [PATCH 27/65] force the use of Abort in the Pipe --- src/Pipe.php | 6 ++---- src/Process.php | 4 +++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Pipe.php b/src/Pipe.php index b0efaa2..73422e2 100644 --- a/src/Pipe.php +++ b/src/Pipe.php @@ -32,12 +32,10 @@ public static function of( } /** - * @param callable(): bool $abort - * * @return Attempt */ public function wait( - callable $abort, + Abort $abort, ?Period $timeout = null, ): Attempt { $start = $this->clock->now(); @@ -108,7 +106,7 @@ public function send(Sequence $messages): Attempt ->encode($message) ->map(Sequence::of(...)) ->flatMap($this->socket->sink(...)) - ->flatMap(fn() => $this->wait(static fn() => false)) + ->flatMap(fn() => $this->wait(Abort::disabled())) ->flatMap(static fn($message) => match ($message->equals(Message::ack())) { true => Attempt::result(SideEffect::identity), false => Attempt::error(new \RuntimeException('Was expecting a message acknowledgement')), diff --git a/src/Process.php b/src/Process.php index ac43548..d58221f 100644 --- a/src/Process.php +++ b/src/Process.php @@ -23,6 +23,7 @@ final class Process private function __construct( private Client $socket, private Pipe $pipe, + private Abort $abort, ) { } @@ -46,6 +47,7 @@ public static function of( $protocol, $clock, ), + Abort::disabled(), )) ->flatMap( static fn($self) => $self @@ -78,7 +80,7 @@ public function send(Sequence $messages): Attempt public function wait(?Period $timeout = null): Attempt { return $this->pipe->wait( - static fn() => false, // todo handle signals ? + $this->abort, $timeout, ); } From 42009d6ddf880c6664fc450f59189d9b2e4add25 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 11:08:42 +0100 Subject: [PATCH 28/65] allow to listen for signals on the client side --- src/IPC.php | 3 +-- src/Process.php | 30 +++++++++++++++++++++--------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/IPC.php b/src/IPC.php index 718c766..ecbd759 100644 --- a/src/IPC.php +++ b/src/IPC.php @@ -94,9 +94,8 @@ public function connectTo( }, )) ->flatMap(fn() => Process::of( - $this->os->sockets(), + $this->os, $this->protocol, - $this->os->clock(), $this->addressOf($name), $this->heartbeat, )); diff --git a/src/Process.php b/src/Process.php index d58221f..aa73073 100644 --- a/src/Process.php +++ b/src/Process.php @@ -3,15 +3,13 @@ namespace Innmind\IPC; -use Innmind\OperatingSystem\Sockets; +use Innmind\OperatingSystem\OperatingSystem; use Innmind\IO\Sockets\{ Clients\Client, Unix\Address, }; -use Innmind\Time\{ - Clock, - Period, -}; +use Innmind\Signals\Signal; +use Innmind\Time\Period; use Innmind\Immutable\{ Sequence, Attempt, @@ -21,6 +19,7 @@ final class Process { private function __construct( + private OperatingSystem $os, private Client $socket, private Pipe $pipe, private Abort $abort, @@ -31,21 +30,22 @@ private function __construct( * @return Attempt */ public static function of( - Sockets $sockets, + OperatingSystem $os, Protocol $protocol, - Clock $clock, Address $address, Period $timeout, ): Attempt { - return $sockets + return $os + ->sockets() ->connectTo($address) ->map(static fn($client) => $client->timeoutAfter($timeout)) ->map(static fn($client) => new self( + $os, $client, Pipe::of( $client, $protocol, - $clock, + $os->clock(), ), Abort::disabled(), )) @@ -85,6 +85,18 @@ public function wait(?Period $timeout = null): Attempt ); } + /** + * @return Attempt + */ + public function listenSignals(): Attempt + { + return $this + ->os + ->process() + ->signals() + ->listen(Signal::terminate, $this->abort->enable(...)); + } + /** * @return Attempt */ From 1cdde0e00d705673b52cce3c645db6a57132a698 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 11:13:41 +0100 Subject: [PATCH 29/65] switch the methods on Abort to simplify removing the object as a listener --- src/Abort.php | 13 +++++++++---- src/Pipe.php | 2 +- src/Process.php | 2 +- src/Server/Client.php | 7 +++---- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/Abort.php b/src/Abort.php index ae6868b..773f03a 100644 --- a/src/Abort.php +++ b/src/Abort.php @@ -3,6 +3,11 @@ namespace Innmind\IPC; +use Innmind\Signals\{ + Signal, + Info, +}; + final class Abort { private function __construct( @@ -10,9 +15,9 @@ private function __construct( ) { } - public function __invoke(): bool + public function __invoke(Signal $signal, Info $info): void { - return $this->value; + $this->value = true; } public static function disabled(): self @@ -20,8 +25,8 @@ public static function disabled(): self return new self; } - public function enable(): void + public function enabled(): bool { - $this->value = true; + return $this->value; } } diff --git a/src/Pipe.php b/src/Pipe.php index 73422e2..808db4e 100644 --- a/src/Pipe.php +++ b/src/Pipe.php @@ -50,7 +50,7 @@ public function wait( ->socket ->heartbeatWith(static fn() => Sequence::of($heartbeat)) ->abortWhen(function() use ($abort, $start, $timeout) { - if ($abort()) { + if ($abort->enabled()) { return true; } diff --git a/src/Process.php b/src/Process.php index aa73073..b40321f 100644 --- a/src/Process.php +++ b/src/Process.php @@ -94,7 +94,7 @@ public function listenSignals(): Attempt ->os ->process() ->signals() - ->listen(Signal::terminate, $this->abort->enable(...)); + ->listen(Signal::terminate, $this->abort); } /** diff --git a/src/Server/Client.php b/src/Server/Client.php index f115b25..d756de3 100644 --- a/src/Server/Client.php +++ b/src/Server/Client.php @@ -50,13 +50,12 @@ public function __invoke(OperatingSystem $os): Attempt ); $identity = $this->monoid->identity(); $abort = $this->abort; - $enable = $this->abort->enable(...); $listen = $this->listen; $handshaked = $os ->process() ->signals() - ->listen(Signal::terminate, $enable) + ->listen(Signal::terminate, $abort) ->flatMap(static fn() => $pipe->send( Sequence::of(Message::connectionStart()), )) @@ -125,12 +124,12 @@ public function __invoke(OperatingSystem $os): Attempt static fn($value) => $os ->process() ->signals() - ->remove($enable) + ->remove($abort) ->map(static fn(): mixed => $value), static fn($e) => $os ->process() ->signals() - ->remove($enable) + ->remove($abort) ->flatMap(static fn() => Attempt::error($e)), ); } From 7b1c090fef4fe5c3610e5fe4a12541f3e6c8d9f8 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 11:15:11 +0100 Subject: [PATCH 30/65] remove the abort listener when the process is closed --- src/Process.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Process.php b/src/Process.php index b40321f..a336521 100644 --- a/src/Process.php +++ b/src/Process.php @@ -102,6 +102,22 @@ public function listenSignals(): Attempt */ public function close(): Attempt { - return $this->socket->close(); + return $this + ->socket + ->close() + ->eitherWay( + fn($value) => $this + ->os + ->process() + ->signals() + ->remove($this->abort) + ->map(static fn() => $value), + fn($e) => $this + ->os + ->process() + ->signals() + ->remove($this->abort) + ->flatMap(static fn() => Attempt::error($e)), + ); } } From d12d56741f5e563a016b5d1853436415f84f3e60 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 11:23:53 +0100 Subject: [PATCH 31/65] check for aborts when sending messages --- src/Pipe.php | 12 ++++++++---- src/Process.php | 5 ++++- src/Server/Client.php | 12 +++++++++--- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/Pipe.php b/src/Pipe.php index 808db4e..17c9892 100644 --- a/src/Pipe.php +++ b/src/Pipe.php @@ -96,8 +96,12 @@ public function wait( * * @return Attempt */ - public function send(Sequence $messages): Attempt - { + public function send( + Abort $abort, + Sequence $messages, + ): Attempt { + $socket = $this->socket->abortWhen(static fn() => $abort->enabled()); + return $messages ->sink(SideEffect::identity) ->attempt( @@ -105,8 +109,8 @@ public function send(Sequence $messages): Attempt ->protocol ->encode($message) ->map(Sequence::of(...)) - ->flatMap($this->socket->sink(...)) - ->flatMap(fn() => $this->wait(Abort::disabled())) + ->flatMap($socket->sink(...)) + ->flatMap(fn() => $this->wait($abort)) ->flatMap(static fn($message) => match ($message->equals(Message::ack())) { true => Attempt::result(SideEffect::identity), false => Attempt::error(new \RuntimeException('Was expecting a message acknowledgement')), diff --git a/src/Process.php b/src/Process.php index a336521..334d1a9 100644 --- a/src/Process.php +++ b/src/Process.php @@ -71,7 +71,10 @@ public static function of( */ public function send(Sequence $messages): Attempt { - return $this->pipe->send($messages); + return $this->pipe->send( + $this->abort, + $messages, + ); } /** diff --git a/src/Server/Client.php b/src/Server/Client.php index d756de3..3f1a7a7 100644 --- a/src/Server/Client.php +++ b/src/Server/Client.php @@ -57,6 +57,7 @@ public function __invoke(OperatingSystem $os): Attempt ->signals() ->listen(Signal::terminate, $abort) ->flatMap(static fn() => $pipe->send( + $abort, Sequence::of(Message::connectionStart()), )) ->flatMap(static fn() => $pipe->wait($abort)) @@ -89,11 +90,15 @@ public function __invoke(OperatingSystem $os): Attempt ->wait($abort) ->flatMap( static fn($message) => $pipe - ->send(Sequence::of(Message::ack())) + ->send( + $abort, + Sequence::of(Message::ack()), + ) ->flatMap( /** @psalm-suppress MixedArgument Don't know why it loses the type */ static fn() => self::handle( $listen, + $abort, $pipe, $identity, $message, @@ -167,16 +172,17 @@ public static function of( */ private static function handle( \Closure $listen, + Abort $abort, Pipe $pipe, mixed $identity, Message $message, ): Attempt { return $listen($message, Continuation::new($identity), $identity)->match( static fn($carry, $messages) => $pipe - ->send($messages) + ->send($abort, $messages) ->map(static fn(): mixed => $carry), static fn($carry, $messages) => $pipe - ->send($messages) + ->send($abort, $messages) ->flatMap(static fn() => Attempt::error(new Stop($carry))), ); } From 9458e4de386904a4e95b97a4e2cc4e23c6a86f57 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 11:30:35 +0100 Subject: [PATCH 32/65] move the abort logic inside the Pipe --- src/Pipe.php | 21 ++++++++++----------- src/Process.php | 15 ++++++--------- src/Server/Client.php | 28 +++++++++++----------------- 3 files changed, 27 insertions(+), 37 deletions(-) diff --git a/src/Pipe.php b/src/Pipe.php index 17c9892..27279b2 100644 --- a/src/Pipe.php +++ b/src/Pipe.php @@ -20,6 +20,7 @@ private function __construct( private Client $socket, private Protocol $protocol, private Clock $clock, + private Abort $abort, ) { } @@ -27,17 +28,17 @@ public static function of( Client $socket, Protocol $protocol, Clock $clock, + Abort $abort, ): self { - return new self($socket, $protocol, $clock); + return new self($socket, $protocol, $clock, $abort); } /** * @return Attempt */ - public function wait( - Abort $abort, - ?Period $timeout = null, - ): Attempt { + public function wait(?Period $timeout = null): Attempt + { + $abort = $this->abort; $start = $this->clock->now(); // It's safe to unwrap as it's an internal message that never fails $heartbeat = $this @@ -96,11 +97,9 @@ public function wait( * * @return Attempt */ - public function send( - Abort $abort, - Sequence $messages, - ): Attempt { - $socket = $this->socket->abortWhen(static fn() => $abort->enabled()); + public function send(Sequence $messages): Attempt + { + $socket = $this->socket->abortWhen($this->abort->enabled(...)); return $messages ->sink(SideEffect::identity) @@ -110,7 +109,7 @@ public function send( ->encode($message) ->map(Sequence::of(...)) ->flatMap($socket->sink(...)) - ->flatMap(fn() => $this->wait($abort)) + ->flatMap(fn() => $this->wait()) ->flatMap(static fn($message) => match ($message->equals(Message::ack())) { true => Attempt::result(SideEffect::identity), false => Attempt::error(new \RuntimeException('Was expecting a message acknowledgement')), diff --git a/src/Process.php b/src/Process.php index 334d1a9..3ad196b 100644 --- a/src/Process.php +++ b/src/Process.php @@ -35,6 +35,8 @@ public static function of( Address $address, Period $timeout, ): Attempt { + $abort = Abort::disabled(); + return $os ->sockets() ->connectTo($address) @@ -46,8 +48,9 @@ public static function of( $client, $protocol, $os->clock(), + $abort, ), - Abort::disabled(), + $abort, )) ->flatMap( static fn($self) => $self @@ -71,10 +74,7 @@ public static function of( */ public function send(Sequence $messages): Attempt { - return $this->pipe->send( - $this->abort, - $messages, - ); + return $this->pipe->send($messages); } /** @@ -82,10 +82,7 @@ public function send(Sequence $messages): Attempt */ public function wait(?Period $timeout = null): Attempt { - return $this->pipe->wait( - $this->abort, - $timeout, - ); + return $this->pipe->wait($timeout); } /** diff --git a/src/Server/Client.php b/src/Server/Client.php index 3f1a7a7..57e2af0 100644 --- a/src/Server/Client.php +++ b/src/Server/Client.php @@ -47,20 +47,19 @@ public function __invoke(OperatingSystem $os): Attempt $this->client, $this->protocol, $os->clock(), + $this->abort, ); $identity = $this->monoid->identity(); - $abort = $this->abort; $listen = $this->listen; $handshaked = $os ->process() ->signals() - ->listen(Signal::terminate, $abort) + ->listen(Signal::terminate, $this->abort) ->flatMap(static fn() => $pipe->send( - $abort, Sequence::of(Message::connectionStart()), )) - ->flatMap(static fn() => $pipe->wait($abort)) + ->flatMap(static fn() => $pipe->wait()) ->flatMap(static fn($message) => match ($message->equals(Message::connectionStartOk())) { true => Attempt::result($identity), false => Attempt::error(new \RuntimeException('Connection handshake failure')), @@ -87,18 +86,14 @@ public function __invoke(OperatingSystem $os): Attempt static fn($identity, $val) => match (true) { $val instanceof Attempt => $val, default => $pipe - ->wait($abort) + ->wait() ->flatMap( static fn($message) => $pipe - ->send( - $abort, - Sequence::of(Message::ack()), - ) + ->send(Sequence::of(Message::ack())) ->flatMap( /** @psalm-suppress MixedArgument Don't know why it loses the type */ static fn() => self::handle( $listen, - $abort, $pipe, $identity, $message, @@ -126,15 +121,15 @@ public function __invoke(OperatingSystem $os): Attempt }, ) ->eitherWay( - static fn($value) => $os + fn($value) => $os ->process() ->signals() - ->remove($abort) + ->remove($this->abort) ->map(static fn(): mixed => $value), - static fn($e) => $os + fn($e) => $os ->process() ->signals() - ->remove($abort) + ->remove($this->abort) ->flatMap(static fn() => Attempt::error($e)), ); } @@ -172,17 +167,16 @@ public static function of( */ private static function handle( \Closure $listen, - Abort $abort, Pipe $pipe, mixed $identity, Message $message, ): Attempt { return $listen($message, Continuation::new($identity), $identity)->match( static fn($carry, $messages) => $pipe - ->send($abort, $messages) + ->send($messages) ->map(static fn(): mixed => $carry), static fn($carry, $messages) => $pipe - ->send($abort, $messages) + ->send($messages) ->flatMap(static fn() => Attempt::error(new Stop($carry))), ); } From c8641907815b98630ddcde37612c27fd2edf594a Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 11:43:05 +0100 Subject: [PATCH 33/65] stop the server when it is signaled to terminate --- src/Server/Instance.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Server/Instance.php b/src/Server/Instance.php index b6ed6c0..31e9b47 100644 --- a/src/Server/Instance.php +++ b/src/Server/Instance.php @@ -7,6 +7,7 @@ Protocol, Continuation as Continuation_, Message, + Abort, }; use Innmind\Async\Scope; use Innmind\OperatingSystem\OperatingSystem; @@ -14,6 +15,7 @@ Servers\Server, Unix\Address, }; +use Innmind\Signals\Signal; use Innmind\Time\Period; use Innmind\Immutable\{ Attempt, @@ -34,6 +36,7 @@ final class Instance private function __construct( private Server|Unstarted $server, private Protocol $protocol, + private Abort $abort, private Period $timeout, private Monoid $monoid, private \Closure $monitor, @@ -59,6 +62,13 @@ public function __invoke( return $server; }) + ->map( + fn($server) => $os + ->process() + ->signals() + ->listen(Signal::terminate, $this->abort) + ->map(static fn() => $server), + ) ->match( static fn() => $continuation, static fn($e) => $continuation @@ -67,6 +77,12 @@ public function __invoke( ); } + if ($this->abort->enabled()) { + return $continuation + ->carryWith(Attempt::error(new \RuntimeException('Server signaled to terminate'))) + ->terminate(); + } + /** @var Sequence */ $all = Sequence::of(); /** @var Sequence> */ @@ -118,6 +134,7 @@ public static function of( $timeout, ), $protocol, + Abort::disabled(), $timeout, $monoid, $monitor, From d34fa797037ae3167f47c58f5e07a58091b44816 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 11:58:58 +0100 Subject: [PATCH 34/65] flag internals --- src/Abort.php | 6 ++++++ src/Continuation.php | 2 ++ src/Continuation/Next.php | 3 +++ src/Message.php | 18 ++++++++++++++++++ src/Message/Generic.php | 3 +++ src/Message/Implementation.php | 3 +++ src/Message/Protocol.php | 6 ++++++ src/Monoid.php | 1 + src/Pipe.php | 6 ++++++ src/Process.php | 2 ++ src/Protocol.php | 6 ++++++ src/Server.php | 2 ++ src/Server/Client.php | 2 ++ src/Server/Client/Stop.php | 3 +++ src/Server/Continuation.php | 2 ++ src/Server/Continuation/Next.php | 3 +++ src/Server/Instance.php | 2 ++ src/Server/Unstarted.php | 6 ++++++ 18 files changed, 76 insertions(+) diff --git a/src/Abort.php b/src/Abort.php index 773f03a..cfea633 100644 --- a/src/Abort.php +++ b/src/Abort.php @@ -8,6 +8,9 @@ Info, }; +/** + * @internal + */ final class Abort { private function __construct( @@ -20,6 +23,9 @@ public function __invoke(Signal $signal, Info $info): void $this->value = true; } + /** + * @internal + */ public static function disabled(): self { return new self; diff --git a/src/Continuation.php b/src/Continuation.php index 657ccc1..06631f3 100644 --- a/src/Continuation.php +++ b/src/Continuation.php @@ -24,6 +24,7 @@ private function __construct( } /** + * @internal * @psalm-pure * @template A * @@ -85,6 +86,7 @@ public function finish(): self } /** + * @internal * @template R * * @param callable(T, Sequence): R $continue diff --git a/src/Continuation/Next.php b/src/Continuation/Next.php index 658cfcd..755d14c 100644 --- a/src/Continuation/Next.php +++ b/src/Continuation/Next.php @@ -3,6 +3,9 @@ namespace Innmind\IPC\Continuation; +/** + * @internal + */ enum Next { case continue; diff --git a/src/Message.php b/src/Message.php index 7efa9d7..70297ee 100644 --- a/src/Message.php +++ b/src/Message.php @@ -23,31 +23,49 @@ public static function of(MediaType $mediaType, Str $content): self return new self(new Generic($mediaType, $content)); } + /** + * @internal + */ public static function connectionStart(): self { return new self(Protocol::connectionStart); } + /** + * @internal + */ public static function connectionStartOk(): self { return new self(Protocol::connectionStartOk); } + /** + * @internal + */ public static function connectionClose(): self { return new self(Protocol::connectionClose); } + /** + * @internal + */ public static function connectionCloseOk(): self { return new self(Protocol::connectionCloseOk); } + /** + * @internal + */ public static function heartbeat(): self { return new self(Protocol::heartbeat); } + /** + * @internal + */ public static function ack(): self { return new self(Protocol::ack); diff --git a/src/Message/Generic.php b/src/Message/Generic.php index 5056eb9..7eca668 100644 --- a/src/Message/Generic.php +++ b/src/Message/Generic.php @@ -6,6 +6,9 @@ use Innmind\MediaType\MediaType; use Innmind\Immutable\Str; +/** + * @internal + */ final class Generic implements Implementation { public function __construct( diff --git a/src/Message/Implementation.php b/src/Message/Implementation.php index 2fdd557..154974e 100644 --- a/src/Message/Implementation.php +++ b/src/Message/Implementation.php @@ -6,6 +6,9 @@ use Innmind\MediaType\MediaType; use Innmind\Immutable\Str; +/** + * @internal + */ interface Implementation { public function mediaType(): MediaType; diff --git a/src/Message/Protocol.php b/src/Message/Protocol.php index 23c7a1f..798b94e 100644 --- a/src/Message/Protocol.php +++ b/src/Message/Protocol.php @@ -10,6 +10,9 @@ }; use Innmind\Immutable\Str; +/** + * @internal + */ enum Protocol implements Implementation { case connectionStart; @@ -19,6 +22,9 @@ enum Protocol implements Implementation case heartbeat; case ack; + /** + * @internal + */ public static function parse(Str $content): ?self { foreach (self::cases() as $case) { diff --git a/src/Monoid.php b/src/Monoid.php index 65846fe..7ee8c43 100644 --- a/src/Monoid.php +++ b/src/Monoid.php @@ -9,6 +9,7 @@ }; /** + * @internal * @psalm-immutable * @implements Monoid_ */ diff --git a/src/Pipe.php b/src/Pipe.php index 27279b2..31d8ac2 100644 --- a/src/Pipe.php +++ b/src/Pipe.php @@ -14,6 +14,9 @@ SideEffect, }; +/** + * @internal + */ final class Pipe { private function __construct( @@ -24,6 +27,9 @@ private function __construct( ) { } + /** + * @internal + */ public static function of( Client $socket, Protocol $protocol, diff --git a/src/Process.php b/src/Process.php index 3ad196b..2bd3e31 100644 --- a/src/Process.php +++ b/src/Process.php @@ -27,6 +27,8 @@ private function __construct( } /** + * @internal + * * @return Attempt */ public static function of( diff --git a/src/Protocol.php b/src/Protocol.php index d369f3e..785272a 100644 --- a/src/Protocol.php +++ b/src/Protocol.php @@ -10,6 +10,9 @@ Attempt, }; +/** + * @internal + */ final class Protocol { /** @@ -20,6 +23,9 @@ private function __construct( ) { } + /** + * @internal + */ public static function binary(): self { $frame = Frame::chunk(2) diff --git a/src/Server.php b/src/Server.php index af040dd..2ea0bb0 100644 --- a/src/Server.php +++ b/src/Server.php @@ -33,6 +33,8 @@ private function __construct( } /** + * @internal + * * @return self */ public static function of( diff --git a/src/Server/Client.php b/src/Server/Client.php index 57e2af0..23b3130 100644 --- a/src/Server/Client.php +++ b/src/Server/Client.php @@ -22,6 +22,7 @@ /** * @template T + * @internal */ final class Client { @@ -135,6 +136,7 @@ public function __invoke(OperatingSystem $os): Attempt } /** + * @internal * @template A * * @param Monoid $monoid diff --git a/src/Server/Client/Stop.php b/src/Server/Client/Stop.php index 219a455..ed667e6 100644 --- a/src/Server/Client/Stop.php +++ b/src/Server/Client/Stop.php @@ -3,6 +3,9 @@ namespace Innmind\IPC\Server\Client; +/** + * @internal + */ final class Stop extends \Exception { public function __construct(private mixed $value) diff --git a/src/Server/Continuation.php b/src/Server/Continuation.php index ed1dc74..477c3aa 100644 --- a/src/Server/Continuation.php +++ b/src/Server/Continuation.php @@ -21,6 +21,7 @@ private function __construct( } /** + * @internal * @psalm-pure * @template A * @@ -55,6 +56,7 @@ public function finish(): self } /** + * @internal * @template R * * @param callable(T): R $continue diff --git a/src/Server/Continuation/Next.php b/src/Server/Continuation/Next.php index 695e001..582e16a 100644 --- a/src/Server/Continuation/Next.php +++ b/src/Server/Continuation/Next.php @@ -3,6 +3,9 @@ namespace Innmind\IPC\Server\Continuation; +/** + * @internal + */ enum Next { case continue; diff --git a/src/Server/Instance.php b/src/Server/Instance.php index 31e9b47..003eee8 100644 --- a/src/Server/Instance.php +++ b/src/Server/Instance.php @@ -24,6 +24,7 @@ }; /** + * @internal * @template T */ final class Instance @@ -112,6 +113,7 @@ public function __invoke( } /** + * @internal * @template A * * @param Monoid $monoid diff --git a/src/Server/Unstarted.php b/src/Server/Unstarted.php index d4e15a1..14cf879 100644 --- a/src/Server/Unstarted.php +++ b/src/Server/Unstarted.php @@ -11,6 +11,9 @@ use Innmind\Time\Period; use Innmind\Immutable\Attempt; +/** + * @internal + */ final class Unstarted { private function __construct( @@ -30,6 +33,9 @@ public function __invoke(OperatingSystem $os): Attempt ->map(fn($server) => $server->timeoutAfter($this->timeout)); } + /** + * @internal + */ public static function of( Address $address, Period $timeout, From 3f11b529bb6eda3b3de654beabdcc1f53382ec5a Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 12:00:25 +0100 Subject: [PATCH 35/65] CS --- src/Server.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Server.php b/src/Server.php index 2ea0bb0..9b1afc2 100644 --- a/src/Server.php +++ b/src/Server.php @@ -10,7 +10,7 @@ use Innmind\Immutable\{ Attempt, SideEffect, - Monoid as Monoid_, + Monoid, }; /** @@ -19,7 +19,7 @@ final class Server { /** - * @param Monoid_ $monoid + * @param Monoid $monoid * @param \Closure(T, Server\Continuation): Server\Continuation $monitor */ private function __construct( @@ -27,7 +27,7 @@ private function __construct( private Protocol $protocol, private Address $address, private Period $timeout, - private Monoid_ $monoid, + private Monoid $monoid, private \Closure $monitor, ) { } @@ -48,8 +48,8 @@ public static function of( $protocol, $address, $timeout, - Monoid::sideEffect, - self::defaultMonitor(Monoid::sideEffect), + namespace\Monoid::sideEffect, + self::defaultMonitor(namespace\Monoid::sideEffect), ); } @@ -57,11 +57,11 @@ public static function of( * @psalm-mutation-free * @template U * - * @param Monoid_ $monoid + * @param Monoid $monoid * * @return self */ - public function sink(Monoid_ $monoid): self + public function sink(Monoid $monoid): self { return new self( $this->os, @@ -120,11 +120,11 @@ public function with(callable $listen): Attempt * @psalm-pure * @template A * - * @param Monoid_ $monoid + * @param Monoid $monoid * * @return \Closure(A, Server\Continuation): Server\Continuation */ - private static function defaultMonitor(Monoid_ $monoid): \Closure + private static function defaultMonitor(Monoid $monoid): \Closure { return static fn($_, Server\Continuation $continuation) => $continuation; } From 06c202010ce9cf45dd849ee5b943988a44041995 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 12:04:39 +0100 Subject: [PATCH 36/65] flag pure methods/classes --- src/Continuation/Next.php | 1 + src/IPC.php | 5 +++++ src/Message.php | 12 ++++++++++++ src/Message/Generic.php | 1 + src/Message/Implementation.php | 1 + src/Message/Protocol.php | 1 + src/Process/Name.php | 7 +++++++ src/Protocol.php | 5 +++++ src/Server.php | 5 +++++ src/Server/Continuation/Next.php | 1 + 10 files changed, 39 insertions(+) diff --git a/src/Continuation/Next.php b/src/Continuation/Next.php index 755d14c..4d3a508 100644 --- a/src/Continuation/Next.php +++ b/src/Continuation/Next.php @@ -5,6 +5,7 @@ /** * @internal + * @psalm-immutable */ enum Next { diff --git a/src/IPC.php b/src/IPC.php index ecbd759..b54085a 100644 --- a/src/IPC.php +++ b/src/IPC.php @@ -102,6 +102,8 @@ public function connectTo( } /** + * @psalm-mutation-free + * * @return Server */ public function serve(Process\Name $name): Server @@ -114,6 +116,9 @@ public function serve(Process\Name $name): Server ); } + /** + * @psalm-mutation-free + */ private function addressOf(Process\Name $name): Address { return Address::of( diff --git a/src/Message.php b/src/Message.php index 70297ee..76ca9ae 100644 --- a/src/Message.php +++ b/src/Message.php @@ -11,6 +11,9 @@ use Innmind\MediaType\MediaType; use Innmind\Immutable\Str; +/** + * @psalm-immutable + */ final class Message { private function __construct( @@ -18,6 +21,9 @@ private function __construct( ) { } + /** + * @psalm-pure + */ public static function of(MediaType $mediaType, Str $content): self { return new self(new Generic($mediaType, $content)); @@ -25,6 +31,7 @@ public static function of(MediaType $mediaType, Str $content): self /** * @internal + * @psalm-pure */ public static function connectionStart(): self { @@ -33,6 +40,7 @@ public static function connectionStart(): self /** * @internal + * @psalm-pure */ public static function connectionStartOk(): self { @@ -41,6 +49,7 @@ public static function connectionStartOk(): self /** * @internal + * @psalm-pure */ public static function connectionClose(): self { @@ -49,6 +58,7 @@ public static function connectionClose(): self /** * @internal + * @psalm-pure */ public static function connectionCloseOk(): self { @@ -57,6 +67,7 @@ public static function connectionCloseOk(): self /** * @internal + * @psalm-pure */ public static function heartbeat(): self { @@ -65,6 +76,7 @@ public static function heartbeat(): self /** * @internal + * @psalm-pure */ public static function ack(): self { diff --git a/src/Message/Generic.php b/src/Message/Generic.php index 7eca668..34b4461 100644 --- a/src/Message/Generic.php +++ b/src/Message/Generic.php @@ -8,6 +8,7 @@ /** * @internal + * @psalm-immutable */ final class Generic implements Implementation { diff --git a/src/Message/Implementation.php b/src/Message/Implementation.php index 154974e..07dd4d3 100644 --- a/src/Message/Implementation.php +++ b/src/Message/Implementation.php @@ -8,6 +8,7 @@ /** * @internal + * @psalm-immutable */ interface Implementation { diff --git a/src/Message/Protocol.php b/src/Message/Protocol.php index 798b94e..d521e4a 100644 --- a/src/Message/Protocol.php +++ b/src/Message/Protocol.php @@ -12,6 +12,7 @@ /** * @internal + * @psalm-immutable */ enum Protocol implements Implementation { diff --git a/src/Process/Name.php b/src/Process/Name.php index 0e54ef9..6c6f020 100644 --- a/src/Process/Name.php +++ b/src/Process/Name.php @@ -8,6 +8,9 @@ Attempt, }; +/** + * @psalm-immutable + */ final class Name { /** @@ -18,6 +21,8 @@ private function __construct(private string $value) } /** + * @psalm-pure + * * @param literal-string $value * * @throws \DomainException @@ -28,6 +33,8 @@ public static function of(string $value): self } /** + * @psalm-pure + * * @return Attempt */ public static function attempt(string $value): Attempt diff --git a/src/Protocol.php b/src/Protocol.php index 785272a..52796bd 100644 --- a/src/Protocol.php +++ b/src/Protocol.php @@ -12,6 +12,7 @@ /** * @internal + * @psalm-immutable */ final class Protocol { @@ -25,6 +26,7 @@ private function __construct( /** * @internal + * @psalm-pure */ public static function binary(): self { @@ -128,6 +130,9 @@ public function frame(): Frame return $this->frame; } + /** + * @psalm-pure + */ private static function end(): int { return 0xCE; diff --git a/src/Server.php b/src/Server.php index 9b1afc2..ea326ef 100644 --- a/src/Server.php +++ b/src/Server.php @@ -19,6 +19,8 @@ final class Server { /** + * @psalm-mutation-free + * * @param Monoid $monoid * @param \Closure(T, Server\Continuation): Server\Continuation $monitor */ @@ -34,6 +36,7 @@ private function __construct( /** * @internal + * @psalm-pure * * @return self */ @@ -74,6 +77,8 @@ public function sink(Monoid $monoid): self } /** + * @psalm-mutation-free + * * @param callable(T, Server\Continuation): Server\Continuation $monitor * * @return self diff --git a/src/Server/Continuation/Next.php b/src/Server/Continuation/Next.php index 582e16a..2311c81 100644 --- a/src/Server/Continuation/Next.php +++ b/src/Server/Continuation/Next.php @@ -5,6 +5,7 @@ /** * @internal + * @psalm-immutable */ enum Next { From cc524a6c2e459a8322308ed7c29e1d910555852e Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 12:08:44 +0100 Subject: [PATCH 37/65] add NoDiscard on methods returning monads --- psalm.xml | 3 +++ src/Process.php | 4 ++++ src/Server.php | 1 + 3 files changed, 8 insertions(+) diff --git a/psalm.xml b/psalm.xml index 510148d..feccc34 100644 --- a/psalm.xml +++ b/psalm.xml @@ -14,4 +14,7 @@ + + + diff --git a/src/Process.php b/src/Process.php index 2bd3e31..9bde75b 100644 --- a/src/Process.php +++ b/src/Process.php @@ -74,6 +74,7 @@ public static function of( * * @return Attempt */ + #[\NoDiscard] public function send(Sequence $messages): Attempt { return $this->pipe->send($messages); @@ -82,6 +83,7 @@ public function send(Sequence $messages): Attempt /** * @return Attempt */ + #[\NoDiscard] public function wait(?Period $timeout = null): Attempt { return $this->pipe->wait($timeout); @@ -90,6 +92,7 @@ public function wait(?Period $timeout = null): Attempt /** * @return Attempt */ + #[\NoDiscard] public function listenSignals(): Attempt { return $this @@ -102,6 +105,7 @@ public function listenSignals(): Attempt /** * @return Attempt */ + #[\NoDiscard] public function close(): Attempt { return $this diff --git a/src/Server.php b/src/Server.php index ea326ef..9a3eca5 100644 --- a/src/Server.php +++ b/src/Server.php @@ -107,6 +107,7 @@ public function monitor(callable $monitor): self * * @return Attempt */ + #[\NoDiscard] public function with(callable $listen): Attempt { return Scheduler::of($this->os) From 768f169463014a318d0b60b677f21451daa52d21 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 14:40:16 +0100 Subject: [PATCH 38/65] make sure to read socket as ascii strings --- src/Pipe.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Pipe.php b/src/Pipe.php index 31d8ac2..fa7cf4d 100644 --- a/src/Pipe.php +++ b/src/Pipe.php @@ -12,6 +12,7 @@ Sequence, Attempt, SideEffect, + Str, }; /** @@ -55,6 +56,7 @@ public function wait(?Period $timeout = null): Attempt do { $result = $this ->socket + ->toEncoding(Str\Encoding::ascii) ->heartbeatWith(static fn() => Sequence::of($heartbeat)) ->abortWhen(function() use ($abort, $start, $timeout) { if ($abort->enabled()) { From 59f2d64d9bfcf9712b0751af32eecd2c25ee87f0 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 14:40:42 +0100 Subject: [PATCH 39/65] use tmp as default folder --- src/IPC.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/IPC.php b/src/IPC.php index b54085a..9ea8c81 100644 --- a/src/IPC.php +++ b/src/IPC.php @@ -30,9 +30,11 @@ private function __construct( public static function of( OperatingSystem $os, - Path $path, + ?Path $path = null, ?Period $heartbeat = null, ): self { + $path ??= $os->status()->tmp(); + return new self( $os, $os From 3b296463e6586df63aad3477c25b505449e4b82d Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 14:41:14 +0100 Subject: [PATCH 40/65] automatically create the folder used to store sockets --- src/IPC.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/IPC.php b/src/IPC.php index 9ea8c81..4128baa 100644 --- a/src/IPC.php +++ b/src/IPC.php @@ -7,6 +7,7 @@ use Innmind\Filesystem\{ Adapter, Name as FileName, + Recover, }; use Innmind\IO\Sockets\Unix\Address; use Innmind\Time\Period; @@ -40,6 +41,7 @@ public static function of( $os ->filesystem() ->mount($path) + ->recover(Recover::mount(...)) ->unwrap(), Protocol::binary(), $path, From 68ba287e4cc90b414c8897bfc2c55f3e351cb1e5 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 14:41:36 +0100 Subject: [PATCH 41/65] fix watching for server sockets files --- src/IPC.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/IPC.php b/src/IPC.php index 4128baa..ddfb38b 100644 --- a/src/IPC.php +++ b/src/IPC.php @@ -58,8 +58,15 @@ public function processes(): Sequence ->filesystem ->root() ->all() + ->map( + static fn($file) => $file + ->name() + ->str() + ->dropEnd(5) + ->toString(), + ) ->flatMap( - static fn($file) => Process\Name::attempt($file->name()->toString()) + static fn($file) => Process\Name::attempt($file) ->maybe() ->toSequence(), ); @@ -72,7 +79,7 @@ public function connectTo( Process\Name $name, ?Period $timeout = null, ): Attempt { - $file = FileName::of($name->toString()); + $file = FileName::of($name->toString().'.sock'); $start = $this->os->clock()->now(); return Sequence::lazy(function() use ($file) { From 9e10ca37a6fa145030913e927be1a0ac4b35f1fc Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 14:52:21 +0100 Subject: [PATCH 42/65] fix waiting for an ack that will never arrive --- src/Pipe.php | 14 ++++++++++++++ src/Server/Client.php | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Pipe.php b/src/Pipe.php index fa7cf4d..2978c43 100644 --- a/src/Pipe.php +++ b/src/Pipe.php @@ -124,4 +124,18 @@ public function send(Sequence $messages): Attempt }), ); } + + /** + * @return Attempt + */ + public function signal(Message $message): Attempt + { + $socket = $this->socket->abortWhen($this->abort->enabled(...)); + + return $this + ->protocol + ->encode($message) + ->map(Sequence::of(...)) + ->flatMap($socket->sink(...)); + } } diff --git a/src/Server/Client.php b/src/Server/Client.php index 23b3130..33eb4a2 100644 --- a/src/Server/Client.php +++ b/src/Server/Client.php @@ -57,8 +57,8 @@ public function __invoke(OperatingSystem $os): Attempt ->process() ->signals() ->listen(Signal::terminate, $this->abort) - ->flatMap(static fn() => $pipe->send( - Sequence::of(Message::connectionStart()), + ->flatMap(static fn() => $pipe->signal( + Message::connectionStart(), )) ->flatMap(static fn() => $pipe->wait()) ->flatMap(static fn($message) => match ($message->equals(Message::connectionStartOk())) { From e568ca7198e5bfcce57a93690fa0a7cfb0db6387 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 15:37:25 +0100 Subject: [PATCH 43/65] fix waiting for an ack message after a connection start ok --- src/Process.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Process.php b/src/Process.php index 9bde75b..208cfc0 100644 --- a/src/Process.php +++ b/src/Process.php @@ -64,7 +64,8 @@ public static function of( ) ->flatMap( static fn($self) => $self - ->send(Sequence::of(Message::connectionStartOk())) + ->pipe + ->signal(Message::connectionStartOk()) ->map(static fn() => $self), ); } From 963e810d6f8331d1d5878484ab9a99c385856b59 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 15:48:35 +0100 Subject: [PATCH 44/65] fix waiting to connect to the server process --- src/IPC.php | 63 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/src/IPC.php b/src/IPC.php index ddfb38b..9a4d5bf 100644 --- a/src/IPC.php +++ b/src/IPC.php @@ -80,36 +80,45 @@ public function connectTo( ?Period $timeout = null, ): Attempt { $file = FileName::of($name->toString().'.sock'); + $name = $this->addressOf($name); $start = $this->os->clock()->now(); + $timeout = $timeout?->asElapsedPeriod(); - return Sequence::lazy(function() use ($file) { - while (!$this->filesystem->contains($file)) { - yield $this->os->clock()->now(); + while (!$this->filesystem->contains($file)) { + $halted = $this + ->os + ->process() + ->halt($this->heartbeat) + ->match( + static fn($sideEffect) => $sideEffect, + static fn($e) => $e, + ); + + if ($halted !== SideEffect::identity) { + return Attempt::error($halted); } - }) - ->map( - fn($now) => $this - ->os - ->process() - ->halt($this->heartbeat) - ->map(static fn() => $now->elapsedSince($start)), - ) - ->sink(SideEffect::identity) - ->attempt(static fn($_, $halted) => $halted->flatMap( - static fn($elapsed) => match ($timeout) { - null => Attempt::result($_), - default => match ($elapsed->longerThan($timeout->asElapsedPeriod())) { - true => Attempt::error(new \RuntimeException('Timeout')), - false => Attempt::result($_), - }, - }, - )) - ->flatMap(fn() => Process::of( - $this->os, - $this->protocol, - $this->addressOf($name), - $this->heartbeat, - )); + + if (\is_null($timeout)) { + continue; + } + + $elapsed = $this + ->os + ->clock() + ->now() + ->elapsedSince($start); + + if ($elapsed->longerThan($timeout)) { + return Attempt::error(new \RuntimeException('Timeout')); + } + } + + return Process::of( + $this->os, + $this->protocol, + $name, + $this->heartbeat, + ); } /** From 8a4cf9335fac8bb118ff536aba677dd6720f283e Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 15:49:07 +0100 Subject: [PATCH 45/65] add server fixture --- .php-cs-fixer.dist.php | 1 + fixtures/server.php | 33 +++++++++++++++++++++++++++++++++ psalm.xml | 1 + 3 files changed, 35 insertions(+) create mode 100644 fixtures/server.php diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 021fd34..87727dc 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -1,6 +1,7 @@ status()->tmp()->resolve(Path::of('innnmind/ipc/')), +) + ->serve(Process\Name::of('server')) + ->with(static function($message, $continuation) { + \sleep((int) $message->content()->toString()); + + return $continuation->respond(Message::of( + MediaType::from(TopLevel::text, 'plain'), + Str::of('ack'), + )); + }) + ->unwrap(); diff --git a/psalm.xml b/psalm.xml index feccc34..151f7b6 100644 --- a/psalm.xml +++ b/psalm.xml @@ -10,6 +10,7 @@ > + From c923c93ae1f75275a22dc3bb4f7fcabec3e4070b Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 16:06:44 +0100 Subject: [PATCH 46/65] add test to check for wait timeout --- fixtures/server.php | 1 + proofs/client.php | 70 +++++++++++++++++++++++++++++++++++++++++++++ proofs/proof.php | 9 ------ 3 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 proofs/client.php delete mode 100644 proofs/proof.php diff --git a/fixtures/server.php b/fixtures/server.php index 2e4a8d3..a3c6887 100644 --- a/fixtures/server.php +++ b/fixtures/server.php @@ -16,6 +16,7 @@ }; use Innmind\Immutable\Str; +echo 'starting'; $os = OperatingSystem::new(); $_ = IPC::of( $os, diff --git a/proofs/client.php b/proofs/client.php new file mode 100644 index 0000000..f6d69c7 --- /dev/null +++ b/proofs/client.php @@ -0,0 +1,70 @@ +control() + ->processes() + ->execute( + Command::foreground('php') + ->withArgument('fixtures/server.php') + ->withEnvironment('TMPDIR', $os->status()->tmp()->toString()) + ->withEnvironment('PATH', $_SERVER['PATH']) + ->withWorkingDirectory(Path::of(__DIR__.'/../')), + ) + ->unwrap(); + // to make sure the server is started + $_ = $process + ->output() + ->take(1) + ->memoize() + ->toList(); + \sleep(1); + + yield test( + 'Client wait timeout', + static function($assert) use ($os) { + $process = IPC::of( + $os, + $os->status()->tmp()->resolve(Path::of('innnmind/ipc/')), + ) + ->connectTo( + Process\Name::of('server'), + Period::second(1), + ) + ->unwrap(); + + $assert->false($process->wait(Period::millisecond(500))->match( + static fn() => true, + static fn() => false, + )); + + $assert->true($process->close()->match( + static fn() => true, + static fn() => false, + )); + }, + ); + + $_ = $process->pid()->match( + static fn($pid) => $os + ->control() + ->processes() + ->kill($pid, Signal::kill) + ->unwrap(), + static fn() => null, + ); +}; diff --git a/proofs/proof.php b/proofs/proof.php deleted file mode 100644 index 187082d..0000000 --- a/proofs/proof.php +++ /dev/null @@ -1,9 +0,0 @@ - $assert->true(true), - ); -}; From a15292bce44818a5aeabb05fa0f941cc6a9ebf36 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 17:35:09 +0100 Subject: [PATCH 47/65] expect closure handshake when closing the process --- src/Process.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Process.php b/src/Process.php index 208cfc0..d2e8c70 100644 --- a/src/Process.php +++ b/src/Process.php @@ -110,8 +110,20 @@ public function listenSignals(): Attempt public function close(): Attempt { return $this - ->socket - ->close() + ->pipe + ->signal(Message::connectionClose()) + ->flatMap(fn() => $this->pipe->wait()) + ->flatMap(static fn($message) => match ($message->equals(Message::connectionCloseOk())) { + true => Attempt::result(SideEffect::identity), + false => Attempt::error(new \RuntimeException('Connection handshake failure')), + }) + ->eitherWay( + fn() => $this->socket->close(), + fn($e) => $this + ->socket + ->close() + ->flatMap(static fn() => Attempt::error($e)), + ) ->eitherWay( fn($value) => $this ->os From dbc8647f9baaa1c9b4492204146aa7ba6b367b74 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 17:45:09 +0100 Subject: [PATCH 48/65] correctly handle when the client ask to close the connection --- src/Exception/ConnectionProperlyClosed.php | 42 ++++++++++++++++++++++ src/Pipe.php | 5 ++- src/Server/Client.php | 10 ++++-- 3 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 src/Exception/ConnectionProperlyClosed.php diff --git a/src/Exception/ConnectionProperlyClosed.php b/src/Exception/ConnectionProperlyClosed.php new file mode 100644 index 0000000..7faf413 --- /dev/null +++ b/src/Exception/ConnectionProperlyClosed.php @@ -0,0 +1,42 @@ + + */ + public function with(mixed $value): self + { + return new self($value); + } + + /** + * @return T + */ + public function unwrap(): mixed + { + return $this->value; + } +} diff --git a/src/Pipe.php b/src/Pipe.php index 2978c43..a922dff 100644 --- a/src/Pipe.php +++ b/src/Pipe.php @@ -3,6 +3,7 @@ namespace Innmind\IPC; +use Innmind\IPC\Exception\ConnectionProperlyClosed; use Innmind\IO\Sockets\Clients\Client; use Innmind\Time\{ Clock, @@ -92,9 +93,7 @@ public function wait(?Period $timeout = null): Attempt ->map(Sequence::of(...)) ->flatMap($this->socket->sink(...)) ->flatMap(fn() => $this->socket->close()) - ->flatMap(static fn() => Attempt::error(new \RuntimeException( - 'Connection closed by the other side', - ))); + ->flatMap(static fn() => Attempt::error(new ConnectionProperlyClosed)); } return Attempt::result($result); diff --git a/src/Server/Client.php b/src/Server/Client.php index 33eb4a2..7bb9967 100644 --- a/src/Server/Client.php +++ b/src/Server/Client.php @@ -10,6 +10,7 @@ Server\Client\Stop, Pipe, Abort, + Exception\ConnectionProperlyClosed, }; use Innmind\OperatingSystem\OperatingSystem; use Innmind\Signals\Signal; @@ -100,12 +101,17 @@ public function __invoke(OperatingSystem $os): Attempt $message, ), ), - ), + ) + ->mapError(static fn($e) => match (true) { + $e instanceof ConnectionProperlyClosed => $e->with($identity), + default => $e, + }), }, ) ->recover( fn($e) => match (true) { - $e instanceof Stop => $this + $e instanceof Stop, + $e instanceof ConnectionProperlyClosed => $this ->client ->close() ->match( // make sure to keep the user provided value From 0c7531a65c8be463bf817bdb0f8f40589e92a0f2 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 18:01:23 +0100 Subject: [PATCH 49/65] test wait for message --- fixtures/server.php | 5 +---- proofs/client.php | 52 +++++++++++++++++++++++++++++++++++++++++++ src/Server/Client.php | 2 +- 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/fixtures/server.php b/fixtures/server.php index a3c6887..0c26273 100644 --- a/fixtures/server.php +++ b/fixtures/server.php @@ -14,7 +14,6 @@ MediaType, TopLevel, }; -use Innmind\Immutable\Str; echo 'starting'; $os = OperatingSystem::new(); @@ -24,11 +23,9 @@ ) ->serve(Process\Name::of('server')) ->with(static function($message, $continuation) { - \sleep((int) $message->content()->toString()); - return $continuation->respond(Message::of( MediaType::from(TopLevel::text, 'plain'), - Str::of('ack'), + $message->content()->prepend('ack from server : '), )); }) ->unwrap(); diff --git a/proofs/client.php b/proofs/client.php index f6d69c7..6525c2b 100644 --- a/proofs/client.php +++ b/proofs/client.php @@ -4,14 +4,24 @@ use Innmind\IPC\{ IPC, Process, + Message, }; use Innmind\OperatingSystem\OperatingSystem; use Innmind\Server\Control\Server\{ Command, Signal, }; +use Innmind\MediaType\{ + MediaType, + TopLevel, +}; use Innmind\Url\Path; use Innmind\Time\Period; +use Innmind\Immutable\{ + Str, + Sequence, + SideEffect, +}; return static function() { $os = OperatingSystem::new(); @@ -59,6 +69,48 @@ static function($assert) use ($os) { }, ); + yield test( + 'Client wait for message', + static function($assert) use ($os) { + $process = IPC::of( + $os, + $os->status()->tmp()->resolve(Path::of('innnmind/ipc/')), + ) + ->connectTo( + Process\Name::of('server'), + Period::second(1), + ) + ->unwrap(); + + $assert->same( + SideEffect::identity, + $process + ->send(Sequence::of(Message::of( + MediaType::from(TopLevel::text, 'plain'), + Str::of('1'), + ))) + ->match( + static fn($value) => $value, + static fn() => null, + ), + ); + $assert->same( + 'ack from server : 1', + $process + ->wait() + ->match( + static fn($message) => $message->content()->toString(), + static fn() => null, + ), + ); + + $assert->true($process->close()->match( + static fn() => true, + static fn() => false, + )); + }, + ); + $_ = $process->pid()->match( static fn($pid) => $os ->control() diff --git a/src/Server/Client.php b/src/Server/Client.php index 7bb9967..4571ba6 100644 --- a/src/Server/Client.php +++ b/src/Server/Client.php @@ -91,7 +91,7 @@ public function __invoke(OperatingSystem $os): Attempt ->wait() ->flatMap( static fn($message) => $pipe - ->send(Sequence::of(Message::ack())) + ->signal(Message::ack()) ->flatMap( /** @psalm-suppress MixedArgument Don't know why it loses the type */ static fn() => self::handle( From 7f04b341469e5bf4368414fb3e479761814b1f3c Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 18:34:11 +0100 Subject: [PATCH 50/65] always take over the server socket --- src/Server/Unstarted.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Server/Unstarted.php b/src/Server/Unstarted.php index 14cf879..f7c669e 100644 --- a/src/Server/Unstarted.php +++ b/src/Server/Unstarted.php @@ -29,7 +29,7 @@ public function __invoke(OperatingSystem $os): Attempt { return $os ->sockets() - ->open($this->address) + ->takeOver($this->address) ->map(fn($server) => $server->timeoutAfter($this->timeout)); } From 1312f09964fc94acfb6f59d01eb27be3fc40f8e3 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 18:35:22 +0100 Subject: [PATCH 51/65] call the monitor only when the carried value changed based on clients results --- src/Server/Instance.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Server/Instance.php b/src/Server/Instance.php index 003eee8..c694ec5 100644 --- a/src/Server/Instance.php +++ b/src/Server/Instance.php @@ -95,6 +95,16 @@ public function __invoke( ->attempt(static fn($all, $result) => $result->map($all)) ->map(fn($results) => $results->fold($this->monoid)); + if ($continuation->results()->empty()) { + /** @psalm-suppress MixedArgument Don't know why it loses the type */ + return $carry->match( + fn($carry) => $this->listen($carry, $continuation), + static fn() => $continuation + ->carryWith($carry) + ->terminate(), + ); + } + /** @psalm-suppress MixedArgument Don't know why it loses the type */ return $carry->match( fn($carry) => ($this->monitor)($carry, Continuation::new($carry))->match( From ac9b9e768b4990657dc95ce02080c4e9ae94b896 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 18:39:56 +0100 Subject: [PATCH 52/65] test the server can accept connections --- fixtures/client.php | 46 ++++++++++++++++++++++++++++ proofs/server.php | 74 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 fixtures/client.php create mode 100644 proofs/server.php diff --git a/fixtures/client.php b/fixtures/client.php new file mode 100644 index 0000000..8173143 --- /dev/null +++ b/fixtures/client.php @@ -0,0 +1,46 @@ +status()->tmp()->resolve(Path::of('innnmind/ipc/')), +) + ->connectTo(Process\Name::of('server')) + ->flatMap( + static fn($process) => $process + ->send(Sequence::of(Message::of( + MediaType::from(TopLevel::text, 'plain'), + Str::of('hello world'), + ))) + ->map(static fn() => $process), + ) + ->flatMap( + static fn($process) => $process + ->wait() + ->flatMap( + static fn($message) => $process + ->close() + ->map(static fn() => $message), + ), + ) + ->unwrap(); +echo $message->content()->toString(); diff --git a/proofs/server.php b/proofs/server.php new file mode 100644 index 0000000..bf2b92f --- /dev/null +++ b/proofs/server.php @@ -0,0 +1,74 @@ +control() + ->processes() + ->execute( + Command::foreground('sleep 2 && php fixtures/client.php') + ->withArgument('fixtures/client.php') + ->withEnvironment('TMPDIR', $os->status()->tmp()->toString()) + ->withEnvironment('PATH', $_SERVER['PATH']) + ->withWorkingDirectory(Path::of(__DIR__.'/../')), + ) + ->unwrap(); + + $result = IPC::of( + $os, + $os->status()->tmp()->resolve(Path::of('innnmind/ipc/')), + ) + ->serve(Process\Name::of('server')) + ->monitor(static fn($_, $continuation) => $continuation->finish()) + ->with(static function($message, $continuation) use ($assert) { + $assert->same( + 'hello world', + $message->content()->toString(), + ); + + return $continuation + ->respond(Message::of( + MediaType::from(TopLevel::text, 'plain'), + Str::of('some output'), + )) + ->finish(); + }) + ->match( + static fn() => true, + static fn() => false, + ); + + $assert->true($result); + $assert->same( + 'some output', + $process + ->output() + ->map(static fn($chunk) => $chunk->data()) + ->fold(Concat::monoid) + ->toString(), + ); + }, + ); +}; From 53533ef476f7ddcad1429a03ab98c8ac6b8c396e Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 18:57:55 +0100 Subject: [PATCH 53/65] fix losing the carried value when the client properly closes the connection when we respond with a message --- proofs/server.php | 29 +++++++++++++++-------------- src/Server/Client.php | 8 ++++++++ 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/proofs/server.php b/proofs/server.php index bf2b92f..6d4882f 100644 --- a/proofs/server.php +++ b/proofs/server.php @@ -36,31 +36,32 @@ static function($assert) use ($os) { ) ->unwrap(); - $result = IPC::of( + $output = IPC::of( $os, $os->status()->tmp()->resolve(Path::of('innnmind/ipc/')), ) ->serve(Process\Name::of('server')) - ->monitor(static fn($_, $continuation) => $continuation->finish()) - ->with(static function($message, $continuation) use ($assert) { - $assert->same( - 'hello world', - $message->content()->toString(), - ); - - return $continuation + ->sink(Concat::monoid) + ->monitor( + static fn($result, $continuation) => $continuation + ->carryWith($result) + ->finish(), + ) + ->with( + static fn($message, $continuation) => $continuation + ->carryWith($message->content()) ->respond(Message::of( MediaType::from(TopLevel::text, 'plain'), Str::of('some output'), )) - ->finish(); - }) + ->finish(), + ) ->match( - static fn() => true, - static fn() => false, + static fn($output) => $output->toString(), + static fn() => null, ); - $assert->true($result); + $assert->same('hello world', $output); $assert->same( 'some output', $process diff --git a/src/Server/Client.php b/src/Server/Client.php index 4571ba6..52a2468 100644 --- a/src/Server/Client.php +++ b/src/Server/Client.php @@ -182,9 +182,17 @@ private static function handle( return $listen($message, Continuation::new($identity), $identity)->match( static fn($carry, $messages) => $pipe ->send($messages) + ->mapError(static fn($e) => match (true) { + $e instanceof ConnectionProperlyClosed => new Stop($carry), + default => $e, + }) ->map(static fn(): mixed => $carry), static fn($carry, $messages) => $pipe ->send($messages) + ->mapError(static fn($e) => match (true) { + $e instanceof ConnectionProperlyClosed => new Stop($carry), + default => $e, + }) ->flatMap(static fn() => Attempt::error(new Stop($carry))), ); } From 245d0ae1bd66b412b2e5858bc63c93f0ad38e645 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 19:04:51 +0100 Subject: [PATCH 54/65] generate messages to prove it is not hardcoded behaviour --- proofs/client.php | 12 ++++++++---- proofs/server.php | 12 ++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/proofs/client.php b/proofs/client.php index 6525c2b..4bda046 100644 --- a/proofs/client.php +++ b/proofs/client.php @@ -22,6 +22,7 @@ Sequence, SideEffect, }; +use Innmind\BlackBox\Set; return static function() { $os = OperatingSystem::new(); @@ -69,9 +70,12 @@ static function($assert) use ($os) { }, ); - yield test( + yield proof( 'Client wait for message', - static function($assert) use ($os) { + given( + Set::strings()->between(1, 10), + ), + static function($assert, $message) use ($os) { $process = IPC::of( $os, $os->status()->tmp()->resolve(Path::of('innnmind/ipc/')), @@ -87,7 +91,7 @@ static function($assert) use ($os) { $process ->send(Sequence::of(Message::of( MediaType::from(TopLevel::text, 'plain'), - Str::of('1'), + Str::of($message), ))) ->match( static fn($value) => $value, @@ -95,7 +99,7 @@ static function($assert) use ($os) { ), ); $assert->same( - 'ack from server : 1', + 'ack from server : '.$message, $process ->wait() ->match( diff --git a/proofs/server.php b/proofs/server.php index 6d4882f..5093156 100644 --- a/proofs/server.php +++ b/proofs/server.php @@ -17,13 +17,17 @@ Str, Monoid\Concat, }; +use Innmind\BlackBox\Set; return static function() { $os = OperatingSystem::new(); - yield test( + yield proof( 'Server wait for client', - static function($assert) use ($os) { + given( + Set::strings()->between(1, 10), + ), + static function($assert, $response) use ($os) { $process = $os ->control() ->processes() @@ -52,7 +56,7 @@ static function($assert) use ($os) { ->carryWith($message->content()) ->respond(Message::of( MediaType::from(TopLevel::text, 'plain'), - Str::of('some output'), + Str::of($response), )) ->finish(), ) @@ -63,7 +67,7 @@ static function($assert) use ($os) { $assert->same('hello world', $output); $assert->same( - 'some output', + $response, $process ->output() ->map(static fn($chunk) => $chunk->data()) From b4822998323486af5a691e1218f61a45301d33c3 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 19:17:57 +0100 Subject: [PATCH 55/65] test for processes --- proofs/client.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/proofs/client.php b/proofs/client.php index 4bda046..ef7bc53 100644 --- a/proofs/client.php +++ b/proofs/client.php @@ -115,6 +115,24 @@ static function($assert, $message) use ($os) { }, ); + yield test( + 'Processes', + static function($assert) use ($os) { + $processes = IPC::of( + $os, + $os->status()->tmp()->resolve(Path::of('innnmind/ipc/')), + ) + ->processes() + ->map(static fn($name) => $name->toString()) + ->toList(); + + $assert->same( + ['server'], + $processes, + ); + }, + ); + $_ = $process->pid()->match( static fn($pid) => $os ->control() From e86a43460d5368f62e20201c58f4ce7b8baef295 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 19:20:05 +0100 Subject: [PATCH 56/65] test defautl folding --- proofs/server.php | 49 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/proofs/server.php b/proofs/server.php index 5093156..5ae4857 100644 --- a/proofs/server.php +++ b/proofs/server.php @@ -15,6 +15,7 @@ use Innmind\Url\Path; use Innmind\Immutable\{ Str, + SideEffect, Monoid\Concat, }; use Innmind\BlackBox\Set; @@ -22,6 +23,54 @@ return static function() { $os = OperatingSystem::new(); + yield test( + 'Server default value folding', + static function($assert) use ($os) { + $process = $os + ->control() + ->processes() + ->execute( + Command::foreground('sleep 2 && php fixtures/client.php') + ->withArgument('fixtures/client.php') + ->withEnvironment('TMPDIR', $os->status()->tmp()->toString()) + ->withEnvironment('PATH', $_SERVER['PATH']) + ->withWorkingDirectory(Path::of(__DIR__.'/../')), + ) + ->unwrap(); + + $result = IPC::of( + $os, + $os->status()->tmp()->resolve(Path::of('innnmind/ipc/')), + ) + ->serve(Process\Name::of('server')) + ->monitor( + static fn($result, $continuation) => $continuation->finish(), + ) + ->with( + static fn($message, $continuation) => $continuation + ->respond(Message::of( + MediaType::from(TopLevel::text, 'plain'), + Str::of('some output'), + )) + ->finish(), + ) + ->match( + static fn($result) => $result, + static fn() => null, + ); + + $assert->same(SideEffect::identity, $result); + $assert->same( + 'some output', + $process + ->output() + ->map(static fn($chunk) => $chunk->data()) + ->fold(Concat::monoid) + ->toString(), + ); + }, + ); + yield proof( 'Server wait for client', given( From dd5ea41aa3b1fc34b0e5a3fc0e4a63d2e0beb4fa Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 19:28:15 +0100 Subject: [PATCH 57/65] test connection timeout --- proofs/client.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/proofs/client.php b/proofs/client.php index ef7bc53..3647924 100644 --- a/proofs/client.php +++ b/proofs/client.php @@ -45,6 +45,26 @@ ->toList(); \sleep(1); + yield test( + 'Client connection timeout', + static function($assert) use ($os) { + $result = IPC::of( + $os, + $os->status()->tmp()->resolve(Path::of('innnmind/ipc/')), + ) + ->connectTo( + Process\Name::of('unknown'), + Period::second(1), + ) + ->match( + static fn() => null, + static fn($e) => $e->getMessage(), + ); + + $assert->same('Timeout', $result); + }, + ); + yield test( 'Client wait timeout', static function($assert) use ($os) { From e046f402d23e637c76e3e86e3de585113c3ed437 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 19:32:05 +0100 Subject: [PATCH 58/65] test listening for signals --- proofs/client.php | 52 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/proofs/client.php b/proofs/client.php index 3647924..442caed 100644 --- a/proofs/client.php +++ b/proofs/client.php @@ -135,6 +135,58 @@ static function($assert, $message) use ($os) { }, ); + yield proof( + 'Client listening for signals doesnt change behaviour', + given( + Set::strings()->between(1, 10), + ), + static function($assert, $message) use ($os) { + $process = IPC::of( + $os, + $os->status()->tmp()->resolve(Path::of('innnmind/ipc/')), + ) + ->connectTo( + Process\Name::of('server'), + Period::second(1), + ) + ->unwrap(); + + $assert->same( + SideEffect::identity, + $process->listenSignals()->match( + static fn($sideEffect) => $sideEffect, + static fn() => null, + ), + ); + $assert->same( + SideEffect::identity, + $process + ->send(Sequence::of(Message::of( + MediaType::from(TopLevel::text, 'plain'), + Str::of($message), + ))) + ->match( + static fn($value) => $value, + static fn() => null, + ), + ); + $assert->same( + 'ack from server : '.$message, + $process + ->wait() + ->match( + static fn($message) => $message->content()->toString(), + static fn() => null, + ), + ); + + $assert->true($process->close()->match( + static fn() => true, + static fn() => false, + )); + }, + ); + yield test( 'Processes', static function($assert) use ($os) { From 9bffabe8d166b16512cf84a5a33d68e714b915e3 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 19:35:40 +0100 Subject: [PATCH 59/65] test message equality --- proofs/message.php | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 proofs/message.php diff --git a/proofs/message.php b/proofs/message.php new file mode 100644 index 0000000..b3fb134 --- /dev/null +++ b/proofs/message.php @@ -0,0 +1,41 @@ +map(Str::of(...)), + Set::strings()->map(Str::of(...)), + ), + static function($assert, $mediaTypeA, $mediaTypeB, $a, $b) { + $assert->true( + Message::of($mediaTypeA, $a)->equals( + Message::of($mediaTypeA, $a), + ), + ); + $assert->false( + Message::of($mediaTypeA, $a)->equals( + Message::of($mediaTypeA, $b), + ), + ); + $assert->false( + Message::of($mediaTypeA, $a)->equals( + Message::of($mediaTypeB, $a), + ), + ); + $assert->false( + Message::of($mediaTypeA, $a)->equals( + Message::of($mediaTypeB, $b), + ), + ); + }, + ); +}; From 8e740d223413ef2138cacde2ef178f650e9ee3bf Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 14 Mar 2026 19:48:09 +0100 Subject: [PATCH 60/65] test client quick connection close --- fixtures/close-client.php | 20 +++++++++++++++++++ proofs/server.php | 41 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 fixtures/close-client.php diff --git a/fixtures/close-client.php b/fixtures/close-client.php new file mode 100644 index 0000000..5daddf6 --- /dev/null +++ b/fixtures/close-client.php @@ -0,0 +1,20 @@ +status()->tmp()->resolve(Path::of('innnmind/ipc/')), +) + ->connectTo(Process\Name::of('server')) + ->flatMap(static fn($process) => $process->close()) + ->unwrap(); diff --git a/proofs/server.php b/proofs/server.php index 5ae4857..f5b59a8 100644 --- a/proofs/server.php +++ b/proofs/server.php @@ -71,6 +71,47 @@ static function($assert) use ($os) { }, ); + yield test( + 'Server properly handle a client that close the connection without sending messages', + static function($assert) use ($os) { + $process = $os + ->control() + ->processes() + ->execute( + Command::foreground('sleep 2 && php fixtures/close-client.php') + ->withArgument('fixtures/client.php') + ->withEnvironment('TMPDIR', $os->status()->tmp()->toString()) + ->withEnvironment('PATH', $_SERVER['PATH']) + ->withWorkingDirectory(Path::of(__DIR__.'/../')), + ) + ->unwrap(); + + $result = IPC::of( + $os, + $os->status()->tmp()->resolve(Path::of('innnmind/ipc/')), + ) + ->serve(Process\Name::of('server')) + ->monitor( + static fn($result, $continuation) => $continuation->finish(), + ) + ->with(static fn($message, $continuation) => $continuation) + ->match( + static fn($result) => $result, + static fn() => null, + ); + + $assert->same(SideEffect::identity, $result); + $assert->same( + '', + $process + ->output() + ->map(static fn($chunk) => $chunk->data()) + ->fold(Concat::monoid) + ->toString(), + ); + }, + ); + yield proof( 'Server wait for client', given( From b39cc5303dba0b19496600bfd38882170a2d4024 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 15 Mar 2026 16:00:21 +0100 Subject: [PATCH 61/65] remove the abort listener when the scope exits --- src/Server/Instance.php | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/src/Server/Instance.php b/src/Server/Instance.php index c694ec5..b85cd35 100644 --- a/src/Server/Instance.php +++ b/src/Server/Instance.php @@ -80,7 +80,10 @@ public function __invoke( if ($this->abort->enabled()) { return $continuation - ->carryWith(Attempt::error(new \RuntimeException('Server signaled to terminate'))) + ->carryWith($this->uninstall( + $os, + Attempt::error(new \RuntimeException('Server signaled to terminate')), + )) ->terminate(); } @@ -99,8 +102,8 @@ public function __invoke( /** @psalm-suppress MixedArgument Don't know why it loses the type */ return $carry->match( fn($carry) => $this->listen($carry, $continuation), - static fn() => $continuation - ->carryWith($carry) + fn() => $continuation + ->carryWith($this->uninstall($os, $carry)) ->terminate(), ); } @@ -112,12 +115,15 @@ public function __invoke( $carry, $continuation, ), - static fn($carry) => $continuation - ->carryWith(Attempt::result($carry)) + fn($carry) => $continuation + ->carryWith($this->uninstall( + $os, + Attempt::result($carry), + )) ->finish(), ), - static fn() => $continuation - ->carryWith($carry) + fn() => $continuation + ->carryWith($this->uninstall($os, $carry)) ->terminate(), ); } @@ -188,4 +194,23 @@ private function listen( ->carryWith(Attempt::result($carry)), ); } + + /** + * @param Attempt $carry + * + * @return Attempt + */ + private function uninstall( + OperatingSystem $os, + Attempt $carry, + ): Attempt { + return $os + ->process() + ->signals() + ->remove($this->abort) + ->eitherWay( + static fn() => $carry, + static fn() => $carry, // silently fail if it fails to remove the listener + ); + } } From b40923f1b510c3c8e9298161058d95fc7129089c Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 15 Mar 2026 16:10:36 +0100 Subject: [PATCH 62/65] update readme --- README.md | 82 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 5ab616e..af1951a 100644 --- a/README.md +++ b/README.md @@ -17,47 +17,87 @@ composer require innmind/ipc ```php # Process A use Innmind\IPC\{ - Factory as IPC, + IPC, Process\Name, + Continuation, + Server, + Message, }; use Innmind\OperatingSystem\Factory; +use Innmind\Immutable\Monoid; -$ipc = IPC::build(Factory::build()); -$counter = $ipc->listen(Name::of('a'))(0, static function($message, $continuation, $counter): void { - if ($counter === 42) { - return $continuation->stop($counter); +/** + * @psalm-immutable + * @implements Monoid + */ +final class Addition implements Monoid +{ + public function identity(): int + { + return 0; + } + + public function combine(mixed $a, mixed $b): int + { + return $a + $b; } +} - return $continuation->respond($counter + 1, $message); -})->match( - static fn($counter) => $counter, - static fn() => throw new \RuntimeException('Unable to start the server'), -); +$ipc = IPC::build(Factory::build()); +$counter = $ipc + ->serve(Name::of('a')) + ->sink(new Addition) + ->monitor(static fn(int $counter, Server\Continuation $continuation) => match ($counter) { + 42 => $continuation->finish(), + default => $continuation, + }) + ->with( + static fn(Message $message, Continuation $continuation, int $counter) => $continuation + ->respond($message) + ->carryWith($counter + 1), + ) + ->unwrap(); // $counter will always be 42 in this case ``` ```php # Process B use Innmind\IPC\{ - Factory as IPC, + IPC, + Process, Process\Name, - Message\Generic as Message, + Message, }; use Innmind\OperatingSystem\Factory; -use Innmind\Immutable\Sequence; +use Innmind\MediaType\{ + MediaType, + TopLevel, +}; +use Innmind\Immutable\{ + Str, + Sequence, +}; $ipc = IPC::build(Factory::build()); $server = Name::of('a'); -$ipc - ->wait(Name::of('a')) - ->flatMap(fn($process) => $process->send(Sequence::of( - Message::of('text/plain', 'hello world'), - ))) - ->flatMap(fn($process) => $process->wait()) +$response = $ipc + ->connectTo($server) + ->flatMap( + static fn(Process $process) => $process + ->send(Sequence::of( + Message::of( + MediaType::from(TopLevel::text, 'plain'), + Str::of('hello world'), + ), + )) + ->map(static fn() => $process), + ) + ->flatMap(fn(Process $process) => $process->wait()) ->match( - static fn($message) => print('server responded '.$message->content()->toString()), - static fn() => print('no response from the server'), + static fn(Message $message) => 'server responded '.$message->content()->toString(), + static fn() => 'no response from the server', ); +print($message); ``` The above example will result in the output `server responded hello world` in the process `B`. From d7a7d007fc52ec871c430a2dfd73d25460a1aaaa Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 15 Mar 2026 16:28:25 +0100 Subject: [PATCH 63/65] update changelog --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59e0813..5c2faa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ - Requires PHP `8.4` - Requires `innmind/foundation:~2.1` +- `Innmind\IPC\IPC` is a final class now +- `Innmind\IPC\Message` is a final class now +- `Innmind\IPC\Process\Name::maybe()` has been replace by `::attempt()` +- `Innmind\IPC\Process` is a final class now +- `Innmind\IPC\Protocol` is an internal final class now +- `Innmind\IPC\Server::__invoke()` has been replaced by `::with()` + +### Removed + +- `Innmind\IPC\Client\Unix` +- `Innmind\IPC\Exception\*` +- `Innmind\IPC\IPC\Unix`, use `Innmind\IPC\IPC` instead +- `Innmind\IPC\Message\*`, use `Innmind\IPC\Message` instead +- `Innmind\IPC\Factory`, use `Innmind\IPC\IPC::of()` instead ### Fixed From 27c05b94aa84e070e95fd8ebb1169588daf1946d Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 15 Mar 2026 16:29:41 +0100 Subject: [PATCH 64/65] remove implemented comment --- src/Server.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Server.php b/src/Server.php index 9a3eca5..25b1e22 100644 --- a/src/Server.php +++ b/src/Server.php @@ -95,13 +95,6 @@ public function monitor(callable $monitor): self ); } - // todo differentiate a reducer for the server loop (aka the scheduler sink) - // and reducer that will operate on each connection - // the server loop must transform the initial carry as an initial carry - // dedicated for the connection reducer - // and there should be a way to fold the returned carry from all connections - // to a single value that will in the end be returned by the server - /** * @param callable(Message, Continuation, T): Continuation $listen * From 3ef658854548b5caf806decd44dc75b4ed1c9857 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 15 Mar 2026 16:34:35 +0100 Subject: [PATCH 65/65] better prove message equality depending on the media type --- proofs/message.php | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/proofs/message.php b/proofs/message.php index b3fb134..9ea6829 100644 --- a/proofs/message.php +++ b/proofs/message.php @@ -8,32 +8,47 @@ return static function() { yield proof( - 'Message::equals()', + 'Message::equals() depends on the media type', given( MediaType::any(), MediaType::any(), Set::strings()->map(Str::of(...)), - Set::strings()->map(Str::of(...)), ), - static function($assert, $mediaTypeA, $mediaTypeB, $a, $b) { + static function($assert, $mediaTypeA, $mediaTypeB, $content) { $assert->true( - Message::of($mediaTypeA, $a)->equals( - Message::of($mediaTypeA, $a), + Message::of($mediaTypeA, $content)->equals( + Message::of($mediaTypeA, $content), ), ); $assert->false( - Message::of($mediaTypeA, $a)->equals( - Message::of($mediaTypeA, $b), + Message::of($mediaTypeA, $content)->equals( + Message::of($mediaTypeB, $content), ), ); - $assert->false( - Message::of($mediaTypeA, $a)->equals( - Message::of($mediaTypeB, $a), + }, + ); + + yield proof( + 'Message::equals()', + given( + MediaType::any(), + Set::strings()->map(Str::of(...)), + Set::strings()->map(Str::of(...)), + )->filter(static fn($_, $a, $b) => !$a->equals($b)), + static function($assert, $mediaType, $a, $b) { + $assert->true( + Message::of($mediaType, $a)->equals( + Message::of($mediaType, $a), + ), + ); + $assert->true( + Message::of($mediaType, $b)->equals( + Message::of($mediaType, $b), ), ); $assert->false( - Message::of($mediaTypeA, $a)->equals( - Message::of($mediaTypeB, $b), + Message::of($mediaType, $a)->equals( + Message::of($mediaType, $b), ), ); },