From e70642c537fd61a941ea44d363f5124e790e867e Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Tue, 15 Sep 2026 16:22:02 +0000 Subject: [PATCH] fix(api): enforce late controller guards --- src/implementations/api/index.ts | 19 +++++----- src/test/controller-resolution.test.ts | 49 ++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/implementations/api/index.ts b/src/implementations/api/index.ts index 0a0d190..8095589 100644 --- a/src/implementations/api/index.ts +++ b/src/implementations/api/index.ts @@ -38,7 +38,7 @@ interface PromiseLikeValue { } interface ComputedPropertyResolver { - key: string; + key: PropertyKey; resolve: ParameterResolver; } @@ -143,12 +143,10 @@ function compileController( controllerClass: ControllerClass, properties: Record, ): ControllerPlan { - const computedProperties = Object.entries(properties).map( - ([key, parameter]) => ({ - key, - resolve: compileParameter(parameter), - }), - ); + const computedProperties = Reflect.ownKeys(properties).map((key) => ({ + key, + resolve: compileParameter(properties[key]), + })); const existingPlan = controllerPlans.get(controllerClass); const plan = existingPlan ?? { controllerClass, @@ -323,13 +321,16 @@ function compileHandler(handler: RouteHandler): HandlerPlan { export const routesProxy = { register: (id: string, handler: RouteHandler): void => { registeredRoutes.set(id, handler); - const plan = compileHandler(handler); + let plan: HandlerPlan | undefined; registerHandler( `dev/${id}`, handler.mode, handler.method, handler.location, - (context: RequestContextDev) => invokeHandler(plan, context), + (context: RequestContextDev) => { + plan ??= compileHandler(handler); + return invokeHandler(plan, context); + }, handler.priority, ); }, diff --git a/src/test/controller-resolution.test.ts b/src/test/controller-resolution.test.ts index bcc38f6..93fcbc0 100644 --- a/src/test/controller-resolution.test.ts +++ b/src/test/controller-resolution.test.ts @@ -14,8 +14,10 @@ import { requestListener } from "../server"; const TEST_HOST = "127.0.0.1"; const TEST_ORIGIN = `http://${TEST_HOST}`; const THEN_PROPERTY = ["th", "en"].join(""); +const SYMBOL_PROPERTY = Symbol("computed-property"); interface TestController { + [SYMBOL_PROPERTY]?: string; requestId?: string; sequence: number; } @@ -254,6 +256,53 @@ describe("Controller resolution", () => { }); }); + it("compiles handler metadata when the first request arrives", async () => { + const Controller = createController(); + const properties: Record = {}; + const location = "/controller-resolution/late-metadata"; + register( + createHandler( + Controller, + function (this: TestController) { + return this.requestId; + }, + location, + [], + properties, + ), + ); + properties.requestId = computedParameter(() => "late metadata"); + + assert.deepEqual(await get(port, location), { + status: 200, + body: "late metadata", + }); + }); + + it("applies symbol-keyed computed metadata", async () => { + const Controller = createController(); + const properties = { + [SYMBOL_PROPERTY]: computedParameter(() => "symbol value"), + }; + const location = "/controller-resolution/symbol-metadata"; + register( + createHandler( + Controller, + function (this: TestController) { + return this[SYMBOL_PROPERTY]; + }, + location, + [], + properties, + ), + ); + + assert.deepEqual(await get(port, location), { + status: 200, + body: "symbol value", + }); + }); + it("resolves computed values and handler parameters with controller this", async () => { const Controller = createController(); const properties = {