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}