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}