Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 106 additions & 42 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -39,14 +42,22 @@ class RouteLevel {
catchAllRoutes: CatchAllRoute[] = [];
}

const roots: Record<string, Record<string, RouteLevel>> = {
const roots: Record<HandlerMode, Record<string, RouteLevel>> = {
handler: {},
prefix: {},
postfix: {},
monitor: {},
websocket: {},
};

const handlerCounts: Record<HandlerMode, number> = {
handler: 0,
prefix: 0,
postfix: 0,
monitor: 0,
websocket: 0,
};

interface HandlerResult {
handler: RouteCallback;
parameters: Record<string, string>;
Expand Down Expand Up @@ -186,42 +197,43 @@ function getHandler(
return multi ? result : undefined;
}

function removeHandler(
id: string,
source: Record<string, RouteLevel>,
): boolean {
function removeHandler(id: string, source: Record<string, RouteLevel>): 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<string, RouteLevel>,
),
)
) {
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 = {
Expand All @@ -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,
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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);
Expand All @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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);
}
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
Expand Down
123 changes: 123 additions & 0 deletions src/test/server-fast-paths.test.ts
Original file line number Diff line number Diff line change
@@ -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<number> {
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<void> {
return new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}

async function request(port: number): Promise<TestResponse> {
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"]);
});
});
Loading