diff --git a/CHANGELOG.md b/CHANGELOG.md index bd4feaa..e048d71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to `detain/phlix-shared` are documented here. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.10.1] - 2026-06-23 + +### Added +- **`ServerInfoDto.libraryCount`** (optional `?int`, default `null`) — the number of + libraries a server last reported via heartbeat (from the hub's `server_libraries` + cache). Round-trips through `fromPayload()`/`toPayload()`; absent/null tolerated so + older payloads keep working. Lets the hub's "My Servers" UI show a real library + count instead of "—". + ## [0.10.0] - 2026-06-23 ### Added diff --git a/src/Hub/ServerInfoDto.php b/src/Hub/ServerInfoDto.php index e0e2697..2488c3b 100644 --- a/src/Hub/ServerInfoDto.php +++ b/src/Hub/ServerInfoDto.php @@ -29,6 +29,9 @@ final class ServerInfoDto * @param string $status One of self::STATUS_*. * @param list $hostnameCandidates Last known reachable hostnames. * @param bool $relayActive Whether a WSS reverse tunnel is currently open (Phase C.6). + * @param int|null $libraryCount Number of libraries the server last reported via heartbeat + * (from the hub's `server_libraries` cache). Null when the + * server has not reported any yet (older servers / pre-heartbeat). */ public function __construct( public readonly string $serverId, @@ -39,6 +42,7 @@ public function __construct( public readonly string $status, public readonly array $hostnameCandidates, public readonly bool $relayActive, + public readonly ?int $libraryCount = null, ) { } @@ -80,6 +84,14 @@ public static function fromPayload(array $payload): self throw new InvalidArgumentException('ServerInfoDto "relayActive" must be a boolean.'); } + $libraryCount = null; + if (array_key_exists('libraryCount', $payload) && $payload['libraryCount'] !== null) { + if (!is_int($payload['libraryCount'])) { + throw new InvalidArgumentException('ServerInfoDto "libraryCount" must be an integer when present.'); + } + $libraryCount = $payload['libraryCount']; + } + return new self( serverId: $serverId, userId: $userId, @@ -89,6 +101,7 @@ public static function fromPayload(array $payload): self status: $status, hostnameCandidates: $hostnameCandidates, relayActive: $payload['relayActive'], + libraryCount: $libraryCount, ); } @@ -106,6 +119,7 @@ public function toPayload(): array 'status' => $this->status, 'hostnameCandidates' => $this->hostnameCandidates, 'relayActive' => $this->relayActive, + 'libraryCount' => $this->libraryCount, ]; } diff --git a/src/Version.php b/src/Version.php index d9e8c84..7193d1a 100644 --- a/src/Version.php +++ b/src/Version.php @@ -24,7 +24,7 @@ final class Version * * @var non-empty-string */ - public const VERSION = '0.10.0'; + public const VERSION = '0.10.1'; /** * Prevent instantiation — static marker only. diff --git a/tests/Hub/ServerInfoDtoTest.php b/tests/Hub/ServerInfoDtoTest.php index facb797..b58ae28 100644 --- a/tests/Hub/ServerInfoDtoTest.php +++ b/tests/Hub/ServerInfoDtoTest.php @@ -27,6 +27,7 @@ private static function full(): array 'status' => ServerInfoDto::STATUS_ONLINE, 'hostnameCandidates' => ['10.0.0.5'], 'relayActive' => true, + 'libraryCount' => 7, ]; } @@ -34,6 +35,26 @@ public function test_round_trip(): void { $dto = ServerInfoDto::fromPayload(self::full()); $this->assertSame(self::full(), $dto->toPayload()); + $this->assertSame(7, $dto->libraryCount); + } + + public function test_libraryCount_absent_defaults_to_null(): void + { + $payload = self::full(); + unset($payload['libraryCount']); + $dto = ServerInfoDto::fromPayload($payload); + $this->assertNull($dto->libraryCount); + $this->assertNull($dto->toPayload()['libraryCount']); + } + + public function test_non_int_libraryCount_throws(): void + { + $payload = self::full(); + $payload['libraryCount'] = 'lots'; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('libraryCount'); + ServerInfoDto::fromPayload($payload); } public function test_lastSeenAt_null_round_trip(): void