001/*
002 * Copyright 2022-2025 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.soklet;
018
019import javax.annotation.Nonnull;
020
021/**
022 * Unicasts a <a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events">Server-Sent Event</a> or comment payload to a specific client listening on a {@link ResourcePath}.
023 * <p>
024 * For example:
025 * <pre>{@code @ServerSentEventSource("/chats/{chatId}/event-source")
026 * public HandshakeResult chatEventSource(
027 *   @PathParameter Long chatId,
028 *   // Browsers will send this header automatically on reconnects
029 *   @RequestHeader(name="Last-Event-ID", optional=true) String lastEventId
030 * ) {
031 *   Chat chat = myChatService.find(chatId);
032 *
033 *   // Exceptions that bubble out will reject the handshake and go through the
034 *   // ResponseMarshaler::forThrowable path, same as non-SSE Resource Methods
035 *   if (chat == null)
036 *     throw new NoSuchChatException();
037 *
038 *   // If a Last-Event-ID header was sent, pull data to "catch up" the client
039 *   List<ChatMessage> catchupMessages = new ArrayList<>();
040 *
041 *   if(lastEventId != null)
042 *     catchupMessages.addAll(myChatService.findCatchups(chatId, lastEventId));
043 *
044 *   // Customize "accept" handshake with a client initializer
045 *   return HandshakeResult.acceptWithDefaults()
046 *     .clientInitializer((unicaster) -> {
047 *       // Unicast "catchup" initialization events to this specific client.
048 *       // The unicaster is guaranteed to write these events before any
049 *       // other broadcaster does, allowing clients to safely catch up
050 *       // without the risk of event interleaving
051 *       catchupMessages.stream()
052 *         .map(catchupMessage -> ServerSentEvent.withEvent("chat-message")
053 *           .id(catchupMessage.id())
054 *           .data(catchupMessage.toJson())
055 *           .retry(Duration.ofSeconds(5))
056 *           .build())
057 *         .forEach(event -> unicaster.unicastEvent(event));
058 *     })
059 *     .build();
060 * }}</pre>
061 * <p>
062 * See <a href="https://www.soklet.com/docs/server-sent-events#client-initialization">https://www.soklet.com/docs/server-sent-events#client-initialization</a> for detailed documentation.
063 * <p>
064 * Formal specification is available at <a href="https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events">https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events</a>.
065 *
066 * @author <a href="https://www.revetkn.com">Mark Allen</a>
067 */
068public interface ServerSentEventUnicaster {
069        /**
070         * Unicasts a single Server-Sent Event payload to a specific client listening to this unicaster's {@link ResourcePath}.
071         * <p>
072         * In practice, implementations will generally return "immediately" and unicast operation[s] will occur on separate threads of execution.
073         * <p>
074         * However, mock implementations may wish to block until the unicast has completed - for example, to simplify automated testing.
075         *
076         * @param serverSentEvent the Server-Sent Event payload to unicast
077         */
078        void unicastEvent(@Nonnull ServerSentEvent serverSentEvent);
079
080        /**
081         * Unicasts a single Server-Sent Event comment to a specific client listening to this unicaster's {@link ResourcePath}.
082         * <p>
083         * Specify a blank string to generate a bare {@code ":"} Server-Sent Event comment line.
084         * <p>
085         * In practice, implementations will generally return "immediately" and unicast operation[s] will occur on separate threads of execution.
086         * <p>
087         * However, mock implementations may wish to block until the unicast has completed - for example, to simplify automated testing.
088         *
089         * @param comment the comment payload to unicast
090         */
091        void unicastComment(@Nonnull String comment);
092
093        /**
094         * The runtime Resource Path with which this unicaster is associated.
095         * <p>
096         * For example, a client may successfully complete a Server-Sent Event handshake for <em>Resource Method</em> {@code @ServerSentEventSource("/examples/{exampleId}")} by making a request to {@code GET /examples/123}. The server, immediately after accepting the handshake, might then acquire a unicaster to "catch up" the client according to the {@code Last-Event-ID} header value (for example).
097         * <p>
098         * A unicaster specific to {@code /examples/123} is then created (if necessary) and managed by Soklet, and can be used to send SSE payloads to that specific client via {@link #unicastEvent(ServerSentEvent)}.
099         *
100         * @return the runtime Resource Path instance with which this unicaster is associated
101         */
102        @Nonnull
103        ResourcePath getResourcePath();
104}