diff --git a/README.md b/README.md
index f7f25c1..c912e82 100644
--- a/README.md
+++ b/README.md
@@ -1,31 +1,83 @@
-# Flash Benchmarking Module
+# Flash
-This module is designed for high-concurrency benchmarks using `wrk`.
+A high-performance HTTP/1.1 server library for Java 21+, built for low-latency workloads.
+
+## Features
+
+- **Virtual threads** — one virtual thread per connection via `Executors.newVirtualThreadPerTaskExecutor()`
+- **Zero-allocation hot path** — header buffer reused across keep-alive requests; `ByteView` slices avoid copies
+- **Fast routing** — `fpr-core` compiles routes into a byte-level finite automaton at first request; path params via `PathParams`
+- **Two-tier router** — mounted sub-routers matched by longest prefix, then `FastPathRouterImpl` fallback
+- **Middleware chain** — composable `Middleware` lambdas fused at registration time (no per-request allocation)
+- **Chunked transfer** — `ChunkedInputStream` de-chunks on the fly, keeps socket positioned for pipelining
+- **Request body** — `bytes()` for full materialization or `stream()` for zero-copy streaming
+
+## Requirements
-## Prerequisite
- Java 21+
-- `wrk` installed (`sudo apt install wrk` on Linux/WSL)
+- Maven 3.8+
-## Running the benchmarks
+## Build
-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
+mvn clean package -DskipTests
+```
+
+## Quick start
+
+```java
+HttpServer server = new HttpServer(
+ HttpServerConfiguration.builder().port(8080).build()
+);
+
+server.get("/hello", (req, res) -> "Hello, world!");
+
+server.register(new MyHandler()); // @Route(path="/...", method=GET)
+
+server.start().thenRun(() -> System.out.println("Listening on :8080"));
+```
+
+## Handler styles
+
+**Lambda:**
+```java
+server.get("/ping", (req, res) -> "pong");
+```
+
+**Class-based (`@Route`):**
+```java
+@Route(path = "/users/{id}", method = HttpMethod.GET)
+public class GetUser extends RequestHandler {
+ @Override
+ public Object handle(Request req, Response res) {
+ String id = req.pathParam("id");
+ return "{\"id\":\"" + id + "\"}";
+ }
+}
+```
+
+**Middleware:**
+```java
+Middleware cors = next -> (req, res) -> {
+ res.header("Access-Control-Allow-Origin", "*");
+ return next.handle(req, res);
+};
+server.register(new GetUser(), cors);
+```
+
+## Maven dependency
+
+```xml
+
+
+ relism-releases
+ https://maven.relism.dev/releases
+
+
+
+
+ dev.relism
+ flash
+ 1.0-SNAPSHOT
+
```
diff --git a/dev/relism/fpr/core/FastPathRouter.class b/dev/relism/fpr/core/FastPathRouter.class
deleted file mode 100644
index 73c7291..0000000
Binary files a/dev/relism/fpr/core/FastPathRouter.class and /dev/null differ
diff --git a/dev/relism/fpr/core/internal/runtime/FrozenRouter.class b/dev/relism/fpr/core/internal/runtime/FrozenRouter.class
deleted file mode 100644
index 217a6ca..0000000
Binary files a/dev/relism/fpr/core/internal/runtime/FrozenRouter.class and /dev/null differ
diff --git a/dev/relism/fpr/core/internal/runtime/RouteSearch.class b/dev/relism/fpr/core/internal/runtime/RouteSearch.class
deleted file mode 100644
index 98f1203..0000000
Binary files a/dev/relism/fpr/core/internal/runtime/RouteSearch.class and /dev/null differ
diff --git a/dev/relism/fpr/core/internal/runtime/SegmentCursor.class b/dev/relism/fpr/core/internal/runtime/SegmentCursor.class
deleted file mode 100644
index dfcf53b..0000000
Binary files a/dev/relism/fpr/core/internal/runtime/SegmentCursor.class and /dev/null differ
diff --git a/docs/lifecycle/request/BODY.md b/docs/lifecycle/request/BODY.md
deleted file mode 100644
index 30a5770..0000000
--- a/docs/lifecycle/request/BODY.md
+++ /dev/null
@@ -1,98 +0,0 @@
-# Request Body
-
-`req.body()` returns a `RequestBody` — a lazy accessor that exposes the HTTP request body in
-two mutually exclusive modes. Choose one per handler based on the payload size and how you
-intend to consume it.
-
----
-
-## Two modes
-
-### `bytes()` — materialise
-
-```java
-byte[] body = req.body().bytes();
-```
-
-Reads the full body into a `byte[]` and caches the result. Safe to call multiple times — the
-second call returns the same cached array, no I/O is performed again.
-
-Use for:
-- JSON parsing (`new String(body, UTF_8)` then parse)
-- Small form data
-- Any payload that must be inspected in full before responding
-
-**Limit:** throws `IllegalStateException` if `Content-Length` exceeds `Integer.MAX_VALUE`
-(~2 GB). For larger bodies use `stream()`.
-
-**Chunked bodies:** reads until `ChunkedInputStream` signals EOF, then caches.
-
----
-
-### `stream()` — zero-copy
-
-```java
-InputStream in = req.body().stream();
-```
-
-Returns a bounded `InputStream` without allocating the full body upfront.
-
-Use for:
-- Large file uploads
-- Bodies passed directly to disk, a database, or another stream
-- Multipart parsing (via `Multipart.of(req)` which calls this internally)
-
-**Fixed-length bodies:** a `SequenceInputStream` of any pre-buffered header bytes (from the
-`RequestParser` read buffer, already in memory) followed by a bounded view of the socket
-stream. The small pre-buffered slice costs nothing extra.
-
-**Chunked bodies:** returns the raw `ChunkedInputStream` directly. It de-chunks on the fly —
-reads the hex chunk-size line, the data, and the trailing CRLF — and signals EOF at the end
-of the last chunk, leaving the socket positioned correctly for the next keep-alive request.
-
-**After `bytes()`:** if the body was already materialised, `stream()` returns a fresh
-`ByteArrayInputStream` over the cached array. No I/O is performed.
-
----
-
-## Mutual exclusivity
-
-```java
-// SAFE — pick one mode
-byte[] b = req.body().bytes();
-InputStream s = req.body().stream();
-
-// UNSAFE — bytes() then stream() is safe (stream() wraps the cache)
-// UNSAFE — stream() partially read, then bytes() → undefined results
-```
-
-The rule: if you call `stream()` first and partially read it, calling `bytes()` afterwards
-will either throw (if `contentLength` > 2 GB) or read only the remaining socket bytes into
-the array, missing the already-consumed portion.
-
----
-
-## Checking the size
-
-```java
-long size = req.body().contentLength();
-```
-
-- **> 0** — fixed-length body; exact byte count
-- **== 0** — empty body (`Content-Length: 0` or no body present)
-- **== -1** — `Transfer-Encoding: chunked`; size is unknown upfront
-
-```java
-boolean empty = req.body().isEmpty(); // true when contentLength == 0
-```
-
----
-
-## Decision guide
-
-| Payload size | Encoding | Use |
-|--------------------|-----------|----------------|
-| < 2 GB, fully needed | any | `bytes()` |
-| Any size, pass-through | any | `stream()` |
-| Multipart form | any | `Multipart.of(req)` (uses `stream()` internally) |
-| Size unknown | chunked | `stream()` |
diff --git a/docs/lifecycle/request/MULTIPART.md b/docs/lifecycle/request/MULTIPART.md
deleted file mode 100644
index 1e15294..0000000
--- a/docs/lifecycle/request/MULTIPART.md
+++ /dev/null
@@ -1,190 +0,0 @@
-# Multipart
-
-`Multipart` is a lazy streaming `multipart/form-data` parser. It reads from `req.body().stream()`
-— the request body is **never fully materialised**. Text fields are buffered eagerly (they are
-small by definition); file part bodies are exposed as zero-copy `InputStream`s backed directly
-by the socket.
-
----
-
-## Creating a parser
-
-```java
-Multipart mp = Multipart.of(req);
-```
-
-Throws `IllegalArgumentException` if the request is not `multipart/form-data` or the
-`boundary` parameter is missing. Throws `IOException` if the initial stream read fails (the
-parser skips the opening `--boundary\r\n` preamble on construction).
-
----
-
-## Reading text fields — `field(String name)`
-
-```java
-String userId = mp.field("userId"); // null if absent or a file part
-String width = mp.field("width");
-```
-
-Scans forward through the stream until the named text field is found. Text parts encountered
-along the way are buffered eagerly. File bodies encountered along the way are **drained
-silently** (discarded without heap allocation).
-
-**Order-independent for text:** once a text field has been scanned, it is cached. Calling
-`mp.field("userId")` after `mp.field("width")` will hit the cache if `userId` was encountered
-first in the stream.
-
-Returns `null` if the field is absent or if the named part is a file (has a `filename`
-attribute). Never returns file content.
-
----
-
-## Reading file parts — `file(String name)`
-
-```java
-Part avatar = mp.file("avatar"); // null if absent or a text field
-```
-
-Scans forward until the named file part is found. Text parts encountered along the way are
-buffered (accessible later via `field()`). Earlier file bodies encountered along the way are
-drained silently.
-
-The returned `Part`'s stream is backed by the socket. **Consume it before calling any other
-scan method** — calling `mp.field()`, `mp.file()`, or `mp.parts()` will drain the active
-stream automatically, but any bytes already read are gone.
-
-Returns `null` if absent or if the named part is a text field.
-
----
-
-## Collecting multiple parts — `parts(String name)` and `parts()`
-
-```java
-List files = mp.parts("files"); // all parts named "files"
-List all = mp.parts(); // every part in declaration order
-```
-
-Forces a **full scan**. All file bodies are materialised into `byte[]` arrays — accept this
-heap cost explicitly when calling these methods. Useful for bulk uploads where you need all
-files before processing any of them.
-
-`parts()` must be called before any `file()` call if you need all file parts, since a previous
-`file()` call may have consumed some of them from the stream.
-
----
-
-## The `Part` object
-
-Every part — whether text or file — is returned as a `Part`.
-
-### Metadata
-
-```java
-part.name() // field name from Content-Disposition
-part.filename() // original filename, null for text fields
-part.contentType() // declared Content-Type, null if not present
-part.isFile() // true when filename() != null
-```
-
-### Body access
-
-```java
-// Zero-copy stream — the default
-InputStream in = part.stream();
-
-// Explicit materialisation — opt-in heap allocation
-byte[] data = part.materialize();
-
-// UTF-8 convenience — materialises if needed, result is cached
-String text = part.text();
-```
-
-#### `stream()`
-
-For **buffered parts** (text fields and `parts()` results): returns a fresh
-`ByteArrayInputStream` over the cached `byte[]`. Repeatable, no I/O.
-
-For **streaming file parts** (from `file()`): returns the raw bounded socket `InputStream`.
-**Read it once only**, and before requesting the next part.
-
-#### `materialize()`
-
-Reads the full part body into a `byte[]` and caches it. Safe to call multiple times on
-buffered parts (returns the same array). For streaming file parts, triggers a full socket read
-on the first call; subsequent calls return the cached array.
-
-#### `text()`
-
-Convenience over `materialize()`. Decodes the body as UTF-8. Result is cached.
-
----
-
-## Memory model
-
-| Operation | Heap cost |
-|---|---|
-| `Multipart.of(req)` | 8 KB window buffer + boundary bytes |
-| `mp.field("x")` | body of the text field only (~bytes of the value) |
-| `mp.file("f")` | none — body is a bounded socket stream |
-| `part.stream()` on a file | none |
-| `part.materialize()` on a file | full file size |
-| `mp.parts()` | full body of every part |
-| Draining a file body (implicit) | 8 KB reused drain buffer |
-
----
-
-## Ordering constraints and safe patterns
-
-### Pattern 1 — text fields + one file (most common)
-
-Text fields are accessible in any order. The file is streamed zero-copy.
-
-```java
-Multipart mp = Multipart.of(req);
-String userId = mp.field("userId"); // order doesn't matter for text
-String label = mp.field("label");
-Part file = mp.file("file"); // stream it after fields
-
-Files.copy(file.stream(), destination, StandardCopyOption.REPLACE_EXISTING);
-return res.body("saved " + file.filename());
-```
-
-### Pattern 2 — echo large file (zero heap)
-
-```java
-Part file = Multipart.of(req).file("file");
-return res.chunked(file.stream()); // socket-in → chunked-out, never touches heap
-```
-
-### Pattern 3 — bulk upload (accept the heap cost)
-
-```java
-List files = Multipart.of(req).parts("files");
-for (Part f : files) {
- Files.write(dir.resolve(f.filename()), f.materialize());
-}
-```
-
-### Pattern 4 — mixed order (file before text in the form)
-
-If the form declares the file before a text field and you need the text first,
-`field()` will drain the file body silently to reach the text part. The file is then gone.
-Either reorder the form fields or use `parts()` if you need both.
-
-```java
-// Form order: file, then userId
-// This works — file is drained silently, userId is buffered
-String userId = mp.field("userId");
-
-// But now mp.file("file") returns null — already drained
-```
-
-If you need both, use `parts()` upfront:
-
-```java
-List all = mp.parts();
-Part file = all.stream().filter(Part::isFile).findFirst().orElse(null);
-String userId = all.stream().filter(p -> "userId".equals(p.name())).findFirst()
- .map(p -> { try { return p.text(); } catch (IOException e) { throw new UncheckedIOException(e); } })
- .orElse(null);
-```
diff --git a/docs/lifecycle/request/REQUEST.md b/docs/lifecycle/request/REQUEST.md
deleted file mode 100644
index b99d289..0000000
--- a/docs/lifecycle/request/REQUEST.md
+++ /dev/null
@@ -1,104 +0,0 @@
-# Request
-
-The `Request` object is the immutable view of an incoming HTTP/1.1 request passed to every
-handler. It is constructed by `RequestParser`, path/query parameters are injected once by the
-router, and then it is handed to your handler unchanged.
-
-```java
-server.get("/users/{id}", (req, res) -> { ... });
-```
-
----
-
-## Method and path
-
-```java
-HttpMethod method = req.method(); // GET, POST, PUT, …
-String path = req.path(); // "/users/42" — decoded UTF-8, no query string
-```
-
-`path()` allocates a `String` by decoding the underlying `ByteView`. It is a convenience for
-handlers that need the raw path; the router itself never calls it (it works on the `ByteView`
-directly for zero-alloc routing).
-
----
-
-## Headers
-
-```java
-String ct = req.header("Content-Type"); // first value, or null
-List accepts = req.headers("Accept"); // all values in order
-List all = req.headers(); // every header value
-```
-
-Lookup is **case-insensitive**: `"content-type"` and `"Content-Type"` resolve to the same header.
-`header()` returns `null` if the header is absent. `headers()` returns an empty list.
-
----
-
-## Path parameters
-
-Path parameters are declared in the route pattern with `{name}` syntax and injected by the
-router before the handler runs.
-
-```java
-server.get("/users/{id}/orders/{orderId}", (req, res) -> {
- String id = req.param("id");
- String orderId = req.param("orderId");
-});
-```
-
-`param()` returns `null` if the route has no such parameter or is not parametric at all.
-It never throws.
-
----
-
-## Query parameters
-
-The query string is parsed lazily on the first `query()` or `queries()` call and cached for
-the lifetime of the request.
-
-```java
-// GET /search?q=flash&page=2&tag=java&tag=jvm
-
-String q = req.query("q"); // "flash"
-String page = req.query("page"); // "2"
-List tags = req.queries("tag"); // ["java", "jvm"]
-String miss = req.query("absent"); // null
-```
-
-`query()` returns the first value for duplicate keys. `queries()` returns all values in
-declaration order. Both return `null` / empty list for absent parameters, never throw.
-
----
-
-## Body
-
-See [BODY.md](BODY.md) for a full reference. Quick summary:
-
-```java
-// Small payloads — materialise into byte[]
-byte[] json = req.body().bytes();
-
-// Large payloads — stream without heap allocation
-InputStream in = req.body().stream();
-
-// Check size before choosing
-long size = req.body().contentLength(); // -1 if Transfer-Encoding: chunked
-```
-
-The two modes are **mutually exclusive per request**. Calling `stream()` after `bytes()` returns
-a `ByteArrayInputStream` over the cached array; calling `bytes()` after `stream()` has been
-partially read produces undefined results.
-
----
-
-## Keep-alive and drain
-
-After the response is fully written to the socket, the server calls `req.drain()` automatically
-on keep-alive connections. This discards any unread body bytes so the socket is correctly
-positioned for the next pipelined request. You never need to call this yourself.
-
-**Important:** drain happens *after* the response write, not immediately after the handler
-returns. This matters for streaming responses that consume the request body on a separate thread
-(e.g. a pipe-fed echo): by the time drain runs, the body has already been consumed.
diff --git a/docs/lifecycle/response/RESPONSE.md b/docs/lifecycle/response/RESPONSE.md
deleted file mode 100644
index 4ef0c7e..0000000
--- a/docs/lifecycle/response/RESPONSE.md
+++ /dev/null
@@ -1,123 +0,0 @@
-# Response
-
-The `Response` object is the mutable view of an outgoing HTTP/1.1 response. It is
-pre-initialised by the server (`200 / text/plain`) and passed to every handler as `res`.
-All mutating methods return `this` for fluent chaining.
-
-```java
-server.get("/hello", (req, res) -> res.body("hello"));
-server.post("/items", (req, res) -> res.status(HttpStatus.CREATED).body("created"));
-```
-
----
-
-## Status code
-
-```java
-res.status(404) // int overload — phrase looked up via HttpStatus.bytesForCode()
-res.status(HttpStatus.NOT_FOUND) // enum overload — pre-encoded bytes written directly, zero lookup
-```
-
-Prefer the `HttpStatus` overload on hot paths: the pre-encoded `byte[]` is written to the socket
-without any lookup. The `int` overload falls back to an O(1) array lookup — still zero allocation,
-just one extra indirection.
-
----
-
-## Content type
-
-```java
-res.type(ContentType.JSON) // enum — pre-encoded bytes, zero allocation
-res.type("application/octet-stream") // String — encoded once at call time
-```
-
-Same pattern as `status`: the `ContentType` enum carries pre-encoded bytes; the `String` overload
-encodes once and stores the result.
-
----
-
-## Body modes
-
-Three mutually exclusive modes. The last one set wins.
-
-### Fixed body
-
-```java
-res.body("hello") // String → UTF-8 bytes
-res.body(new byte[]{...}) // raw bytes
-```
-
-Written with `Content-Length`. The byte array is stored as-is; no copy on the write path.
-
-### Known-length stream
-
-```java
-res.stream(Files.newInputStream(path), Files.size(path))
-```
-
-Written with `Content-Length`. The stream is drained directly to the socket in the write path —
-zero intermediate buffer beyond the OS socket buffer.
-
-### Unknown-length stream (chunked)
-
-```java
-res.chunked(source)
-```
-
-Written with `Transfer-Encoding: chunked`. The server reads 8 KB at a time from `source`,
-writes each chunk with its hex-encoded length, and closes with the terminal `0\r\n\r\n`.
-No `Content-Length` header is emitted.
-
-Use when the body length is not known upfront: generated content, pipe-fed streams, proxied
-responses.
-
----
-
-## Custom headers
-
-```java
-res.header("X-Request-Id", "abc123")
- .header("Cache-Control", "no-store")
-```
-
-Each header is encoded as `"Name: Value\r\n"` bytes **at call time** and stored in a list.
-On the write path the pre-encoded arrays are written directly — zero allocation per request.
-
----
-
-## Fluent chaining
-
-All mutating methods return `this`. A complete response in one expression:
-
-```java
-return res.status(HttpStatus.CREATED)
- .type(ContentType.JSON)
- .header("X-Trace-Id", traceId)
- .body(json);
-```
-
----
-
-## Body mode decision guide
-
-| Payload | Mode | Header emitted |
-|----------------------------------|-------------------------------|-------------------------|
-| Small / fully known | `body()` | `Content-Length` |
-| File or stream with known size | `stream(is, length)` | `Content-Length` |
-| Generated / piped / unknown size | `chunked(is)` | `Transfer-Encoding: chunked` |
-
----
-
-## Memory model
-
-| Call | Allocation |
-|-----------------------------|-------------------------------------------------|
-| `status(HttpStatus)` | none — writes pre-encoded `byte[]` from enum |
-| `status(int)` | none — O(1) array lookup at write time |
-| `type(ContentType)` | none — pre-encoded `byte[]` from enum |
-| `type(String)` | encodes once at call time, stored as `byte[]` |
-| `body(String)` | one UTF-8 encode at call time |
-| `body(byte[])` | none — array stored by reference |
-| `stream(is, len)` | none — stream drained directly to socket |
-| `chunked(is)` | 8 KB read buffer per write call (stack-local) |
-| `header(name, value)` | one UTF-8 encode at call time per header |
diff --git a/flash-bench/dependency-reduced-pom.xml b/flash-bench/dependency-reduced-pom.xml
deleted file mode 100644
index 01864f1..0000000
--- a/flash-bench/dependency-reduced-pom.xml
+++ /dev/null
@@ -1,58 +0,0 @@
-
-
-
- flash-parent
- dev.relism
- 1.0-SNAPSHOT
-
- 4.0.0
- flash-bench
-
- flash-bench
-
-
- maven-shade-plugin
- 3.5.3
-
-
- package
-
- shade
-
-
-
-
- dev.relism.bench.Main
-
-
-
-
-
- *:*
-
- META-INF/*.SF
- META-INF/*.DSA
- META-INF/*.RSA
-
-
-
-
-
-
-
-
-
-
-
- org.projectlombok
- lombok
- 1.18.44
- provided
-
-
-
- 21
- UTF-8
- 2.12.3
-
-
diff --git a/flash-bench/pom.xml b/flash-bench/pom.xml
deleted file mode 100644
index 92a1e88..0000000
--- a/flash-bench/pom.xml
+++ /dev/null
@@ -1,79 +0,0 @@
-
- 4.0.0
-
-
- dev.relism
- flash-parent
- 1.0-SNAPSHOT
-
-
- flash-bench
-
-
- 21
- UTF-8
- 2.12.3
-
-
-
-
- dev.relism
- flash
-
-
- org.slf4j
- slf4j-simple
-
-
- org.projectlombok
- lombok
-
-
- org.hibernate.orm
- hibernate-core
- 6.6.4.Final
-
-
- com.h2database
- h2
- 2.3.232
-
-
-
-
- flash-bench
-
-
- org.apache.maven.plugins
- maven-shade-plugin
- 3.5.3
-
-
- package
- shade
-
-
-
- dev.relism.bench.Main
-
-
-
-
-
- *:*
-
- META-INF/*.SF
- META-INF/*.DSA
- META-INF/*.RSA
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/flash-bench/src/main/java/dev/relism/bench/Main.java b/flash-bench/src/main/java/dev/relism/bench/Main.java
deleted file mode 100644
index 8483367..0000000
--- a/flash-bench/src/main/java/dev/relism/bench/Main.java
+++ /dev/null
@@ -1,148 +0,0 @@
-package dev.relism.bench;
-
-import dev.relism.HttpServer;
-import dev.relism.HttpServerConfiguration;
-import dev.relism.http.ContentType;
-import org.asynchttpclient.AsyncHttpClient;
-import org.asynchttpclient.DefaultAsyncHttpClientConfig;
-import org.asynchttpclient.Dsl;
-import org.asynchttpclient.Request;
-import org.asynchttpclient.RequestBuilder;
-
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Path;
-import java.util.Arrays;
-
-public class Main {
-
- // Lazy holder — Netty starts only on the first AHC request, not at class load.
- // Routes like /plaintext that never call AHC are unaffected.
- private static final class AhcHolder {
- static final AsyncHttpClient AHC = Dsl.asyncHttpClient(
- new DefaultAsyncHttpClientConfig.Builder()
- .setMaxConnections(256)
- .setMaxConnectionsPerHost(256)
- .setKeepAlive(true)
- .setPooledConnectionIdleTimeout(60_000)
- .setConnectTimeout(5_000)
- .setRequestTimeout(10_000)
- .setIoThreadsCount(1)
- .build()
- );
- }
-
- // Pre-built request objects — URI parsing done once at startup.
- private static final Request ELEMENT_REQ = new RequestBuilder("GET")
- .setUrl("http://web-data-source/element.json").build();
- private static final Request SHELLS_REQ = new RequestBuilder("GET")
- .setUrl("http://web-data-source/shells.json").build();
-
- private static final byte[] PREFIX_SHELLS = "{\"shells\":".getBytes(StandardCharsets.UTF_8);
- private static final byte[] SUFFIX = "}".getBytes(StandardCharsets.UTF_8);
-
- public static void main(String[] args) throws Exception {
- int port = args.length > 0 ? Integer.parseInt(args[0]) : 3000;
-
- HttpServer server = new HttpServer(
- HttpServerConfiguration.builder()
- .port(port).host("0.0.0.0").build()
- );
-
- server.get("/api/v1/periodic-table/element", (req, res) ->
- fetchAndFilter(req.query("symbol"), false));
- server.get("/api/v1/periodic-table/shells", (req, res) ->
- fetchAndFilter(req.query("symbol"), true));
- server.get("/plaintext", (req, res) -> "Hello, World!");
- server.get("/json", (req, res) -> {
- res.setContentType(ContentType.JSON);
- return "{\"message\":\"Hello, World!\"}";
- });
-
- Path frontendDist = Path.of("C:\\Users\\elorc\\Documents\\Coding\\web\\PixelDocs\\.vitepress\\dist");
- StaticResourceManager cdn = new StaticResourceManager(frontendDist, "/");
-
- cdn.register(server);
-
- server.start().thenRun(() -> System.out.println("Flash Sharkbench online on port " + port));
- }
-
- private static dev.relism.models.Response fetchAndFilter(String symbol, boolean wrapShells) {
- if (symbol == null) return new dev.relism.models.Response(400, "Missing symbol", ContentType.TEXT_PLAIN);
- try {
- // executeRequest() returns a ListenableFuture; .get() parks the calling
- // virtual thread (cheap) while Netty's I/O thread handles the socket.
- byte[] body = AhcHolder.AHC.executeRequest(wrapShells ? SHELLS_REQ : ELEMENT_REQ)
- .get()
- .getResponseBodyAsBytes();
-
- byte[] value = extractJsonValue(body, symbol);
- if (value == null) return new dev.relism.models.Response(404, "Not Found", ContentType.TEXT_PLAIN);
-
- if (wrapShells) {
- byte[] wrapped = new byte[PREFIX_SHELLS.length + value.length + SUFFIX.length];
- System.arraycopy(PREFIX_SHELLS, 0, wrapped, 0, PREFIX_SHELLS.length);
- System.arraycopy(value, 0, wrapped, PREFIX_SHELLS.length, value.length);
- System.arraycopy(SUFFIX, 0, wrapped, PREFIX_SHELLS.length + value.length, SUFFIX.length);
- return new dev.relism.models.Response(200, wrapped, ContentType.JSON);
- }
-
- return new dev.relism.models.Response(200, value, ContentType.JSON);
- } catch (Exception e) {
- return new dev.relism.models.Response(500, "Internal Error", ContentType.TEXT_PLAIN);
- }
- }
-
- /**
- * Extracts the JSON value for a given key from a flat JSON object.
- * Zero Jackson allocations — pure byte scan.
- */
- private static byte[] extractJsonValue(byte[] json, String key) {
- byte[] keyBytes = ("\"" + key + "\":").getBytes(StandardCharsets.UTF_8);
-
- int pos = indexOf(json, keyBytes);
- if (pos == -1) return null;
- pos += keyBytes.length;
-
- while (pos < json.length && json[pos] == ' ') pos++;
- if (pos >= json.length) return null;
-
- byte opener = json[pos];
- byte closer;
- if (opener == '{') closer = '}';
- else if (opener == '[') closer = ']';
- else return null;
-
- int depth = 0;
- boolean inString = false;
- int start = pos;
-
- while (pos < json.length) {
- byte b = json[pos];
- if (b == '"' && !isEscaped(json, pos)) inString = !inString;
- if (!inString) {
- if (b == opener) depth++;
- else if (b == closer) { if (--depth == 0) { pos++; break; } }
- }
- pos++;
- }
-
- return Arrays.copyOfRange(json, start, pos);
- }
-
- private static int indexOf(byte[] haystack, byte[] needle) {
- outer:
- for (int i = 0; i <= haystack.length - needle.length; i++) {
- for (int j = 0; j < needle.length; j++) {
- if (haystack[i + j] != needle[j]) continue outer;
- }
- return i;
- }
- return -1;
- }
-
- private static boolean isEscaped(byte[] data, int pos) {
- int backslashes = 0;
- while (--pos >= 0 && data[pos] == '\\') backslashes++;
- return (backslashes & 1) == 1;
- }
-}
\ No newline at end of file
diff --git a/flash-bench/src/main/java/dev/relism/bench/StaticResourceManager.java b/flash-bench/src/main/java/dev/relism/bench/StaticResourceManager.java
deleted file mode 100644
index 64d8efb..0000000
--- a/flash-bench/src/main/java/dev/relism/bench/StaticResourceManager.java
+++ /dev/null
@@ -1,176 +0,0 @@
-package dev.relism.bench;
-
-import dev.relism.HttpServer;
-import dev.relism.http.ContentType;
-import dev.relism.http.HttpStatus;
-import lombok.extern.slf4j.Slf4j;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.zip.GZIPOutputStream;
-
-@Slf4j
-public class StaticResourceManager {
- private static final int GZIP_THRESHOLD = 1024;
- private static final Map MIME_TYPES = new HashMap<>();
-
- static {
- MIME_TYPES.put("html", ContentType.TEXT_HTML);
- MIME_TYPES.put("css", ContentType.TEXT_CSS);
- MIME_TYPES.put("js", ContentType.TEXT_JAVASCRIPT);
- MIME_TYPES.put("mjs", ContentType.TEXT_JAVASCRIPT);
- MIME_TYPES.put("png", ContentType.IMAGE_PNG);
- MIME_TYPES.put("jpg", ContentType.IMAGE_JPEG);
- MIME_TYPES.put("jpeg", ContentType.IMAGE_JPEG);
- MIME_TYPES.put("svg", ContentType.IMAGE_SVG);
- MIME_TYPES.put("json", ContentType.JSON);
- MIME_TYPES.put("ico", ContentType.BINARY);
- MIME_TYPES.put("webp", ContentType.BINARY);
- }
-
- public record BakedAsset(byte[] raw, byte[] gzipped, ContentType contentType, boolean immutable, String etag) {}
-
- private final Map cache = new ConcurrentHashMap<>();
- private final String mountPath;
- private BakedAsset indexFallback;
-
- // Campi per le statistiche del report
- private long totalRawSize = 0, totalCompressedSize = 0;
- private int compressedCount = 0;
-
- public StaticResourceManager(Path rootPath, String mountPath) {
- this.mountPath = normalizeMountPath(mountPath);
-
- long start = System.currentTimeMillis();
- bakeAll(rootPath);
- long duration = System.currentTimeMillis() - start;
-
- // Richiamo del report qui
- printReport(duration);
- }
-
- private String normalizeMountPath(String path) {
- String p = path.startsWith("/") ? path : "/" + path;
- return p.endsWith("/") ? p.substring(0, p.length() - 1) : p;
- }
-
- private void bakeAll(Path rootPath) {
- if (!Files.exists(rootPath)) return;
- try (var stream = Files.walk(rootPath)) {
- stream.filter(Files::isRegularFile).forEach(file -> {
- try {
- String relative = rootPath.relativize(file).toString().replace("\\", "/");
- byte[] raw = Files.readAllBytes(file);
- byte[] gzipped = compressIfBeneficial(raw, relative);
- String etag = "\"" + Integer.toHexString(Arrays.hashCode(raw)) + "-" + raw.length + "\"";
- boolean isImmutable = relative.matches(".*\\.[a-f0-9]{8,}\\..*");
-
- BakedAsset asset = new BakedAsset(raw, gzipped, resolveContentType(relative), isImmutable, etag);
- cache.put(relative, asset);
-
- // Aggiornamento statistiche
- totalRawSize += raw.length;
- totalCompressedSize += (gzipped != null) ? gzipped.length : raw.length;
- if (gzipped != null) compressedCount++;
- if ("index.html".equals(relative)) indexFallback = asset;
- } catch (IOException ignored) {}
- });
- } catch (IOException ignored) {}
- }
-
- // --- IL METODO PRINTREPORT ---
- private void printReport(long duration) {
- double saved = totalRawSize > 0 ? (1.0 - (double)totalCompressedSize / totalRawSize) * 100 : 0;
- long immutableCount = cache.values().stream().filter(BakedAsset::immutable).count();
-
- System.out.println("\n" + "=".repeat(45));
- System.out.printf("🚀 FLASH ASSET BAKE COMPLETE [%dms]\n", duration);
- System.out.println("-".repeat(45));
- System.out.printf("📦 Total Assets: %d\n", cache.size());
- System.out.printf("🤐 Gzipped (On-Disk): %d\n", compressedCount);
- System.out.printf("💾 RAM Usage: %.2f MB\n", (double)totalRawSize / (1024 * 1024));
- System.out.printf("📉 Bandwidth Saving: %.1f%%\n", saved);
- System.out.println("-".repeat(45));
- System.out.println("🛡️ STRATEGIES ENABLED:");
- System.out.println(" • ETag Validation: [ACTIVE] (304 Not Modified)");
- System.out.printf(" • Immutable Assets: [%d files] (Cache: 1 year)\n", immutableCount);
- System.out.println(" • SPA Fallback: [ENABLED] (Route -> index.html)");
- System.out.println("=".repeat(45) + "\n");
- }
-
- public void register(HttpServer server) {
- server.get(mountPath + "/**", (req, res) -> {
- String path = req.getRequestLine().getPath().toString();
- String subPath = path.substring(mountPath.length()).replaceFirst("^/", "");
-
- BakedAsset asset = subPath.isEmpty() ? indexFallback : cache.get(subPath);
-
- if (asset == null) {
- if (!subPath.contains(".") && indexFallback != null) {
- return serve(res, indexFallback, req);
- }
- return res.status(HttpStatus.NOT_FOUND);
- }
- return serve(res, asset, req);
- });
- }
-
- private Object serve(dev.relism.models.Response res, BakedAsset asset, dev.relism.models.Request req) {
- // Caching condizionale (ETag)
- String ifNoneMatch = req.header("If-None-Match");
- if (asset.etag().equals(ifNoneMatch)) {
- return res.status(HttpStatus.NOT_MODIFIED);
- }
-
- res.type(asset.contentType());
- res.header("ETag", asset.etag());
-
- // Cache-Control Strategy
- if (asset.contentType() == ContentType.TEXT_HTML) {
- res.header("Cache-Control", "no-cache, must-revalidate");
- } else if (asset.immutable()) {
- res.header("Cache-Control", "public, max-age=31536000, immutable");
- } else {
- res.header("Cache-Control", "public, max-age=3600");
- }
-
- if (asset.gzipped() != null && acceptsGzip(req)) {
- res.header("Content-Encoding", "gzip");
- res.header("Vary", "Accept-Encoding");
- return res.body(asset.gzipped());
- }
- return res.body(asset.raw());
- }
-
- private boolean acceptsGzip(dev.relism.models.Request req) {
- String enc = req.header("Accept-Encoding");
- return enc != null && enc.contains("gzip");
- }
-
- private byte[] compressIfBeneficial(byte[] data, String name) {
- if (data.length < GZIP_THRESHOLD || isAlreadyCompressed(name)) return null;
- try (var baos = new ByteArrayOutputStream(); var gzip = new GZIPOutputStream(baos)) {
- gzip.write(data);
- gzip.finish();
- byte[] compressed = baos.toByteArray();
- return compressed.length < (data.length * 0.9) ? compressed : null;
- } catch (IOException e) { return null; }
- }
-
- private boolean isAlreadyCompressed(String n) {
- n = n.toLowerCase();
- return n.endsWith(".png") || n.endsWith(".jpg") || n.endsWith(".jpeg") || n.endsWith(".ico") || n.endsWith(".webp");
- }
-
- private ContentType resolveContentType(String filename) {
- int dot = filename.lastIndexOf('.');
- String ext = (dot > 0) ? filename.substring(dot + 1).toLowerCase() : "";
- return MIME_TYPES.getOrDefault(ext, ContentType.BINARY);
- }
-}
\ No newline at end of file
diff --git a/flash-bench/src/main/resources/element.json b/flash-bench/src/main/resources/element.json
deleted file mode 100644
index 0353f15..0000000
--- a/flash-bench/src/main/resources/element.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "H": {"name": "Hydrogen", "number": 1, "group": 1},
- "He": {"name": "Helium", "number": 2, "group": 18},
- "Li": {"name": "Lithium", "number": 3, "group": 1},
- "Be": {"name": "Beryllium", "number": 4, "group": 2},
- "B": {"name": "Boron", "number": 5, "group": 13},
- "C": {"name": "Carbon", "number": 6, "group": 14},
- "N": {"name": "Nitrogen", "number": 7, "group": 15},
- "O": {"name": "Oxygen", "number": 8, "group": 16},
- "F": {"name": "Fluorine", "number": 9, "group": 17},
- "Ne": {"name": "Neon", "number": 10, "group": 18}
-}
diff --git a/flash-bench/src/main/resources/shells.json b/flash-bench/src/main/resources/shells.json
deleted file mode 100644
index e645b57..0000000
--- a/flash-bench/src/main/resources/shells.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "H": {"shells": [1]},
- "He": {"shells": [2]},
- "Li": {"shells": [2, 1]},
- "Be": {"shells": [2, 2]},
- "B": {"shells": [2, 3]},
- "C": {"shells": [2, 4]},
- "N": {"shells": [2, 5]},
- "O": {"shells": [2, 6]},
- "F": {"shells": [2, 7]},
- "Ne": {"shells": [2, 8]}
-}
diff --git a/flash-bench/src/main/resources/simplelogger.properties b/flash-bench/src/main/resources/simplelogger.properties
deleted file mode 100644
index 263f2a3..0000000
--- a/flash-bench/src/main/resources/simplelogger.properties
+++ /dev/null
@@ -1,2 +0,0 @@
-# Silence all logs during benchmarks to avoid I/O becoming the bottleneck
-org.slf4j.simpleLogger.defaultLogLevel=off
diff --git a/jmh-result.text b/jmh-result.text
deleted file mode 100644
index 48aabd1..0000000
--- a/jmh-result.text
+++ /dev/null
@@ -1 +0,0 @@
-Benchmark Mode Cnt Score Error Units
diff --git a/pom.xml b/pom.xml
index be9c68e..61f2318 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,7 +11,6 @@
flash
- flash-bench