Implement instant websocket room updates

This commit is contained in:
2026-08-25 22:23:38 +02:00
parent bfc93dceba
commit d6e5455e90
18 changed files with 303 additions and 130 deletions
+8 -3
View File
@@ -5,18 +5,22 @@ namespace App\Events;
use App\Enums\RoomStateChange;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class RoomStateChanged implements ShouldBroadcast
class RoomStateChanged implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* @param array<string, mixed>|null $state
*/
public function __construct(
public readonly string $roomId,
public readonly int $version,
public readonly RoomStateChange $change,
public readonly ?array $state = null,
) {}
public function broadcastOn(): Channel
@@ -30,13 +34,14 @@ class RoomStateChanged implements ShouldBroadcast
}
/**
* @return array{version: int, change: string}
* @return array{version: int, change: string, state: array<string, mixed>|null}
*/
public function broadcastWith(): array
{
return [
'version' => $this->version,
'change' => $this->change->value,
'state' => $this->state,
];
}
}
+7 -7
View File
@@ -5,8 +5,8 @@ namespace App\Http\Controllers;
use App\Models\Player;
use App\Models\Room;
use App\Services\Rooms\RoundService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class RoundController extends Controller
{
@@ -14,28 +14,28 @@ class RoundController extends Controller
private readonly RoundService $rounds,
) {}
public function reset(Request $request, Room $room): RedirectResponse
public function reset(Request $request, Room $room): Response
{
$this->ensureModerator($request, $room);
$this->rounds->reset($room);
return back();
return response()->noContent();
}
public function reveal(Request $request, Room $room): RedirectResponse
public function reveal(Request $request, Room $room): Response
{
$this->ensureModerator($request, $room);
$this->rounds->reveal($room);
return back();
return response()->noContent();
}
public function nextRound(Request $request, Room $room): RedirectResponse
public function nextRound(Request $request, Room $room): Response
{
$this->ensureModerator($request, $room);
$this->rounds->startNext($room);
return back();
return response()->noContent();
}
private function ensureModerator(Request $request, Room $room): void
+3 -3
View File
@@ -6,7 +6,7 @@ use App\Http\Requests\SubmitVoteRequest;
use App\Models\Player;
use App\Models\Room;
use App\Services\Rooms\VoteService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Response;
class VoteController extends Controller
{
@@ -17,7 +17,7 @@ class VoteController extends Controller
public function store(
SubmitVoteRequest $request,
Room $room,
): RedirectResponse {
): Response {
$player = $request->attributes->get('currentPlayer');
abort_unless(
@@ -32,6 +32,6 @@ class VoteController extends Controller
$request->validated('value'),
);
return back();
return response()->noContent();
}
}
@@ -11,6 +11,10 @@ use Illuminate\Support\Facades\DB;
class RoomLifecycleService
{
public function __construct(
private readonly RoomRealtimeStateService $realtimeState,
) {}
/**
* @return array{player: Player, was_reopened: bool}
*/
@@ -49,6 +53,7 @@ class RoomLifecycleService
$room->id,
$stateVersion,
RoomStateChange::RoomReopened,
$this->realtimeState->for($room),
);
}
@@ -12,6 +12,10 @@ use Illuminate\Validation\ValidationException;
class RoomParticipationService
{
public function __construct(
private readonly RoomRealtimeStateService $realtimeState,
) {}
public function findByJoinCode(string $joinCode): ?Room
{
if ($joinCode === '') {
@@ -92,6 +96,7 @@ class RoomParticipationService
$room->id,
$room->state_version,
RoomStateChange::PlayerJoined,
$this->realtimeState->for($room),
);
return [$room, $player];
@@ -0,0 +1,64 @@
<?php
namespace App\Services\Rooms;
use App\Models\Room;
class RoomRealtimeStateService
{
public function __construct(
private readonly RoomStatisticsService $statistics,
) {}
/**
* @return array{
* current_round: int,
* revealed: bool,
* state_version: int,
* statistics: array{average: string|null, median: string|null}|null,
* players: array<int, array{
* id: int,
* name: string,
* is_moderator: bool,
* is_owner: bool,
* has_voted: bool,
* vote: string|null
* }>
* }
*/
public function for(Room $room): array
{
$room->refresh();
$room->load([
'players' => fn ($query) => $query
->active()
->select(['id', 'room_id', 'user_id', 'name', 'is_moderator', 'created_at'])
->orderByDesc('is_moderator')
->orderBy('created_at'),
'players.votes' => fn ($query) => $query
->where('room_id', $room->id)
->where('round', $room->current_round),
]);
return [
'current_round' => $room->current_round,
'revealed' => $room->revealed_at !== null,
'state_version' => $room->state_version,
'statistics' => $this->statistics->currentRound($room),
'players' => $room->players
->map(function ($player) use ($room): array {
$vote = $player->votes->first();
return [
'id' => $player->id,
'name' => $player->name,
'is_moderator' => $player->is_moderator,
'is_owner' => $player->user_id === $room->owner_id,
'has_voted' => $vote !== null,
'vote' => $room->revealed_at !== null ? $vote?->value : null,
];
})
->all(),
];
}
}
+7
View File
@@ -11,6 +11,10 @@ use Illuminate\Validation\ValidationException;
class RoundService
{
public function __construct(
private readonly RoomRealtimeStateService $realtimeState,
) {}
public function reset(Room $room): void
{
$stateVersion = DB::transaction(function () use ($room): int {
@@ -38,6 +42,7 @@ class RoundService
$room->id,
$stateVersion,
RoomStateChange::VotesReset,
$this->realtimeState->for($room),
);
}
@@ -69,6 +74,7 @@ class RoundService
$room->id,
$stateVersion,
RoomStateChange::CardsRevealed,
$this->realtimeState->for($room),
);
}
@@ -101,6 +107,7 @@ class RoundService
$room->id,
$stateVersion,
RoomStateChange::NextRound,
$this->realtimeState->for($room),
);
}
+5
View File
@@ -12,6 +12,10 @@ use Illuminate\Validation\ValidationException;
class VoteService
{
public function __construct(
private readonly RoomRealtimeStateService $realtimeState,
) {}
public function submit(Room $room, Player $player, string $value): void
{
$stateVersion = DB::transaction(function () use ($room, $player, $value): int {
@@ -52,6 +56,7 @@ class VoteService
$room->id,
$stateVersion,
RoomStateChange::VoteChanged,
$this->realtimeState->for($room),
);
}
}