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
19 changes: 10 additions & 9 deletions src/implementations/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ interface PromiseLikeValue {
}

interface ComputedPropertyResolver {
key: string;
key: PropertyKey;
resolve: ParameterResolver;
}

Expand Down Expand Up @@ -144,12 +144,10 @@ function compileController(
controllerClass: ControllerClass,
properties: Record<PropertyKey, ComputedParameter>,
): 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,
Expand Down Expand Up @@ -324,13 +322,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,
);
},
Expand Down
49 changes: 49 additions & 0 deletions src/test/controller-resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
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;
}
Expand Down Expand Up @@ -64,14 +66,14 @@
return { readCount: () => reads, value: thenable };
}

function createHandler(
Controller: ControllerConstructor,
callback: RouteHandler["callback"],
location: string,
parameters: RouteHandler["parameters"] = [],
properties: RouteHandler["properties"] = {},
mode: RouteHandler["mode"] = "handler",
): RouteHandler {

Check warning on line 76 in src/test/controller-resolution.test.ts

View workflow job for this annotation

GitHub Actions / checks

eslint(max-params)

src/test/controller-resolution.test.ts:69:23: Function 'createHandler' has too many parameters (6). Maximum allowed is 5.
return {
mode,
method: "get",
Expand Down Expand Up @@ -112,300 +114,347 @@
return { status: response.status, body: await response.text() };
}

describe("Controller resolution", () => {
const routeIds: string[] = [];
let server: Server;
let port: number;
let nextRouteId = 0;
let listenerResult: unknown;

function register(handler: RouteHandler): void {
const id = `controller-resolution-${nextRouteId++}`;
routeIds.push(id);
routesProxy.register(id, handler);
}

before(async () => {
server = createServer((request, response) => {
listenerResult = requestListener(request, response, "http");
});
port = await listen(server);
});

after(async () => {
routeIds.forEach((id) => {
routesProxy.unregister(id);
});
await close(server);
});

it("attaches the full implementation through the nested interface contract", () => {
const result = spawnSync(
process.execPath,
[
"-e",
`const { ImplementInterface } = require("@antelopejs/interface-core");
ImplementInterface(
require("@antelopejs/interface-api"),
require("./dist/implementations/api"),
);`,
],
{ cwd: resolve(__dirname, "../.."), encoding: "utf8" },
);

assert.equal(result.status, 0, result.stderr);
});

it("isolates computed properties across concurrent requests", async () => {
const Controller = createController();
const properties = {
requestId: computedParameter(
(context) =>
new Promise((resolve) => {
setTimeout(
() => resolve(context.rawRequest.headers["x-request-id"]),
1,
);
}),
),
};
register(
createHandler(
Controller,
function (this: TestController) {
this.sequence += 1;
return `${this.requestId}:${this.sequence}`;
},
"/controller-resolution/isolation",
[],
properties,
),
);

const identifiers = Array.from(
{ length: 40 },
(_, index) => `request-${index}`,
);
const responses = await Promise.all(
identifiers.map((id) =>
get(port, "/controller-resolution/isolation", id),
),
);

assert.deepEqual(
responses.map((response) => response.body),
identifiers.map((id) => `${id}:1`),
);
});

it("reuses one controller within a request", async () => {
const Controller = createController();
const location = "/controller-resolution/reuse";
register(
createHandler(
Controller,
function (this: TestController) {
this.sequence += 1;
},
location,
[],
{},
"prefix",
),
);
register(
createHandler(
Controller,
function (this: TestController) {
this.sequence += 1;
return this.sequence.toString();
},
location,
),
);

assert.deepEqual(await get(port, location), { status: 200, body: "2" });
assert.deepEqual(await get(port, location), { status: 200, body: "2" });
});

it("applies inherited computed metadata", async () => {
const Parent = createController();
class Child extends Parent {}
const parentMetadata = GetMetadata(Parent, ControllerMeta);
parentMetadata.computed_props.requestId = computedParameter(
(context) => context.rawRequest.headers["x-request-id"],
);
const childMetadata = GetMetadata(Child, ControllerMeta);
const location = "/controller-resolution/inheritance";
register(
createHandler(
Child,
function (this: TestController) {
return this.requestId;
},
location,
[],
childMetadata.computed_props,
),
);

assert.deepEqual(await get(port, location, "inherited"), {
status: 200,
body: "inherited",
});
});

it("compiles handler metadata when the first request arrives", async () => {
const Controller = createController();
const properties: Record<PropertyKey, ComputedParameter> = {};
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 = {
requestId: computedParameter(
function (this: TestController, context) {
this.sequence += 1;
return context.rawRequest.headers["x-request-id"];
},
[
async function (this: TestController, _context, value) {
this.sequence += 1;
return `${value}:computed`;
},
],
),
};
const parameters = [
computedParameter(function (this: TestController) {
this.sequence += 1;
return this.requestId;
}),
computedParameter(async function (this: TestController) {
this.sequence += 1;
return this.sequence;
}),
null,
];
const location = "/controller-resolution/computed";
register(
createHandler(
Controller,
function (this: TestController, value, sequence, missing) {
return `${value}:${sequence}:${missing}:${this.sequence}`;
},
location,
parameters,
properties,
),
);

assert.deepEqual(await get(port, location, "value"), {
status: 200,
body: "value:computed:4:undefined:4",
});
});

it("keeps synchronous modifier chains on the synchronous request path", async () => {
const Controller = createController();
const location = "/controller-resolution/synchronous-modifiers";
register(
createHandler(
Controller,
function (this: TestController, value) {
return `${value}:${this.sequence}`;
},
location,
[
computedParameter(
function (this: TestController) {
this.sequence += 1;
return "provider";
},
[
function (this: TestController, _context, value) {
this.sequence += 1;
return `${value}:first`;
},
function (this: TestController, _context, value) {
this.sequence += 1;
return `${value}:second`;
},
],
),
],
),
);

assert.deepEqual(await get(port, location), {
status: 200,
body: "provider:first:second:3",
});
assert.equal(listenerResult, undefined);
});

it("continues remaining modifiers after the first asynchronous value", async () => {
const Controller = createController();
const events: string[] = [];
const thenable = statefulThenable("value:thenable");
const location = "/controller-resolution/mixed-modifiers";
register(
createHandler(Controller, (value) => value, location, [
computedParameter(() => {
events.push("provider");
return thenable.value;
}, [
(_context, value) => {
events.push("async");
return Promise.resolve(`${value}:async`);
},
(_context, value) => {
events.push("remaining");
return `${value}:remaining`;
},
]),
]),
);

assert.deepEqual(await get(port, location), {
status: 200,
body: "value:thenable:async:remaining",
});
assert.deepEqual(events, ["provider", "async", "remaining"]);
assert.equal(thenable.readCount(), 1);
});

it("turns provider and modifier failures into request errors", async () => {
const ProviderController = createController();
const ModifierController = createController();
register(
createHandler(
ProviderController,
() => "unreachable",
"/controller-resolution/provider-error",
[computedParameter(() => Promise.reject(new Error("provider failed")))],
),
);
register(
createHandler(
ModifierController,
() => "unreachable",
"/controller-resolution/modifier-error",
[
computedParameter(
() => "value",
[
() => {
throw new Error("modifier failed");
},
],
),
],
),
);

assert.deepEqual(await get(port, "/controller-resolution/provider-error"), {
status: 500,
body: "provider failed",
});
assert.deepEqual(await get(port, "/controller-resolution/modifier-error"), {
status: 500,
body: "modifier failed",
});
});
});

Check warning on line 460 in src/test/controller-resolution.test.ts

View workflow job for this annotation

GitHub Actions / checks

eslint(max-lines-per-function)

src/test/controller-resolution.test.ts:117:35: The function has too many lines (320). Maximum allowed is 120.
Loading