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;
020import javax.annotation.Nullable;
021import javax.annotation.concurrent.NotThreadSafe;
022import java.time.Duration;
023import java.util.Optional;
024import java.util.concurrent.ExecutorService;
025import java.util.function.Consumer;
026import java.util.function.Supplier;
027
028import static java.util.Objects.requireNonNull;
029
030/**
031 * A special HTTP server whose only purpose is to provide <a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events">Server-Sent Event</a> functionality.
032 * <p>
033 * A Soklet application which supports Server-Sent Events will be configured with both a {@link Server} and a {@link ServerSentEventServer}.
034 * <p>
035 * For example:
036 * <pre>{@code // Set up our HTTP and SSE servers
037 * Server server = Server.withPort(8080).build;
038 * ServerSentEventServer sseServer = ServerSentEventServer.withPort(8081).build();
039 *
040 * // Wire servers into our config
041 * SokletConfig config = SokletConfig.withServer(server)
042 *   .serverSentEventServer(sseServer)
043 *   .build();
044 *
045 * // Run the app
046 * try (Soklet soklet = Soklet.withConfig(config)) {
047 *   soklet.start();
048 *   System.out.println("Soklet started, press [enter] to exit");
049 *   soklet.awaitShutdown(ShutdownTrigger.ENTER_KEY);
050 * }}</pre>
051 * <p>
052 * See <a href="https://www.soklet.com/docs/server-sent-events">https://www.soklet.com/docs/server-sent-events</a> for detailed documentation.
053 *
054 * @author <a href="https://www.revetkn.com">Mark Allen</a>
055 */
056public interface ServerSentEventServer extends AutoCloseable {
057        /**
058         * Starts the SSE server, which makes it able to accept requests from clients.
059         * <p>
060         * If the server is already started, no action is taken.
061         * <p>
062         * <strong>This method is designed for internal use by {@link com.soklet.Soklet} only and should not be invoked elsewhere.</strong>
063         */
064        void start();
065
066        /**
067         * Stops the SSE server, which makes it unable to accept requests from clients.
068         * <p>
069         * If the server is already stopped, no action is taken.
070         * <p>
071         * <strong>This method is designed for internal use by {@link com.soklet.Soklet} only and should not be invoked elsewhere.</strong>
072         */
073        void stop();
074
075        /**
076         * Is this SSE server started (that is, able to handle requests from clients)?
077         *
078         * @return {@code true} if the server is started, {@code false} otherwise
079         */
080        @Nonnull
081        Boolean isStarted();
082
083        /**
084         * {@link AutoCloseable}-enabled synonym for {@link #stop()}.
085         * <p>
086         * <strong>This method is designed for internal use by {@link com.soklet.Soklet} only and should not be invoked elsewhere.</strong>
087         *
088         * @throws Exception if an exception occurs while stopping the server
089         */
090        @Override
091        default void close() throws Exception {
092                stop();
093        }
094
095        /**
096         * Given a {@link ResourcePath} that corresponds to a <em>Resource Method</em> annotated with {@link com.soklet.annotation.ServerSentEventSource}, acquire a {@link ServerSentEventBroadcaster} which is capable of "pushing" messages to all connected Server-Sent Event clients.
097         * <p>
098         * When using the default {@link ServerSentEventServer}, Soklet guarantees exactly one {@link ServerSentEventBroadcaster} instance exists per {@link ResourcePath} (within the same JVM process).  Soklet is responsible for the creation and management of {@link ServerSentEventBroadcaster} instances.
099         * <p>
100         * Your code should not hold long-lived references to {@link ServerSentEventBroadcaster} instances (e.g. in a cache or instance variables) - the recommended usage pattern is to invoke {@link #acquireBroadcaster(ResourcePath)} every time you need a broadcaster reference.
101         * <p>
102         * See <a href="https://www.soklet.com/docs/server-sent-events">https://www.soklet.com/docs/server-sent-events</a> for detailed documentation.
103         *
104         * @param resourcePath the {@link com.soklet.annotation.ServerSentEventSource}-annotated <em>Resource Method</em> for which to acquire a broadcaster
105         * @return a broadcaster for the given {@link ResourcePath}, or {@link Optional#empty()} if there is no broadcaster available
106         */
107        @Nonnull
108        Optional<? extends ServerSentEventBroadcaster> acquireBroadcaster(@Nullable ResourcePath resourcePath);
109
110        /**
111         * The {@link com.soklet.Soklet} instance which manages this {@link ServerSentEventServer} will invoke this method exactly once at initialization time - this allows {@link com.soklet.Soklet} to "talk" to your {@link ServerSentEventServer}.
112         * <p>
113         * <strong>This method is designed for internal use by {@link com.soklet.Soklet} only and should not be invoked elsewhere.</strong>
114         *
115         * @param sokletConfig   configuration for the Soklet instance that controls this server
116         * @param requestHandler a {@link com.soklet.Soklet}-internal request handler which takes a {@link ServerSentEventServer}-provided request as input and supplies a {@link MarshaledResponse} as output for the {@link ServerSentEventServer} to write back to the client
117         */
118        void initialize(@Nonnull SokletConfig sokletConfig,
119                                                                        @Nonnull RequestHandler requestHandler);
120
121        /**
122         * Request/response processing contract for {@link ServerSentEventServer} implementations.
123         * <p>
124         * This is used internally by {@link com.soklet.Soklet} instances to "talk" to a {@link ServerSentEventServer} via {@link ServerSentEventServer#initialize(SokletConfig, RequestHandler)}.
125         * It's the responsibility of the {@link ServerSentEventServer} to implement HTTP mechanics: read bytes from the request, write bytes to the response, and so forth.
126         * <p>
127         * <strong>Most Soklet applications will use Soklet's default {@link ServerSentEventServer} implementation and therefore do not need to implement this interface directly.</strong>
128         *
129         * @author <a href="https://www.revetkn.com">Mark Allen</a>
130         */
131        @FunctionalInterface
132        interface RequestHandler {
133                /**
134                 * Callback to be invoked by a {@link ServerSentEventServer} implementation after it has received a Server-Sent Event Source HTTP request but prior to writing initial data to the HTTP response.
135                 * <p>
136                 * <strong>Note: this method is only invoked during the initial request "handshake" - it is not called for subsequent Server-Sent Event stream writes performed via {@link ServerSentEventBroadcaster#broadcastEvent(ServerSentEvent)} invocations.</strong>
137                 * <p>
138                 * For example, when a Server-Sent Event Source HTTP request is received, you might immediately write an HTTP 200 OK response if all looks good, or reject with a 401 due to invalid credentials.
139                 * That is the extent of the request-handling logic performed here.  The Server-Sent Event stream then remains open and can be written to via {@link ServerSentEventBroadcaster#broadcastEvent(ServerSentEvent)}.
140                 * <p>
141                 * The {@link ServerSentEventServer} is responsible for converting its internal request representation into a {@link Request}, which a {@link com.soklet.Soklet} instance consumes and performs Soklet application request processing logic.
142                 * <p>
143                 * The {@link com.soklet.Soklet} instance will generate a {@link MarshaledResponse} for the request, which it "hands back" to the {@link ServerSentEventServer} to be sent over the wire to the client.
144                 *
145                 * @param request               a Soklet {@link Request} representation of the {@link ServerSentEventServer}'s internal HTTP request data
146                 * @param requestResultConsumer invoked by {@link com.soklet.Soklet} when it's time for the {@link ServerSentEventServer} to write HTTP response data to the client
147                 */
148                void handleRequest(@Nonnull Request request,
149                                                                                         @Nonnull Consumer<RequestResult> requestResultConsumer);
150        }
151
152        /**
153         * Acquires a builder for {@link ServerSentEventServer} instances.
154         *
155         * @param port the port number on which the server should listen
156         * @return the builder
157         */
158        @Nonnull
159        static Builder withPort(@Nonnull Integer port) {
160                requireNonNull(port);
161                return new Builder(port);
162        }
163
164        /**
165         * Builder used to construct a standard implementation of {@link ServerSentEventServer}.
166         * <p>
167         * This class is intended for use by a single thread.
168         *
169         * @author <a href="https://www.revetkn.com">Mark Allen</a>
170         */
171        @NotThreadSafe
172        final class Builder {
173                @Nonnull
174                Integer port;
175                @Nullable
176                String host;
177                @Nullable
178                Duration requestTimeout;
179                @Nullable
180                Duration shutdownTimeout;
181                @Nullable
182                Duration heartbeatInterval;
183                @Nullable
184                Integer maximumRequestSizeInBytes;
185                @Nullable
186                Integer requestReadBufferSizeInBytes;
187                @Nullable
188                Supplier<ExecutorService> requestHandlerExecutorServiceSupplier;
189                @Nullable
190                Integer concurrentConnectionLimit;
191                @Nullable
192                Integer broadcasterCacheCapacity;
193                @Nullable
194                Integer resourcePathCacheCapacity;
195                @Nullable
196                Integer connectionQueueCapacity;
197                @Nullable
198                Boolean verifyConnectionOnceEstablished;
199                @Nullable
200                IdGenerator<?> idGenerator;
201
202                @Nonnull
203                protected Builder(@Nonnull Integer port) {
204                        requireNonNull(port);
205                        this.port = port;
206                }
207
208                @Nonnull
209                public Builder port(@Nonnull Integer port) {
210                        requireNonNull(port);
211                        this.port = port;
212                        return this;
213                }
214
215                @Nonnull
216                public Builder host(@Nullable String host) {
217                        this.host = host;
218                        return this;
219                }
220
221                @Nonnull
222                public Builder requestTimeout(@Nullable Duration requestTimeout) {
223                        this.requestTimeout = requestTimeout;
224                        return this;
225                }
226
227                @Nonnull
228                public Builder shutdownTimeout(@Nullable Duration shutdownTimeout) {
229                        this.shutdownTimeout = shutdownTimeout;
230                        return this;
231                }
232
233                @Nonnull
234                public Builder heartbeatInterval(@Nullable Duration heartbeatInterval) {
235                        this.heartbeatInterval = heartbeatInterval;
236                        return this;
237                }
238
239                @Nonnull
240                public Builder maximumRequestSizeInBytes(@Nullable Integer maximumRequestSizeInBytes) {
241                        this.maximumRequestSizeInBytes = maximumRequestSizeInBytes;
242                        return this;
243                }
244
245                @Nonnull
246                public Builder requestReadBufferSizeInBytes(@Nullable Integer requestReadBufferSizeInBytes) {
247                        this.requestReadBufferSizeInBytes = requestReadBufferSizeInBytes;
248                        return this;
249                }
250
251                @Nonnull
252                public Builder requestHandlerExecutorServiceSupplier(@Nullable Supplier<ExecutorService> requestHandlerExecutorServiceSupplier) {
253                        this.requestHandlerExecutorServiceSupplier = requestHandlerExecutorServiceSupplier;
254                        return this;
255                }
256
257                @Nonnull
258                public Builder concurrentConnectionLimit(@Nullable Integer concurrentConnectionLimit) {
259                        this.concurrentConnectionLimit = concurrentConnectionLimit;
260                        return this;
261                }
262
263                @Nonnull
264                public Builder broadcasterCacheCapacity(@Nullable Integer broadcasterCacheCapacity) {
265                        this.broadcasterCacheCapacity = broadcasterCacheCapacity;
266                        return this;
267                }
268
269                @Nonnull
270                public Builder resourcePathCacheCapacity(@Nullable Integer resourcePathCacheCapacity) {
271                        this.resourcePathCacheCapacity = resourcePathCacheCapacity;
272                        return this;
273                }
274
275                @Nonnull
276                public Builder connectionQueueCapacity(@Nullable Integer connectionQueueCapacity) {
277                        this.connectionQueueCapacity = connectionQueueCapacity;
278                        return this;
279                }
280
281                @Nonnull
282                public Builder verifyConnectionOnceEstablished(@Nullable Boolean verifyConnectionOnceEstablished) {
283                        this.verifyConnectionOnceEstablished = verifyConnectionOnceEstablished;
284                        return this;
285                }
286
287                @Nonnull
288                public Builder idGenerator(@Nullable IdGenerator<?> idGenerator) {
289                        this.idGenerator = idGenerator;
290                        return this;
291                }
292
293                @Nonnull
294                public ServerSentEventServer build() {
295                        return new DefaultServerSentEventServer(this);
296                }
297        }
298}