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/eventsIn 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/eventsHTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 0