ListSyncBroadcaster.java

package com.oliwier.listmebackend.websocket;

import com.oliwier.listmebackend.api.dto.CrdtOperationResponse;
import com.oliwier.listmebackend.domain.model.CrdtOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Component;

import java.util.UUID;

/**
 * Broadcasts CRDT operations and presence events to WebSocket subscribers.
 *
 * Topic layout:
 *   /topic/list/{listId}          — CrdtOperationResponse (item changes)
 *   /topic/list/{listId}/presence — PresenceMessage (join/leave/snapshot)
 */
@Component
@RequiredArgsConstructor
public class ListSyncBroadcaster {

    private final SimpMessagingTemplate messaging;
    private final PresenceTracker presenceTracker;

    public void broadcastOp(UUID listId, CrdtOperation op) {
        messaging.convertAndSend(
                "/topic/list/" + listId,
                CrdtOperationResponse.from(op)
        );
    }

    public void broadcastJoin(UUID listId, String deviceId) {
        presenceTracker.join(listId, deviceId);
        messaging.convertAndSend(
                "/topic/list/" + listId + "/presence",
                new PresenceMessage("joined", deviceId, presenceTracker.getOnlineDevices(listId))
        );
    }

    public void broadcastLeave(UUID listId, String deviceId) {
        presenceTracker.leave(listId, deviceId);
        messaging.convertAndSend(
                "/topic/list/" + listId + "/presence",
                new PresenceMessage("left", deviceId, presenceTracker.getOnlineDevices(listId))
        );
    }

    public void sendSnapshot(UUID listId, String deviceId) {
        messaging.convertAndSend(
                "/topic/list/" + listId + "/presence",
                new PresenceMessage("snapshot", deviceId, presenceTracker.getOnlineDevices(listId))
        );
    }
}