"""Ends the game at PlaceSettings.placement_ends_at: a task queued to run at that time broadcasts the wipe to everyone connected, so they all see the board cleared at the same moment (no client-side timer to drift). The wipe is only visual. Nothing is deleted, the Redis canvas still holds the final board (admin stats, heatmaps, resync keep working), it's just that the canvas endpoint serves a blank board once placement is over (see api.py). """ from datetime import timedelta from asgiref.sync import async_to_sync from channels.layers import get_channel_layer from django.core.cache import cache from django.tasks import task from django.utils import timezone from .broadcast import place_group from .models import PlaceSettings from .wire import to_epoch_ms # Kept a bit past the end time, so re-saving the settings with the same end # time (or with only the size changed) doesn't queue the same wipe twice. WIPE_GUARD_MARGIN = timedelta(hours=1) def _wipe_guard_key(settings_id: int, ends_at_ms: int) -> str: return f"place:wipe:{settings_id}:{ends_at_ms}" def schedule_wipe(place_settings: PlaceSettings) -> bool: """Queue the wipe for this end time, unless it's already queued or the end time is already past. Safe to call on every save.""" ends_at = place_settings.placement_ends_at if ends_at is None or ends_at <= timezone.now(): return False ends_at_ms = to_epoch_ms(ends_at) timeout = int((ends_at - timezone.now() + WIPE_GUARD_MARGIN).total_seconds()) if not cache.add(_wipe_guard_key(place_settings.id, ends_at_ms), True, timeout): return False wipe_canvas_task.using(run_after=ends_at).enqueue(place_settings.id, ends_at_ms) return True @task def wipe_canvas_task(settings_id: int, ends_at_ms: int): """Broadcast the wipe wave, if `ends_at_ms` is still the event's end time - an admin moving or clearing it leaves the old job queued, it just stops here.""" place_settings = ( PlaceSettings.objects.filter(id=settings_id).select_related("event").first() ) if place_settings is None or place_settings.placement_ends_at is None: return if to_epoch_ms(place_settings.placement_ends_at) != ends_at_ms: return channel_layer = get_channel_layer() if channel_layer is None: return async_to_sync(channel_layer.group_send)( place_group(place_settings.event.path), {"type": "wipe"} )