001/*
002 * Copyright 2022-2026 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 org.jspecify.annotations.NonNull;
020import org.jspecify.annotations.Nullable;
021
022import javax.annotation.concurrent.NotThreadSafe;
023import java.time.Duration;
024import java.util.Optional;
025import java.util.concurrent.ExecutorService;
026import java.util.function.Consumer;
027import java.util.function.Supplier;
028
029import static java.util.Objects.requireNonNull;
030
031/**
032 * 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.
033 * <p>
034 * A Soklet application which supports Server-Sent Events will be configured with both a {@link Server} and a {@link ServerSentEventServer}.
035 * <p>
036 * For example:
037 * <pre>{@code // Set up our HTTP and SSE servers
038 * Server server = Server.fromPort(8080);
039 * ServerSentEventServer sseServer = ServerSentEventServer.fromPort(8081);
040 *
041 * // Wire servers into our config
042 * SokletConfig config = SokletConfig.withServer(server)
043 *   .serverSentEventServer(sseServer)
044 *   .build();
045 *
046 * // Run the app
047 * try (Soklet soklet = Soklet.fromConfig(config)) {
048 *   soklet.start();
049 *   System.out.println("Soklet started, press [enter] to exit");
050 *   soklet.awaitShutdown(ShutdownTrigger.ENTER_KEY);
051 * }}</pre>
052 * <p>
053 * See <a href="https://www.soklet.com/docs/server-sent-events">https://www.soklet.com/docs/server-sent-events</a> for detailed documentation.
054 *
055 * @author <a href="https://www.revetkn.com">Mark Allen</a>
056 */
057public interface ServerSentEventServer extends AutoCloseable {
058        /**
059         * Starts the SSE server, which makes it able to accept requests from clients.
060         * <p>
061         * If the server is already started, no action is taken.
062         * <p>
063         * <strong>This method is designed for internal use by {@link com.soklet.Soklet} only and should not be invoked elsewhere.</strong>
064         */
065        void start();
066
067        /**
068         * Stops the SSE server, which makes it unable to accept requests from clients.
069         * <p>
070         * If the server is already stopped, no action is taken.
071         * <p>
072         * <strong>This method is designed for internal use by {@link com.soklet.Soklet} only and should not be invoked elsewhere.</strong>
073         */
074        void stop();
075
076        /**
077         * Is this SSE server started (that is, able to handle requests from clients)?
078         *
079         * @return {@code true} if the server is started, {@code false} otherwise
080         */
081        @NonNull
082        Boolean isStarted();
083
084        /**
085         * {@link AutoCloseable}-enabled synonym for {@link #stop()}.
086         * <p>
087         * <strong>This method is designed for internal use by {@link com.soklet.Soklet} only and should not be invoked elsewhere.</strong>
088         *
089         * @throws Exception if an exception occurs while stopping the server
090         */
091        @Override
092        default void close() throws Exception {
093                stop();
094        }
095
096        /**
097         * 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.
098         * <p>
099         * 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.
100         * <p>
101         * 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.
102         * <p>
103         * See <a href="https://www.soklet.com/docs/server-sent-events">https://www.soklet.com/docs/server-sent-events</a> for detailed documentation.
104         *
105         * @param resourcePath the {@link com.soklet.annotation.ServerSentEventSource}-annotated <em>Resource Method</em> for which to acquire a broadcaster
106         * @return a broadcaster for the given {@link ResourcePath}, or {@link Optional#empty()} if there is no broadcaster available
107         */
108        @NonNull
109        Optional<? extends ServerSentEventBroadcaster> acquireBroadcaster(@Nullable ResourcePath resourcePath);
110
111        /**
112         * 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}.
113         * <p>
114         * <strong>This method is designed for internal use by {@link com.soklet.Soklet} only and should not be invoked elsewhere.</strong>
115         *
116         * @param sokletConfig   configuration for the Soklet instance that controls this server
117         * @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
118         */
119        void initialize(@NonNull SokletConfig sokletConfig,
120                                                                        @NonNull RequestHandler requestHandler);
121
122        /**
123         * Request/response processing contract for {@link ServerSentEventServer} implementations.
124         * <p>
125         * This is used internally by {@link com.soklet.Soklet} instances to "talk" to a {@link ServerSentEventServer} via {@link ServerSentEventServer#initialize(SokletConfig, RequestHandler)}.
126         * 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.
127         * <p>
128         * <strong>Most Soklet applications will use Soklet's default {@link ServerSentEventServer} implementation and therefore do not need to implement this interface directly.</strong>
129         *
130         * @author <a href="https://www.revetkn.com">Mark Allen</a>
131         */
132        @FunctionalInterface
133        interface RequestHandler {
134                /**
135                 * 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.
136                 * <p>
137                 * <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>
138                 * <p>
139                 * 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.
140                 * 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)}.
141                 * <p>
142                 * 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.
143                 * <p>
144                 * 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.
145                 *
146                 * @param request               a Soklet {@link Request} representation of the {@link ServerSentEventServer}'s internal HTTP request data
147                 * @param requestResultConsumer invoked by {@link com.soklet.Soklet} when it's time for the {@link ServerSentEventServer} to write HTTP response data to the client
148                 */
149                void handleRequest(@NonNull Request request,
150                                                                                         @NonNull Consumer<RequestResult> requestResultConsumer);
151        }
152
153        /**
154         * Acquires a builder for {@link ServerSentEventServer} instances.
155         *
156         * @param port the port number on which the server should listen
157         * @return the builder
158         */
159        @NonNull
160        static Builder withPort(@NonNull Integer port) {
161                requireNonNull(port);
162                return new Builder(port);
163        }
164
165        /**
166         * Creates a {@link ServerSentEventServer} configured with the given port and default settings.
167         *
168         * @param port the port number on which the server should listen
169         * @return a {@link ServerSentEventServer} instance
170         */
171        @NonNull
172        static ServerSentEventServer fromPort(@NonNull Integer port) {
173                return withPort(port).build();
174        }
175
176        /**
177         * Builder used to construct a standard implementation of {@link ServerSentEventServer}.
178         * <p>
179         * This class is intended for use by a single thread.
180         *
181         * @author <a href="https://www.revetkn.com">Mark Allen</a>
182         */
183        @NotThreadSafe
184        final class Builder {
185                @NonNull
186                Integer port;
187                @Nullable
188                String host;
189                @Nullable
190                Duration requestTimeout;
191                @Nullable
192                Duration requestHandlerTimeout;
193                @Nullable
194                Integer requestHandlerConcurrency;
195                @Nullable
196                Integer requestHandlerQueueCapacity;
197                @Nullable
198                Duration writeTimeout;
199                @Nullable
200                Duration shutdownTimeout;
201                @Nullable
202                Duration heartbeatInterval;
203                @Nullable
204                Integer maximumRequestSizeInBytes;
205                @Nullable
206                Integer requestReadBufferSizeInBytes;
207                @Nullable
208                Supplier<ExecutorService> requestHandlerExecutorServiceSupplier;
209                @Nullable
210                Integer concurrentConnectionLimit;
211                @Nullable
212                Integer broadcasterCacheCapacity;
213                @Nullable
214                Integer resourcePathCacheCapacity;
215                @Nullable
216                Integer connectionQueueCapacity;
217                @Nullable
218                Boolean verifyConnectionOnceEstablished;
219                @Nullable
220                IdGenerator<?> idGenerator;
221
222                @NonNull
223                protected Builder(@NonNull Integer port) {
224                        requireNonNull(port);
225                        this.port = port;
226                }
227
228                @NonNull
229                public Builder port(@NonNull Integer port) {
230                        requireNonNull(port);
231                        this.port = port;
232                        return this;
233                }
234
235                @NonNull
236                public Builder host(@Nullable String host) {
237                        this.host = host;
238                        return this;
239                }
240
241                @NonNull
242                public Builder requestTimeout(@Nullable Duration requestTimeout) {
243                        this.requestTimeout = requestTimeout;
244                        return this;
245                }
246
247                @NonNull
248                public Builder requestHandlerTimeout(@Nullable Duration requestHandlerTimeout) {
249                        this.requestHandlerTimeout = requestHandlerTimeout;
250                        return this;
251                }
252
253                @NonNull
254                public Builder requestHandlerConcurrency(@Nullable Integer requestHandlerConcurrency) {
255                        this.requestHandlerConcurrency = requestHandlerConcurrency;
256                        return this;
257                }
258
259                @NonNull
260                public Builder requestHandlerQueueCapacity(@Nullable Integer requestHandlerQueueCapacity) {
261                        this.requestHandlerQueueCapacity = requestHandlerQueueCapacity;
262                        return this;
263                }
264
265                @NonNull
266                public Builder writeTimeout(@Nullable Duration writeTimeout) {
267                        this.writeTimeout = writeTimeout;
268                        return this;
269                }
270
271                @NonNull
272                public Builder shutdownTimeout(@Nullable Duration shutdownTimeout) {
273                        this.shutdownTimeout = shutdownTimeout;
274                        return this;
275                }
276
277                @NonNull
278                public Builder heartbeatInterval(@Nullable Duration heartbeatInterval) {
279                        this.heartbeatInterval = heartbeatInterval;
280                        return this;
281                }
282
283                @NonNull
284                public Builder maximumRequestSizeInBytes(@Nullable Integer maximumRequestSizeInBytes) {
285                        this.maximumRequestSizeInBytes = maximumRequestSizeInBytes;
286                        return this;
287                }
288
289                @NonNull
290                public Builder requestReadBufferSizeInBytes(@Nullable Integer requestReadBufferSizeInBytes) {
291                        this.requestReadBufferSizeInBytes = requestReadBufferSizeInBytes;
292                        return this;
293                }
294
295                @NonNull
296                public Builder requestHandlerExecutorServiceSupplier(@Nullable Supplier<ExecutorService> requestHandlerExecutorServiceSupplier) {
297                        this.requestHandlerExecutorServiceSupplier = requestHandlerExecutorServiceSupplier;
298                        return this;
299                }
300
301                @NonNull
302                public Builder concurrentConnectionLimit(@Nullable Integer concurrentConnectionLimit) {
303                        this.concurrentConnectionLimit = concurrentConnectionLimit;
304                        return this;
305                }
306
307                @NonNull
308                public Builder broadcasterCacheCapacity(@Nullable Integer broadcasterCacheCapacity) {
309                        this.broadcasterCacheCapacity = broadcasterCacheCapacity;
310                        return this;
311                }
312
313                @NonNull
314                public Builder resourcePathCacheCapacity(@Nullable Integer resourcePathCacheCapacity) {
315                        this.resourcePathCacheCapacity = resourcePathCacheCapacity;
316                        return this;
317                }
318
319                @NonNull
320                public Builder connectionQueueCapacity(@Nullable Integer connectionQueueCapacity) {
321                        this.connectionQueueCapacity = connectionQueueCapacity;
322                        return this;
323                }
324
325                @NonNull
326                public Builder verifyConnectionOnceEstablished(@Nullable Boolean verifyConnectionOnceEstablished) {
327                        this.verifyConnectionOnceEstablished = verifyConnectionOnceEstablished;
328                        return this;
329                }
330
331                @NonNull
332                public Builder idGenerator(@Nullable IdGenerator<?> idGenerator) {
333                        this.idGenerator = idGenerator;
334                        return this;
335                }
336
337                @NonNull
338                public ServerSentEventServer build() {
339                        return new DefaultServerSentEventServer(this);
340                }
341        }
342}