refactor middleware pipeline and improve ci checks #4
@@ -17,13 +17,36 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Deno
|
- uses: denoland/setup-deno@v1
|
||||||
uses: denoland/setup-deno@v1
|
|
||||||
with:
|
with:
|
||||||
deno-version: v2.x
|
deno-version: v2.x
|
||||||
|
|
||||||
- name: Run CI Checks
|
- 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: |
|
run: |
|
||||||
deno fmt --check
|
echo "::error::One or more steps failed"
|
||||||
deno lint
|
exit 1
|
||||||
deno task test
|
|
||||||
|
27
.gitea/workflows/sync-release-to-github.yml
Normal file
27
.gitea/workflows/sync-release-to-github.yml
Normal 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
|
@@ -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
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
|||||||
coverage/
|
coverage/
|
||||||
logs/
|
logs/
|
||||||
.locale/
|
.locale/
|
||||||
|
.local/
|
||||||
cache/
|
cache/
|
||||||
out.py
|
out.py
|
||||||
output.txt
|
output.txt
|
||||||
|
16
CHANGELOG.md
16
CHANGELOG.md
@@ -6,6 +6,10 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
### 🚀 Features
|
### 🚀 Features
|
||||||
|
|
||||||
|
- *(workflows)* Add GitHub release sync workflow - ([828fcd4](https://git.0xmax42.io/maxp/http-kernel/commit/828fcd4ef090114d5412ad547af72387b5db393f))
|
||||||
|
- *(ci)* Enhance CI workflow with granular steps and error handling - ([d8a7686](https://git.0xmax42.io/maxp/http-kernel/commit/d8a768655713bbe84c3dfe337d62fe1cc6cba4e1))
|
||||||
|
- *(route-builder)* Add middleware chain compilation - ([91708a0](https://git.0xmax42.io/maxp/http-kernel/commit/91708a0499a98dc9c3b43e423d66fc12abc1cc21))
|
||||||
|
- *(interfaces)* Add runRoute method to IInternalRoute - ([beadcef](https://git.0xmax42.io/maxp/http-kernel/commit/beadcefc6af6c31e5f123ca7348acb224ac930aa))
|
||||||
- *(workflows)* Conditionally generate changelog - ([b44bb2d](https://git.0xmax42.io/maxp/http-kernel/commit/b44bb2ddafe99c85b25229d2c4a0dfeacf750052))
|
- *(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))
|
- *(workflows)* Add CI for Deno project tests - ([9d5db4f](https://git.0xmax42.io/maxp/http-kernel/commit/9d5db4f414cf961248f2b879f2b132b81a32cb92))
|
||||||
|
|
||||||
@@ -15,6 +19,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
### 🚜 Refactor
|
### 🚜 Refactor
|
||||||
|
|
||||||
|
- *(kernel)* Simplify middleware and handler execution - ([8243b07](https://git.0xmax42.io/maxp/http-kernel/commit/8243b07a5af921c2c81f9b7d10765138aa274915))
|
||||||
- *(imports)* Use explicit type-only imports across codebase - ([b83aa33](https://git.0xmax42.io/maxp/http-kernel/commit/b83aa330b34523e5102ab98ee61dedbbd62d4656))
|
- *(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))
|
- *(workflows)* Rename changelog file for consistency - ([b9d25f2](https://git.0xmax42.io/maxp/http-kernel/commit/b9d25f23fc6ad7696deee319024aa5b1af4d98c0))
|
||||||
|
|
||||||
@@ -24,8 +29,19 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Add README for HttpKernel project - ([a1ce306](https://git.0xmax42.io/maxp/http-kernel/commit/a1ce30627c68a3f869eb6a104308322af8596dc1))
|
- 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))
|
- Add MIT license file - ([5118a19](https://git.0xmax42.io/maxp/http-kernel/commit/5118a19aeaa1102591aa7fe093fdec1aa19dc7f5))
|
||||||
|
|
||||||
|
### 🧪 Testing
|
||||||
|
|
||||||
|
- *(routebuilder)* Add validation tests for handler and middleware - ([801d06e](https://git.0xmax42.io/maxp/http-kernel/commit/801d06ebf8127e47919d96ce72da94fe528d1818))
|
||||||
|
- *(httpkernel)* Enforce compile-time validation for signatures - ([b3ed8dd](https://git.0xmax42.io/maxp/http-kernel/commit/b3ed8dd52c25a7c2653245749ce62ecb81001573))
|
||||||
|
- *(bench)* Add parallel benchmarks for HTTP kernel - ([ba3d2e3](https://git.0xmax42.io/maxp/http-kernel/commit/ba3d2e33f2a3bd690cbc1f291fea07466a94b7d5))
|
||||||
|
|
||||||
### ⚙️ Miscellaneous Tasks
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(config)* Add CI task for local checks - ([39e39a9](https://git.0xmax42.io/maxp/http-kernel/commit/39e39a9699982653f0d1df2e845d8098fd79dbd9))
|
||||||
|
- *(gitignore)* Add .local directory to ignored files - ([2d3dacc](https://git.0xmax42.io/maxp/http-kernel/commit/2d3dacc35802d2ffd6c6eda22df5761dd2d03f74))
|
||||||
|
- *(ci)* Update deno tasks in CI workflow - ([af32f3b](https://git.0xmax42.io/maxp/http-kernel/commit/af32f3b9f4905469def8c22239ef3de4b5c3ea54))
|
||||||
|
- *(tasks)* Add benchmark, format, and lint commands - ([d9a984c](https://git.0xmax42.io/maxp/http-kernel/commit/d9a984cbea887495944f9b7d7430650a040241dc))
|
||||||
|
- *(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))
|
- *(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))
|
- *(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)* Refine branch handling in release process - ([71ea424](https://git.0xmax42.io/maxp/http-kernel/commit/71ea4247b35dc4afe5090d3c6502bfa936b5a947))
|
||||||
|
@@ -6,7 +6,11 @@
|
|||||||
},
|
},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"test": "deno test --allow-net --allow-env --unstable-kv --allow-read --allow-write --coverage **/__tests__/*.test.ts",
|
"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": {
|
"compilerOptions": {
|
||||||
"lib": [
|
"lib": [
|
||||||
@@ -29,5 +33,4 @@
|
|||||||
"main.ts"
|
"main.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
//"importMap": "./import_map.json"
|
|
||||||
}
|
}
|
@@ -8,13 +8,9 @@ import type {
|
|||||||
} from './Interfaces/mod.ts';
|
} from './Interfaces/mod.ts';
|
||||||
import {
|
import {
|
||||||
type DeepPartial,
|
type DeepPartial,
|
||||||
type Handler,
|
|
||||||
HTTP_404_NOT_FOUND,
|
HTTP_404_NOT_FOUND,
|
||||||
HTTP_500_INTERNAL_SERVER_ERROR,
|
HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
HttpStatusTextMap,
|
HttpStatusTextMap,
|
||||||
isHandler,
|
|
||||||
isMiddleware,
|
|
||||||
type Middleware,
|
|
||||||
} from './Types/mod.ts';
|
} from './Types/mod.ts';
|
||||||
import { RouteBuilder } from './RouteBuilder.ts';
|
import { RouteBuilder } from './RouteBuilder.ts';
|
||||||
import { createEmptyContext, normalizeError } from './Utils/mod.ts';
|
import { createEmptyContext, normalizeError } from './Utils/mod.ts';
|
||||||
@@ -108,11 +104,12 @@ export class HttpKernel<TContext extends IContext = IContext>
|
|||||||
query: match.query,
|
query: match.query,
|
||||||
state: {},
|
state: {},
|
||||||
} as TContext;
|
} as TContext;
|
||||||
return await this.executePipeline(
|
try {
|
||||||
ctx,
|
const response = await route.runRoute(ctx);
|
||||||
route.middlewares,
|
return this.cfg.decorateResponse(response, ctx);
|
||||||
route.handler,
|
} 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>);
|
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,
|
ctx: TContext,
|
||||||
middleware: Middleware<TContext>[],
|
err?: unknown,
|
||||||
handler: Handler<TContext>,
|
): Response | Promise<Response> => {
|
||||||
): Promise<Response> {
|
return this.cfg.httpErrorHandlers[HTTP_500_INTERNAL_SERVER_ERROR](
|
||||||
const handleInternalError = (ctx: TContext, err?: unknown) =>
|
ctx,
|
||||||
this.cfg.httpErrorHandlers[HTTP_500_INTERNAL_SERVER_ERROR](
|
normalizeError(err),
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -36,4 +36,29 @@ export interface IInternalRoute<TContext extends IContext = IContext> {
|
|||||||
* The final handler that generates the HTTP response after all middleware has run.
|
* The final handler that generates the HTTP response after all middleware has run.
|
||||||
*/
|
*/
|
||||||
handler: Handler<TContext>;
|
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;
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,17 @@
|
|||||||
import type { IRouteMatcherFactory } from './Interfaces/IRouteMatcher.ts';
|
import type { IRouteMatcherFactory } from './Interfaces/IRouteMatcher.ts';
|
||||||
import type {
|
import type {
|
||||||
IContext,
|
IContext,
|
||||||
|
IInternalRoute,
|
||||||
IRouteBuilder,
|
IRouteBuilder,
|
||||||
IRouteDefinition,
|
IRouteDefinition,
|
||||||
} from './Interfaces/mod.ts';
|
} from './Interfaces/mod.ts';
|
||||||
import type { Handler, Middleware, RegisterRoute } from './Types/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';
|
import { createRouteMatcher } from './Utils/createRouteMatcher.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,6 +73,76 @@ export class RouteBuilder<TContext extends IContext = IContext>
|
|||||||
matcher,
|
matcher,
|
||||||
middlewares: this.mws,
|
middlewares: this.mws,
|
||||||
handler: handler,
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
87
src/__bench__/HttpKernel.bench.ts
Normal file
87
src/__bench__/HttpKernel.bench.ts
Normal 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();
|
||||||
|
});
|
@@ -1,4 +1,7 @@
|
|||||||
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 { HttpKernel } from '../HttpKernel.ts';
|
||||||
import type { IRouteDefinition } from '../Interfaces/mod.ts';
|
import type { IRouteDefinition } from '../Interfaces/mod.ts';
|
||||||
|
|
||||||
@@ -88,30 +91,32 @@ Deno.test('HttpKernel: middleware short-circuits pipeline', async () => {
|
|||||||
assertEquals(calls, ['mw1']);
|
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();
|
const kernel = new HttpKernel();
|
||||||
|
|
||||||
// Middleware with wrong signature (missing ctx, next)
|
// Middleware with wrong signature (missing ctx, next)
|
||||||
kernel.route({ method: 'GET', path: '/bad-mw' })
|
assertThrows(
|
||||||
// @ts-expect-error invalid middleware
|
() => {
|
||||||
.middleware(() => new Response('invalid'))
|
kernel.route({ method: 'GET', path: '/bad-mw' })
|
||||||
.handle((_ctx) => Promise.resolve(new Response('ok')));
|
// @ts-expect-error invalid middleware
|
||||||
|
.middleware(() => new Response('invalid'))
|
||||||
const res1 = await kernel.handle(new Request('http://localhost/bad-mw'));
|
.handle((_ctx) => Promise.resolve(new Response('ok')));
|
||||||
assertEquals(res1.status, 500);
|
},
|
||||||
assertEquals(await res1.text(), 'Internal Server Error');
|
TypeError,
|
||||||
|
'Middleware at index 0 is not a valid function.',
|
||||||
|
);
|
||||||
|
|
||||||
// Handler with wrong signature (no ctx)
|
// Handler with wrong signature (no ctx)
|
||||||
kernel.route({ method: 'GET', path: '/bad-handler' })
|
assertThrows(
|
||||||
.middleware(async (_ctx, next) => await next())
|
() => {
|
||||||
// @ts-expect-error invalid handler
|
kernel.route({ method: 'GET', path: '/bad-handler' })
|
||||||
.handle(() => new Response('invalid'));
|
.middleware(async (_ctx, next) => await next())
|
||||||
|
// @ts-expect-error invalid handler
|
||||||
const res2 = await kernel.handle(
|
.handle(() => new Response('invalid'));
|
||||||
new Request('http://localhost/bad-handler'),
|
},
|
||||||
|
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 () => {
|
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();
|
const kernel = new HttpKernel();
|
||||||
|
|
||||||
kernel.route({ method: 'POST', path: '/only-post' })
|
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(
|
const res = await kernel.handle(
|
||||||
new Request('http://localhost/only-post', { method: 'GET' }),
|
new Request('http://localhost/only-post', { method: 'GET' }),
|
||||||
@@ -152,7 +157,7 @@ Deno.test('HttpKernel: handler throws → error propagates', async () => {
|
|||||||
const kernel = new HttpKernel();
|
const kernel = new HttpKernel();
|
||||||
|
|
||||||
kernel.route({ method: 'GET', path: '/throw' })
|
kernel.route({ method: 'GET', path: '/throw' })
|
||||||
.handle(() => {
|
.handle((_ctx) => {
|
||||||
throw new Error('fail!');
|
throw new Error('fail!');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -10,11 +10,24 @@ import type { Handler, Middleware } from '../Types/mod.ts';
|
|||||||
|
|
||||||
// Dummy objects
|
// Dummy objects
|
||||||
// deno-lint-ignore require-await
|
// 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();
|
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 dummyDef: IRouteDefinition = { method: 'GET', path: '/hello' };
|
||||||
const dummyMatcher = () => ({ params: {} });
|
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', () => {
|
Deno.test('middleware: single middleware is registered correctly', () => {
|
||||||
let registered: IInternalRoute | null = null as IInternalRoute | null;
|
let registered: IInternalRoute | null = null as IInternalRoute | null;
|
||||||
|
|
||||||
@@ -51,6 +64,15 @@ Deno.test('middleware: preserves order of middleware', () => {
|
|||||||
assertEquals(result!.middlewares, [mw1, mw2]);
|
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', () => {
|
Deno.test('handle: uppercases method', () => {
|
||||||
let result: IInternalRoute | null = null as IInternalRoute | null;
|
let result: IInternalRoute | null = null as IInternalRoute | null;
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user