"""Redis-backed live canvas state, one canvas per event. Redis is read/written on the hot path (every placement); the DB (PixelPlacement) is the durable history and is never queried on the hot path. Each event's canvas is its own string key, one byte per pixel (row-major), holding the palette index - SETRANGE self-pads with zero bytes, so there's no separate "ensure canvas exists" bootstrap step. The grid's width/height aren't fixed - they live on PlaceSettings (see models.py) and an admin can change them per event - so every function here takes them as arguments rather than reading a constant. Callers that already have a PlaceSettings instance in hand (consumers, the resync admin action) just pass its width/height through. """ import redis from django.conf import settings from .models import PixelPlacement _client = redis.Redis.from_url(settings.PLACE_REDIS_URL) def _canvas_key(event_path: str) -> str: return f"place:{event_path}:canvas" def read_canvas(event_path: str, width: int, height: int) -> bytes: data = _client.get(_canvas_key(event_path)) if data is None: return bytes(width * height) return data def write_pixel(event_path: str, x: int, y: int, color_index: int, width: int) -> None: _client.setrange(_canvas_key(event_path), y * width + x, bytes([color_index])) def rebuild_canvas_from_history(event, width: int, height: int) -> int: """Replays the durable PixelPlacement history for `event`, in placement order, into a fresh canvas sized width x height and overwrites the live Redis copy with it in one atomic SET. Used to repair Redis after it's lost/diverged from the DB (a flush, a restart without persistence, a resize - the old Redis bytes don't match the new dimensions - or a write that raced another one to the same pixel and landed out of order). Placements that fall outside the current dimensions (from a placement made before a shrink) are skipped rather than raising. Returns the number of placements replayed. """ grid = bytearray(width * height) count = 0 placements = ( PixelPlacement.objects.filter(event=event) .order_by("placed_at") .values_list("x", "y", "color_index") ) for x, y, color_index in placements.iterator(): if x >= width or y >= height: continue grid[y * width + x] = color_index count += 1 _client.set(_canvas_key(event.path), bytes(grid)) return count