ServerSentEventsAdminController.java
/*
* Copyright (C) 2023 B3Partners B.V.
*
* SPDX-License-Identifier: MIT
*/
package org.tailormap.api.controller.admin;
import static ch.rasc.sse.eventbus.SseEvent.DEFAULT_EVENT;
import static org.tailormap.api.admin.model.ServerSentEvent.EventTypeEnum.KEEP_ALIVE;
import ch.rasc.sse.eventbus.SseEvent;
import ch.rasc.sse.eventbus.SseEventBus;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.lang.invoke.MethodHandles;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import org.tailormap.api.admin.model.ServerSentEvent;
@RestController
public class ServerSentEventsAdminController {
private static final Logger logger =
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private final SseEventBus eventBus;
private final ObjectMapper objectMapper;
public ServerSentEventsAdminController(SseEventBus eventBus, ObjectMapper objectMapper) {
this.eventBus = eventBus;
this.objectMapper = objectMapper;
}
/**
* Endpoint for the single-page admin frontend to receive updates to entities from other windows. The frontend does
* not load all data on each navigation as a traditional frontend and loads and stores entities in memory. This
* requires updating stale entities when these get updated in other windows (from other sessions or the same session
* in a different browser tab), otherwise data might get lost or overwritten.
*
* <p>This endpoint uses <a href="https://html.spec.whatwg.org/multipage/server-sent-events.html">Server-sent
* events</a>, which technically is a long-poll HTTP request which allows unidirectional server-initiated
* communication. Not bidirectional like websockets, but simpler because of the long-poll HTTP request. The
* webserver should ideally use HTTP/2 or higher because HTTP/1.1 limits the amount of simultaneous connections to a
* server per browser to 6.
*
* @return the server-sent events emitter
*/
@GetMapping(path = "${tailormap-api.admin.base-path}/events/{clientId}")
public SseEmitter entityEvents(
/*@RequestParam(required = false) String[] events,*/ @PathVariable("clientId") String clientId) {
logger.debug("New SSE client: {}, all clients: {}", clientId, this.eventBus.getAllClientIds());
return this.eventBus.createSseEmitter(clientId, 3600_000L, DEFAULT_EVENT);
}
@Scheduled(fixedRate = 60_000)
public void keepAlive() throws JsonProcessingException {
this.eventBus.handleEvent(
SseEvent.ofData(objectMapper.writeValueAsString(new ServerSentEvent().eventType(KEEP_ALIVE))));
}
}