11package com.softeno.template.app.user.service
22
3+ import com.fasterxml.jackson.databind.ObjectMapper
34import com.softeno.template.app.common.ErrorFactory
45import com.softeno.template.app.common.PrincipalHandler
56import com.softeno.template.app.common.getPageRequest
@@ -16,6 +17,8 @@ import com.softeno.template.app.user.db.UserCoroutineRepository
1617import com.softeno.template.app.user.db.UserDocument
1718import com.softeno.template.app.user.mapper.toDocument
1819import com.softeno.template.app.user.mapper.toDomain
20+ import com.softeno.template.sample.websocket.Message
21+ import com.softeno.template.sample.websocket.toJson
1922import io.micrometer.tracing.Tracer
2023import kotlinx.coroutines.flow.Flow
2124import kotlinx.coroutines.flow.asFlow
@@ -26,9 +29,15 @@ import kotlinx.coroutines.withContext
2629import org.apache.commons.logging.LogFactory
2730import org.slf4j.MDC
2831import org.springframework.context.ApplicationEventPublisher
32+ import org.springframework.http.codec.ServerSentEvent
33+ import org.springframework.stereotype.Component
2934import org.springframework.stereotype.Service
35+ import reactor.core.publisher.Flux
3036import reactor.core.publisher.Mono
37+ import reactor.core.publisher.Sinks
38+ import reactor.util.concurrent.Queues
3139import java.security.Principal
40+ import java.time.Duration
3241
3342
3443@Service
@@ -149,3 +158,71 @@ class UserDocumentService(
149158 return @withContext userCoroutineRepository.deleteById(id)
150159 }
151160}
161+
162+ @Component
163+ class UserUpdateEmitter (
164+ private val objectMapper : ObjectMapper ,
165+ ) {
166+ private val sink: Sinks .Many <ServerSentEvent <String >> = Sinks .many().multicast().onBackpressureBuffer(Queues .SMALL_BUFFER_SIZE , false )
167+ private val log = LogFactory .getLog(javaClass)
168+
169+ fun getSink (): Flux <ServerSentEvent <String >> {
170+ val heartbeatFlux = Flux .interval(Duration .ofSeconds(10 ))
171+ .map {
172+ ServerSentEvent .builder<String >()
173+ .event(" heartbeat" )
174+ .data(Message (from = " SYSTEM" , to = " ALL" , content = " ping" ).toJson(objectMapper))
175+ .build()
176+ }.doOnError { error -> log.error(" Heartbeat error" , error) }
177+
178+
179+ val events = sink.asFlux()
180+ .doOnSubscribe { log.info(" New SSE client subscribed" ) }
181+ .doOnCancel { log.info(" SSE client disconnected" ) }
182+ .doOnTerminate { log.info(" SSE client terminated" ) }
183+ .doOnError { error -> log.error(" Event stream error" , error) }
184+
185+
186+ return Flux .merge(heartbeatFlux, events)
187+ .doOnCancel { log.info(" Canceling SSE stream" ) }
188+ .doOnTerminate { log.info(" Terminating SSE stream" ) }
189+ .onErrorResume { error ->
190+ log.error(" SSE stream error, sending error event" , error)
191+ Mono .just(
192+ ServerSentEvent .builder<String >()
193+ .event(" error" )
194+ .data(Message (from = " SYSTEM" , to = " ALL" , content = " Connection error: ${error.message} " ).toJson(objectMapper))
195+ .build()
196+ )
197+ }
198+ }
199+
200+ fun broadcast (message : Message ): Boolean =
201+ try {
202+ val payload = message.toJson(objectMapper)
203+ val sse = ServerSentEvent .builder(payload).event(" update" ).build()
204+ val result = sink.tryEmitNext(sse)
205+
206+ when (result) {
207+ Sinks .EmitResult .OK -> {
208+ log.debug(" Message broadcasted successfully: ${message.content} " )
209+ true
210+ }
211+ Sinks .EmitResult .FAIL_CANCELLED -> {
212+ log.warn(" Failed to broadcast message - emitter cancelled" )
213+ false
214+ }
215+ Sinks .EmitResult .FAIL_OVERFLOW -> {
216+ log.warn(" Failed to broadcast message - buffer overflow" )
217+ false
218+ }
219+ else -> {
220+ log.error(" Failed to broadcast message: $result " )
221+ false
222+ }
223+ }
224+ } catch (e: Exception ) {
225+ log.error(" Error broadcasting message" , e)
226+ false
227+ }
228+ }
0 commit comments