"""Realtime synchronization for chat access changes. The channel layer is an integration boundary: model signals publish only a small access invalidation event, while the consumer owns group membership. """ from __future__ import annotations import logging from asgiref.sync import async_to_sync from channels.layers import get_channel_layer from django.db import transaction logger = logging.getLogger(__name__) def schedule_chat_access_changed( *, chat_id: int, user_id: int, membership_active: bool, ) -> None: """Notify every live session after the membership transaction commits.""" def _send() -> None: channel_layer = get_channel_layer() if channel_layer is None: logger.warning( "Chat access update skipped because channel layer is unavailable", extra={ "event": "chat.realtime_access.channel_layer_unavailable", "chat_id": chat_id, "participant_id": user_id, }, ) return try: async_to_sync(channel_layer.group_send)( f"user_{user_id}", { "type": "chat_access_changed", "chat_id": chat_id, "membership_active": membership_active, }, ) except Exception as error: logger.exception( "Chat access realtime update failed", extra={ "event": "chat.realtime_access.send_failed", "chat_id": chat_id, "participant_id": user_id, "membership_active": membership_active, "error_type": type(error).__name__, }, ) transaction.on_commit(_send)