Add guest document SSE stream
All checks were successful
build-and-deploy / build-deploy (push) Successful in 31s
All checks were successful
build-and-deploy / build-deploy (push) Successful in 31s
This commit is contained in:
@@ -0,0 +1,108 @@
|
|||||||
|
package com.android.trisolarisserver.component
|
||||||
|
|
||||||
|
import com.android.trisolarisserver.controller.GuestDocumentResponse
|
||||||
|
import com.android.trisolarisserver.db.repo.GuestDocumentRepo
|
||||||
|
import com.android.trisolarisserver.models.booking.GuestDocument
|
||||||
|
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<GuestDocKey, CopyOnWriteArrayList<SseEmitter>> = 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<SseEmitter>()
|
||||||
|
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<SseEmitter>()
|
||||||
|
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<GuestDocumentResponse> {
|
||||||
|
return guestDocumentRepo
|
||||||
|
.findByPropertyIdAndGuestIdOrderByUploadedAtDesc(propertyId, guestId)
|
||||||
|
.map { it.toResponse(objectMapper) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class GuestDocKey(
|
||||||
|
val propertyId: UUID,
|
||||||
|
val guestId: UUID
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun GuestDocument.toResponse(objectMapper: ObjectMapper): GuestDocumentResponse {
|
||||||
|
val id = id ?: throw IllegalStateException("Document id missing")
|
||||||
|
val extracted: Map<String, String>? = extractedData?.let {
|
||||||
|
try {
|
||||||
|
val raw = objectMapper.readValue(it, Map::class.java)
|
||||||
|
raw.entries.associate { entry ->
|
||||||
|
entry.key.toString() to (entry.value?.toString() ?: "")
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return GuestDocumentResponse(
|
||||||
|
id = id,
|
||||||
|
propertyId = property.id!!,
|
||||||
|
guestId = guest.id!!,
|
||||||
|
bookingId = booking.id!!,
|
||||||
|
uploadedByUserId = uploadedBy.id!!,
|
||||||
|
uploadedAt = uploadedAt.toString(),
|
||||||
|
originalFilename = originalFilename,
|
||||||
|
contentType = contentType,
|
||||||
|
sizeBytes = sizeBytes,
|
||||||
|
extractedData = extracted,
|
||||||
|
extractedAt = extractedAt?.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package com.android.trisolarisserver.controller
|
|||||||
import com.android.trisolarisserver.component.DocumentStorage
|
import com.android.trisolarisserver.component.DocumentStorage
|
||||||
import com.android.trisolarisserver.component.DocumentTokenService
|
import com.android.trisolarisserver.component.DocumentTokenService
|
||||||
import com.android.trisolarisserver.component.ExtractionQueue
|
import com.android.trisolarisserver.component.ExtractionQueue
|
||||||
|
import com.android.trisolarisserver.component.GuestDocumentEvents
|
||||||
import com.android.trisolarisserver.component.LlamaClient
|
import com.android.trisolarisserver.component.LlamaClient
|
||||||
import com.android.trisolarisserver.component.PropertyAccess
|
import com.android.trisolarisserver.component.PropertyAccess
|
||||||
import com.android.trisolarisserver.db.repo.BookingRepo
|
import com.android.trisolarisserver.db.repo.BookingRepo
|
||||||
@@ -42,6 +43,7 @@ class GuestDocuments(
|
|||||||
private val storage: DocumentStorage,
|
private val storage: DocumentStorage,
|
||||||
private val tokenService: DocumentTokenService,
|
private val tokenService: DocumentTokenService,
|
||||||
private val extractionQueue: ExtractionQueue,
|
private val extractionQueue: ExtractionQueue,
|
||||||
|
private val guestDocumentEvents: GuestDocumentEvents,
|
||||||
private val llamaClient: LlamaClient,
|
private val llamaClient: LlamaClient,
|
||||||
private val objectMapper: ObjectMapper,
|
private val objectMapper: ObjectMapper,
|
||||||
@org.springframework.beans.factory.annotation.Value("\${storage.documents.publicBaseUrl}")
|
@org.springframework.beans.factory.annotation.Value("\${storage.documents.publicBaseUrl}")
|
||||||
@@ -107,6 +109,7 @@ class GuestDocuments(
|
|||||||
)
|
)
|
||||||
val saved = guestDocumentRepo.save(document)
|
val saved = guestDocumentRepo.save(document)
|
||||||
runExtraction(saved.id!!, propertyId, guestId)
|
runExtraction(saved.id!!, propertyId, guestId)
|
||||||
|
guestDocumentEvents.emit(propertyId, guestId)
|
||||||
return saved.toResponse(objectMapper)
|
return saved.toResponse(objectMapper)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +126,16 @@ class GuestDocuments(
|
|||||||
.map { it.toResponse(objectMapper) }
|
.map { it.toResponse(objectMapper) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/stream")
|
||||||
|
fun streamDocuments(
|
||||||
|
@PathVariable propertyId: UUID,
|
||||||
|
@PathVariable guestId: UUID,
|
||||||
|
@AuthenticationPrincipal principal: MyPrincipal?
|
||||||
|
): org.springframework.web.servlet.mvc.method.annotation.SseEmitter {
|
||||||
|
requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER)
|
||||||
|
return guestDocumentEvents.subscribe(propertyId, guestId)
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{documentId}/file")
|
@GetMapping("/{documentId}/file")
|
||||||
fun downloadDocument(
|
fun downloadDocument(
|
||||||
@PathVariable propertyId: UUID,
|
@PathVariable propertyId: UUID,
|
||||||
@@ -183,6 +196,7 @@ class GuestDocuments(
|
|||||||
throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to delete file")
|
throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to delete file")
|
||||||
}
|
}
|
||||||
guestDocumentRepo.delete(document)
|
guestDocumentRepo.delete(document)
|
||||||
|
guestDocumentEvents.emit(propertyId, guestId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun runExtraction(documentId: UUID, propertyId: UUID, guestId: UUID) {
|
private fun runExtraction(documentId: UUID, propertyId: UUID, guestId: UUID) {
|
||||||
@@ -243,6 +257,7 @@ class GuestDocuments(
|
|||||||
document.extractedData = objectMapper.writeValueAsString(results)
|
document.extractedData = objectMapper.writeValueAsString(results)
|
||||||
document.extractedAt = OffsetDateTime.now()
|
document.extractedAt = OffsetDateTime.now()
|
||||||
guestDocumentRepo.save(document)
|
guestDocumentRepo.save(document)
|
||||||
|
guestDocumentEvents.emit(propertyId, guestId)
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
// Keep upload successful even if AI extraction fails.
|
// Keep upload successful even if AI extraction fails.
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user