Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

SSE: notifications about new posts

You do not always need two-way WebSocket. If data goes only from the server to the client — new posts, notifications, the progress of a long task — Server-Sent Events (SSE) are enough. It is an ordinary HTTP response that does not end: the server keeps the connection open and from time to time writes events into it. In the browser SSE is received by the built-in EventSource class, which reconnects by itself if the connection drops.

Event format

Each event is a few lines of text with an empty line at the end:

event: post
data: {"id":3,"title":"Live update"}

event is the event name, data is the data. Javalin builds these lines for you.

A class with the list of clients

Let us make every connected client receive a post event when a post is created:

// 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 is a connected client. Without keepAlive() Javalin closes the connection as soon as the connect method ends. sendEvent sends an event with a name and data; the Post object is turned into JSON.

Wiring it in

Pass PostEvents to the controller and call it after the post is saved:

// 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 rules
            .get();
    Post post = repo.create(data);
    events.published(post);
    ctx.status(HttpStatus.CREATED).json(post);
}

In Main create the object and the route:

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

Checking it

In the first terminal subscribe to the events. The -N option turns off buffering so lines appear right away:

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

In the second terminal create a post:

curl -u anna:changeme -X POST http://localhost:8080/posts \
     -d '{"title":"Live update","body":"Sent over SSE.","author":"Anna"}'

The first terminal immediately shows:

event: post
data: {"id":3,"title":"Live update","body":"Sent over SSE.","author":"Anna"}

In the browser the same takes two lines: const source = new EventSource("/events"); and source.addEventListener("post", e => console.log(JSON.parse(e.data)));.

Common mistake

Javalin answers with an event stream only if the client sent the Accept: text/event-stream header. EventSource in the browser does this by itself, but in curl it is easy to forget. Without the header the connection closes at once with an empty response:

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

Thanks for reading!

Donate to the site — we work hard for you.