package com.android.trisolarisserver.component import com.android.trisolarisserver.controller.GuestDocumentResponse import com.android.trisolarisserver.controller.toResponse import com.android.trisolarisserver.db.repo.GuestDocumentRepo import com.fasterxml.jackson.databind.ObjectMapper import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Component import org.springframework.web.servlet.mvc.method.annotation.SseEmitter import java.io.IOException import java.util.UUID import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList @Component class GuestDocumentEvents( private val guestDocumentRepo: GuestDocumentRepo, private val objectMapper: ObjectMapper ) { private val emitters: MutableMap> = ConcurrentHashMap() fun subscribe(propertyId: UUID, guestId: UUID): SseEmitter { val key = GuestDocKey(propertyId, guestId) val emitter = SseEmitter(0L) emitters.computeIfAbsent(key) { CopyOnWriteArrayList() }.add(emitter) emitter.onCompletion { emitters[key]?.remove(emitter) } emitter.onTimeout { emitters[key]?.remove(emitter) } emitter.onError { emitters[key]?.remove(emitter) } try { emitter.send(SseEmitter.event().name("guest-documents").data(buildSnapshot(propertyId, guestId))) } catch (_: IOException) { emitters[key]?.remove(emitter) } return emitter } fun emit(propertyId: UUID, guestId: UUID) { val key = GuestDocKey(propertyId, guestId) val list = emitters[key] ?: return val data = buildSnapshot(propertyId, guestId) val dead = mutableListOf() for (emitter in list) { try { emitter.send(SseEmitter.event().name("guest-documents").data(data)) } catch (_: IOException) { dead.add(emitter) } } if (dead.isNotEmpty()) { list.removeAll(dead.toSet()) } } @Scheduled(fixedDelayString = "25000") fun heartbeat() { emitters.forEach { (_, list) -> val dead = mutableListOf() for (emitter in list) { try { emitter.send(SseEmitter.event().name("ping").data("ok")) } catch (_: IOException) { dead.add(emitter) } } if (dead.isNotEmpty()) { list.removeAll(dead.toSet()) } } } private fun buildSnapshot(propertyId: UUID, guestId: UUID): List { return guestDocumentRepo .findByPropertyIdAndGuestIdOrderByUploadedAtDesc(propertyId, guestId) .map { it.toResponse(objectMapper) } } } private data class GuestDocKey( val propertyId: UUID, val guestId: UUID )