diff --git a/src/server.ts b/src/server.ts index ea7089e..43932c4 100644 --- a/src/server.ts +++ b/src/server.ts @@ -10,6 +10,9 @@ export interface IdentifiableRouteCallback { priority: HandlerPriority; } +type HandlerMode = "prefix" | "postfix" | "handler" | "monitor" | "websocket"; +type MiddlewareMode = "prefix" | "postfix"; + export interface RequestContext { rawRequest: IncomingMessage; rawResponse: ServerResponse; @@ -39,7 +42,7 @@ class RouteLevel { catchAllRoutes: CatchAllRoute[] = []; } -const roots: Record> = { +const roots: Record> = { handler: {}, prefix: {}, postfix: {}, @@ -47,6 +50,14 @@ const roots: Record> = { websocket: {}, }; +const handlerCounts: Record = { + handler: 0, + prefix: 0, + postfix: 0, + monitor: 0, + websocket: 0, +}; + interface HandlerResult { handler: RouteCallback; parameters: Record; @@ -186,42 +197,43 @@ function getHandler( return multi ? result : undefined; } -function removeHandler( - id: string, - source: Record, -): boolean { +function removeHandler(id: string, source: Record): number { for (const level of Object.values(source)) { const handlerLength = level.handlers.length; level.handlers = level.handlers.filter((handler) => handler.id !== id); + const removedCount = handlerLength - level.handlers.length; - if (handlerLength !== level.handlers.length) { - return true; + if (removedCount > 0) { + return removedCount; } - if (removeHandler(id, level.staticRoutes)) { - return true; + const staticRemovedCount = removeHandler(id, level.staticRoutes); + if (staticRemovedCount > 0) { + return staticRemovedCount; } - if ( - removeHandler( - id, - Object.keys(level.dynamicRoutes).reduce( - (acc, key) => ({ ...acc, [key]: level.dynamicRoutes[key].sub }), - {} as Record, - ), - ) - ) { - return true; + const dynamicRemovedCount = removeHandler( + id, + Object.fromEntries( + Object.entries(level.dynamicRoutes).map(([key, route]) => [ + key, + route.sub, + ]), + ), + ); + if (dynamicRemovedCount > 0) { + return dynamicRemovedCount; } for (const catchAll of level.catchAllRoutes) { - if (removeHandler(id, { _: catchAll.level })) { - return true; + const catchAllRemovedCount = removeHandler(id, { _: catchAll.level }); + if (catchAllRemovedCount > 0) { + return catchAllRemovedCount; } } } - return false; + return 0; } const special = { @@ -240,7 +252,7 @@ const special = { }; export function registerHandler( id: string, - mode: "prefix" | "postfix" | "handler" | "monitor" | "websocket", + mode: HandlerMode, method: string | undefined, location: string, handler: RouteCallback, @@ -332,11 +344,14 @@ export function registerHandler( } } level.handlers.push({ id, callback: handler, priority }); + handlerCounts[mode] += 1; } export function unregisterHandler(id: string) { - for (const source of Object.values(roots)) { - if (removeHandler(id, source)) { + for (const [mode, source] of Object.entries(roots)) { + const removedCount = removeHandler(id, source); + if (removedCount > 0) { + handlerCounts[mode as HandlerMode] -= removedCount; return; } } @@ -405,7 +420,9 @@ async function executePriorityHandlers( handlers: HandlerResult[], requestContext: RequestContext, ) { - handlers.sort((a, b) => a.priority - b.priority); + if (handlers.length > 1) { + handlers.sort((a, b) => a.priority - b.priority); + } for (const { handler, parameters } of handlers) { requestContext.routeParameters = parameters; const result = await handler(requestContext); @@ -416,19 +433,29 @@ async function executePriorityHandlers( return undefined; } -async function executeMonitors( +function executeMiddleware( + mode: MiddlewareMode, method: string, path: string[], requestContext: RequestContext, ) { - const monitors = getMultiHandlers(method, path, roots.monitor); - const monitorContext: RequestContext = { - ...requestContext, - routeParameters: {}, - response: cloneResponse(requestContext.response), - }; + if (handlerCounts[mode] === 0) { + return undefined; + } + const handlers = getMultiHandlers(method, path, roots[mode]); + if (handlers.length === 0) { + return undefined; + } + return executePriorityHandlers(handlers, requestContext); +} - monitors.sort((a, b) => a.priority - b.priority); +async function runMonitors( + monitors: HandlerResult[], + monitorContext: RequestContext, +) { + if (monitors.length > 1) { + monitors.sort((a, b) => a.priority - b.priority); + } for (const { handler, parameters } of monitors) { monitorContext.routeParameters = parameters; try { @@ -439,6 +466,26 @@ async function executeMonitors( } } +function executeMonitors( + method: string, + path: string[], + requestContext: RequestContext, +) { + if (handlerCounts.monitor === 0) { + return; + } + const monitors = getMultiHandlers(method, path, roots.monitor); + if (monitors.length === 0) { + return; + } + const monitorContext: RequestContext = { + ...requestContext, + routeParameters: {}, + response: cloneResponse(requestContext.response), + }; + return runMonitors(monitors, monitorContext); +} + export async function requestListener( req: IncomingMessage, res: ServerResponse, @@ -469,10 +516,13 @@ export async function requestListener( if (!handler && method !== "options") { // Fall through to finally block to execute monitors. } else { - const prefixResult = await executePriorityHandlers( - getMultiHandlers(method, path, roots.prefix), + const prefixExecution = executeMiddleware( + "prefix", + method, + path, requestContext, ); + const prefixResult = prefixExecution ? await prefixExecution : undefined; if (prefixResult) { requestContext.response = HTTPResult.withHeaders( prefixResult, @@ -488,10 +538,15 @@ export async function requestListener( setHandlerResponse(requestContext, result); } - const postfixResult = await executePriorityHandlers( - getMultiHandlers(method, path, roots.postfix), + const postfixExecution = executeMiddleware( + "postfix", + method, + path, requestContext, ); + const postfixResult = postfixExecution + ? await postfixExecution + : undefined; if (postfixResult) { requestContext.response = HTTPResult.withHeaders( postfixResult, @@ -510,7 +565,10 @@ export async function requestListener( ); } finally { requestContext.error = requestError; - await executeMonitors(method, path, requestContext); + const monitorExecution = executeMonitors(method, path, requestContext); + if (monitorExecution) { + await monitorExecution; + } handleResult(isHeadRequest, requestContext.response, res); } } @@ -554,10 +612,13 @@ export async function upgradeListener( mustDestroySocket = true; // Fall through to finally block to execute monitors. } else { - const prefixResult = await executePriorityHandlers( - getMultiHandlers(method, path, roots.prefix), + const prefixExecution = executeMiddleware( + "prefix", + method, + path, requestContext, ); + const prefixResult = prefixExecution ? await prefixExecution : undefined; if (prefixResult) { requestContext.response = HTTPResult.withHeaders( prefixResult, @@ -587,7 +648,10 @@ export async function upgradeListener( } } finally { requestContext.error = requestError; - await executeMonitors(method, path, requestContext); + const monitorExecution = executeMonitors(method, path, requestContext); + if (monitorExecution) { + await monitorExecution; + } if (mustSendResponse) { handleResult(false, requestContext.response, res); } diff --git a/src/test/server-fast-paths.test.ts b/src/test/server-fast-paths.test.ts new file mode 100644 index 0000000..4e3fcb1 --- /dev/null +++ b/src/test/server-fast-paths.test.ts @@ -0,0 +1,123 @@ +import assert from "node:assert"; +import { createServer, type Server } from "node:http"; +import { HandlerPriority } from "@antelopejs/interface-api"; +import { registerHandler, requestListener, unregisterHandler } from "../server"; + +const TEST_HOST = "127.0.0.1"; +const TEST_PATH = "/server-fast-path-lifecycle"; +const HANDLER_ID = "server-fast-path-handler"; +const PREFIX_HIGH_ID = "server-fast-path-prefix-high"; +const PREFIX_LOW_ID = "server-fast-path-prefix-low"; +const POSTFIX_ID = "server-fast-path-postfix"; +const MONITOR_ID = "server-fast-path-monitor"; +const ROUTE_IDS = [ + HANDLER_ID, + PREFIX_HIGH_ID, + PREFIX_LOW_ID, + POSTFIX_ID, + MONITOR_ID, +]; + +interface TestResponse { + status: number; + body: string; +} + +function listen(server: Server): Promise { + return new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, TEST_HOST, () => { + const address = server.address(); + if (!address || typeof address === "string") { + reject(new Error("Test server did not bind a TCP port")); + return; + } + resolve(address.port); + }); + }); +} + +function close(server: Server): Promise { + return new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }); +} + +async function request(port: number): Promise { + const response = await fetch(`http://${TEST_HOST}:${port}${TEST_PATH}`); + return { status: response.status, body: await response.text() }; +} + +describe("Server middleware fast paths", () => { + const events: string[] = []; + const server = createServer((incoming, response) => { + void requestListener(incoming, response, "http"); + }); + let port: number; + + before(async () => { + port = await listen(server); + registerHandler(HANDLER_ID, "handler", "get", TEST_PATH, () => { + events.push("handler"); + return "ok"; + }); + }); + + after(async () => { + ROUTE_IDS.forEach(unregisterHandler); + await close(server); + }); + + beforeEach(() => { + events.length = 0; + }); + + it("preserves lifecycle behavior as middleware is added and removed", async () => { + assert.deepEqual(await request(port), { status: 200, body: "ok" }); + assert.deepEqual(events, ["handler"]); + + registerHandler( + PREFIX_LOW_ID, + "prefix", + "get", + TEST_PATH, + () => { + events.push("prefix-low"); + }, + HandlerPriority.LOW, + ); + registerHandler( + PREFIX_HIGH_ID, + "prefix", + "get", + TEST_PATH, + () => { + events.push("prefix-high"); + }, + HandlerPriority.HIGH, + ); + registerHandler(POSTFIX_ID, "postfix", "get", TEST_PATH, () => { + events.push("postfix"); + }); + registerHandler(MONITOR_ID, "monitor", "get", TEST_PATH, () => { + events.push("monitor"); + }); + + events.length = 0; + assert.deepEqual(await request(port), { status: 200, body: "ok" }); + assert.deepEqual(events, [ + "prefix-high", + "prefix-low", + "handler", + "postfix", + "monitor", + ]); + + [PREFIX_HIGH_ID, PREFIX_LOW_ID, POSTFIX_ID, MONITOR_ID].forEach( + unregisterHandler, + ); + events.length = 0; + assert.deepEqual(await request(port), { status: 200, body: "ok" }); + assert.deepEqual(events, ["handler"]); + }); +});