47 Commits

Author SHA1 Message Date
4a06bc67ea chore(changelog): update changelog for v0.2.0
Some checks failed
Sync Release to GitHub / build-and-publish (release) Failing after 7s
2025-05-27 13:08:31 +00:00
68cec85380 chore(version): bump version to 0.2.0
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 3s
Auto Changelog & Release / changelog-only (push) Has been skipped
CI / build (push) Successful in 15s
Auto Changelog & Release / release (push) Successful in 7s
2025-05-27 15:08:14 +02:00
86895f41f1 chore(changelog): update unreleased changelog 2025-05-27 13:06:24 +00:00
de6d3ee389 feat(workflows): add GitHub release sync workflow
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 2s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 6s
CI / build (push) Successful in 17s
- Add a workflow to synchronize Gitea releases with GitHub
- Remove deprecated upload-assets workflow example
2025-05-27 15:06:11 +02:00
54cfa1888e feat(ci): enhance CI workflow with granular steps and error handling
- Add separate steps for formatting, linting, testing, and benchmarking
- Introduce failure detection to halt workflow on step failure
- Improve maintainability and visibility of CI process outcomes
2025-05-27 15:06:11 +02:00
c207dc7392 chore(config): add CI task for local checks
- Introduces a new "ci" task to streamline local CI workflows
- Combines formatting, linting, testing, and benchmarking for convenience
2025-05-27 15:06:11 +02:00
b14e9acc5f test(routebuilder): add validation tests for handler and middleware
- Add tests to validate middleware and handler signatures
- Ensure TypeError is thrown for invalid signatures
2025-05-27 15:06:11 +02:00
731bba22d8 test(httpkernel): enforce compile-time validation for signatures
- Replace runtime 500 errors with compile-time validation for invalid
  middleware and handler signatures using `assertThrows`
- Improve test clarity by explicitly checking for expected exceptions
  and error messages
2025-05-27 15:06:11 +02:00
aea3fb45e7 refactor(kernel): simplify middleware and handler execution
- Replace middleware pipeline logic with route-specific execution
- Remove redundant type checks and streamline error handling
- Improve maintainability by delegating response decoration to routes
2025-05-27 15:06:11 +02:00
35d83c073e feat(route-builder): add middleware chain compilation
- Introduces a `compile` method to compose middleware and handler
  into a single executable function.
- Enhances runtime safety by ensuring `next()` is called only once
  per middleware and validating middleware/handler signatures.
- Improves code structure and maintainability for route execution.
2025-05-27 15:06:11 +02:00
67ebb4307a feat(interfaces): add runRoute method to IInternalRoute
- Introduces a statically compiled `runRoute` method to encapsulate
  the middleware chain and final handler execution for routes.
- Ensures consistent pipeline execution with safeguards like single
  `next()` invocation and a guaranteed `Response` return type.
2025-05-27 15:06:11 +02:00
3da34e2684 test(bench): add parallel benchmarks for HTTP kernel
- Introduce parallel benchmarks for simple and complex HTTP requests
- Improve performance testing with higher concurrency (10,000 requests)
- Comment out single-request benchmarks for future reference
2025-05-27 15:06:11 +02:00
26198ccdcd chore(changelog): update unreleased changelog 2025-05-27 13:04:23 +00:00
1b447f5190 chore(gitignore): add .local directory to ignored files
Some checks failed
Auto Changelog & Release / detect-version-change (push) Successful in 3s
Auto Changelog & Release / release (push) Has been skipped
CI / build (push) Failing after 7s
Auto Changelog & Release / changelog-only (push) Successful in 6s
- Updates .gitignore to include the .local directory
- Prevents accidental commits of local configuration files
2025-05-27 15:04:09 +02:00
38c00b035b chore(ci): update deno tasks in CI workflow
- Replace individual deno commands with `deno task` equivalents
- Add benchmark task to the CI workflow for performance testing
2025-05-27 15:04:09 +02:00
6e6e61693f chore(tasks): add benchmark, format, and lint commands
- Introduces a benchmark task for running performance tests
- Adds format and lint tasks to enforce code style and quality
2025-05-27 15:04:09 +02:00
d8283ea83a chore(changelog): update unreleased changelog 2025-05-27 11:25:56 +00:00
ec1697df94 ci(workflows): consolidate and update CI configuration
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 3s
CI / build (push) Successful in 7s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 6s
- Replace `test.yml` with `ci.yml` for streamlined workflows
- Add support for `deno fmt` and `deno lint` in CI checks
- Ensure tests run on `main` branch pushes and PR events
2025-05-27 13:25:44 +02:00
e416af9754 chore(changelog): update unreleased changelog 2025-05-27 11:18:44 +00:00
b83aa330b3 refactor(imports): use explicit type-only imports across codebase
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 3s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 6s
- Replace standard imports with type-only imports to improve clarity
  and align with TypeScript best practices.
- Ensure consistency across modules by modifying all relevant files.
2025-05-27 13:18:31 +02:00
c28eb7f28d chore(config): add exports field to module metadata
- Introduces an `exports` field to map module entry points
2025-05-27 13:18:18 +02:00
d7460c4b1d chore(changelog): update unreleased changelog 2025-05-10 15:51:02 +00:00
6a0f1c774b docs(release): update guidelines for handling changelog
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 4s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 7s
- Add instructions to avoid merge conflicts in `CHANGELOG.md`
- Provide steps for using `.gitattributes` to automate conflict resolution
- Clarify recommended workflows for feature branches and merging
2025-05-10 17:50:44 +02:00
7327ed5c1b chore(changelog): update unreleased changelog 2025-05-10 15:44:02 +00:00
b44bb2ddaf feat(workflows): conditionally generate changelog
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 5s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 11s
- Add logic to generate changelog only if the file exists or on the main branch
- Prevent unnecessary changelog generation in other contexts
2025-05-10 17:43:39 +02:00
dd60c2cdc7 chore(changelog): update unreleased changelog 2025-05-10 15:26:06 +00:00
6399113e12 chore(git): ignore merge conflicts for CHANGELOG.md
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 5s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 8s
2025-05-10 17:25:51 +02:00
813c734ae6 chore(changelog): update unreleased changelog 2025-05-10 15:00:52 +00:00
3707242d27 fix(workflows): ensure version detection output is always set
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 4s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 8s
- Move VERSION file change detection behind a branch guard (main only)
- Use a fallback in the output step to ensure 'version_changed' is always defined
- Prevent job skipping and output access errors in feature branches
2025-05-10 17:00:27 +02:00
adc1a4276e chore(changelog): update unreleased changelog 2025-05-10 14:36:02 +00:00
71ea4247b3 chore(workflows): refine branch handling in release process
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 4s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 8s
- Adjusts workflow to support pushes from all branches
- Ensures main branch-specific conditions for version detection
- Modifies changelog and release steps for non-main branch handling
2025-05-10 16:35:39 +02:00
b624415320 chore(changelog): update unreleased changelog 2025-05-08 18:19:58 +00:00
a88b4d112f chore(workflows): update changelog file extension to .md and revert b9d25f23fc
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 5s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 5s
2025-05-08 20:19:43 +02:00
4f2b65049f chore: rename changelog and readme files to use .md extension 2025-05-08 20:19:43 +02:00
c9de4669c7 chore(changelog): update unreleased changelog 2025-05-08 18:16:14 +00:00
a1ce30627c docs: add README for HttpKernel project
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 3s
Auto Changelog & Release / changelog-only (push) Successful in 7s
Auto Changelog & Release / release (push) Has been skipped
- Introduce HttpKernel with an overview of its purpose and features
- Provide a quick start guide and API reference for developers
- Include testing instructions, configuration options, and roadmap
- Add license information for clarity and compliance
2025-05-08 20:15:58 +02:00
5118a19aea docs: add MIT license file 2025-05-08 20:15:48 +02:00
0a09c8c324 chore(changelog): update unreleased changelog 2025-05-08 18:09:16 +00:00
b9d25f23fc refactor(workflows): rename changelog file for consistency
All checks were successful
Test http-kernel / Run Tests (pull_request) Successful in 10s
Auto Changelog & Release / detect-version-change (push) Successful in 7s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 12s
- Rename `CHANGELOG.md` to `CHANGELOG` in workflows
- Update related commands and references for consistency
2025-05-08 19:49:24 +02:00
92f09e6e60 Merge pull request 'chore(workflows): update test workflow for http-kernel project' (#1) from fix/test-workflow into main
Some checks failed
Auto Changelog & Release / detect-version-change (push) Successful in 3s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Failing after 10s
Reviewed-on: #1
2025-05-08 19:46:00 +02:00
03115464e0 chore(workflows): update test workflow for http-kernel project
All checks were successful
Test http-kernel / Run Tests (pull_request) Successful in 10s
- Rename workflow and job to align with the http-kernel project
- Improve clarity and maintain consistency in workflow naming
2025-05-08 19:38:49 +02:00
11a2273240 chore(changelog): update unreleased changelog 2025-05-08 17:35:37 +00:00
1233a0b720 chore(workflows): update Deno setup action to v2
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 6s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 10s
- Updates the Deno setup action to the latest major version (v2)
- Ensures compatibility and benefits from improvements in the new version
2025-05-08 19:35:11 +02:00
195619ca99 chore(changelog): update unreleased changelog 2025-05-08 17:33:23 +00:00
9d5db4f414 feat(workflows): add CI for Deno project tests
Some checks are pending
Auto Changelog & Release / release (push) Blocked by required conditions
Auto Changelog & Release / detect-version-change (push) Successful in 5s
Auto Changelog & Release / changelog-only (push) Successful in 18s
- Introduce a GitHub Actions workflow to run tests for the Deno project
- Configure workflow to trigger on pull requests across all branches
- Include steps for repository checkout, Deno setup, and test execution
2025-05-08 19:32:51 +02:00
16c0053964 chore(workflows): remove unused workflow_dispatch trigger
- Eliminates the workflow_dispatch trigger from the release workflow
  as it is no longer required, simplifying the configuration.
2025-05-08 19:32:42 +02:00
04029f87a3 chore(tasks): remove commented-out start and watch scripts
- Cleans up unused task definitions for better maintainability
- Reduces clutter in the task configuration
2025-05-08 19:32:34 +02:00
37 changed files with 699 additions and 211 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
CHANGELOG.md merge=ours

View File

@@ -122,3 +122,77 @@ git commit -m "chore(version): bump to 1.2.3"
```
> Nur die ersten beiden erscheinen im Changelog – der dritte wird **automatisch übersprungen**.
---
## 🧾 Umgang mit `CHANGELOG.md` beim Mergen und Releasen
Wenn du automatisiert einen Changelog mit `git-cliff` erzeugst, ist `CHANGELOG.md` ein **generiertes Artefakt** – und kein handgepflegter Quelltext.
Beim Mergen von Feature-Branches in `main` kann es deshalb zu **unnötigen Konflikten** in dieser Datei kommen, obwohl der Inhalt später sowieso neu erzeugt wird.
---
## 🧼 Umgang mit `CHANGELOG.md` in Feature-Branches
Wenn du mit **Feature-Branches** arbeitest, wird `CHANGELOG.md` dort oft automatisch erzeugt.
Das kann beim späteren Merge in `main` zu **unnötigen Merge-Konflikten** führen.
### ✅ Empfohlene Vorgehensweise
**Bevor du den Branch mit `main` zusammenführst** (Merge oder Cherry-Pick):
```bash
git rm CHANGELOG.md
git commit -m "chore(changelog): remove generated CHANGELOG.md before merge"
git push
```
Dadurch:
* verhinderst du Merge-Konflikte mit `CHANGELOG.md`
* wird die Datei bei Feature-Branches nicht mehr automatisch erzeugt
* bleibt deine Historie sauber und konfliktfrei
> 💡 Der Workflow erzeugt `CHANGELOG.md` automatisch **nur**, wenn:
>
> * die Datei schon vorhanden ist **oder**
> * der Branch `main` heißt
---
## 🧩 Merge-Konflikte verhindern mit `.gitattributes`
Damit Git bei Konflikten in `CHANGELOG.md` **automatisch deine Version bevorzugt**, kannst du folgende Zeile in die Datei `.gitattributes` aufnehmen:
```gitattributes
CHANGELOG.md merge=ours
```
Das bedeutet:
* Beim Merge wird die Version aus dem aktuellen Branch (`ours`) behalten
* Änderungen aus dem Ziel-Branch (`theirs`) werden verworfen
### ✅ So verwendest du es richtig:
1. **Füge die Regel in `main` hinzu**:
```bash
echo "CHANGELOG.md merge=ours" >> .gitattributes
git add .gitattributes
git commit -m "chore(git): prevent merge conflicts in CHANGELOG.md"
git push origin main
```
2. **Hole sie in deinen Feature-Branch**:
```bash
git checkout feature/xyz
git rebase origin/main
```
3. **Ab sofort werden Konflikte in `CHANGELOG.md` automatisch aufgelöst** – lokal.
> ⚠️ Hinweis: Plattformen wie **Gitea, GitHub oder GitLab ignorieren `.gitattributes` beim Merge über die Web-Oberfläche**.
> Führe Merges daher **lokal** durch, wenn du Konflikte verhindern willst.

52
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,52 @@
name: CI
on:
workflow_dispatch:
push:
branches:
- main
pull_request:
types:
- opened
- synchronize
- reopened
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: denoland/setup-deno@v1
with:
deno-version: v2.x
- name: Format
id: format
continue-on-error: true
run: deno task fmt
- name: Lint
id: lint
continue-on-error: true
run: deno task lint
- name: Test
id: test
continue-on-error: true
run: deno task test
- name: Benchmark
id: benchmark
continue-on-error: true
run: deno task benchmark
- name: Fail if any step failed
if: |
steps.format.outcome != 'success' ||
steps.lint.outcome != 'success' ||
steps.test.outcome != 'success' ||
steps.benchmark.outcome != 'success'
run: |
echo "::error::One or more steps failed"
exit 1

View File

@@ -1,42 +1,47 @@
name: Auto Changelog & Release
on:
workflow_dispatch:
push:
branches:
- main
- '**'
jobs:
detect-version-change:
runs-on: ubuntu-latest
outputs:
version_changed: ${{ steps.check.outputs.version_changed }}
version_changed: ${{ steps.set.outputs.version_changed }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check if VERSION file changed
id: check
if: github.ref == 'refs/heads/main'
run: |
echo "🔍 Vergleich mit github.event.before:"
echo "Before: ${{ github.event.before }}"
echo "After: ${{ github.sha }}"
echo "📄 Changed files between before and after:"
git diff --name-only ${{ github.event.before }} ${{ github.sha }} || echo "(diff failed)"
if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -q '^VERSION$'; then
echo "✅ VERSION file was changed between before and after"
echo "version_changed=true" >> $GITHUB_OUTPUT
echo "✅ VERSION file was changed"
echo "VERSION_CHANGED=true" >> $GITHUB_ENV
else
echo "ℹ️ VERSION file not changed between before and after"
echo "version_changed=false" >> $GITHUB_OUTPUT
echo "ℹ️ VERSION file not changed"
echo "VERSION_CHANGED=false" >> $GITHUB_ENV
fi
- name: Set output (always)
id: set
run: |
echo "version_changed=${VERSION_CHANGED:-false}" >> $GITHUB_OUTPUT
changelog-only:
needs: detect-version-change
if: needs.detect-version-change.outputs.version_changed == 'false'
if: github.ref != 'refs/heads/main' || needs.detect-version-change.outputs.version_changed == 'false'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -66,22 +71,28 @@ jobs:
run: |
cargo install git-cliff --version "${{ steps.cliff_version.outputs.version }}" --features gitea
- name: Generate unreleased changelog
run: git-cliff -c cliff.toml -o CHANGELOG.md
- name: Generate unreleased changelog (if file exists or on main)
run: |
if [[ -f CHANGELOG.md || "${GITHUB_REF##refs/heads/}" == "main" ]]; then
echo "Generating CHANGELOG.md..."
git-cliff -c cliff.toml -o CHANGELOG.md
else
echo "CHANGELOG.md does not exist and this is not 'main'. Skipping generation."
fi
- name: Commit updated CHANGELOG.md
- name: Commit updated CHANGELOG
run: |
git add CHANGELOG.md
if git diff --cached --quiet; then
echo "No changes to commit"
else
git commit -m "chore(changelog): update unreleased changelog"
git push origin main
git push origin "${GITHUB_REF##refs/heads/}"
fi
release:
needs: detect-version-change
if: needs.detect-version-change.outputs.version_changed == 'true'
if: needs.detect-version-change.outputs.version_changed == 'true' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -142,7 +153,7 @@ jobs:
echo "changelog_body_path=$BODY" >> $GITHUB_OUTPUT
- name: Commit updated CHANGELOG.md
- name: Commit updated CHANGELOG
run: |
git add CHANGELOG.md
if git diff --cached --quiet; then

View File

@@ -0,0 +1,27 @@
name: Sync Release to GitHub
on:
release:
types: [published]
jobs:
build-and-publish:
runs-on: ubuntu-22.04
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name }}
- name: Run Releases Sync Action
uses: https://git.0xmax42.io/actions/releases-sync@main
with:
gitea_token: $ACTIONS_RUNTIME_TOKEN
gitea_url: https://git.0xmax42.io
gitea_owner: maxp
gitea_repo: http-kernel
tag_name: ${{ inputs.tag || github.event.release.tag_name }}
github_token: ${{ secrets.SYNC_GITHUB_TOKEN }}
github_owner: 0xMax42
github_repo: http-kernel

View File

@@ -1,47 +0,0 @@
# ========================
# 📦 Upload Assets Template
# ========================
# Dieser Workflow wird automatisch ausgelöst, wenn ein Release
# in Gitea veröffentlicht wurde (event: release.published).
#
# Er dient dem Zweck, Release-Artefakte (wie z. B. Binary-Dateien,
# Changelogs oder Build-Zips) nachträglich mit dem Release zu verknüpfen.
#
# Voraussetzung: Zwei Shell-Skripte liegen im Projekt:
# - .gitea/scripts/get-release-id.sh → ermittelt Release-ID per Tag
# - .gitea/scripts/upload-asset.sh → lädt Datei als Release-Asset hoch
#
# --------------------------------------
name: Upload Assets
on:
release:
types: [published] # Nur bei Veröffentlichung eines Releases (nicht bei Entwürfen)
jobs:
upload-assets:
runs-on: ubuntu-latest
steps:
# 📥 Checke den Stand des Repos aus, exakt auf dem veröffentlichten Tag
# So ist garantiert, dass die Artefakte dem Zustand des Releases entsprechen.
- uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name }} # z. B. "v1.2.3"
fetch-depth: 0 # vollständige Git-Historie (für z. B. git-cliff, logs etc.)
# 🆔 Hole die Release-ID basierend auf dem Tag
# Die ID wird als Umgebungsvariable RELEASE_ID über $GITHUB_ENV verfügbar gemacht.
- name: Get Release ID from tag
run: .gitea/scripts/get-release-id.sh "${{ github.event.release.tag_name }}"
# 🔼 Upload eines Release-Assets
# Beispiel: Lade CHANGELOG.md als Datei mit abweichendem Namen "RELEASE-NOTES.md" hoch
#
# Du kannst beliebig viele Upload-Schritte hinzufügen oder in einer Schleife iterieren.
#
# Hinweis: RELEASE_ID wird automatisch verwendet, da get-release-id.sh sie exportiert.
#
# - name: Upload CHANGELOG.md as RELEASE-NOTES.md
# run: .gitea/scripts/upload-asset.sh ./CHANGELOG.md RELEASE-NOTES.md

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
coverage/
logs/
.locale/
.local/
cache/
out.py
output.txt

View File

@@ -2,6 +2,56 @@
All notable changes to this project will be documented in this file.
## [0.2.0](https://git.0xmax42.io/maxp/http-kernel/compare/v0.1.0..v0.2.0) - 2025-05-27
### 🚀 Features
- *(workflows)* Add GitHub release sync workflow - ([de6d3ee](https://git.0xmax42.io/maxp/http-kernel/commit/de6d3ee389b0d92c5056e47be85da1d0c41f62af))
- *(ci)* Enhance CI workflow with granular steps and error handling - ([54cfa18](https://git.0xmax42.io/maxp/http-kernel/commit/54cfa1888e13d0872b5411e83d3d45925f2687ee))
- *(route-builder)* Add middleware chain compilation - ([35d83c0](https://git.0xmax42.io/maxp/http-kernel/commit/35d83c073ef8644d657195c332b463d18e856e18))
- *(interfaces)* Add runRoute method to IInternalRoute - ([67ebb43](https://git.0xmax42.io/maxp/http-kernel/commit/67ebb4307a2a1c588b78f8f0c498d1a4276ad09b))
- *(workflows)* Conditionally generate changelog - ([b44bb2d](https://git.0xmax42.io/maxp/http-kernel/commit/b44bb2ddafe99c85b25229d2c4a0dfeacf750052))
- *(workflows)* Add CI for Deno project tests - ([9d5db4f](https://git.0xmax42.io/maxp/http-kernel/commit/9d5db4f414cf961248f2b879f2b132b81a32cb92))
### 🐛 Bug Fixes
- *(workflows)* Ensure version detection output is always set - ([3707242](https://git.0xmax42.io/maxp/http-kernel/commit/3707242d278e15c55a41056bb64810f6824d24b3))
### 🚜 Refactor
- *(kernel)* Simplify middleware and handler execution - ([aea3fb4](https://git.0xmax42.io/maxp/http-kernel/commit/aea3fb45e7c099a38440c85783747e80fca54ba6))
- *(imports)* Use explicit type-only imports across codebase - ([b83aa33](https://git.0xmax42.io/maxp/http-kernel/commit/b83aa330b34523e5102ab98ee61dedbbd62d4656))
- *(workflows)* Rename changelog file for consistency - ([b9d25f2](https://git.0xmax42.io/maxp/http-kernel/commit/b9d25f23fc6ad7696deee319024aa5b1af4d98c0))
### 📚 Documentation
- *(release)* Update guidelines for handling changelog - ([6a0f1c7](https://git.0xmax42.io/maxp/http-kernel/commit/6a0f1c774bc01ab976090612bbc361576feb3942))
- Add README for HttpKernel project - ([a1ce306](https://git.0xmax42.io/maxp/http-kernel/commit/a1ce30627c68a3f869eb6a104308322af8596dc1))
- Add MIT license file - ([5118a19](https://git.0xmax42.io/maxp/http-kernel/commit/5118a19aeaa1102591aa7fe093fdec1aa19dc7f5))
### 🧪 Testing
- *(routebuilder)* Add validation tests for handler and middleware - ([b14e9ac](https://git.0xmax42.io/maxp/http-kernel/commit/b14e9acc5f9617a01886e7734b2ae717b86de03e))
- *(httpkernel)* Enforce compile-time validation for signatures - ([731bba2](https://git.0xmax42.io/maxp/http-kernel/commit/731bba22d88df077b0a39293ddd1a3eec3bf96e8))
- *(bench)* Add parallel benchmarks for HTTP kernel - ([3da34e2](https://git.0xmax42.io/maxp/http-kernel/commit/3da34e268426b92510c7f9b730a2fa297dca6fbf))
### ⚙️ Miscellaneous Tasks
- *(config)* Add CI task for local checks - ([c207dc7](https://git.0xmax42.io/maxp/http-kernel/commit/c207dc7392d9f40e7b7c736eadf6c9c7bbf9b7d4))
- *(gitignore)* Add .local directory to ignored files - ([1b447f5](https://git.0xmax42.io/maxp/http-kernel/commit/1b447f51900b3a1a7f1be9d5192fd5aba37bdbc4))
- *(ci)* Update deno tasks in CI workflow - ([38c00b0](https://git.0xmax42.io/maxp/http-kernel/commit/38c00b035bfd05c83d5898c97c9423a653db0840))
- *(tasks)* Add benchmark, format, and lint commands - ([6e6e616](https://git.0xmax42.io/maxp/http-kernel/commit/6e6e61693fef3b11a81ce260d80bc93edae1e718))
- *(workflows)* Consolidate and update CI configuration - ([ec1697d](https://git.0xmax42.io/maxp/http-kernel/commit/ec1697df94b5378f1766663e278a41d403a64336))
- *(config)* Add exports field to module metadata - ([c28eb7f](https://git.0xmax42.io/maxp/http-kernel/commit/c28eb7f28dfaa8d3fdc540c4bcc306a3a8b9d6f8))
- *(git)* Ignore merge conflicts for CHANGELOG.md - ([6399113](https://git.0xmax42.io/maxp/http-kernel/commit/6399113e122e1207ebf4113aebd250358e31f461))
- *(workflows)* Refine branch handling in release process - ([71ea424](https://git.0xmax42.io/maxp/http-kernel/commit/71ea4247b35dc4afe5090d3c6502bfa936b5a947))
- *(workflows)* Update changelog file extension to .md and revert b9d25f23fc - ([a88b4d1](https://git.0xmax42.io/maxp/http-kernel/commit/a88b4d112f5c07664d41f6e9d03246307551f25d))
- Rename changelog and readme files to use .md extension - ([4f2b650](https://git.0xmax42.io/maxp/http-kernel/commit/4f2b65049f461ef377e7231905fd066cbc3c7fe0))
- *(workflows)* Update test workflow for http-kernel project - ([0311546](https://git.0xmax42.io/maxp/http-kernel/commit/03115464e0fb01b8ca00a2fdabde013d004ae8a2))
- *(workflows)* Update Deno setup action to v2 - ([1233a0b](https://git.0xmax42.io/maxp/http-kernel/commit/1233a0b7204d12a60f4b7bd1199242a4cb7c4579))
- *(workflows)* Remove unused workflow_dispatch trigger - ([16c0053](https://git.0xmax42.io/maxp/http-kernel/commit/16c0053964c72d01e5f555ec8f33c9eead160e69))
- *(tasks)* Remove commented-out start and watch scripts - ([04029f8](https://git.0xmax42.io/maxp/http-kernel/commit/04029f87a3b9dd24e8792b852ead9097e18d23c7))
## [0.1.0] - 2025-05-08
### 🚀 Features

18
LICENSE Normal file
View File

@@ -0,0 +1,18 @@
MIT License
Copyright (c) 2025 0xMax42
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.

135
README.md Normal file
View File

@@ -0,0 +1,135 @@
# HttpKernel – A Type-Safe Router & Middleware Kernel for Deno
> Fluent routing • Zero-dependency core • 100 % TypeScript
HttpKernel is a small but powerful dispatching engine that turns an ordinary
`Deno.serve()` loop into a structured, middleware-driven HTTP server.
It focuses on **type safety**, **immutability**, and an **expressive builder API**
while staying framework-agnostic and dependency‑free.
---
## ✨ Key Features
* **Fluent Route Builder** – chain middleware and handlers without side effects
* **Static *and* Dynamic Matching** – use URL patterns *or* custom matcher functions
* **First-Class Generics** – strongly‑typed `ctx.params`, `ctx.query`, and `ctx.state`
* **Pluggable Error Handling** – override 404/500 (and any other status) per kernel
* **Response Decorators** – inject CORS headers, security headers, logging, … in one place
* **100 % Test Coverage** – built‑in unit tests ensure every edge case is covered
---
## 🚀 Quick Start
```ts
// Import directly from your repo or deno.land/x
import { HttpKernel } from "https://deno.land/x/httpkernel/mod.ts";
// 1) Create a kernel (optionally pass overrides)
const kernel = new HttpKernel();
// 2) Register a route with fluent chaining
kernel
.route({ method: "GET", path: "/hello/:name" })
.middleware(async (ctx, next) => {
console.log("Incoming request for", ctx.params.name);
return await next(); // continue pipeline
})
.handle(async (ctx) =>
new Response(`Hello ${ctx.params.name}!`, { status: 200 })
);
// 3) Let Deno serve the kernel
Deno.serve(kernel.handle);
```
Run it:
```bash
deno run --allow-net main.ts
# → GET http://localhost:8000/hello/Isaac
```
---
## 🧩 API Overview
| Method / Type | Purpose | Hints |
| --------------------- | ---------------------------------------------- | ------------------------------------------------------------- |
| `kernel.route(def)` | Begin defining a new route. Returns `RouteBuilder`. | `def` can be `{ method, path }` **or** `{ method, matcher }`. |
| `.middleware(fn)` | Add a middleware to the current builder. | Each call returns a *new* builder (immutability). |
| `.handle(fn)` | Finalise the route and register the handler. | Must be called exactly once per route. |
| `kernel.handle(req)` | Kernel entry point you pass to `Deno.serve()`. | Resolves to a `Response`. |
### Context Shape
```ts
interface Context<S = Record<string, unknown>> {
req: Request; // original request
params: Record<string>; // route params e.g. { id: "42" }
query: Record<string | string[]>; // parsed query string
state: S; // per‑request mutable storage
}
```
Generics let you supply your own param / query / state types for full IntelliSense.
---
## 🛠️ Configuration
```ts
new HttpKernel({
decorateResponse: (res, ctx) => {
// add CORS header globally
const headers = new Headers(res.headers);
headers.set("Access-Control-Allow-Origin", "*");
return new Response(res.body, { ...res, headers });
},
httpErrorHandlers: {
404: () => new Response("Nothing here ☹️", { status: 404 }),
500: (_ctx, err) => {
console.error(err);
return new Response("Custom 500", { status: 500 });
},
},
});
```
Everything is optional – omit what you do not override.
---
## 🧪 Testing
All logic is covered by unit tests using `std@0.204.0/testing`.
Run them with:
```bash
deno test -A
```
The CI suite checks:
* Route guards (`isStaticRouteDefinition`, `isDynamicRouteDefinition`)
* Builder immutability & middleware order
* 404 / 500 fall-backs and error propagation
* Middleware mis-use (double `next()`, wrong signatures, …)
---
## 📦 Roadmap
* 🔌 Adapter helpers for Oak / Fresh / any framework that can delegate to `kernel.handle`
* 🔍 Built‑in logger & timing middleware
* 🔒 CSRF & auth middleware presets
* 📝 OpenAPI route generator
Contributions & ideas are welcome – feel free to open an issue or PR.
---
## 📄 License
[MIT](LICENSE)

View File

@@ -1 +1 @@
0.1.0
0.2.0

View File

@@ -1,11 +1,16 @@
{
"name": "@0xmax42/http-kernel",
"description": "A simple HTTP kernel for Deno",
"exports": {
"./mod.ts": "./src/mod.ts"
},
"tasks": {
// "start": "deno run --allow-net --allow-env --unstable-kv --allow-read --allow-write --env-file src/main.ts -- --verbose",
// "watch": "deno run --watch --allow-net --allow-env --unstable-kv --allow-read --allow-write --env-file src/main.ts -- --verbose",
"test": "deno test --allow-net --allow-env --unstable-kv --allow-read --allow-write --coverage **/__tests__/*.test.ts",
"test:watch": "deno test --watch --allow-net --allow-env --unstable-kv --allow-read --allow-write **/__tests__/*.test.ts"
"test:watch": "deno test --watch --allow-net --allow-env --unstable-kv --allow-read --allow-write **/__tests__/*.test.ts",
"benchmark": "deno bench --allow-net --allow-env --unstable-kv --allow-read --allow-write **/__bench__/*.bench.ts",
"fmt": "deno fmt --check",
"lint": "deno lint",
"ci": "deno task fmt && deno task lint && deno task test && deno task benchmark" // For local CI checks
},
"compilerOptions": {
"lib": [
@@ -28,5 +33,4 @@
"main.ts"
]
}
//"importMap": "./import_map.json"
}

View File

@@ -1,4 +1,4 @@
import {
import type {
IContext,
IHttpKernel,
IHttpKernelConfig,
@@ -7,14 +7,10 @@ import {
IRouteDefinition,
} from './Interfaces/mod.ts';
import {
DeepPartial,
Handler,
type DeepPartial,
HTTP_404_NOT_FOUND,
HTTP_500_INTERNAL_SERVER_ERROR,
HttpStatusTextMap,
isHandler,
isMiddleware,
Middleware,
} from './Types/mod.ts';
import { RouteBuilder } from './RouteBuilder.ts';
import { createEmptyContext, normalizeError } from './Utils/mod.ts';
@@ -108,11 +104,12 @@ export class HttpKernel<TContext extends IContext = IContext>
query: match.query,
state: {},
} as TContext;
return await this.executePipeline(
ctx,
route.middlewares,
route.handler,
);
try {
const response = await route.runRoute(ctx);
return this.cfg.decorateResponse(response, ctx);
} catch (e) {
return await this.handleInternalError(ctx, e);
}
}
}
@@ -135,65 +132,13 @@ export class HttpKernel<TContext extends IContext = IContext>
this.routes.push(route as unknown as IInternalRoute<TContext>);
}
/**
* Executes the middleware and handler pipeline for a matched route.
*
* This function:
* - Enforces linear middleware execution with `next()` tracking
* - Validates middleware and handler types at runtime
* - Applies the optional response decorator post-processing
* - Handles all runtime errors via the configured 500 handler
*
* @param ctx - The active request context passed to middleware and handler.
* @param middleware - Ordered middleware functions for this route.
* @param handler - The final handler responsible for generating a response.
* @returns The final HTTP `Response`, possibly decorated.
*/
private async executePipeline(
private handleInternalError = (
ctx: TContext,
middleware: Middleware<TContext>[],
handler: Handler<TContext>,
): Promise<Response> {
const handleInternalError = (ctx: TContext, err?: unknown) =>
this.cfg.httpErrorHandlers[HTTP_500_INTERNAL_SERVER_ERROR](
ctx,
normalizeError(err),
);
let lastIndex = -1;
const dispatch = async (currentIndex: number): Promise<Response> => {
if (currentIndex <= lastIndex) {
throw new Error('Middleware called `next()` multiple times');
}
lastIndex = currentIndex;
const isWithinMiddleware = currentIndex < middleware.length;
const fn = isWithinMiddleware ? middleware[currentIndex] : handler;
if (isWithinMiddleware) {
if (!isMiddleware(fn)) {
throw new Error(
'Expected middleware function, but received invalid value',
);
}
return await fn(ctx, () => dispatch(currentIndex + 1));
}
if (!isHandler(fn)) {
throw new Error(
'Expected request handler, but received invalid value',
);
}
return await fn(ctx);
};
try {
const response = await dispatch(0);
return this.cfg.decorateResponse(response, ctx);
} catch (e) {
return handleInternalError(ctx, e);
}
}
err?: unknown,
): Response | Promise<Response> => {
return this.cfg.httpErrorHandlers[HTTP_500_INTERNAL_SERVER_ERROR](
ctx,
normalizeError(err),
);
};
}

View File

@@ -1,4 +1,4 @@
import { Params, Query, State } from '../Types/mod.ts';
import type { Params, Query, State } from '../Types/mod.ts';
/**
* Represents the complete context for a single HTTP request,

View File

@@ -1,5 +1,5 @@
import { IContext } from '../Interfaces/mod.ts';
import { HttpErrorHandler, validHttpErrorCodes } from '../Types/mod.ts';
import type { IContext } from '../Interfaces/mod.ts';
import type { HttpErrorHandler, validHttpErrorCodes } from '../Types/mod.ts';
/**
* A mapping of HTTP status codes to their corresponding error handlers.

View File

@@ -1,6 +1,6 @@
import { IContext } from './IContext.ts';
import { IRouteBuilder } from './IRouteBuilder.ts';
import { IRouteDefinition } from './IRouteDefinition.ts';
import type { IContext } from './IContext.ts';
import type { IRouteBuilder } from './IRouteBuilder.ts';
import type { IRouteDefinition } from './IRouteDefinition.ts';
/**
* The `IHttpKernel` interface defines the public API for a type-safe, middleware-driven HTTP dispatching system.

View File

@@ -1,7 +1,7 @@
import { ResponseDecorator } from '../Types/mod.ts';
import { IContext } from './IContext.ts';
import { IHttpErrorHandlers } from './IHttpErrorHandlers.ts';
import { IRouteBuilderFactory } from './IRouteBuilder.ts';
import type { ResponseDecorator } from '../Types/mod.ts';
import type { IContext } from './IContext.ts';
import type { IHttpErrorHandlers } from './IHttpErrorHandlers.ts';
import type { IRouteBuilderFactory } from './IRouteBuilder.ts';
export interface IHttpKernelConfig<TContext extends IContext = IContext> {
decorateResponse: ResponseDecorator<TContext>;

View File

@@ -1,5 +1,5 @@
import { Handler, HttpMethod, Middleware } from '../Types/mod.ts';
import { IContext, IRouteMatcher } from './mod.ts';
import type { Handler, HttpMethod, Middleware } from '../Types/mod.ts';
import type { IContext, IRouteMatcher } from './mod.ts';
/**
* Represents an internally registered route within the HttpKernel.
@@ -36,4 +36,29 @@ export interface IInternalRoute<TContext extends IContext = IContext> {
* The final handler that generates the HTTP response after all middleware has run.
*/
handler: Handler<TContext>;
/**
* The fully compiled execution pipeline for this route.
*
* This function is generated at route registration time and encapsulates the
* entire middleware chain as well as the final handler. It is called by the
* HttpKernel during request dispatch when a route has been matched.
*
* Internally, `runRoute` ensures that each middleware is invoked in the correct order
* and receives a `next()` callback to pass control downstream. The final handler is
* invoked once all middleware has completed or short-circuited the pipeline.
*
* It is guaranteed that:
* - The function is statically compiled and does not perform dynamic dispatching.
* - Each middleware can only call `next()` once; repeated invocations will throw.
* - The return value is either a `Response` or a Promise resolving to one.
*
* @param ctx - The context object carrying route, request, response and other scoped data.
* @returns A `Response` object or a Promise resolving to a `Response`.
*
* @throws {Error} If a middleware calls `next()` more than once.
*/
runRoute: (
ctx: TContext,
) => Promise<Response> | Response;
}

View File

@@ -1,7 +1,7 @@
import { Handler, Middleware } from '../Types/mod.ts';
import { IInternalRoute } from './IInternalRoute.ts';
import { IRouteDefinition } from './IRouteDefinition.ts';
import { IContext } from './mod.ts';
import type { Handler, Middleware } from '../Types/mod.ts';
import type { IInternalRoute } from './IInternalRoute.ts';
import type { IRouteDefinition } from './IRouteDefinition.ts';
import type { IContext } from './mod.ts';
export interface IRouteBuilderFactory<TContext extends IContext = IContext> {
new (

View File

@@ -1,5 +1,5 @@
import { HttpMethod, isHttpMethod } from '../Types/mod.ts';
import { IRouteMatcher } from './IRouteMatcher.ts';
import { type HttpMethod, isHttpMethod } from '../Types/mod.ts';
import type { IRouteMatcher } from './IRouteMatcher.ts';
/**
* Defines a static route using a path pattern with optional parameters.

View File

@@ -1,4 +1,4 @@
import { Params, Query } from '../Types/mod.ts';
import type { Params, Query } from '../Types/mod.ts';
export interface IRouteMatch {
params?: Params;

View File

@@ -1,6 +1,5 @@
import { Params } from '../Types/mod.ts';
import { IRouteDefinition } from './IRouteDefinition.ts';
import { IRouteMatch } from './IRouteMatch.ts';
import type { IRouteDefinition } from './IRouteDefinition.ts';
import type { IRouteMatch } from './IRouteMatch.ts';
/**
* Defines a route matcher function that evaluates whether a route applies to a given request.

View File

@@ -1,6 +1,6 @@
import { assertEquals } from 'https://deno.land/std@0.204.0/assert/mod.ts';
import {
IRouteDefinition,
type IRouteDefinition,
isDynamicRouteDefinition,
isStaticRouteDefinition,
} from '../IRouteDefinition.ts';

View File

@@ -1,6 +1,17 @@
import { IRouteMatcherFactory } from './Interfaces/IRouteMatcher.ts';
import { IContext, IRouteBuilder, IRouteDefinition } from './Interfaces/mod.ts';
import { Handler, Middleware, RegisterRoute } from './Types/mod.ts';
import type { IRouteMatcherFactory } from './Interfaces/IRouteMatcher.ts';
import type {
IContext,
IInternalRoute,
IRouteBuilder,
IRouteDefinition,
} from './Interfaces/mod.ts';
import { isHandler } from './Types/Handler.ts';
import {
type Handler,
isMiddleware,
type Middleware,
type RegisterRoute,
} from './Types/mod.ts';
import { createRouteMatcher } from './Utils/createRouteMatcher.ts';
/**
@@ -62,6 +73,76 @@ export class RouteBuilder<TContext extends IContext = IContext>
matcher,
middlewares: this.mws,
handler: handler,
runRoute: this.compile({
middlewares: this.mws,
handler: handler,
}),
});
}
/**
* Compiles the middleware chain and handler into a single executable function.
*
* This method constructs a statically linked function chain by reducing all middleware
* and the final handler into one composed `runRoute` function. Each middleware receives
* a `next()` callback that invokes the next function in the chain.
*
* Additionally, the returned function ensures that `next()` can only be called once
* per middleware. If `next()` is invoked multiple times within the same middleware,
* a runtime `Error` is thrown, preventing unintended double-processing.
*
* Type safety is enforced at compile time:
* - If the final handler does not match the expected signature, a `TypeError` is thrown.
* - If any middleware does not conform to the middleware interface, a `TypeError` is thrown.
*
* @param route - A partial route object containing middleware and handler,
* excluding `matcher`, `method`, and `runRoute`.
* @returns A composed route execution function that takes a context object
* and returns a `Promise<Response>`.
*
* @throws {TypeError} If the handler or any middleware function is invalid.
* @throws {Error} If a middleware calls `next()` more than once during execution.
*/
private compile(
route: Omit<
IInternalRoute<TContext>,
'runRoute' | 'matcher' | 'method'
>,
): (
ctx: TContext,
) => Promise<Response> {
if (!isHandler<TContext>(route.handler)) {
throw new TypeError(
'Route handler must be a function returning a Promise<Response>.',
);
}
let composed = route.handler;
for (let i = route.middlewares.length - 1; i >= 0; i--) {
if (!isMiddleware<TContext>(route.middlewares[i])) {
throw new TypeError(
`Middleware at index ${i} is not a valid function.`,
);
}
const current = route.middlewares[i];
const next = composed;
composed = async (ctx: TContext): Promise<Response> => {
let called = false;
return await current(ctx, async () => {
if (called) {
throw new Error(
`next() called multiple times in middleware at index ${i}`,
);
}
called = true;
return await next(ctx);
});
};
}
return composed;
}
}

View File

@@ -1,4 +1,4 @@
import { IContext } from '../Interfaces/mod.ts';
import type { IContext } from '../Interfaces/mod.ts';
/**
* Represents a final request handler responsible for producing an HTTP response.

View File

@@ -1,4 +1,4 @@
import { IContext } from '../Interfaces/mod.ts';
import type { IContext } from '../Interfaces/mod.ts';
/**
* Defines a handler function for errors that occur during the execution

View File

@@ -1,4 +1,4 @@
import { IContext } from '../Interfaces/IContext.ts';
import type { IContext } from '../Interfaces/IContext.ts';
/**
* Represents a middleware function in the HTTP request pipeline.

View File

@@ -1,5 +1,5 @@
import { IContext } from '../Interfaces/IContext.ts';
import { IInternalRoute } from '../Interfaces/mod.ts';
import type { IContext } from '../Interfaces/IContext.ts';
import type { IInternalRoute } from '../Interfaces/mod.ts';
/**
* A type alias for the internal route registration function used by the `HttpKernel`.

View File

@@ -1,4 +1,4 @@
import { IContext } from '../Interfaces/mod.ts';
import type { IContext } from '../Interfaces/mod.ts';
/**
* A function that modifies or enriches an outgoing HTTP response before it is returned to the client.

View File

@@ -1,6 +1,6 @@
import { assertEquals } from 'https://deno.land/std/assert/mod.ts';
import { createEmptyContext } from '../createEmptyContext.ts';
import { IContext } from '../../Interfaces/mod.ts';
import type { IContext } from '../../Interfaces/mod.ts';
Deno.test('createEmptyContext: returns default-initialized context', () => {
const request = new Request('http://localhost');

View File

@@ -3,7 +3,7 @@ import {
assertEquals,
assertStrictEquals,
} from 'https://deno.land/std/assert/mod.ts';
import { IRouteDefinition } from '../../Interfaces/mod.ts';
import type { IRouteDefinition } from '../../Interfaces/mod.ts';
import { createRouteMatcher } from '../../mod.ts';
// Dummy request

View File

@@ -1,5 +1,5 @@
import { IContext } from '../Interfaces/mod.ts';
import { Params, Query, State } from '../Types/mod.ts';
import type { IContext } from '../Interfaces/mod.ts';
import type { Params, Query, State } from '../Types/mod.ts';
/**
* Creates an empty request context suitable for fallback handlers (e.g., 404 or 500 errors).

View File

@@ -1,12 +1,12 @@
// createRouteMatcher.ts
import {
IRouteDefinition,
IRouteMatch,
IRouteMatcher,
type IRouteDefinition,
type IRouteMatch,
type IRouteMatcher,
isDynamicRouteDefinition,
} from '../Interfaces/mod.ts';
import { Params, Query } from '../Types/mod.ts';
import type { Params, Query } from '../Types/mod.ts';
/**
* Transforms a route definition into a matcher using Deno's URLPattern API.

View File

@@ -22,11 +22,9 @@
* ```
*/
export function normalizeError(unknownError: unknown): Error {
return unknownError instanceof Error
? unknownError
: new Error(
typeof unknownError === 'string'
? unknownError
: JSON.stringify(unknownError),
);
return unknownError instanceof Error ? unknownError : new Error(
typeof unknownError === 'string'
? unknownError
: JSON.stringify(unknownError),
);
}

View File

@@ -0,0 +1,87 @@
import type { IRouteDefinition } from '../Interfaces/mod.ts';
import { HttpKernel } from '../mod.ts';
const CONCURRENT_REQUESTS = 10000;
// Deno.bench('Simple request', async (b) => {
// const kernel = new HttpKernel();
// const def: IRouteDefinition = { method: 'GET', path: '/hello' };
// kernel.route(def).handle((_ctx) => {
// return Promise.resolve(new Response('OK', { status: 200 }));
// });
// b.start();
// await kernel.handle(
// new Request('http://localhost/hello', { method: 'GET' }),
// );
// b.end();
// });
Deno.bench('Simple request (parallel)', async (b) => {
const kernel = new HttpKernel();
const def: IRouteDefinition = { method: 'GET', path: '/hello' };
kernel.route(def).handle((_ctx) => {
return Promise.resolve(new Response('OK', { status: 200 }));
});
const requests = Array.from(
{ length: CONCURRENT_REQUESTS },
() =>
kernel.handle(
new Request('http://localhost/hello', { method: 'GET' }),
),
);
b.start();
await Promise.all(requests);
b.end();
});
// Deno.bench('Complex request', async (b) => {
// const kernel = new HttpKernel();
// kernel.route({ method: 'GET', path: '/test' })
// .middleware(async (_ctx, next) => {
// return await next();
// })
// .middleware(async (_ctx, next) => {
// return await next();
// })
// .handle((_ctx) => {
// return Promise.resolve(new Response('done'));
// });
// b.start();
// await kernel.handle(
// new Request('http://localhost/test', { method: 'GET' }),
// );
// b.end();
// });
Deno.bench('Complex request (parallel)', async (b) => {
const kernel = new HttpKernel();
kernel.route({ method: 'GET', path: '/test' })
.middleware(async (_ctx, next) => {
return await next();
})
.middleware(async (_ctx, next) => {
return await next();
})
.handle((_ctx) => {
return Promise.resolve(new Response('done'));
});
const requests = Array.from(
{ length: CONCURRENT_REQUESTS },
() =>
kernel.handle(
new Request('http://localhost/test', { method: 'GET' }),
),
);
b.start();
await Promise.all(requests);
b.end();
});

View File

@@ -1,6 +1,9 @@
import { assertEquals } from 'https://deno.land/std@0.204.0/assert/mod.ts';
import {
assertEquals,
assertThrows,
} from 'https://deno.land/std@0.204.0/assert/mod.ts';
import { HttpKernel } from '../HttpKernel.ts';
import { IRouteDefinition } from '../Interfaces/mod.ts';
import type { IRouteDefinition } from '../Interfaces/mod.ts';
Deno.test('HttpKernel: matches static route and executes handler', async () => {
const kernel = new HttpKernel();
@@ -88,30 +91,32 @@ Deno.test('HttpKernel: middleware short-circuits pipeline', async () => {
assertEquals(calls, ['mw1']);
});
Deno.test('HttpKernel: invalid middleware or handler signature triggers 500', async () => {
Deno.test('HttpKernel: invalid middleware or handler signature throws at compile time', () => {
const kernel = new HttpKernel();
// Middleware with wrong signature (missing ctx, next)
kernel.route({ method: 'GET', path: '/bad-mw' })
// @ts-expect-error invalid middleware
.middleware(() => new Response('invalid'))
.handle((_ctx) => Promise.resolve(new Response('ok')));
const res1 = await kernel.handle(new Request('http://localhost/bad-mw'));
assertEquals(res1.status, 500);
assertEquals(await res1.text(), 'Internal Server Error');
assertThrows(
() => {
kernel.route({ method: 'GET', path: '/bad-mw' })
// @ts-expect-error invalid middleware
.middleware(() => new Response('invalid'))
.handle((_ctx) => Promise.resolve(new Response('ok')));
},
TypeError,
'Middleware at index 0 is not a valid function.',
);
// Handler with wrong signature (no ctx)
kernel.route({ method: 'GET', path: '/bad-handler' })
.middleware(async (_ctx, next) => await next())
// @ts-expect-error invalid handler
.handle(() => new Response('invalid'));
const res2 = await kernel.handle(
new Request('http://localhost/bad-handler'),
assertThrows(
() => {
kernel.route({ method: 'GET', path: '/bad-handler' })
.middleware(async (_ctx, next) => await next())
// @ts-expect-error invalid handler
.handle(() => new Response('invalid'));
},
TypeError,
'Route handler must be a function returning a Promise<Response>.',
);
assertEquals(res2.status, 500);
assertEquals(await res2.text(), 'Internal Server Error');
});
Deno.test('HttpKernel: 404 for unmatched route', async () => {
@@ -124,7 +129,7 @@ Deno.test('HttpKernel: skips route with wrong method', async () => {
const kernel = new HttpKernel();
kernel.route({ method: 'POST', path: '/only-post' })
.handle(() => Promise.resolve(new Response('nope')));
.handle((_ctx) => Promise.resolve(new Response('nope')));
const res = await kernel.handle(
new Request('http://localhost/only-post', { method: 'GET' }),
@@ -152,7 +157,7 @@ Deno.test('HttpKernel: handler throws → error propagates', async () => {
const kernel = new HttpKernel();
kernel.route({ method: 'GET', path: '/throw' })
.handle(() => {
.handle((_ctx) => {
throw new Error('fail!');
});

View File

@@ -4,17 +4,30 @@ import {
assertNotEquals,
assertThrows,
} from 'https://deno.land/std@0.204.0/assert/mod.ts';
import { IInternalRoute, IRouteDefinition } from '../Interfaces/mod.ts';
import type { IInternalRoute, IRouteDefinition } from '../Interfaces/mod.ts';
import { RouteBuilder } from '../mod.ts';
import { Handler, Middleware } from '../Types/mod.ts';
import type { Handler, Middleware } from '../Types/mod.ts';
// Dummy objects
// deno-lint-ignore require-await
const dummyHandler: Handler = async () => new Response('ok');
const dummyHandler: Handler = async (_) => new Response('ok');
// deno-lint-ignore require-await
const wrongHandler: Handler = async () => new Response('ok'); // Wrong signature, no ctx
const dummyMiddleware: Middleware = async (_, next) => await next();
// deno-lint-ignore require-await
const wrongMiddleware: Middleware = async () => new Response('ok'); // Wrong signature, no ctx, next
const dummyDef: IRouteDefinition = { method: 'GET', path: '/hello' };
const dummyMatcher = () => ({ params: {} });
Deno.test('middleware: throws if middleware signature is wrong', () => {
const builder = new RouteBuilder(() => {}, dummyDef);
assertThrows(
() => builder.middleware(wrongMiddleware).handle(dummyHandler),
TypeError,
'Middleware at index 0 is not a valid function.',
);
});
Deno.test('middleware: single middleware is registered correctly', () => {
let registered: IInternalRoute | null = null as IInternalRoute | null;
@@ -51,6 +64,15 @@ Deno.test('middleware: preserves order of middleware', () => {
assertEquals(result!.middlewares, [mw1, mw2]);
});
Deno.test('handle: throws if handler signature is wrong', () => {
const builder = new RouteBuilder(() => {}, dummyDef);
assertThrows(
() => builder.handle(wrongHandler),
TypeError,
'Route handler must be a function returning a Promise<Response>.',
);
});
Deno.test('handle: uppercases method', () => {
let result: IInternalRoute | null = null as IInternalRoute | null;
@@ -91,7 +113,7 @@ Deno.test('handle: works with no middleware', async () => {
Deno.test('handle: uses custom matcher factory', () => {
let called = false;
const factory = (def: IRouteDefinition) => {
const factory = (_def: IRouteDefinition) => {
called = true;
return dummyMatcher;
};