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
4 changes: 4 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ const performanceConcerns = [
selector: "ImportDeclaration[source.value=/package.json$/]", // #2974
message: "it can not be tree shaken, use tsdown and process.env instead",
},
{
selector: "CallExpression[callee.property.name=/^(shift|unshift)$/]", // #3343
message: "shifting is 2-20x slower than index-based iteration",
},
];

const tsFactoryConcerns = [
Expand Down
64 changes: 0 additions & 64 deletions express-zod-api/bench/can-merge.bench.ts

This file was deleted.

42 changes: 42 additions & 0 deletions express-zod-api/bench/queue-ops.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { bench, describe } from "vitest";

const smallQueue = Array.from({ length: 5 }, (_, i) => ({
id: i,
data: `item-${i}`,
}));
const mediumQueue = Array.from({ length: 20 }, (_, i) => ({
id: i,
data: `item-${i}`,
}));
const largeQueue = Array.from({ length: 100 }, (_, i) => ({
id: i,
data: `item-${i}`,
}));

const queues = [
[smallQueue, "small (5)"],
[mediumQueue, "medium (20)"],
[largeQueue, "large (100)"],
] as const;

describe.each(queues)("$1", (queue) => {
bench("shift() approach", () => {
const q = [...queue];
while (q.length) q.shift();
});

bench("index approach", () => {
const q = [...queue];
for (let idx = 0; idx < q.length; idx++) q[idx]; // eslint-disable-line @typescript-eslint/no-unused-expressions
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

bench("pop() reverse", () => {
const q = [...queue].reverse();
while (q.length) q.pop();
});

bench("forEach", () => {
const q = [...queue];
q.forEach(() => {});
});
});
4 changes: 2 additions & 2 deletions express-zod-api/src/deep-checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ export const hasCycle = (
) => {
const json = z.toJSONSchema(subject, { io, unrepresentable: "any" });
const stack: unknown[] = [json];
while (stack.length) {
const entry = stack.shift()!;
for (let idx = 0; idx < stack.length; idx++) {
const entry = stack[idx];
if (R.is(Object, entry)) {
if ((entry as z.core.JSONSchema.BaseSchema).$ref === "#") return true;
stack.push(...R.values(entry));
Expand Down
4 changes: 2 additions & 2 deletions express-zod-api/src/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ export class Diagnostics {
const stack: z.core.JSONSchema.BaseSchema[] = [
z.toJSONSchema(endpoint[`${dir}Schema`], { unrepresentable: "any" }),
];
while (stack.length > 0) {
const entry = stack.shift()!;
for (let idx = 0; idx < stack.length; idx++) {
const entry = stack[idx];
if (entry.type && entry.type !== "object")
this.logger.warn(`Endpoint ${dir} schema is not object-based`, ctx);
for (const prop of ["allOf", "oneOf", "anyOf"] as const)
Expand Down
4 changes: 2 additions & 2 deletions express-zod-api/src/documentation-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,8 +371,8 @@ const fixReferences = (
ctx: OpenAPIContext,
) => {
const stack: unknown[] = [subject, defs];
while (stack.length) {
const entry = stack.shift()!;
for (let idx = 0; idx < stack.length; idx++) {
const entry = stack[idx];
if (R.is(Object, entry)) {
if (isReferenceObject(entry) && !entry.$ref.startsWith("#/components")) {
const actualName = entry.$ref.split("/").pop()!;
Expand Down
1 change: 1 addition & 0 deletions express-zod-api/src/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export class Integration extends IntegrationBase {
config,
onEndpoint: hasHeadMethod ? withHead(onEndpoint) : onEndpoint,
});
// eslint-disable-next-line no-restricted-syntax -- accumulated late, acceptable for generator
this.#program.unshift(...this.#aliases.values());
this.#program.push(
this.makePathType(),
Expand Down
4 changes: 2 additions & 2 deletions express-zod-api/src/json-schema-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ export const flattenIO = (
const stack: Stack = [R.pair(false, jsonSchema)]; // [isOptional, JSON Schema]
const flat: FlattenObjectSchema = { type: "object", properties: {} };
const flatRequired: string[] = [];
while (stack.length) {
const [isOptional, entry] = stack.shift()!;
for (let idx = 0; idx < stack.length; idx++) {
const [isOptional, entry] = stack[idx];
if (entry.description) flat.description ??= entry.description;
stack.push(...processAllOf(entry, mode, isOptional));
stack.push(...processVariants(entry));
Expand Down
6 changes: 3 additions & 3 deletions express-zod-api/src/routing-walker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ export const walkRouting = ({
}: RoutingWalkerParams) => {
const stack = processEntries(config, routing);
const visited = new Set<string>();
while (stack.length) {
const [path, element, explicitMethod] = stack.shift()!;
for (let idx = 0; idx < stack.length; idx++) {
const [path, element, explicitMethod] = stack[idx];
if (element instanceof AbstractEndpoint) {
if (explicitMethod) {
checkDuplicate(explicitMethod, path, visited);
Expand All @@ -110,7 +110,7 @@ export const walkRouting = ({
if (element instanceof ServeStatic) {
if (onStatic) element.apply(path, onStatic);
} else {
stack.unshift(...processEntries(config, element, path));
stack.splice(idx + 1, 0, ...processEntries(config, element, path));
}
}
}
Expand Down
14 changes: 6 additions & 8 deletions express-zod-api/src/routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,9 @@ export const initRouting = ({ app, config, getLogger, ...rest }: InitProps) => {
/** @link https://github.com/RobinTail/express-zod-api/discussions/2791#discussioncomment-13745912 */
if (accessMethods.includes("get")) accessMethods.push("head");
for (const [method, [matchingParsers, endpoint]] of methods) {
const handlers = matchingParsers
.slice() // must be immutable
.concat(async (request, response) => {
const logger = getLogger(request);
return endpoint.execute({ request, response, logger, config });
});
const handlers: RequestHandler[] = []; // issue #2706: CORS must go before parsers:
if (config.cors) {
// issue #2706, must go before parsers:
handlers.unshift(async (request, response, next) => {
handlers.push(async (request, response, next) => {
const logger = getLogger(request);
const defaultHeaders = makeCorsHeaders(accessMethods);
const headers =
Expand All @@ -108,6 +102,10 @@ export const initRouting = ({ app, config, getLogger, ...rest }: InitProps) => {
next();
});
}
handlers.push(...matchingParsers, async (request, response) => {
const logger = getLogger(request);
return endpoint.execute({ request, response, logger, config });
});
app[method](path, ...handlers);
}
if (config.wrongMethodBehavior === 404) continue;
Expand Down
30 changes: 15 additions & 15 deletions express-zod-api/src/zts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,27 +40,27 @@ const onTemplateLiteral: Producer = (
{ _zod: { def } }: z.core.$ZodTemplateLiteral,
{ next, api },
) => {
const parts = [...def.parts];
const shiftText = () => {
const { parts } = def;
let idx = 0;
const readText = () => {
let text = "";
while (parts.length) {
const part = parts.shift();
if (isSchema(part)) {
parts.unshift(part);
break;
}
while (idx < parts.length) {
const part = parts[idx];
if (isSchema(part)) break;
idx++;
text += part ?? ""; // Handle potential undefined values
}
return text;
};
const head = api.f.createTemplateHead(shiftText());
const head = api.f.createTemplateHead(readText());
const spans: ts.TemplateLiteralTypeSpan[] = [];
while (parts.length) {
const schema = next(parts.shift() as z.core.$ZodType);
const text = shiftText();
const textWrapper = parts.length
? api.f.createTemplateMiddle
: api.f.createTemplateTail;
while (idx < parts.length) {
const schema = next(parts[idx++] as z.core.$ZodType);
const text = readText();
const textWrapper =
idx < parts.length
? api.f.createTemplateMiddle
: api.f.createTemplateTail;
spans.push(api.f.createTemplateLiteralTypeSpan(schema, textWrapper(text)));
}
if (!spans.length) return api.makeLiteralType(head.text);
Expand Down
36 changes: 36 additions & 0 deletions express-zod-api/tests/__snapshots__/routing-walker.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`walkRouting() > should process endpoints in depth-first order 0 1`] = `
[
[
"get",
"/v1/user/retrieve",
Endpoint {},
],
[
"get",
"/v1/user/create",
Endpoint {},
],
[
"get",
"/v1/record",
Endpoint {},
],
]
`;

exports[`walkRouting() > should process endpoints in depth-first order 1 1`] = `
[
[
"get",
"/a/b/c",
Endpoint {},
],
[
"get",
"/a/d",
Endpoint {},
],
]
`;
27 changes: 27 additions & 0 deletions express-zod-api/tests/routing-walker.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { defaultEndpointsFactory, Routing } from "../src";
import { walkRouting } from "../src/routing-walker";

describe("walkRouting()", () => {
const endpoint = defaultEndpointsFactory.buildVoid({
handler: vi.fn(),
});

const onEndpoint = vi.fn();

afterEach(() => {
onEndpoint.mockClear();
});

test.each<Routing>([
{
v1: {
user: { retrieve: endpoint, create: endpoint },
record: endpoint,
},
},
{ a: { b: { c: endpoint }, d: endpoint } },
])("should process endpoints in depth-first order %#", (routing) => {
walkRouting({ routing, config: { cors: false }, onEndpoint });
expect(onEndpoint.mock.calls).toMatchSnapshot();
});
});
Loading