Server Sent Events

Server Sent Events (SSE) allow you to publish data over a long-lived response to a client.

To publish, create a handler and then create an SsePublisherpublisher using the SsePublisher.start(request, response) method.

The following example creates a thread that will send 1000 messages to the browser and then close the stream:

public class ServerSentEventsExample {
    public static void main(String[] args) {
        MuServer server = httpServer()
            .addHandler(Method.GET, "/sse/counter", (request, response, pathParams) -> {

                SsePublisher publisher = SsePublisher.start(request, response);
                new Thread(() -> count(publisher)).start();

            })
            .addHandler(ResourceHandlerBuilder.fileOrClasspath("src/main/resources/samples", "/samples"))
            .start();
        System.out.println("Open " + server.uri().resolve("/sse.html") + " to see some numbers.");
    }

    public static void count(SsePublisher publisher) {
        for (int i = 0; i < 100; i++) {
            try {
                publisher.send("Number " + i);
                Thread.sleep(1000);
            } catch (Exception e) {
                // The user has probably disconnected so stopping
                break;
            }
        }
        publisher.close();
    }
}
(see full file)

This page connects to the endpoint above and prints each message as it comes in:

<p>Connection status: <span class="status">Not started. <input type="button" value="start" id="startButton"></span></p>
<p>Messages:</p>
<div class="messages"></div>

<script>
    document.getElementById('startButton').addEventListener('click', _ => {
        let $ = document.querySelector.bind(document);
        let status = $('.status');
        let messages = $('.messages');
        let source = new EventSource('/sse/counter');

        source.addEventListener('open', e => {
            console.log('Connected', e);
            status.textContent = 'Connected';
        });

        source.addEventListener('error', e => {
            console.log('error', e);
            status.textContent = 'Error';
        });

        source.addEventListener('message', e => {
            messages.appendChild(document.createTextNode(e.data));
            messages.appendChild(document.createElement('br'));
        });
    });
</script>
(see full file)

Note that in these examples a thread is kept open per client, and each call to send is blocking. See AsyncSsePublisherfor an async version that has callbacks.

Try it out

Connection status: Not started.

Messages:

JAX RS SSE Publishing

If using JAX-RS resources, you can use an SSE broadcaster to broadcast to multiple clients.

In the following example, there is a single incrementing counter and clients can register to updates to the counter. Note that no matter how many clients are connected, they will all share the same counter and only a single thread is used.

public class JaxRSBroadcastExample {

    public static void main(String[] args) {
        TimeResource timeResource = new TimeResource();
        timeResource.start();

        MuServer server = httpServer()
            .addHandler(RestHandlerBuilder.restHandler(timeResource))
            .addHandler(ResourceHandlerBuilder.fileOrClasspath("src/main/resources/samples", "/samples"))
            .start();
        System.out.println("Example started at " + server.uri().resolve("/sse.html"));
    }

}

@Path("/sse/counter")
class TimeResource {

    long count = 0;
    ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
    Sse sse = MuRuntimeDelegate.createSseFactory();
    SseBroadcaster broadcaster = sse.newBroadcaster();

    @GET
    @Produces("text/event-stream")
    public void registerListener(@Context SseEventSink eventSink) {
        broadcaster.register(eventSink);
    }

    public void start() {
        executor.scheduleAtFixedRate(() -> {
            count++;
            String data = "Number " + count;
            broadcaster.broadcast(sse.newEvent(data));
        }, 0, 100, TimeUnit.MILLISECONDS);
    }

}
(see full file)

The same HTML as for the previous example can be used.