ratpack.sse.ServerSentEvents Maven / Gradle / Ivy
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ratpack.sse;
import io.netty.buffer.ByteBufAllocator;
import org.reactivestreams.Publisher;
import ratpack.func.Action;
import ratpack.handling.Context;
import ratpack.http.Response;
import ratpack.http.internal.HttpHeaderConstants;
import ratpack.render.Renderable;
import ratpack.sse.internal.DefaultEvent;
import ratpack.sse.internal.ServerSentEventEncoder;
import ratpack.stream.Streams;
/**
* A {@link ratpack.handling.Context#render(Object) renderable} object for streaming server side events.
*
* A {@link ratpack.render.Renderer renderer} for this type is implicitly provided by Ratpack core.
*
* Example usage:
*
{@code
* import org.reactivestreams.Publisher;
* import ratpack.http.client.ReceivedResponse;
* import ratpack.sse.ServerSentEvents;
* import ratpack.test.embed.EmbeddedApp;
*
* import java.time.Duration;
* import java.util.Arrays;
* import java.util.Objects;
*
* import static ratpack.sse.ServerSentEvents.serverSentEvents;
* import static ratpack.stream.Streams.periodically;
*
* import static java.util.stream.Collectors.joining;
*
* import static org.junit.Assert.assertEquals;
*
* public class Example {
* public static void main(String[] args) throws Exception {
* EmbeddedApp.fromHandler(context -> {
* Publisher stream = periodically(context, Duration.ofMillis(5), i ->
* i < 5 ? i.toString() : null
* );
*
* ServerSentEvents events = serverSentEvents(stream, e ->
* e.id(Objects::toString).event("counter").data(i -> "event " + i)
* );
*
* context.render(events);
* }).test(httpClient -> {
* ReceivedResponse response = httpClient.get();
* assertEquals("text/event-stream;charset=UTF-8", response.getHeaders().get("Content-Type"));
*
* String expectedOutput = Arrays.asList(0, 1, 2, 3, 4)
* .stream()
* .map(i -> "event: counter\ndata: event " + i + "\nid: " + i + "\n")
* .collect(joining("\n"))
* + "\n";
*
* assertEquals(expectedOutput, response.getBody().getText());
* });
* }
* }
* }
*
* @see Wikipedia - Using server-sent events
* @see MDN - Using server-sent events
*/
public class ServerSentEvents implements Renderable {
private final Publisher extends Event>> publisher;
/**
* Creates a new renderable object wrapping the event stream.
*
* Takes a publisher of any type, and an action that mutates a created {@link Event} object for each stream item.
* The action is executed for each item in the stream as it is emitted before being sent as a server sent event.
* The state of the event object when the action completes will be used as the event.
*
* The action MUST set one of the {@code id}, {@code event}, {@code data}.
*
* @param publisher the event stream
* @param action the conversion of stream items to event objects
* @param the type of object in the event stream
* @return a {@link ratpack.handling.Context#render(Object) renderable} object
*/
public static ServerSentEvents serverSentEvents(Publisher publisher, Action super Event> action) {
return new ServerSentEvents(Streams.map(publisher, item -> {
Event event = action.with(new DefaultEvent<>(item));
if (event.getId() == null && event.getEvent() == null && event.getData() == null) {
throw new IllegalArgumentException("You must supply at least one of data, event, id");
}
return event;
}));
}
private ServerSentEvents(Publisher extends Event>> publisher) {
this.publisher = publisher;
}
/**
* The stream of events.
*
* @return the stream of events
*/
public Publisher extends Event>> getPublisher() {
return publisher;
}
/**
* {@inheritDoc}
*/
@Override
public void render(Context context) throws Exception {
ByteBufAllocator bufferAllocator = context.get(ByteBufAllocator.class);
Response response = context.getResponse();
response.getHeaders().add(HttpHeaderConstants.CONTENT_TYPE, HttpHeaderConstants.TEXT_EVENT_STREAM_CHARSET_UTF_8);
response.getHeaders().add(HttpHeaderConstants.CACHE_CONTROL, HttpHeaderConstants.NO_CACHE_FULL);
response.getHeaders().add(HttpHeaderConstants.PRAGMA, HttpHeaderConstants.NO_CACHE);
response.forceCloseConnection();
response.sendStream(Streams.map(publisher, i -> ServerSentEventEncoder.INSTANCE.encode(i, bufferAllocator)));
}
}