Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions src/Hub/ServerInfoDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ final class ServerInfoDto
* @param string $status One of self::STATUS_*.
* @param list<string> $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,
Expand All @@ -39,6 +42,7 @@ public function __construct(
public readonly string $status,
public readonly array $hostnameCandidates,
public readonly bool $relayActive,
public readonly ?int $libraryCount = null,
) {
}

Expand Down Expand Up @@ -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,
Expand All @@ -89,6 +101,7 @@ public static function fromPayload(array $payload): self
status: $status,
hostnameCandidates: $hostnameCandidates,
relayActive: $payload['relayActive'],
libraryCount: $libraryCount,
);
}

Expand All @@ -106,6 +119,7 @@ public function toPayload(): array
'status' => $this->status,
'hostnameCandidates' => $this->hostnameCandidates,
'relayActive' => $this->relayActive,
'libraryCount' => $this->libraryCount,
];
}

Expand Down
2 changes: 1 addition & 1 deletion src/Version.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
21 changes: 21 additions & 0 deletions tests/Hub/ServerInfoDtoTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,34 @@ private static function full(): array
'status' => ServerInfoDto::STATUS_ONLINE,
'hostnameCandidates' => ['10.0.0.5'],
'relayActive' => true,
'libraryCount' => 7,
];
}

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
Expand Down
Loading