Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

SSE: уведомления о новых постах

Не всегда нужен двусторонний WebSocket. Если данные идут только от сервера к клиенту — новые посты, уведомления, ход долгой задачи, — хватит Server-Sent Events (SSE). Это обычный HTTP-ответ, который не заканчивается: сервер держит соединение открытым и время от времени дописывает в него события. В браузере SSE принимает встроенный класс EventSource, который сам переподключается при обрыве.

Формат событий

Каждое событие — несколько строк текста и пустая строка в конце:

event: post
data: {"id":3,"title":"Живое обновление"}

event — имя события, data — данные. Javalin формирует эти строки сам.

Класс со списком клиентов

Сделаем так, чтобы при создании поста все подключённые клиенты получали событие post:

// src/main/java/com/example/blog/PostEvents.java
package com.example.blog;
 
import io.javalin.http.sse.SseClient;
 
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
 
public class PostEvents {
    private final Queue<SseClient> clients = new ConcurrentLinkedQueue<>();
 
    public void connect(SseClient client) {
        client.keepAlive();
        client.onClose(() -> clients.remove(client));
        clients.add(client);
    }
 
    public void published(Post post) {
        for (SseClient client : clients) {
            client.sendEvent("post", post);
        }
    }
}

SseClient — подключённый клиент. Без keepAlive() Javalin закроет соединение, как только метод connect закончится. sendEvent отправляет событие с именем и данными; объект Post превращается в JSON.

Подключение

Передайте PostEvents в контроллер и вызовите его после сохранения поста:

// src/main/java/com/example/blog/PostController.java
public PostController(PostRepository repo, PostEvents events) {
    this.repo = repo;
    this.events = events;
}
 
public void create(Context ctx) {
    NewPost data = ctx.bodyValidator(NewPost.class)
            // ... правила check
            .get();
    Post post = repo.create(data);
    events.published(post);
    ctx.status(HttpStatus.CREATED).json(post);
}

В Main создайте объект и маршрут:

// src/main/java/com/example/blog/Main.java
var events = new PostEvents();
var posts = new PostController(repo, events);
 
// внутри Javalin.create(config -> { ... })
config.routes.sse("/events", events::connect);

Проверка

В первом терминале подпишитесь на события. Ключ -N выключает буферизацию, чтобы строки появлялись сразу:

curl -N -H "Accept: text/event-stream" http://localhost:8080/events

Во втором терминале создайте пост:

curl -u anna:changeme -X POST http://localhost:8080/posts \
     -d '{"title":"Живое обновление","body":"Пришло через SSE.","author":"Анна"}'

В первом терминале тут же появится:

event: post
data: {"id":3,"title":"Живое обновление","body":"Пришло через SSE.","author":"Анна"}

В браузере то же делается в две строки: const source = new EventSource("/events"); и source.addEventListener("post", e => console.log(JSON.parse(e.data)));.

Частая ошибка

Javalin отвечает потоком событий, только если клиент прислал заголовок Accept: text/event-stream. EventSource в браузере делает это сам, а в curl его легко забыть. Без заголовка соединение сразу закрывается пустым ответом:

curl -i http://localhost:8080/events
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 0

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.