Compare commits
13 Commits
main
...
feature/pi
Author | SHA1 | Date | |
---|---|---|---|
5d1b0517a5 | |||
6ce73c14fa
|
|||
0aac2337a0 | |||
2ab6f1b8db
|
|||
b69a51247d | |||
2ab74b9859
|
|||
d04dfcd63e
|
|||
32f3fe5f52 | |||
abd2d6e840
|
|||
927a9081d4
|
|||
8f94cc915c
|
|||
3f114bb68d
|
|||
0846dbb758
|
@@ -1,52 +0,0 @@
|
||||
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
|
@@ -4,7 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "**"
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
detect-version-change:
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
- name: Install git-cliff
|
||||
if: steps.restore-cliff.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cargo install git-cliff --locked --version "${{ steps.cliff_version.outputs.version }}" --features gitea
|
||||
cargo install git-cliff --version "${{ steps.cliff_version.outputs.version }}" --features gitea
|
||||
|
||||
- name: Generate unreleased changelog (if file exists or on main)
|
||||
run: |
|
||||
@@ -124,8 +124,8 @@ jobs:
|
||||
- name: Install git-cliff
|
||||
if: steps.restore-cliff.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cargo install git-cliff --locked --version "${{ steps.cliff_version.outputs.version }}" --features gitea
|
||||
|
||||
cargo install git-cliff --version "${{ steps.cliff_version.outputs.version }}" --features gitea
|
||||
|
||||
- name: Generate changelog for release and tag
|
||||
id: generate-changelog
|
||||
run: |
|
||||
@@ -152,6 +152,7 @@ jobs:
|
||||
|
||||
echo "changelog_body_path=$BODY" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
- name: Commit updated CHANGELOG
|
||||
run: |
|
||||
git add CHANGELOG.md
|
||||
@@ -168,14 +169,12 @@ jobs:
|
||||
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
|
||||
echo "Tag v$VERSION already exists, skipping tag creation."
|
||||
else
|
||||
export GIT_AUTHOR_DATE="$(date --iso-8601=seconds)"
|
||||
export GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"
|
||||
git tag -a "v$VERSION" -F "${{ steps.generate-changelog.outputs.changelog_body_path }}" --cleanup=verbatim
|
||||
git push origin "v$VERSION"
|
||||
fi
|
||||
|
||||
- name: Create Gitea release
|
||||
env:
|
||||
env:
|
||||
RELEASE_PUBLISH_TOKEN: ${{ secrets.RELEASE_PUBLISH_TOKEN }}
|
||||
run: |
|
||||
VERSION=${{ steps.version.outputs.value }}
|
||||
|
@@ -1,27 +0,0 @@
|
||||
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: ${{ github.event.release.tag_name }}
|
||||
github_token: ${{ secrets.SYNC_GITHUB_TOKEN }}
|
||||
github_owner: 0xMax42
|
||||
github_repo: http-kernel
|
22
.gitea/workflows/test.yml
Normal file
22
.gitea/workflows/test.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
name: Test http-kernel
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
run-test:
|
||||
name: Run Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Deno
|
||||
uses: denoland/setup-deno@v2
|
||||
|
||||
- name: Run Tests
|
||||
run: deno task test
|
47
.gitea/workflows/upload-assets.yml.example
Normal file
47
.gitea/workflows/upload-assets.yml.example
Normal file
@@ -0,0 +1,47 @@
|
||||
# ========================
|
||||
# 📦 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
1
.gitignore
vendored
@@ -2,7 +2,6 @@
|
||||
coverage/
|
||||
logs/
|
||||
.locale/
|
||||
.local/
|
||||
cache/
|
||||
out.py
|
||||
output.txt
|
||||
|
41
CHANGELOG.md
41
CHANGELOG.md
@@ -4,57 +4,32 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [unreleased]
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(workflows)* Remove redundant tag fallback in sync job - ([5686940](https://git.0xmax42.io/maxp/http-kernel/commit/5686940fe26b699bffa62af7fb0efc42cc85a6b3))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(workflows)* Update release workflow for consistency - ([f177746](https://git.0xmax42.io/maxp/http-kernel/commit/f1777467607874f6bc83e1d7e37433298e25607c))
|
||||
|
||||
## [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)* Conditionally generate changelog - ([2ab6f1b](https://git.0xmax42.io/maxp/http-kernel/commit/2ab6f1b8db2d7bd31ca30248d0de183f17a5738c))
|
||||
- *(interfaces)* Add pipeline executor interface - ([927a908](https://git.0xmax42.io/maxp/http-kernel/commit/927a9081d4f363202520d017eb424c7c097ced94))
|
||||
- *(pipeline)* Add configuration and hooks for pipeline execution - ([3f114bb](https://git.0xmax42.io/maxp/http-kernel/commit/3f114bb68d94c48a53514752d57cb4f01adeaae3))
|
||||
- *(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))
|
||||
- *(workflows)* Ensure version detection output is always set - ([abd2d6e](https://git.0xmax42.io/maxp/http-kernel/commit/abd2d6e8402662f863d9974aaa0bc228a4777724))
|
||||
|
||||
### 🚜 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))
|
||||
- *(release)* Update guidelines for handling changelog - ([6ce73c1](https://git.0xmax42.io/maxp/http-kernel/commit/6ce73c14fa6736b622e646feb61522e6ec1f4c5a))
|
||||
- *(pipeline)* Add design plan for PipelineExecutor class - ([0846dbb](https://git.0xmax42.io/maxp/http-kernel/commit/0846dbb758ba788f969a381c56498920ee0f9562))
|
||||
- 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))
|
||||
- *(git)* Ignore merge conflicts for CHANGELOG.md - ([d04dfcd](https://git.0xmax42.io/maxp/http-kernel/commit/d04dfcd63ea2478ffdff2e966d310194dafd8d7d))
|
||||
- *(workflows)* Refine branch handling in release process - ([8f94cc9](https://git.0xmax42.io/maxp/http-kernel/commit/8f94cc915c75a11efa1a8e3bdc51ffea9c2f19b5))
|
||||
- *(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))
|
||||
|
10
deno.jsonc
10
deno.jsonc
@@ -1,16 +1,9 @@
|
||||
{
|
||||
"name": "@0xmax42/http-kernel",
|
||||
"description": "A simple HTTP kernel for Deno",
|
||||
"exports": {
|
||||
"./mod.ts": "./src/mod.ts"
|
||||
},
|
||||
"tasks": {
|
||||
"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",
|
||||
"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
|
||||
"test:watch": "deno test --watch --allow-net --allow-env --unstable-kv --allow-read --allow-write **/__tests__/*.test.ts"
|
||||
},
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
@@ -33,4 +26,5 @@
|
||||
"main.ts"
|
||||
]
|
||||
}
|
||||
//"importMap": "./import_map.json"
|
||||
}
|
72
docs/pipeline_executor_plan.md
Normal file
72
docs/pipeline_executor_plan.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# 🧩 Plan: `PipelineExecutor<TContext>`
|
||||
|
||||
## 🎯 Ziel
|
||||
Eine eigenständige, testbare Klasse zur Ausführung einer Middleware- und Handler-Pipeline, die:
|
||||
- Linear und sauber das `next()`-Verhalten abbildet
|
||||
- Typvalidierung durchführt (`isMiddleware`, `isHandler`)
|
||||
- Fehler behandelt und an konfigurierbare Handler weiterleitet
|
||||
- Optionale Hooks zur Tracing-Integration bietet (z. B. für Zeitmessung, Logging)
|
||||
- Am Ende eine dekorierte `Response` zurückliefert
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Schnittstelle (API)
|
||||
|
||||
```ts
|
||||
class PipelineExecutor<TContext extends IContext> {
|
||||
constructor(cfg: IHttpKernelConfig<TContext>);
|
||||
|
||||
run(
|
||||
ctx: TContext,
|
||||
middleware: Middleware<TContext>[],
|
||||
handler: Handler<TContext>,
|
||||
hooks?: IPipelineHooks<TContext>, // optional
|
||||
): Promise<Response>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🪝 Hook-Schnittstelle (`IPipelineHooks`)
|
||||
|
||||
```ts
|
||||
interface IPipelineHooks<TContext> {
|
||||
onPipelineStart?(ctx: TContext): void;
|
||||
onStepStart?(name: string | undefined, ctx: TContext): void;
|
||||
onStepEnd?(name: string | undefined, ctx: TContext, duration: number): void;
|
||||
onPipelineEnd?(ctx: TContext, totalDuration: number): void;
|
||||
}
|
||||
```
|
||||
|
||||
- `name` ist `undefined`, wenn keine `.name` am Handler/Middleware gesetzt ist
|
||||
- Diese Hooks ermöglichen später Logging, Zeitmessung, Statistiken etc.
|
||||
- Der `TraceManager` wird dieses Interface implementieren
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Interne Aufgaben / Ablauf
|
||||
|
||||
1. `run(...)` beginnt mit Aufruf `onPipelineStart(ctx)`
|
||||
2. Zeitmessung (`performance.now()`)
|
||||
3. Dispatcher-Funktion führt jede Middleware mit `next()`-Kette aus
|
||||
4. Vor jedem Aufruf: `onStepStart(name, ctx)`
|
||||
5. Nach jedem Aufruf: `onStepEnd(name, ctx, duration)`
|
||||
6. Nach letztem Handler: `onPipelineEnd(ctx, totalDuration)`
|
||||
7. Ergebnis wird durch `cfg.decorateResponse(res, ctx)` geschickt
|
||||
8. Im Fehlerfall: `cfg.httpErrorHandlers[500](ctx, error)`
|
||||
|
||||
---
|
||||
|
||||
## ✅ Vorteile
|
||||
|
||||
- `HttpKernel` ist von Ausführungsdetails entkoppelt
|
||||
- Tracing-/Logging-System kann ohne Invasivität angeschlossen werden
|
||||
- Sehr gut testbar (z. B. Middleware-Mock + Hook-Aufrufe prüfen)
|
||||
- Erweiterbar für Timeout, Async-Context, Abbruchlogik etc.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Dateiname-Vorschlag
|
||||
|
||||
- `src/Core/PipelineExecutor.ts` oder
|
||||
- `src/HttpKernel/PipelineExecutor.ts`
|
@@ -1,4 +1,4 @@
|
||||
import type {
|
||||
import {
|
||||
IContext,
|
||||
IHttpKernel,
|
||||
IHttpKernelConfig,
|
||||
@@ -7,10 +7,14 @@ import type {
|
||||
IRouteDefinition,
|
||||
} from './Interfaces/mod.ts';
|
||||
import {
|
||||
type DeepPartial,
|
||||
DeepPartial,
|
||||
Handler,
|
||||
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';
|
||||
@@ -104,12 +108,11 @@ export class HttpKernel<TContext extends IContext = IContext>
|
||||
query: match.query,
|
||||
state: {},
|
||||
} as TContext;
|
||||
try {
|
||||
const response = await route.runRoute(ctx);
|
||||
return this.cfg.decorateResponse(response, ctx);
|
||||
} catch (e) {
|
||||
return await this.handleInternalError(ctx, e);
|
||||
}
|
||||
return await this.executePipeline(
|
||||
ctx,
|
||||
route.middlewares,
|
||||
route.handler,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,13 +135,65 @@ export class HttpKernel<TContext extends IContext = IContext>
|
||||
this.routes.push(route as unknown as IInternalRoute<TContext>);
|
||||
}
|
||||
|
||||
private handleInternalError = (
|
||||
/**
|
||||
* 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(
|
||||
ctx: TContext,
|
||||
err?: unknown,
|
||||
): Response | Promise<Response> => {
|
||||
return this.cfg.httpErrorHandlers[HTTP_500_INTERNAL_SERVER_ERROR](
|
||||
ctx,
|
||||
normalizeError(err),
|
||||
);
|
||||
};
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import type { Params, Query, State } from '../Types/mod.ts';
|
||||
import { Params, Query, State } from '../Types/mod.ts';
|
||||
|
||||
/**
|
||||
* Represents the complete context for a single HTTP request,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import type { IContext } from '../Interfaces/mod.ts';
|
||||
import type { HttpErrorHandler, validHttpErrorCodes } from '../Types/mod.ts';
|
||||
import { IContext } from '../Interfaces/mod.ts';
|
||||
import { HttpErrorHandler, validHttpErrorCodes } from '../Types/mod.ts';
|
||||
|
||||
/**
|
||||
* A mapping of HTTP status codes to their corresponding error handlers.
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type { IContext } from './IContext.ts';
|
||||
import type { IRouteBuilder } from './IRouteBuilder.ts';
|
||||
import type { IRouteDefinition } from './IRouteDefinition.ts';
|
||||
import { IContext } from './IContext.ts';
|
||||
import { IRouteBuilder } from './IRouteBuilder.ts';
|
||||
import { IRouteDefinition } from './IRouteDefinition.ts';
|
||||
|
||||
/**
|
||||
* The `IHttpKernel` interface defines the public API for a type-safe, middleware-driven HTTP dispatching system.
|
||||
|
@@ -1,7 +1,7 @@
|
||||
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';
|
||||
import { ResponseDecorator } from '../Types/mod.ts';
|
||||
import { IContext } from './IContext.ts';
|
||||
import { IHttpErrorHandlers } from './IHttpErrorHandlers.ts';
|
||||
import { IRouteBuilderFactory } from './IRouteBuilder.ts';
|
||||
|
||||
export interface IHttpKernelConfig<TContext extends IContext = IContext> {
|
||||
decorateResponse: ResponseDecorator<TContext>;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import type { Handler, HttpMethod, Middleware } from '../Types/mod.ts';
|
||||
import type { IContext, IRouteMatcher } from './mod.ts';
|
||||
import { Handler, HttpMethod, Middleware } from '../Types/mod.ts';
|
||||
import { IContext, IRouteMatcher } from './mod.ts';
|
||||
|
||||
/**
|
||||
* Represents an internally registered route within the HttpKernel.
|
||||
@@ -36,29 +36,4 @@ 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;
|
||||
}
|
||||
|
50
src/Interfaces/IPipelineExecutor.ts
Normal file
50
src/Interfaces/IPipelineExecutor.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Handler, Middleware } from '../Types/mod.ts';
|
||||
import { IContext } from './IContext.ts';
|
||||
import { IPipelineExecutorConfig } from './IPipelineExecutorConfig.ts';
|
||||
|
||||
/**
|
||||
* Constructor type for a class implementing the IPipelineExecutor interface.
|
||||
*
|
||||
* This can be used for dependency injection, factory-based initialization,
|
||||
* or dynamic instantiation of pipeline executors.
|
||||
*
|
||||
* @template TContext - The extended context type passed through the pipeline.
|
||||
*/
|
||||
export interface PipelineExecutorFactory<TContext extends IContext = IContext> {
|
||||
/**
|
||||
* Creates a new instance of a pipeline executor.
|
||||
*
|
||||
* @param config - Configuration used to control error handling,
|
||||
* response decoration and lifecycle hooks.
|
||||
*/
|
||||
new (
|
||||
config: IPipelineExecutorConfig<TContext>,
|
||||
): IPipelineExecutor<TContext>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the contract for executing a middleware and handler pipeline.
|
||||
*
|
||||
* The pipeline is responsible for:
|
||||
* - Executing middleware in order with `next()` chaining
|
||||
* - Invoking the final handler
|
||||
* - Applying optional lifecycle hooks
|
||||
* - Producing and decorating a Response
|
||||
*
|
||||
* @template TContext - The context type flowing through the pipeline.
|
||||
*/
|
||||
export interface IPipelineExecutor<TContext extends IContext = IContext> {
|
||||
/**
|
||||
* Executes the middleware pipeline and returns the final Response.
|
||||
*
|
||||
* @param ctx - The context object representing the current HTTP request state.
|
||||
* @param middleware - An ordered array of middleware functions to be executed.
|
||||
* @param handler - The final route handler to be called after all middleware.
|
||||
* @returns A Promise resolving to the final HTTP Response.
|
||||
*/
|
||||
run(
|
||||
ctx: TContext,
|
||||
middleware: Middleware<TContext>[],
|
||||
handler: Handler<TContext>,
|
||||
): Promise<Response>;
|
||||
}
|
33
src/Interfaces/IPipelineExecutorConfig.ts
Normal file
33
src/Interfaces/IPipelineExecutorConfig.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ResponseDecorator } from '../Types/ResponseDecorator.ts';
|
||||
import { IContext } from './IContext.ts';
|
||||
import { IHttpErrorHandlers } from './IHttpErrorHandlers.ts';
|
||||
import { IPipelineHooks } from './IPipelineHooks.ts';
|
||||
|
||||
/**
|
||||
* Configuration object for the PipelineExecutor, defining how to handle
|
||||
* errors, responses, and tracing hooks.
|
||||
*
|
||||
* This allows the execution logic to remain decoupled from kernel-level behavior
|
||||
* while still supporting custom behavior injection.
|
||||
*
|
||||
* @template TContext - The context type propagated during pipeline execution.
|
||||
*/
|
||||
export interface IPipelineExecutorConfig<TContext extends IContext = IContext> {
|
||||
/**
|
||||
* Optional function used to transform or decorate the final Response object
|
||||
* before it is returned to the client.
|
||||
*/
|
||||
decorateResponse?: ResponseDecorator<TContext>;
|
||||
|
||||
/**
|
||||
* Optional map of error handlers, keyed by HTTP status codes (e.g., 404, 500).
|
||||
* These handlers are invoked if an error occurs during middleware or handler execution.
|
||||
*/
|
||||
errorHandlers?: IHttpErrorHandlers<TContext>;
|
||||
|
||||
/**
|
||||
* Optional hooks that allow tracing and lifecycle monitoring during pipeline execution.
|
||||
* Each hook is called at a specific phase of the middleware/handler lifecycle.
|
||||
*/
|
||||
pipelineHooks?: IPipelineHooks<TContext>;
|
||||
}
|
36
src/Interfaces/IPipelineHooks.ts
Normal file
36
src/Interfaces/IPipelineHooks.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
OnPipelineEnd,
|
||||
OnPipelineStart,
|
||||
OnStepEnd,
|
||||
OnStepStart,
|
||||
} from '../Types/mod.ts';
|
||||
import { IContext } from './IContext.ts';
|
||||
|
||||
/**
|
||||
* A set of optional hook functions that can be triggered during pipeline execution.
|
||||
* These hooks allow tracing, performance measurement, and logging to be integrated
|
||||
* without altering middleware or handler logic.
|
||||
*
|
||||
* @template TContext - The custom context type used within the application.
|
||||
*/
|
||||
export interface IPipelineHooks<TContext extends IContext = IContext> {
|
||||
/**
|
||||
* Triggered once before any middleware or handler is executed.
|
||||
*/
|
||||
onPipelineStart?: OnPipelineStart<TContext>;
|
||||
|
||||
/**
|
||||
* Triggered immediately before each middleware or handler runs.
|
||||
*/
|
||||
onStepStart?: OnStepStart<TContext>;
|
||||
|
||||
/**
|
||||
* Triggered immediately after each middleware or handler has finished executing.
|
||||
*/
|
||||
onStepEnd?: OnStepEnd<TContext>;
|
||||
|
||||
/**
|
||||
* Triggered after the entire pipeline completes execution.
|
||||
*/
|
||||
onPipelineEnd?: OnPipelineEnd<TContext>;
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
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';
|
||||
import { Handler, Middleware } from '../Types/mod.ts';
|
||||
import { IInternalRoute } from './IInternalRoute.ts';
|
||||
import { IRouteDefinition } from './IRouteDefinition.ts';
|
||||
import { IContext } from './mod.ts';
|
||||
|
||||
export interface IRouteBuilderFactory<TContext extends IContext = IContext> {
|
||||
new (
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { type HttpMethod, isHttpMethod } from '../Types/mod.ts';
|
||||
import type { IRouteMatcher } from './IRouteMatcher.ts';
|
||||
import { HttpMethod, isHttpMethod } from '../Types/mod.ts';
|
||||
import { IRouteMatcher } from './IRouteMatcher.ts';
|
||||
|
||||
/**
|
||||
* Defines a static route using a path pattern with optional parameters.
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import type { Params, Query } from '../Types/mod.ts';
|
||||
import { Params, Query } from '../Types/mod.ts';
|
||||
|
||||
export interface IRouteMatch {
|
||||
params?: Params;
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import type { IRouteDefinition } from './IRouteDefinition.ts';
|
||||
import type { IRouteMatch } from './IRouteMatch.ts';
|
||||
import { Params } from '../Types/mod.ts';
|
||||
import { IRouteDefinition } from './IRouteDefinition.ts';
|
||||
import { IRouteMatch } from './IRouteMatch.ts';
|
||||
|
||||
/**
|
||||
* Defines a route matcher function that evaluates whether a route applies to a given request.
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { assertEquals } from 'https://deno.land/std@0.204.0/assert/mod.ts';
|
||||
import {
|
||||
type IRouteDefinition,
|
||||
IRouteDefinition,
|
||||
isDynamicRouteDefinition,
|
||||
isStaticRouteDefinition,
|
||||
} from '../IRouteDefinition.ts';
|
||||
|
@@ -5,6 +5,12 @@ export type { IHttpErrorHandlers } from './IHttpErrorHandlers.ts';
|
||||
export type { IHttpKernel } from './IHttpKernel.ts';
|
||||
export type { IHttpKernelConfig } from './IHttpKernelConfig.ts';
|
||||
export type { IInternalRoute } from './IInternalRoute.ts';
|
||||
export type {
|
||||
IPipelineExecutor,
|
||||
PipelineExecutorFactory,
|
||||
} from './IPipelineExecutor.ts';
|
||||
export type { IPipelineExecutorConfig } from './IPipelineExecutorConfig.ts';
|
||||
export type { IPipelineHooks } from './IPipelineHooks.ts';
|
||||
export type { IRouteBuilder, IRouteBuilderFactory } from './IRouteBuilder.ts';
|
||||
export {
|
||||
isDynamicRouteDefinition,
|
||||
|
@@ -1,17 +1,6 @@
|
||||
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 { IRouteMatcherFactory } from './Interfaces/IRouteMatcher.ts';
|
||||
import { IContext, IRouteBuilder, IRouteDefinition } from './Interfaces/mod.ts';
|
||||
import { Handler, Middleware, RegisterRoute } from './Types/mod.ts';
|
||||
import { createRouteMatcher } from './Utils/createRouteMatcher.ts';
|
||||
|
||||
/**
|
||||
@@ -73,76 +62,6 @@ 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;
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import type { IContext } from '../Interfaces/mod.ts';
|
||||
import { IContext } from '../Interfaces/mod.ts';
|
||||
|
||||
/**
|
||||
* Represents a final request handler responsible for producing an HTTP response.
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import type { IContext } from '../Interfaces/mod.ts';
|
||||
import { IContext } from '../Interfaces/mod.ts';
|
||||
|
||||
/**
|
||||
* Defines a handler function for errors that occur during the execution
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import type { IContext } from '../Interfaces/IContext.ts';
|
||||
import { IContext } from '../Interfaces/IContext.ts';
|
||||
|
||||
/**
|
||||
* Represents a middleware function in the HTTP request pipeline.
|
||||
|
49
src/Types/PipelineHooks.ts
Normal file
49
src/Types/PipelineHooks.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { IContext } from '../Interfaces/mod.ts';
|
||||
|
||||
/**
|
||||
* A callback invoked when the middleware pipeline starts.
|
||||
*
|
||||
* @template TContext - The context type passed throughout the pipeline.
|
||||
* @param ctx - The context object for the current request.
|
||||
*/
|
||||
export type OnPipelineStart<TContext extends IContext> = (
|
||||
ctx: TContext,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* A callback invoked immediately before a middleware or handler is executed.
|
||||
*
|
||||
* @template TContext - The context type passed throughout the pipeline.
|
||||
* @param name - Optional name of the current middleware or handler, if defined.
|
||||
* @param ctx - The context object for the current request.
|
||||
*/
|
||||
export type OnStepStart<TContext extends IContext> = (
|
||||
name: string | undefined,
|
||||
ctx: TContext,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* A callback invoked immediately after a middleware or handler has completed.
|
||||
*
|
||||
* @template TContext - The context type passed throughout the pipeline.
|
||||
* @param name - Optional name of the current middleware or handler, if defined.
|
||||
* @param ctx - The context object for the current request.
|
||||
* @param duration - Execution time in milliseconds.
|
||||
*/
|
||||
export type OnStepEnd<TContext extends IContext> = (
|
||||
name: string | undefined,
|
||||
ctx: TContext,
|
||||
duration: number,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* A callback invoked after the entire pipeline has completed execution.
|
||||
*
|
||||
* @template TContext - The context type passed throughout the pipeline.
|
||||
* @param ctx - The context object for the current request.
|
||||
* @param totalDuration - Total execution time of the pipeline in milliseconds.
|
||||
*/
|
||||
export type OnPipelineEnd<TContext extends IContext> = (
|
||||
ctx: TContext,
|
||||
totalDuration: number,
|
||||
) => void;
|
@@ -1,5 +1,5 @@
|
||||
import type { IContext } from '../Interfaces/IContext.ts';
|
||||
import type { IInternalRoute } from '../Interfaces/mod.ts';
|
||||
import { IContext } from '../Interfaces/IContext.ts';
|
||||
import { IInternalRoute } from '../Interfaces/mod.ts';
|
||||
|
||||
/**
|
||||
* A type alias for the internal route registration function used by the `HttpKernel`.
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import type { IContext } from '../Interfaces/mod.ts';
|
||||
import { IContext } from '../Interfaces/mod.ts';
|
||||
|
||||
/**
|
||||
* A function that modifies or enriches an outgoing HTTP response before it is returned to the client.
|
||||
|
@@ -39,6 +39,12 @@ export type { HttpStatusCode } from './HttpStatusCode.ts';
|
||||
export { isMiddleware } from './Middleware.ts';
|
||||
export type { Middleware } from './Middleware.ts';
|
||||
export type { Params } from './Params.ts';
|
||||
export type {
|
||||
OnPipelineEnd,
|
||||
OnPipelineStart,
|
||||
OnStepEnd,
|
||||
OnStepStart,
|
||||
} from './PipelineHooks.ts';
|
||||
export type { Query } from './Query.ts';
|
||||
export type { RegisterRoute } from './RegisterRoute.ts';
|
||||
export type { ResponseDecorator } from './ResponseDecorator.ts';
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { assertEquals } from 'https://deno.land/std/assert/mod.ts';
|
||||
import { createEmptyContext } from '../createEmptyContext.ts';
|
||||
import type { IContext } from '../../Interfaces/mod.ts';
|
||||
import { IContext } from '../../Interfaces/mod.ts';
|
||||
|
||||
Deno.test('createEmptyContext: returns default-initialized context', () => {
|
||||
const request = new Request('http://localhost');
|
||||
|
@@ -3,7 +3,7 @@ import {
|
||||
assertEquals,
|
||||
assertStrictEquals,
|
||||
} from 'https://deno.land/std/assert/mod.ts';
|
||||
import type { IRouteDefinition } from '../../Interfaces/mod.ts';
|
||||
import { IRouteDefinition } from '../../Interfaces/mod.ts';
|
||||
import { createRouteMatcher } from '../../mod.ts';
|
||||
|
||||
// Dummy request
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import type { IContext } from '../Interfaces/mod.ts';
|
||||
import type { Params, Query, State } from '../Types/mod.ts';
|
||||
import { IContext } from '../Interfaces/mod.ts';
|
||||
import { Params, Query, State } from '../Types/mod.ts';
|
||||
|
||||
/**
|
||||
* Creates an empty request context suitable for fallback handlers (e.g., 404 or 500 errors).
|
||||
|
@@ -1,12 +1,12 @@
|
||||
// createRouteMatcher.ts
|
||||
|
||||
import {
|
||||
type IRouteDefinition,
|
||||
type IRouteMatch,
|
||||
type IRouteMatcher,
|
||||
IRouteDefinition,
|
||||
IRouteMatch,
|
||||
IRouteMatcher,
|
||||
isDynamicRouteDefinition,
|
||||
} from '../Interfaces/mod.ts';
|
||||
import type { Params, Query } from '../Types/mod.ts';
|
||||
import { Params, Query } from '../Types/mod.ts';
|
||||
|
||||
/**
|
||||
* Transforms a route definition into a matcher using Deno's URLPattern API.
|
||||
|
@@ -22,9 +22,11 @@
|
||||
* ```
|
||||
*/
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
@@ -1,87 +0,0 @@
|
||||
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();
|
||||
});
|
@@ -1,9 +1,6 @@
|
||||
import {
|
||||
assertEquals,
|
||||
assertThrows,
|
||||
} from 'https://deno.land/std@0.204.0/assert/mod.ts';
|
||||
import { assertEquals } from 'https://deno.land/std@0.204.0/assert/mod.ts';
|
||||
import { HttpKernel } from '../HttpKernel.ts';
|
||||
import type { IRouteDefinition } from '../Interfaces/mod.ts';
|
||||
import { IRouteDefinition } from '../Interfaces/mod.ts';
|
||||
|
||||
Deno.test('HttpKernel: matches static route and executes handler', async () => {
|
||||
const kernel = new HttpKernel();
|
||||
@@ -91,32 +88,30 @@ Deno.test('HttpKernel: middleware short-circuits pipeline', async () => {
|
||||
assertEquals(calls, ['mw1']);
|
||||
});
|
||||
|
||||
Deno.test('HttpKernel: invalid middleware or handler signature throws at compile time', () => {
|
||||
Deno.test('HttpKernel: invalid middleware or handler signature triggers 500', async () => {
|
||||
const kernel = new HttpKernel();
|
||||
|
||||
// Middleware with wrong signature (missing ctx, next)
|
||||
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.',
|
||||
);
|
||||
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');
|
||||
|
||||
// Handler with wrong signature (no ctx)
|
||||
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>.',
|
||||
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'),
|
||||
);
|
||||
assertEquals(res2.status, 500);
|
||||
assertEquals(await res2.text(), 'Internal Server Error');
|
||||
});
|
||||
|
||||
Deno.test('HttpKernel: 404 for unmatched route', async () => {
|
||||
@@ -129,7 +124,7 @@ Deno.test('HttpKernel: skips route with wrong method', async () => {
|
||||
const kernel = new HttpKernel();
|
||||
|
||||
kernel.route({ method: 'POST', path: '/only-post' })
|
||||
.handle((_ctx) => Promise.resolve(new Response('nope')));
|
||||
.handle(() => Promise.resolve(new Response('nope')));
|
||||
|
||||
const res = await kernel.handle(
|
||||
new Request('http://localhost/only-post', { method: 'GET' }),
|
||||
@@ -157,7 +152,7 @@ Deno.test('HttpKernel: handler throws → error propagates', async () => {
|
||||
const kernel = new HttpKernel();
|
||||
|
||||
kernel.route({ method: 'GET', path: '/throw' })
|
||||
.handle((_ctx) => {
|
||||
.handle(() => {
|
||||
throw new Error('fail!');
|
||||
});
|
||||
|
||||
|
@@ -4,30 +4,17 @@ import {
|
||||
assertNotEquals,
|
||||
assertThrows,
|
||||
} from 'https://deno.land/std@0.204.0/assert/mod.ts';
|
||||
import type { IInternalRoute, IRouteDefinition } from '../Interfaces/mod.ts';
|
||||
import { IInternalRoute, IRouteDefinition } from '../Interfaces/mod.ts';
|
||||
import { RouteBuilder } from '../mod.ts';
|
||||
import type { Handler, Middleware } from '../Types/mod.ts';
|
||||
import { Handler, Middleware } from '../Types/mod.ts';
|
||||
|
||||
// Dummy objects
|
||||
// deno-lint-ignore require-await
|
||||
const dummyHandler: Handler = async (_) => new Response('ok');
|
||||
// deno-lint-ignore require-await
|
||||
const wrongHandler: Handler = async () => new Response('ok'); // Wrong signature, no ctx
|
||||
const dummyHandler: Handler = async () => new Response('ok');
|
||||
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;
|
||||
|
||||
@@ -64,15 +51,6 @@ 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;
|
||||
|
||||
@@ -113,7 +91,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;
|
||||
};
|
||||
|
Reference in New Issue
Block a user