+
+
+
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
deleted file mode 100644
index f06836d..0000000
--- a/CLAUDE.md
+++ /dev/null
@@ -1,78 +0,0 @@
-# CLAUDE.md
-
-This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
-
-## Build & Run Commands
-
-```bash
-# Build the project
-mvn compile
-
-# Package (JAR)
-mvn package
-
-# Run (entry point is dev.relism.Main)
-mvn exec:java -Dexec.mainClass="dev.relism.Main"
-
-# Clean build artifacts
-mvn clean
-
-# Full clean build
-mvn clean package
-```
-
-No tests exist yet. The project resolves the `fpr-core` dependency from a private Reposilite repository at `https://maven.relism.dev/releases`.
-
-## Architecture Overview
-
-**Flash** is a hand-rolled, zero-allocation HTTP/1.1 server built on raw Java sockets with virtual threads (Java 21). The central design goal is extreme low latency — no intermediate String allocations on the hot path.
-
-### Request Lifecycle
-
-```
-Socket → RequestParser → GlobalRouter → AbstractRouter impl → RequestHandler → Response bytes → Socket
-```
-
-1. **`RequestParser`** — Reads raw bytes from the socket into an 8 KB buffer and scans for method, path, headers, and body without creating intermediate String objects. All parsed values are wrapped in `ByteView` implementations (see `FastPathViews`).
-
-2. **`GlobalRouter`** — The top-level dispatcher. Holds a sorted list of mounted sub-routers (longest namespace prefix first) and a default internal router. Dispatches each request via byte-level prefix matching (`ByteUtils.startsWith`).
-
-3. **`FastPathRouterImpl`** — The primary router implementation, backed by the external `fpr-core` library. Builds a compiled state machine (`RouterBuilder → FastPathRouter`) for ultra-fast byte-level method+path matching. Routes are marked dirty (`router = null`) when added and lazily recompiled on first request. Thread-local `MatchResult` objects avoid per-request allocation.
-
-4. **`RadixPathRouterImpl`** — Stub; not yet implemented.
-
-### Key Abstractions
-
-- **`AbstractRouter`** — Base class for all routers. Has a `namespace` (byte array for zero-alloc prefix checks) and two abstract methods: `route(Request)` and `addRoute(HttpMethod, String, RequestHandler)`.
-- **`RequestHandler`** — Abstract class handlers extend. The `handle(Request, Response)` method returns either a `Response` object or any other value (serialized as the body) or `null`.
-- **`SimpleHandler`** — Wraps a `FunctionalHandler` lambda for the fluent DSL (`server.get(path, handler)`).
-- **`@Route` annotation** — Applied to `RequestHandler` subclasses to declare their HTTP method and path. Used by `AbstractRouter.register()`.
-- **`ByteView`** (`fpr-core` interface) — Zero-copy abstraction over byte sequences. Implemented by `RequestByteView` (slice into the raw buffer), `MethodPathByteView` (virtual concatenation of method + path), `SocketByteView`, and `StringByteView` — all in `FastPathViews`.
-
-### Routing Registration (Two Styles)
-
-**Annotation-based (class handlers):**
-```java
-@Route(method = "GET", path = "/profile")
-public static class ProfileHandler extends RequestHandler { ... }
-
-apiRouter.register(new ProfileHandler()); // namespace "/api" + path "/profile" = "/api/profile"
-```
-
-**Fluent DSL (lambdas):**
-```java
-server.get("/hello", (req, res) -> "Hello World");
-```
-
-Paths registered via `addRoute` are prefixed with the router's namespace inside `FastPathRouterImpl.addRoute`.
-
-### Response Writing
-
-`HttpServer` writes HTTP/1.1 responses with pre-calculated static byte arrays (`HTTP_1_1`, `CRLF`, `CONTENT_TYPE`, `CONTENT_LENGTH`) to avoid per-response allocations. `IoUtils.writeInt` writes integer values without String conversion.
-
-### Performance Invariants to Maintain
-
-- No `String` creation on the routing hot path (use `ByteView` and byte arrays).
-- Thread-local `MatchResult` reuse in `FastPathRouterContext` — do not allocate per request.
-- `GlobalRouter.route` must stay allocation-free (sorted list iterated directly).
-- `RequestParser` assumes headers fit in one 8 KB burst read.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f7f25c1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,31 @@
+# Flash Benchmarking Module
+
+This module is designed for high-concurrency benchmarks using `wrk`.
+
+## Prerequisite
+- Java 21+
+- `wrk` installed (`sudo apt install wrk` on Linux/WSL)
+
+## Running the benchmarks
+
+1. **Build the project** (from the root directory):
+ ```bash
+ mvn clean package -DskipTests
+ ```
+
+2. **Start the benchmark server**:
+ ```bash
+ java -jar flash-bench/target/flash-bench-1.0-SNAPSHOT.jar
+ ```
+
+3. **Execute the benchmark suite** (from a new terminal):
+ ```bash
+ cd flash-bench
+ ./benchmark.sh
+ ```
+
+### Advanced: Random IDs Benchmarking
+To test the speed of parameter extraction across millions of unique IDs:
+```bash
+wrk -t12 -c500 -d30s -s pipeline.lua http://127.0.0.1:8080
+```
diff --git a/flash-bench/pom.xml b/flash-bench/pom.xml
new file mode 100644
index 0000000..82a84d9
--- /dev/null
+++ b/flash-bench/pom.xml
@@ -0,0 +1,95 @@
+
+
+ 4.0.0
+
+
+ dev.relism
+ flash-parent
+ 1.0-SNAPSHOT
+
+
+ flash-bench
+ jar
+
+
+ 1.37
+
+
+
+
+ dev.relism
+ flash
+
+
+ org.slf4j
+ slf4j-simple
+
+
+ org.openjdk.jmh
+ jmh-generator-annprocess
+ ${jmh.version}
+ provided
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+ org.openjdk.jmh
+ jmh-generator-annprocess
+ ${jmh.version}
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.3
+
+
+ package
+ shade
+
+ false
+
+
+ *:*
+
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+
+
+
+
+
+ dev.relism.bench.Main
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flash-bench/src/main/java/dev/relism/bench/Main.java b/flash-bench/src/main/java/dev/relism/bench/Main.java
new file mode 100644
index 0000000..ef47e9b
--- /dev/null
+++ b/flash-bench/src/main/java/dev/relism/bench/Main.java
@@ -0,0 +1,24 @@
+package dev.relism.bench;
+
+import org.openjdk.jmh.results.format.ResultFormatType;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+public class Main {
+
+ public static void main(String[] args) throws Exception {
+ if (args.length > 0 && args[0].equalsIgnoreCase("external")) {
+ ExternalBenchmark.main(args);
+ return;
+ }
+
+ System.out.println("Starting JMH Benchmarks... (Pass 'external' as arg to start standalone server)");
+ Options opts = new OptionsBuilder()
+ .include(FlashBenchmark.class.getSimpleName())
+ .resultFormat(ResultFormatType.TEXT)
+ .build();
+
+ new Runner(opts).run();
+ }
+}
diff --git a/flash-bench/src/main/resources/simplelogger.properties b/flash-bench/src/main/resources/simplelogger.properties
new file mode 100644
index 0000000..263f2a3
--- /dev/null
+++ b/flash-bench/src/main/resources/simplelogger.properties
@@ -0,0 +1,2 @@
+# Silence all logs during benchmarks to avoid I/O becoming the bottleneck
+org.slf4j.simpleLogger.defaultLogLevel=off
diff --git a/flash/pom.xml b/flash/pom.xml
new file mode 100644
index 0000000..58faf47
--- /dev/null
+++ b/flash/pom.xml
@@ -0,0 +1,39 @@
+
+
+ 4.0.0
+
+
+ dev.relism
+ flash-parent
+ 1.0-SNAPSHOT
+
+
+ flash
+ jar
+
+
+
+ dev.relism
+ fpr-core
+
+
+ org.projectlombok
+ lombok
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.slf4j
+ slf4j-simple
+
+
+ org.junit.jupiter
+ junit-jupiter
+
+
+
+
diff --git a/src/main/java/dev/relism/Flash.java b/flash/src/main/java/dev/relism/Flash.java
similarity index 100%
rename from src/main/java/dev/relism/Flash.java
rename to flash/src/main/java/dev/relism/Flash.java
diff --git a/flash/src/main/java/dev/relism/HttpServer.java b/flash/src/main/java/dev/relism/HttpServer.java
new file mode 100644
index 0000000..cc6cd61
--- /dev/null
+++ b/flash/src/main/java/dev/relism/HttpServer.java
@@ -0,0 +1,310 @@
+package dev.relism;
+
+import dev.relism.fpr.core.ByteView;
+import dev.relism.http.ContentType;
+import dev.relism.http.HttpStatus;
+import dev.relism.models.*;
+import dev.relism.routing.AbstractRouter;
+import dev.relism.routing.GlobalRouter;
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.*;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Entry point for the Flash HTTP server.
+ *
+ *
+ * Internally, the server owns a {@link GlobalRouter} with two tiers of routing:
+ *
+ *
Mounted sub-routers — registered via {@link #mount}. Each owns a
+ * namespace
+ * prefix (e.g. {@code /api}) and handles all paths under it. Matched by longest
+ * prefix first.
+ *
Internal router — the fallback used when no sub-router claims the
+ * path.
+ * Routes registered directly on the server ({@link #get}, {@link #post}, etc.)
+ * go here.
+ *
+ *
+ *
+ * Error handlers ({@link #onNotFound}, {@link #onException}) registered on the
+ * server
+ * apply only to the internal router. Each mounted sub-router has its own
+ * independent handlers.
+ */
+@Slf4j
+public class HttpServer {
+ private final HttpServerConfiguration configuration;
+ private final ServerSocket serverSocket;
+ private final GlobalRouter globalRouter = new GlobalRouter();
+ private final ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
+ private volatile boolean stopped = false;
+ private final CompletableFuture readyFuture = new CompletableFuture<>();
+
+ private static final byte[] HTTP_1_1 = "HTTP/1.1 ".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] CRLF = "\r\n".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] CONTENT_TYPE = "Content-Type: ".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] CONTENT_LENGTH = "Content-Length: ".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] CONNECTION_CLOSE = "Connection: close\r\n".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] CONNECTION_KEEPALIVE = "Connection: keep-alive\r\n".getBytes(StandardCharsets.UTF_8);
+
+ private static final byte[] UNKNOWN_STATUS_SUFFIX = " Unknown".getBytes(StandardCharsets.UTF_8);
+
+ private static final byte[][] DIGITS = new byte[10][1];
+ static {
+ for (int i = 0; i < 10; i++)
+ DIGITS[i] = String.valueOf(i).getBytes(StandardCharsets.UTF_8);
+ }
+
+ public HttpServer(HttpServerConfiguration configuration) throws IOException {
+ this.configuration = configuration;
+ this.serverSocket = new ServerSocket(configuration.getPort());
+ }
+
+ /**
+ * Starts the server on a new non-daemon virtual thread and returns a future
+ * that completes
+ * when the accept loop is running and the server is ready to serve requests.
+ */
+ public CompletableFuture start() {
+ Thread.ofPlatform().name("flash-accept-loop").daemon(false).start(this::run);
+ return readyFuture;
+ }
+
+ private void run() {
+ try (executorService) {
+ readyFuture.complete(null);
+ while (!stopped) {
+ Socket clientSocket = serverSocket.accept();
+ process(clientSocket);
+ }
+ } catch (IOException e) {
+ if (!stopped) {
+ readyFuture.completeExceptionally(e);
+ log.error("Accept loop error", e);
+ }
+ }
+ }
+
+ /**
+ * Stops the server and waits for in-flight requests to complete.
+ * The returned future is already completed when this method returns.
+ */
+ public CompletableFuture stop() {
+ stopped = true;
+ try {
+ serverSocket.close();
+ } catch (IOException e) {
+ log.error("Error closing server socket", e);
+ }
+ executorService.shutdown();
+ try {
+ if (!executorService.awaitTermination(30, TimeUnit.SECONDS))
+ executorService.shutdownNow();
+ } catch (InterruptedException e) {
+ executorService.shutdownNow();
+ Thread.currentThread().interrupt();
+ }
+ return CompletableFuture.completedFuture(null);
+ }
+
+ // --- Routing ---
+
+ /**
+ * Mounts a sub-router under the given namespace prefix.
+ *
+ *
+ * Any request whose path starts with {@code namespace} will be dispatched to
+ * {@code router}
+ * instead of the internal router. When multiple namespaces match, the longest
+ * prefix wins.
+ * The router's own {@code onNotFound} and {@code onException} handlers are used
+ * for its paths —
+ * the server-level handlers do not apply.
+ *
+ * @throws dev.relism.exceptions.DuplicateNamespaceException if
+ * {@code namespace}
+ * is already mounted
+ */
+ public HttpServer mount(String namespace, AbstractRouter router) {
+ globalRouter.mount(namespace, router);
+ return this;
+ }
+
+ // --- Global error handlers ---
+
+ /**
+ * Sets the 404 handler for routes registered directly on this server.
+ * Does not affect mounted sub-routers, which each carry their own not-found
+ * handler.
+ */
+ public HttpServer onNotFound(SimpleHandler.FunctionalHandler handler) {
+ globalRouter.onNotFound(handler);
+ return this;
+ }
+
+ /**
+ * Sets the exception handler for routes registered directly on this server.
+ * Does not affect mounted sub-routers, which each carry their own exception
+ * handler.
+ */
+ public HttpServer onException(AbstractRouter.ExceptionHandler handler) {
+ globalRouter.onException(handler);
+ return this;
+ }
+
+ // --- DSL ---
+
+ /**
+ * Registers an annotation-based handler on the internal router.
+ * The handler's class must carry a {@link dev.relism.routing.Route @Route}
+ * annotation
+ * declaring the HTTP method and path. To register on a specific sub-router,
+ * call
+ * {@link AbstractRouter#register} on that router directly.
+ */
+ public HttpServer register(RequestHandler handler) {
+ globalRouter.register(handler);
+ return this;
+ }
+
+ /**
+ * Registers a route on the internal router (not on any mounted sub-router).
+ * The handler's return value is used as the response body; returning a
+ * {@link dev.relism.models.Response}
+ * instance replaces the entire response object.
+ */
+ public HttpServer get(String path, SimpleHandler.FunctionalHandler h) {
+ globalRouter.get(path, h);
+ return this;
+ }
+
+ /** @see #get(String, SimpleHandler.FunctionalHandler) */
+ public HttpServer post(String path, SimpleHandler.FunctionalHandler h) {
+ globalRouter.post(path, h);
+ return this;
+ }
+
+ /** @see #get(String, SimpleHandler.FunctionalHandler) */
+ public HttpServer put(String path, SimpleHandler.FunctionalHandler h) {
+ globalRouter.put(path, h);
+ return this;
+ }
+
+ /** @see #get(String, SimpleHandler.FunctionalHandler) */
+ public HttpServer delete(String path, SimpleHandler.FunctionalHandler h) {
+ globalRouter.delete(path, h);
+ return this;
+ }
+
+ // --- Request processing ---
+
+ private void process(Socket socket) {
+ executorService.submit(() -> {
+ try (socket;
+ InputStream in = socket.getInputStream();
+ OutputStream out = new BufferedOutputStream(socket.getOutputStream())) {
+ while (!stopped) {
+ Request request = RequestParser.parse(in);
+ if (request == null)
+ break;
+
+ boolean keepAlive = isKeepAlive(request);
+
+ Response response = new Response(200, new byte[0], ContentType.TEXT_PLAIN);
+ RequestHandler handler = globalRouter.route(request);
+
+ try {
+ Object result = handler.handle(request, response);
+ if (result instanceof Response r)
+ response = r;
+ else if (result != null)
+ response.setBody(result);
+ } catch (Exception ex) {
+ Object result = globalRouter.resolveExceptionHandler(request).handle(ex, request, response);
+ if (result instanceof Response r)
+ response = r;
+ else if (result != null)
+ response.setBody(result);
+ }
+
+ // Drain any unread body bytes so the stream is positioned at the next request.
+ request.getBody();
+
+ out.write(HTTP_1_1);
+ writeStatusPhrase(out, response.getStatusCode());
+ out.write(CRLF);
+ out.write(CONTENT_TYPE);
+ out.write(response.getContentType());
+ out.write(CRLF);
+ out.write(CONTENT_LENGTH);
+ writeInt(out, response.getBody() != null ? response.getBody().length : 0);
+ out.write(CRLF);
+ out.write(keepAlive ? CONNECTION_KEEPALIVE : CONNECTION_CLOSE);
+ out.write(CRLF);
+ if (response.getBody() != null)
+ out.write(response.getBody());
+ out.flush();
+
+ if (!keepAlive)
+ break;
+ }
+ } catch (IOException e) {
+ if (!stopped)
+ log.error("I/O error handling request", e);
+ }
+ });
+ }
+
+ /**
+ * Returns true if the connection should be kept alive after this request.
+ * HTTP/1.1 default: keep-alive. HTTP/1.0 default: close.
+ * Explicit {@code Connection} header overrides the default.
+ */
+ private static boolean isKeepAlive(Request request) {
+ String connection = request.getHeader("Connection");
+ if (connection != null) {
+ return !"close".equalsIgnoreCase(connection);
+ }
+ // Detect HTTP version from last byte of protocol field: "HTTP/1.1" → '1',
+ // "HTTP/1.0" → '0'
+ ByteView protocol = request.getRequestLine().getProtocol();
+ return protocol.length() == 8 && protocol.byteAt(7) == '1';
+ }
+
+ private static void writeStatusPhrase(OutputStream out, int statusCode) throws IOException {
+ byte[] phrase = HttpStatus.bytesForCode(statusCode);
+ if (phrase != null) {
+ out.write(phrase);
+ } else {
+ writeInt(out, statusCode);
+ out.write(UNKNOWN_STATUS_SUFFIX);
+ }
+ }
+
+ private static void writeInt(OutputStream out, int value) throws IOException {
+ if (value == 0) {
+ out.write(DIGITS[0]);
+ return;
+ }
+ if (value < 0) {
+ out.write('-');
+ value = -value;
+ }
+ int divisor = 1;
+ while (value / divisor >= 10)
+ divisor *= 10;
+ while (divisor > 0) {
+ out.write(DIGITS[(value / divisor) % 10]);
+ divisor /= 10;
+ }
+ }
+}
diff --git a/src/main/java/dev/relism/HttpServerConfiguration.java b/flash/src/main/java/dev/relism/HttpServerConfiguration.java
similarity index 100%
rename from src/main/java/dev/relism/HttpServerConfiguration.java
rename to flash/src/main/java/dev/relism/HttpServerConfiguration.java
diff --git a/src/main/java/dev/relism/Main.java b/flash/src/main/java/dev/relism/Main.java
similarity index 99%
rename from src/main/java/dev/relism/Main.java
rename to flash/src/main/java/dev/relism/Main.java
index 9e5ac34..ca72bc8 100644
--- a/src/main/java/dev/relism/Main.java
+++ b/flash/src/main/java/dev/relism/Main.java
@@ -40,6 +40,8 @@ public class Main {
throw new RuntimeException(req.getQueryParam("test"));
});
+
+
server.start().thenRun(() -> log.info("Server started on port: " + PORT));
}
}
diff --git a/src/main/java/dev/relism/RequestParser.java b/flash/src/main/java/dev/relism/RequestParser.java
similarity index 95%
rename from src/main/java/dev/relism/RequestParser.java
rename to flash/src/main/java/dev/relism/RequestParser.java
index c32dce3..6ff4eb8 100644
--- a/src/main/java/dev/relism/RequestParser.java
+++ b/flash/src/main/java/dev/relism/RequestParser.java
@@ -41,9 +41,6 @@ public class RequestParser {
throw new IOException("Headers too large: buffer exhausted without finding \\r\\n\\r\\n");
}
- // Timer starts here — after I/O, measuring only CPU parse time
- long start = System.nanoTime();
-
// 2. Scan Request Line: METHOD PATH PROTOCOL
int methodEnd = find(buffer, 0, headerEndIdx, (byte) ' ');
if (methodEnd == -1) throw new IOException("Invalid request line (method)");
@@ -93,9 +90,6 @@ public class RequestParser {
current = lineEnd + 2; // skip \r\n
}
- long elapsed = System.nanoTime() - start;
- log.debug("Request parsed in {}ns: {} {} {}", elapsed, method, pathView, protocolView);
-
// Body is read lazily — only materialized if the handler calls req.getBody().
// Bytes already in the buffer past \r\n\r\n are handed off to LazyBody as read-ahead.
int bodyStart = headerEndIdx + 4;
diff --git a/src/main/java/dev/relism/exceptions/DuplicateNamespaceException.java b/flash/src/main/java/dev/relism/exceptions/DuplicateNamespaceException.java
similarity index 100%
rename from src/main/java/dev/relism/exceptions/DuplicateNamespaceException.java
rename to flash/src/main/java/dev/relism/exceptions/DuplicateNamespaceException.java
diff --git a/src/main/java/dev/relism/http/ContentType.java b/flash/src/main/java/dev/relism/http/ContentType.java
similarity index 100%
rename from src/main/java/dev/relism/http/ContentType.java
rename to flash/src/main/java/dev/relism/http/ContentType.java
diff --git a/src/main/java/dev/relism/http/HttpMethod.java b/flash/src/main/java/dev/relism/http/HttpMethod.java
similarity index 100%
rename from src/main/java/dev/relism/http/HttpMethod.java
rename to flash/src/main/java/dev/relism/http/HttpMethod.java
diff --git a/src/main/java/dev/relism/http/HttpStatus.java b/flash/src/main/java/dev/relism/http/HttpStatus.java
similarity index 81%
rename from src/main/java/dev/relism/http/HttpStatus.java
rename to flash/src/main/java/dev/relism/http/HttpStatus.java
index 5e6e8e9..98f9476 100644
--- a/src/main/java/dev/relism/http/HttpStatus.java
+++ b/flash/src/main/java/dev/relism/http/HttpStatus.java
@@ -1,12 +1,10 @@
package dev.relism.http;
import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
-import java.util.Map;
/**
- * Pre-compiled byte representations of standard HTTP status lines (e.g. {@code "200 OK"}).
- * {@link #bytesForCode(int)} returns the cached array for known codes, {@code null} otherwise.
+ * Pre-compiled byte representations of standard HTTP status lines.
+ * Uses a direct-access array for O(1) lookup with zero allocation.
*/
public enum HttpStatus {
@@ -51,21 +49,30 @@ public enum HttpStatus {
SERVICE_UNAVAILABLE (503, "Service Unavailable"),
GATEWAY_TIMEOUT (504, "Gateway Timeout");
+ private static final int MAX_STATUS_CODE = 504;
+ private static final byte[][] INDEX = new byte[MAX_STATUS_CODE + 1][];
+
+ static {
+ for (HttpStatus s : values()) {
+ INDEX[s.code] = s.bytes;
+ }
+ }
+
private final int code;
private final byte[] bytes;
- private static final Map INDEX = new HashMap<>();
- static {
- for (HttpStatus s : values()) INDEX.put(s.code, s.bytes);
- }
-
HttpStatus(int code, String reason) {
this.code = code;
this.bytes = (code + " " + reason).getBytes(StandardCharsets.UTF_8);
}
- /** Returns pre-compiled {@code "CODE Reason"} bytes for the given code, or {@code null} if unknown. */
+ /** * Returns pre-compiled status bytes for the given code.
+ * Access is O(1) and generates zero garbage.
+ */
public static byte[] bytesForCode(int code) {
- return INDEX.get(code);
+ if (code >= 0 && code <= MAX_STATUS_CODE) {
+ return INDEX[code];
+ }
+ return null;
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/dev/relism/models/HeaderMap.java b/flash/src/main/java/dev/relism/models/HeaderMap.java
similarity index 100%
rename from src/main/java/dev/relism/models/HeaderMap.java
rename to flash/src/main/java/dev/relism/models/HeaderMap.java
diff --git a/src/main/java/dev/relism/models/LazyBody.java b/flash/src/main/java/dev/relism/models/LazyBody.java
similarity index 100%
rename from src/main/java/dev/relism/models/LazyBody.java
rename to flash/src/main/java/dev/relism/models/LazyBody.java
diff --git a/src/main/java/dev/relism/models/PathParams.java b/flash/src/main/java/dev/relism/models/PathParams.java
similarity index 100%
rename from src/main/java/dev/relism/models/PathParams.java
rename to flash/src/main/java/dev/relism/models/PathParams.java
diff --git a/src/main/java/dev/relism/models/QueryParams.java b/flash/src/main/java/dev/relism/models/QueryParams.java
similarity index 100%
rename from src/main/java/dev/relism/models/QueryParams.java
rename to flash/src/main/java/dev/relism/models/QueryParams.java
diff --git a/src/main/java/dev/relism/models/Request.java b/flash/src/main/java/dev/relism/models/Request.java
similarity index 100%
rename from src/main/java/dev/relism/models/Request.java
rename to flash/src/main/java/dev/relism/models/Request.java
diff --git a/src/main/java/dev/relism/models/RequestHandler.java b/flash/src/main/java/dev/relism/models/RequestHandler.java
similarity index 100%
rename from src/main/java/dev/relism/models/RequestHandler.java
rename to flash/src/main/java/dev/relism/models/RequestHandler.java
diff --git a/src/main/java/dev/relism/models/RequestLine.java b/flash/src/main/java/dev/relism/models/RequestLine.java
similarity index 100%
rename from src/main/java/dev/relism/models/RequestLine.java
rename to flash/src/main/java/dev/relism/models/RequestLine.java
diff --git a/src/main/java/dev/relism/models/Response.java b/flash/src/main/java/dev/relism/models/Response.java
similarity index 100%
rename from src/main/java/dev/relism/models/Response.java
rename to flash/src/main/java/dev/relism/models/Response.java
diff --git a/src/main/java/dev/relism/models/SimpleHandler.java b/flash/src/main/java/dev/relism/models/SimpleHandler.java
similarity index 100%
rename from src/main/java/dev/relism/models/SimpleHandler.java
rename to flash/src/main/java/dev/relism/models/SimpleHandler.java
diff --git a/src/main/java/dev/relism/routing/AbstractRouter.java b/flash/src/main/java/dev/relism/routing/AbstractRouter.java
similarity index 100%
rename from src/main/java/dev/relism/routing/AbstractRouter.java
rename to flash/src/main/java/dev/relism/routing/AbstractRouter.java
diff --git a/src/main/java/dev/relism/routing/GlobalRouter.java b/flash/src/main/java/dev/relism/routing/GlobalRouter.java
similarity index 100%
rename from src/main/java/dev/relism/routing/GlobalRouter.java
rename to flash/src/main/java/dev/relism/routing/GlobalRouter.java
diff --git a/src/main/java/dev/relism/routing/PathUtils.java b/flash/src/main/java/dev/relism/routing/PathUtils.java
similarity index 100%
rename from src/main/java/dev/relism/routing/PathUtils.java
rename to flash/src/main/java/dev/relism/routing/PathUtils.java
diff --git a/src/main/java/dev/relism/routing/Route.java b/flash/src/main/java/dev/relism/routing/Route.java
similarity index 100%
rename from src/main/java/dev/relism/routing/Route.java
rename to flash/src/main/java/dev/relism/routing/Route.java
diff --git a/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImpl.java b/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImpl.java
similarity index 100%
rename from src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImpl.java
rename to flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImpl.java
diff --git a/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathViews.java b/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathViews.java
similarity index 100%
rename from src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathViews.java
rename to flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathViews.java
diff --git a/src/main/java/dev/relism/routing/routers/radix/RadixPathRouterImpl.java b/flash/src/main/java/dev/relism/routing/routers/radix/RadixPathRouterImpl.java
similarity index 100%
rename from src/main/java/dev/relism/routing/routers/radix/RadixPathRouterImpl.java
rename to flash/src/main/java/dev/relism/routing/routers/radix/RadixPathRouterImpl.java
diff --git a/src/main/java/dev/relism/template/ByteTemplate.java b/flash/src/main/java/dev/relism/template/ByteTemplate.java
similarity index 100%
rename from src/main/java/dev/relism/template/ByteTemplate.java
rename to flash/src/main/java/dev/relism/template/ByteTemplate.java
diff --git a/src/main/java/dev/relism/template/ErrorPages.java b/flash/src/main/java/dev/relism/template/ErrorPages.java
similarity index 100%
rename from src/main/java/dev/relism/template/ErrorPages.java
rename to flash/src/main/java/dev/relism/template/ErrorPages.java
diff --git a/src/main/resources/assets/html/default_404.html b/flash/src/main/resources/assets/html/default_404.html
similarity index 100%
rename from src/main/resources/assets/html/default_404.html
rename to flash/src/main/resources/assets/html/default_404.html
diff --git a/src/main/resources/assets/html/default_exception.html b/flash/src/main/resources/assets/html/default_exception.html
similarity index 100%
rename from src/main/resources/assets/html/default_exception.html
rename to flash/src/main/resources/assets/html/default_exception.html
diff --git a/src/main/resources/assets/logo.png b/flash/src/main/resources/assets/logo.png
similarity index 100%
rename from src/main/resources/assets/logo.png
rename to flash/src/main/resources/assets/logo.png
diff --git a/src/test/java/dev/relism/HttpServerConcurrencyTest.java b/flash/src/test/java/dev/relism/HttpServerConcurrencyTest.java
similarity index 100%
rename from src/test/java/dev/relism/HttpServerConcurrencyTest.java
rename to flash/src/test/java/dev/relism/HttpServerConcurrencyTest.java
diff --git a/src/test/java/dev/relism/HttpServerTest.java b/flash/src/test/java/dev/relism/HttpServerTest.java
similarity index 100%
rename from src/test/java/dev/relism/HttpServerTest.java
rename to flash/src/test/java/dev/relism/HttpServerTest.java
diff --git a/src/test/java/dev/relism/RequestParserTest.java b/flash/src/test/java/dev/relism/RequestParserTest.java
similarity index 100%
rename from src/test/java/dev/relism/RequestParserTest.java
rename to flash/src/test/java/dev/relism/RequestParserTest.java
diff --git a/src/test/java/dev/relism/http/ContentTypeTest.java b/flash/src/test/java/dev/relism/http/ContentTypeTest.java
similarity index 100%
rename from src/test/java/dev/relism/http/ContentTypeTest.java
rename to flash/src/test/java/dev/relism/http/ContentTypeTest.java
diff --git a/src/test/java/dev/relism/http/HttpMethodTest.java b/flash/src/test/java/dev/relism/http/HttpMethodTest.java
similarity index 100%
rename from src/test/java/dev/relism/http/HttpMethodTest.java
rename to flash/src/test/java/dev/relism/http/HttpMethodTest.java
diff --git a/src/test/java/dev/relism/http/HttpStatusTest.java b/flash/src/test/java/dev/relism/http/HttpStatusTest.java
similarity index 100%
rename from src/test/java/dev/relism/http/HttpStatusTest.java
rename to flash/src/test/java/dev/relism/http/HttpStatusTest.java
diff --git a/src/test/java/dev/relism/models/HeaderMapTest.java b/flash/src/test/java/dev/relism/models/HeaderMapTest.java
similarity index 100%
rename from src/test/java/dev/relism/models/HeaderMapTest.java
rename to flash/src/test/java/dev/relism/models/HeaderMapTest.java
diff --git a/src/test/java/dev/relism/models/PathParamsTest.java b/flash/src/test/java/dev/relism/models/PathParamsTest.java
similarity index 100%
rename from src/test/java/dev/relism/models/PathParamsTest.java
rename to flash/src/test/java/dev/relism/models/PathParamsTest.java
diff --git a/src/test/java/dev/relism/models/QueryParamsTest.java b/flash/src/test/java/dev/relism/models/QueryParamsTest.java
similarity index 100%
rename from src/test/java/dev/relism/models/QueryParamsTest.java
rename to flash/src/test/java/dev/relism/models/QueryParamsTest.java
diff --git a/src/test/java/dev/relism/models/RequestLineTest.java b/flash/src/test/java/dev/relism/models/RequestLineTest.java
similarity index 100%
rename from src/test/java/dev/relism/models/RequestLineTest.java
rename to flash/src/test/java/dev/relism/models/RequestLineTest.java
diff --git a/src/test/java/dev/relism/models/RequestTest.java b/flash/src/test/java/dev/relism/models/RequestTest.java
similarity index 100%
rename from src/test/java/dev/relism/models/RequestTest.java
rename to flash/src/test/java/dev/relism/models/RequestTest.java
diff --git a/src/test/java/dev/relism/models/ResponseTest.java b/flash/src/test/java/dev/relism/models/ResponseTest.java
similarity index 100%
rename from src/test/java/dev/relism/models/ResponseTest.java
rename to flash/src/test/java/dev/relism/models/ResponseTest.java
diff --git a/src/test/java/dev/relism/models/SimpleHandlerTest.java b/flash/src/test/java/dev/relism/models/SimpleHandlerTest.java
similarity index 100%
rename from src/test/java/dev/relism/models/SimpleHandlerTest.java
rename to flash/src/test/java/dev/relism/models/SimpleHandlerTest.java
diff --git a/src/test/java/dev/relism/routing/AbstractRouterTest.java b/flash/src/test/java/dev/relism/routing/AbstractRouterTest.java
similarity index 100%
rename from src/test/java/dev/relism/routing/AbstractRouterTest.java
rename to flash/src/test/java/dev/relism/routing/AbstractRouterTest.java
diff --git a/src/test/java/dev/relism/routing/GlobalRouterTest.java b/flash/src/test/java/dev/relism/routing/GlobalRouterTest.java
similarity index 100%
rename from src/test/java/dev/relism/routing/GlobalRouterTest.java
rename to flash/src/test/java/dev/relism/routing/GlobalRouterTest.java
diff --git a/src/test/java/dev/relism/routing/PathUtilsTest.java b/flash/src/test/java/dev/relism/routing/PathUtilsTest.java
similarity index 100%
rename from src/test/java/dev/relism/routing/PathUtilsTest.java
rename to flash/src/test/java/dev/relism/routing/PathUtilsTest.java
diff --git a/src/test/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImplTest.java b/flash/src/test/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImplTest.java
similarity index 100%
rename from src/test/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImplTest.java
rename to flash/src/test/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImplTest.java
diff --git a/src/test/java/dev/relism/routing/routers/fastpathrouter/FastPathViewsTest.java b/flash/src/test/java/dev/relism/routing/routers/fastpathrouter/FastPathViewsTest.java
similarity index 100%
rename from src/test/java/dev/relism/routing/routers/fastpathrouter/FastPathViewsTest.java
rename to flash/src/test/java/dev/relism/routing/routers/fastpathrouter/FastPathViewsTest.java
diff --git a/src/test/java/dev/relism/template/ByteTemplateTest.java b/flash/src/test/java/dev/relism/template/ByteTemplateTest.java
similarity index 100%
rename from src/test/java/dev/relism/template/ByteTemplateTest.java
rename to flash/src/test/java/dev/relism/template/ByteTemplateTest.java
diff --git a/src/test/java/dev/relism/template/ErrorPagesTest.java b/flash/src/test/java/dev/relism/template/ErrorPagesTest.java
similarity index 100%
rename from src/test/java/dev/relism/template/ErrorPagesTest.java
rename to flash/src/test/java/dev/relism/template/ErrorPagesTest.java
diff --git a/pom.xml b/pom.xml
index d108e88..be9c68e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,14 +5,21 @@
4.0.0dev.relism
- Flash
+ flash-parent1.0-SNAPSHOT
- jar
+ pom
+
+
+ flash
+ flash-bench
+ 2121UTF-8
+ 1.18.44
+ 2.0.16
@@ -23,66 +30,67 @@
-
-
- org.junit.jupiter
- junit-jupiter
- 5.11.0
- test
-
-
- org.projectlombok
- lombok
- 1.18.44
- provided
-
-
- dev.relism
- fpr-core
- 1.1.0
-
-
-
- org.slf4j
- slf4j-api
- 2.0.16
-
-
-
- org.slf4j
- slf4j-simple
- 2.0.16
- runtime
-
-
- org.projectlombok
- lombok
- 1.18.42
- provided
-
-
+
+
+
+ dev.relism
+ flash
+ ${project.version}
+
+
+ dev.relism
+ fpr-core
+ 1.1.0
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+ provided
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+
+
+ org.slf4j
+ slf4j-simple
+ ${slf4j.version}
+ runtime
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.11.0
+ test
+
+
+
-
-
- org.apache.maven.plugins
- maven-surefire-plugin
- 3.2.5
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
-
-
-
- org.projectlombok
- lombok
- 1.18.44
-
-
-
-
-
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/src/main/java/dev/relism/HttpServer.java b/src/main/java/dev/relism/HttpServer.java
deleted file mode 100644
index 805528e..0000000
--- a/src/main/java/dev/relism/HttpServer.java
+++ /dev/null
@@ -1,226 +0,0 @@
-package dev.relism;
-
-import dev.relism.http.ContentType;
-import dev.relism.http.HttpStatus;
-import dev.relism.models.*;
-import dev.relism.routing.AbstractRouter;
-import dev.relism.routing.GlobalRouter;
-
-import lombok.extern.slf4j.Slf4j;
-
-import java.io.*;
-import java.net.ServerSocket;
-import java.net.Socket;
-import java.nio.charset.StandardCharsets;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Entry point for the Flash HTTP server.
- *
- *
Internally, the server owns a {@link GlobalRouter} with two tiers of routing:
- *
- *
Mounted sub-routers — registered via {@link #mount}. Each owns a namespace
- * prefix (e.g. {@code /api}) and handles all paths under it. Matched by longest prefix first.
- *
Internal router — the fallback used when no sub-router claims the path.
- * Routes registered directly on the server ({@link #get}, {@link #post}, etc.) go here.
- *
- *
- *
Error handlers ({@link #onNotFound}, {@link #onException}) registered on the server
- * apply only to the internal router. Each mounted sub-router has its own independent handlers.
- */
-@Slf4j
-public class HttpServer {
- private final HttpServerConfiguration configuration;
- private final ServerSocket serverSocket;
- private final GlobalRouter globalRouter = new GlobalRouter();
- private final ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
- private volatile boolean stopped = false;
- private final CompletableFuture readyFuture = new CompletableFuture<>();
-
- private static final byte[] HTTP_1_1 = "HTTP/1.1 ".getBytes(StandardCharsets.UTF_8);
- private static final byte[] CRLF = "\r\n".getBytes(StandardCharsets.UTF_8);
- private static final byte[] CONTENT_TYPE = "Content-Type: ".getBytes(StandardCharsets.UTF_8);
- private static final byte[] CONTENT_LENGTH = "Content-Length: ".getBytes(StandardCharsets.UTF_8);
- private static final byte[] CONNECTION_CLOSE = "Connection: close\r\n".getBytes(StandardCharsets.UTF_8);
-
- private static final byte[] UNKNOWN_STATUS_SUFFIX = " Unknown".getBytes(StandardCharsets.UTF_8);
-
- private static final byte[][] DIGITS = new byte[10][1];
- static {
- for (int i = 0; i < 10; i++) DIGITS[i] = String.valueOf(i).getBytes(StandardCharsets.UTF_8);
- }
-
- public HttpServer(HttpServerConfiguration configuration) throws IOException {
- this.configuration = configuration;
- this.serverSocket = new ServerSocket(configuration.getPort());
- }
-
- /**
- * Starts the server on a new non-daemon virtual thread and returns a future that completes
- * when the accept loop is running and the server is ready to serve requests.
- */
- public CompletableFuture start() {
- Thread.ofPlatform().name("flash-accept-loop").daemon(false).start(this::run);
- return readyFuture;
- }
-
- private void run() {
- try (executorService) {
- readyFuture.complete(null);
- while (!stopped) {
- Socket clientSocket = serverSocket.accept();
- process(clientSocket);
- }
- } catch (IOException e) {
- if (!stopped) {
- readyFuture.completeExceptionally(e);
- log.error("Accept loop error", e);
- }
- }
- }
-
- /**
- * Stops the server and waits for in-flight requests to complete.
- * The returned future is already completed when this method returns.
- */
- public CompletableFuture stop() {
- stopped = true;
- try { serverSocket.close(); } catch (IOException e) { log.error("Error closing server socket", e); }
- executorService.shutdown();
- try {
- if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) executorService.shutdownNow();
- } catch (InterruptedException e) {
- executorService.shutdownNow();
- Thread.currentThread().interrupt();
- }
- return CompletableFuture.completedFuture(null);
- }
-
- // --- Routing ---
-
- /**
- * Mounts a sub-router under the given namespace prefix.
- *
- *
Any request whose path starts with {@code namespace} will be dispatched to {@code router}
- * instead of the internal router. When multiple namespaces match, the longest prefix wins.
- * The router's own {@code onNotFound} and {@code onException} handlers are used for its paths —
- * the server-level handlers do not apply.
- *
- * @throws dev.relism.exceptions.DuplicateNamespaceException if {@code namespace} is already mounted
- */
- public HttpServer mount(String namespace, AbstractRouter router) {
- globalRouter.mount(namespace, router);
- return this;
- }
-
- // --- Global error handlers ---
-
- /**
- * Sets the 404 handler for routes registered directly on this server.
- * Does not affect mounted sub-routers, which each carry their own not-found handler.
- */
- public HttpServer onNotFound(SimpleHandler.FunctionalHandler handler) {
- globalRouter.onNotFound(handler);
- return this;
- }
-
- /**
- * Sets the exception handler for routes registered directly on this server.
- * Does not affect mounted sub-routers, which each carry their own exception handler.
- */
- public HttpServer onException(AbstractRouter.ExceptionHandler handler) {
- globalRouter.onException(handler);
- return this;
- }
-
- // --- DSL ---
-
- /**
- * Registers an annotation-based handler on the internal router.
- * The handler's class must carry a {@link dev.relism.routing.Route @Route} annotation
- * declaring the HTTP method and path. To register on a specific sub-router, call
- * {@link AbstractRouter#register} on that router directly.
- */
- public HttpServer register(RequestHandler handler) { globalRouter.register(handler); return this; }
-
- /**
- * Registers a route on the internal router (not on any mounted sub-router).
- * The handler's return value is used as the response body; returning a {@link dev.relism.models.Response}
- * instance replaces the entire response object.
- */
- public HttpServer get(String path, SimpleHandler.FunctionalHandler h) { globalRouter.get(path, h); return this; }
- /** @see #get(String, SimpleHandler.FunctionalHandler) */
- public HttpServer post(String path, SimpleHandler.FunctionalHandler h) { globalRouter.post(path, h); return this; }
- /** @see #get(String, SimpleHandler.FunctionalHandler) */
- public HttpServer put(String path, SimpleHandler.FunctionalHandler h) { globalRouter.put(path, h); return this; }
- /** @see #get(String, SimpleHandler.FunctionalHandler) */
- public HttpServer delete(String path, SimpleHandler.FunctionalHandler h) { globalRouter.delete(path, h); return this; }
-
- // --- Request processing ---
-
- private void process(Socket socket) {
- executorService.submit(() -> {
- try (socket;
- InputStream in = socket.getInputStream();
- OutputStream out = new BufferedOutputStream(socket.getOutputStream())
- ) {
- long start = System.nanoTime();
- Request request = RequestParser.parse(in);
- if (request == null) return;
-
- Response response = new Response(200, new byte[0], ContentType.TEXT_PLAIN);
- RequestHandler handler = globalRouter.route(request);
-
- try {
- Object result = handler.handle(request, response);
- if (result instanceof Response r) response = r;
- else if (result != null) response.setBody(result);
- } catch (Exception ex) {
- Object result = globalRouter.resolveExceptionHandler(request).handle(ex, request, response);
- if (result instanceof Response r) response = r;
- else if (result != null) response.setBody(result);
- }
-
- out.write(HTTP_1_1);
- writeStatusPhrase(out, response.getStatusCode());
- out.write(CRLF);
- out.write(CONTENT_TYPE);
- out.write(response.getContentType());
- out.write(CRLF);
- out.write(CONTENT_LENGTH);
- writeInt(out, response.getBody() != null ? response.getBody().length : 0);
- out.write(CRLF);
- out.write(CONNECTION_CLOSE);
- out.write(CRLF);
- if (response.getBody() != null) out.write(response.getBody());
- out.flush();
- log.info("{} ms: {}", (System.nanoTime() - start) / 1_000_000.0, request.getRequestLine());
-
- } catch (IOException e) {
- log.error("I/O error handling request", e);
- }
- });
- }
-
- private static void writeStatusPhrase(OutputStream out, int statusCode) throws IOException {
- byte[] phrase = HttpStatus.bytesForCode(statusCode);
- if (phrase != null) {
- out.write(phrase);
- } else {
- writeInt(out, statusCode);
- out.write(UNKNOWN_STATUS_SUFFIX);
- }
- }
-
-
- private static void writeInt(OutputStream out, int value) throws IOException {
- if (value == 0) { out.write(DIGITS[0]); return; }
- if (value < 0) { out.write('-'); value = -value; }
- int divisor = 1;
- while (value / divisor >= 10) divisor *= 10;
- while (divisor > 0) { out.write(DIGITS[(value / divisor) % 10]); divisor /= 10; }
- }
-}