From 35d83c073ef8644d657195c332b463d18e856e18 Mon Sep 17 00:00:00 2001 From: "Max P." Date: Tue, 27 May 2025 14:49:32 +0200 Subject: [PATCH] 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. --- src/RouteBuilder.ts | 79 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/src/RouteBuilder.ts b/src/RouteBuilder.ts index d09a11a..7635058 100644 --- a/src/RouteBuilder.ts +++ b/src/RouteBuilder.ts @@ -1,10 +1,17 @@ import type { IRouteMatcherFactory } from './Interfaces/IRouteMatcher.ts'; import type { IContext, + IInternalRoute, IRouteBuilder, IRouteDefinition, } 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'; /** @@ -66,6 +73,76 @@ export class RouteBuilder 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`. + * + * @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, + 'runRoute' | 'matcher' | 'method' + >, + ): ( + ctx: TContext, + ) => Promise { + if (!isHandler(route.handler)) { + throw new TypeError( + 'Route handler must be a function returning a Promise.', + ); + } + let composed = route.handler; + + for (let i = route.middlewares.length - 1; i >= 0; i--) { + if (!isMiddleware(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 => { + 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; + } }