kick forever
CI/CD / test_and_check_app (push) Successful in 2m19s
CI/CD / Publish immutable image (push) Successful in 1m16s

This commit is contained in:
2026-09-24 14:38:18 +02:00
parent b190ff719e
commit 4dfe79ce5c
22 changed files with 271 additions and 113 deletions
@@ -32,7 +32,6 @@ class RoomJoinController extends Controller
$room !== null
&& ! $room->isClosed()
&& $existingPlayer instanceof Player
&& $existingPlayer->inactive_at === null
) {
return to_route('room.show', $room);
}
@@ -113,12 +113,6 @@ class EnsureRoomParticipant
]);
}
if ($player->inactive_at !== null) {
return to_route('room.join', [
'join_code' => $room->join_code,
]);
}
$player->markAsSeen();
$request->attributes->set('currentPlayer', $player);
+1 -1
View File
@@ -48,7 +48,7 @@ class KickRoomPlayerRequest extends FormRequest
return null;
}
return $room->activePlayers()->find((int) $playerId);
return $room->players()->find((int) $playerId);
}
/**
+2 -3
View File
@@ -18,9 +18,8 @@ class DashboardRoomResource extends JsonResource
'join_code' => $this->resource->join_code,
'current_round' => $this->resource->current_round,
'players_count' => $this->resource->players_count,
'active_player_count' => $this->resource->active_player_count,
'players' => $this->resource->relationLoaded('activePlayers')
? $this->resource->activePlayers
'players' => $this->resource->relationLoaded('players')
? $this->resource->players
->map(fn ($player): array => [
'id' => $player->id,
'name' => $player->name,
-22
View File
@@ -1,22 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\Player;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class MarkInactivePlayers implements ShouldQueue
{
use Queueable;
public function handle(): void
{
Player::query()
->whereNull('inactive_at')
->where('last_seen_at', '<=', now()->subHour())
->update([
'inactive_at' => now(),
]);
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
namespace App\Jobs;
use App\Enums\RoomStateChange;
use App\Events\RoomStateChanged;
use App\Models\Player;
use App\Models\Room;
use App\Services\Rooms\RoomRealtimeStateService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\DB;
class RemoveAbsentPlayers implements ShouldQueue
{
use Queueable;
public function handle(RoomRealtimeStateService $realtimeState): void
{
$cutoff = now()->subHour();
Player::query()
->where(function ($query) use ($cutoff): void {
$query->where('last_seen_at', '<=', $cutoff)
->orWhere(function ($query) use ($cutoff): void {
$query->whereNull('last_seen_at')
->where('created_at', '<=', $cutoff);
});
})
->select(['id', 'room_id'])
->chunkById(100, function ($players) use ($cutoff, $realtimeState): void {
foreach ($players as $player) {
$room = DB::transaction(function () use ($player, $cutoff): ?Room {
$room = Room::withTrashed()
->lockForUpdate()
->find($player->room_id);
if ($room === null) {
return null;
}
$currentPlayer = $room->players()
->lockForUpdate()
->find($player->id);
if ($currentPlayer === null) {
return null;
}
$lastActivity = $currentPlayer->last_seen_at ?? $currentPlayer->created_at;
if ($lastActivity === null || $lastActivity->greaterThan($cutoff)) {
return null;
}
$currentPlayer->delete();
$room->update([
'state_version' => $room->state_version + 1,
]);
return $room;
});
if ($room !== null && ! $room->trashed()) {
RoomStateChanged::dispatch(
$room->id,
$room->state_version,
RoomStateChange::PlayerKicked,
$realtimeState->for($room),
$player->id,
);
}
}
});
}
}
-14
View File
@@ -2,7 +2,6 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -16,7 +15,6 @@ use Illuminate\Support\Carbon;
* @property string $name
* @property bool $is_moderator
* @property Carbon|null $last_seen_at
* @property Carbon|null $inactive_at
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property-read Room $room
@@ -30,7 +28,6 @@ class Player extends Model
'name',
'is_moderator',
'last_seen_at',
'inactive_at',
];
protected function casts(): array
@@ -38,7 +35,6 @@ class Player extends Model
return [
'is_moderator' => 'boolean',
'last_seen_at' => 'datetime',
'inactive_at' => 'datetime',
];
}
@@ -85,14 +81,4 @@ class Player extends Model
'last_seen_at' => now(),
]);
}
/**
* @param Builder<Player> $query
* @return Builder<Player>
*/
public function scopeActive(Builder $query): Builder
{
return $query->whereNull('inactive_at');
}
}
-6
View File
@@ -93,12 +93,6 @@ class Room extends Model
}
/** @return HasMany<Player, $this> */
public function activePlayers(): HasMany
{
return $this->hasMany(Player::class)->whereNull('inactive_at');
}
/** @return BelongsTo<User, $this> */
public function owner(): BelongsTo
{
+1 -3
View File
@@ -16,13 +16,12 @@ class DashboardRoomService
$ownedRooms = $user
->ownedRooms()
->with([
'activePlayers' => fn ($query) => $query
'players' => fn ($query) => $query
->select(['id', 'room_id', 'user_id', 'name', 'is_moderator', 'created_at'])
->orderByDesc('is_moderator')
->orderBy('created_at'),
])
->withCount('players')
->withCount(['activePlayers as active_player_count'])
->orderByRaw('closed_at IS NOT NULL')
->orderByDesc('last_activity_at')
->get();
@@ -30,7 +29,6 @@ class DashboardRoomService
$favoriteRooms = $user
->favoriteRooms()
->withCount('players')
->withCount(['activePlayers as active_player_count'])
->orderByRaw('closed_at IS NOT NULL')
->orderByDesc('last_activity_at')
->get();
-1
View File
@@ -20,7 +20,6 @@ class RoomPageService
$room->load([
'players' => fn ($query) => $query
->active()
->select(['id', 'room_id', 'user_id', 'name', 'is_moderator', 'created_at'])
->orderByDesc('is_moderator')
->orderBy('created_at'),
@@ -80,7 +80,6 @@ class RoomParticipationService
} else {
$player->update([
'last_seen_at' => now(),
'inactive_at' => null,
]);
}
@@ -150,16 +149,13 @@ class RoomParticipationService
]);
$change = RoomStateChange::PlayerJoined;
} else {
if ($player->inactive_at !== null) {
$change = RoomStateChange::PlayerJoined;
} elseif ($player->name !== $playerName) {
if ($player->name !== $playerName) {
$change = RoomStateChange::PlayerUpdated;
}
$updates = [
'name' => $playerName,
'last_seen_at' => now(),
'inactive_at' => null,
];
if (
@@ -282,14 +278,14 @@ class RoomParticipationService
?User $user,
?Player $currentPlayer,
): void {
$stateVersion = DB::transaction(function () use ($room, $player, $user, $currentPlayer): ?int {
$stateVersion = DB::transaction(function () use ($room, $player, $user, $currentPlayer): int {
$lockedRoom = Room::query()
->lockForUpdate()
->findOrFail($room->id);
$lockedCurrentPlayer = $currentPlayer === null
? null
: $lockedRoom->activePlayers()
: $lockedRoom->players()
->lockForUpdate()
->find($currentPlayer->id);
$isOwner = $user !== null && $lockedRoom->owner_id === $user->id;
@@ -312,13 +308,7 @@ class RoomParticipationService
403,
);
if ($lockedPlayer->inactive_at !== null) {
return null;
}
$lockedPlayer->update([
'inactive_at' => now(),
]);
$lockedPlayer->delete();
$lockedRoom->update([
'last_activity_at' => now(),
'state_version' => $lockedRoom->state_version + 1,
@@ -327,10 +317,6 @@ class RoomParticipationService
return $lockedRoom->state_version;
});
if ($stateVersion === null) {
return;
}
RoomStateChanged::dispatch(
$room->id,
$stateVersion,
@@ -32,7 +32,6 @@ class RoomRealtimeStateService
$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'),