refactored, pre-buffer reuse

This commit is contained in:
Relism
2026-03-15 14:31:52 +01:00
parent f1beb160aa
commit b0d606cb5f
64 changed files with 888 additions and 400 deletions
+3 -1
View File
@@ -36,4 +36,6 @@ build/
.vscode/
### Mac OS ###
.DS_Store
.DS_Store
CLAUDE.md
-10
View File
@@ -1,10 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/
+2
View File
@@ -3,6 +3,8 @@
<component name="Encoding">
<file url="file://$PROJECT_DIR$/flash-bench/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/flash-bench/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/flash/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/flash/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
+1 -5
View File
@@ -10,12 +10,8 @@
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
<option name="ignoredFiles">
<set>
<option value="$PROJECT_DIR$/flash-bench/pom.xml" />
</set>
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
Generated
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="" vcs="Git" />
</component>
</project>
+290
View File
@@ -0,0 +1,290 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="fc757130-fe3e-4ea9-8d69-c26ffb8545a4" name="Changes" comment="pre-module refactor">
<change afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
<change afterPath="$PROJECT_DIR$/flash-bench/pom.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/flash-bench/src/main/java/dev/relism/bench/Main.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/flash-bench/src/main/resources/simplelogger.properties" afterDir="false" />
<change afterPath="$PROJECT_DIR$/flash/pom.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/.gitignore" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/encodings.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/encodings.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/vcs.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/CLAUDE.md" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/pom.xml" beforeDir="false" afterPath="$PROJECT_DIR$/pom.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/Flash.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/Flash.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/HttpServer.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/HttpServer.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/HttpServerConfiguration.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/HttpServerConfiguration.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/Main.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/Main.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/RequestParser.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/RequestParser.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/exceptions/DuplicateNamespaceException.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/exceptions/DuplicateNamespaceException.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/http/ContentType.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/http/ContentType.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/http/HttpMethod.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/http/HttpMethod.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/http/HttpStatus.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/http/HttpStatus.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/HeaderMap.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/HeaderMap.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/LazyBody.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/LazyBody.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/PathParams.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/PathParams.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/QueryParams.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/QueryParams.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/Request.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/Request.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/RequestHandler.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/RequestHandler.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/RequestLine.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/RequestLine.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/Response.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/Response.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/SimpleHandler.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/SimpleHandler.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/routing/AbstractRouter.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/AbstractRouter.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/routing/GlobalRouter.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/GlobalRouter.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/routing/PathUtils.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/PathUtils.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/routing/Route.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/Route.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImpl.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImpl.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathViews.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathViews.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/routing/routers/radix/RadixPathRouterImpl.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/routers/radix/RadixPathRouterImpl.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/template/ByteTemplate.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/template/ByteTemplate.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/template/ErrorPages.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/template/ErrorPages.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/resources/assets/html/default_404.html" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/resources/assets/html/default_404.html" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/resources/assets/html/default_exception.html" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/resources/assets/html/default_exception.html" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/resources/assets/logo.png" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/resources/assets/logo.png" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/HttpServerConcurrencyTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/HttpServerConcurrencyTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/HttpServerTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/HttpServerTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/RequestParserTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/RequestParserTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/http/ContentTypeTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/http/ContentTypeTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/http/HttpMethodTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/http/HttpMethodTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/http/HttpStatusTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/http/HttpStatusTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/models/HeaderMapTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/HeaderMapTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/models/PathParamsTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/PathParamsTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/models/QueryParamsTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/QueryParamsTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/models/RequestLineTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/RequestLineTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/models/RequestTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/RequestTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/models/ResponseTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/ResponseTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/models/SimpleHandlerTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/SimpleHandlerTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/routing/AbstractRouterTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/routing/AbstractRouterTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/routing/GlobalRouterTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/routing/GlobalRouterTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/routing/PathUtilsTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/routing/PathUtilsTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImplTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImplTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/routing/routers/fastpathrouter/FastPathViewsTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/routing/routers/fastpathrouter/FastPathViewsTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/template/ByteTemplateTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/template/ByteTemplateTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/template/ErrorPagesTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/template/ErrorPagesTest.java" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="CopilotPersistence">
<persistenceIdMap>
<entry key="_C:/Users/elorc/Documents/Coding/Java/practice/Flash" value="3Axc6mzLyNvh4TgGFvNSCSq83xw" />
<entry key="_C:/Users/elorc/Documents/Coding/Java/practice/FlashPractice" value="3AoiAx4zfxMcuO6fDI9oALOPcq1" />
</persistenceIdMap>
</component>
<component name="EmbeddingIndexingInfo">
<option name="cachedIndexableFilesCount" value="88" />
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Enum" />
<option value="Class" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="GitHubPullRequestSearchHistory">{
&quot;lastFilter&quot;: {
&quot;state&quot;: &quot;OPEN&quot;,
&quot;assignee&quot;: &quot;Relism&quot;
}
}</component>
<component name="GithubPullRequestsUISettings">{
&quot;selectedUrlAndAccountId&quot;: {
&quot;url&quot;: &quot;https://github.com/Relism/Flash5.git&quot;,
&quot;accountId&quot;: &quot;86d8a39c-af27-4b79-8d5d-dc74375c3348&quot;
}
}</component>
<component name="McpProjectServerCommands">
<commands />
<urls />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 3
}</component>
<component name="ProjectId" id="3AoiAx4zfxMcuO6fDI9oALOPcq1" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;Application.ExternalBenchmark.executor&quot;: &quot;Run&quot;,
&quot;Application.Main.executor&quot;: &quot;Run&quot;,
&quot;Application.dev.relism.bench.Main.executor&quot;: &quot;Run&quot;,
&quot;JUnit.RequestParserTest.executor&quot;: &quot;Run&quot;,
&quot;JUnit.RequestParserTest.headers_caseInsensitive.executor&quot;: &quot;Debug&quot;,
&quot;Maven.FlashPractice [test].executor&quot;: &quot;Run&quot;,
&quot;Maven.flash [compile].executor&quot;: &quot;Run&quot;,
&quot;Maven.flash [install].executor&quot;: &quot;Run&quot;,
&quot;Maven.flash-bench [clean].executor&quot;: &quot;Run&quot;,
&quot;Maven.flash-bench [install].executor&quot;: &quot;Run&quot;,
&quot;Maven.flash-bench [validate].executor&quot;: &quot;Run&quot;,
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
&quot;RunOnceActivity.MCP Project settings loaded&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;RunOnceActivity.typescript.service.memoryLimit.init&quot;: &quot;true&quot;,
&quot;SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;,
&quot;ignore.virus.scanning.warn.message&quot;: &quot;true&quot;,
&quot;kotlin-language-version-configured&quot;: &quot;true&quot;,
&quot;last_opened_file_path&quot;: &quot;C:/Users/elorc/Documents/Coding/Java/practice/Flash&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;onboarding.tips.debug.path&quot;: &quot;C:/Users/elorc/Documents/Coding/Java/practice/FlashPractice/flash-bench/src/main/java/dev/relism/Main.java&quot;,
&quot;project.structure.last.edited&quot;: &quot;Modules&quot;,
&quot;project.structure.proportion&quot;: &quot;0.15&quot;,
&quot;project.structure.side.proportion&quot;: &quot;0.1150748&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;ml.llm.LLMThirdPartyAiProvidersConfigurable&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}</component>
<component name="RecentsManager">
<key name="MoveFile.RECENT_KEYS">
<recent name="C:\Users\elorc\Documents\Coding\Java\practice\Flash" />
</key>
</component>
<component name="RunManager" selected="Application.ExternalBenchmark">
<configuration name="ExternalBenchmark" type="Application" factoryName="Application" temporary="true" nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="dev.relism.bench.ExternalBenchmark" />
<module name="flash-bench" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="dev.relism.bench.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<configuration name="Main" type="Application" factoryName="Application" temporary="true" nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="dev.relism.Main" />
<module name="flash" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="dev.relism.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<configuration name="dev.relism.bench.Main" type="Application" factoryName="Application" temporary="true" nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="dev.relism.bench.Main" />
<module name="flash-bench" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="dev.relism.bench.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<recent_temporary>
<list>
<item itemvalue="Application.ExternalBenchmark" />
<item itemvalue="Application.dev.relism.bench.Main" />
<item itemvalue="Application.Main" />
</list>
</recent_temporary>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-jdk-30f59d01ecdd-2fc7cc6b9a17-intellij.indexing.shared.core-IU-253.30387.90" />
<option value="bundled-js-predefined-d6986cc7102b-9b0f141eb926-JavaScript-IU-253.30387.90" />
</set>
</attachedChunks>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="fc757130-fe3e-4ea9-8d69-c26ffb8545a4" name="Changes" comment="" />
<created>1773265186049</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1773265186049</updated>
<workItem from="1773265187409" duration="5496000" />
<workItem from="1773313172181" duration="9617000" />
<workItem from="1773325837582" duration="3918000" />
<workItem from="1773400995673" duration="23805000" />
<workItem from="1773442006686" duration="2722000" />
<workItem from="1773491408276" duration="23095000" />
<workItem from="1773528595525" duration="7400000" />
<workItem from="1773536222450" duration="680000" />
<workItem from="1773536904928" duration="395000" />
<workItem from="1773537302622" duration="150000" />
<workItem from="1773537489622" duration="5048000" />
<workItem from="1773576241900" duration="5173000" />
</task>
<task id="LOCAL-00001" summary="Initial">
<option name="closed" value="true" />
<created>1773536078762</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1773536078762</updated>
</task>
<task id="LOCAL-00002" summary="pre-module refactor">
<option name="closed" value="true" />
<created>1773536678416</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1773536678416</updated>
</task>
<option name="localTasksCounter" value="3" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State />
</value>
</entry>
</map>
</option>
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="Initial" />
<MESSAGE value="pre-module refactor" />
<option name="LAST_COMMIT_MESSAGE" value="pre-module refactor" />
</component>
<component name="XSLT-Support.FileAssociations.UIState">
<expand />
<select />
</component>
<component name="github-copilot-workspace">
<instructionFileLocations>
<option value=".github/instructions" />
</instructionFileLocations>
<promptFileLocations>
<option value=".github/prompts" />
</promptFileLocations>
</component>
</project>
-78
View File
@@ -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.
+31
View File
@@ -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
```
+95
View File
@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>dev.relism</groupId>
<artifactId>flash-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>flash-bench</artifactId>
<packaging>jar</packaging>
<properties>
<jmh.version>1.37</jmh.version>
</properties>
<dependencies>
<dependency>
<groupId>dev.relism</groupId>
<artifactId>flash</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.3</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>dev.relism.bench.Main</mainClass>
</transformer>
<!-- Required by JMH to merge service-loader files -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -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();
}
}
@@ -0,0 +1,2 @@
# Silence all logs during benchmarks to avoid I/O becoming the bottleneck
org.slf4j.simpleLogger.defaultLogLevel=off
+39
View File
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>dev.relism</groupId>
<artifactId>flash-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>flash</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>dev.relism</groupId>
<artifactId>fpr-core</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
</dependency>
</dependencies>
</project>
@@ -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.
*
* <p>
* Internally, the server owns a {@link GlobalRouter} with two tiers of routing:
* <ol>
* <li><b>Mounted sub-routers</b> — registered via {@link #mount}. Each owns a
* namespace
* prefix (e.g. {@code /api}) and handles all paths under it. Matched by longest
* prefix first.</li>
* <li><b>Internal router</b> — the fallback used when no sub-router claims the
* path.
* Routes registered directly on the server ({@link #get}, {@link #post}, etc.)
* go here.</li>
* </ol>
*
* <p>
* 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<Void> 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<Void> 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<Void> 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.
*
* <p>
* 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;
}
}
}
@@ -40,6 +40,8 @@ public class Main {
throw new RuntimeException(req.getQueryParam("test"));
});
server.start().thenRun(() -> log.info("Server started on port: " + PORT));
}
}
@@ -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;
@@ -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<Integer, byte[]> 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;
}
}
}

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

+69 -61
View File
@@ -5,14 +5,21 @@
<modelVersion>4.0.0</modelVersion>
<groupId>dev.relism</groupId>
<artifactId>Flash</artifactId>
<artifactId>flash-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<packaging>pom</packaging>
<modules>
<module>flash</module>
<module>flash-bench</module>
</modules>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<lombok.version>1.18.44</lombok.version>
<slf4j.version>2.0.16</slf4j.version>
</properties>
<repositories>
@@ -23,66 +30,67 @@
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.44</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>dev.relism</groupId>
<artifactId>fpr-core</artifactId>
<version>1.1.0</version>
</dependency>
<!-- Logging facade — always on classpath; users provide their own SLF4J binding -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.16</version>
</dependency>
<!-- Dev-only binding (simple stdout); excluded from any distributed artifact -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.16</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.42</version>
<scope>provided</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>dev.relism</groupId>
<artifactId>flash</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>dev.relism</groupId>
<artifactId>fpr-core</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.44</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
</project>
-226
View File
@@ -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.
*
* <p>Internally, the server owns a {@link GlobalRouter} with two tiers of routing:
* <ol>
* <li><b>Mounted sub-routers</b> — registered via {@link #mount}. Each owns a namespace
* prefix (e.g. {@code /api}) and handles all paths under it. Matched by longest prefix first.</li>
* <li><b>Internal router</b> — the fallback used when no sub-router claims the path.
* Routes registered directly on the server ({@link #get}, {@link #post}, etc.) go here.</li>
* </ol>
*
* <p>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<Void> 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<Void> 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<Void> 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.
*
* <p>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; }
}
}