Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Traverse without recursion #2168

Merged
merged 15 commits into from
Nov 15, 2024
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
- The `Endpoint::getMethods()` method may now return `undefined`;
- The `testEndpoint()` method can no longer test CORS headers — that function moved to `Routing` traverse;
- Public properties `pairs`, `firstEndpoint` and `siblingMethods` of `DependsOnMethod` replaced with `entries`.
- Routing traverse improvement: performance +5%, memory consumption -17%.
- Consider the automated migration using the built-in ESLint rule.

```js
Expand Down
5 changes: 5 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ export default [
selector: "ImportDeclaration[source.value=/assert/]",
message: "assert is slow, use throw",
},
{
selector:
"CallExpression > MemberExpression[object.name='Object'][property.name='entries']",
message: "Object.entries() is 2x slower than R.toPairs()",
},
],
},
},
Expand Down
28 changes: 16 additions & 12 deletions src/routing-walker.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { toPairs } from "ramda";
import { DependsOnMethod } from "./depends-on-method";
import { AbstractEndpoint } from "./endpoint";
import { RoutingError } from "./errors";
Expand All @@ -17,22 +18,25 @@ export interface RoutingWalkerParams {
parentPath?: string;
}

export const walkRouting = ({
routing,
onEndpoint,
onStatic,
parentPath,
}: RoutingWalkerParams) => {
const pairs = Object.entries(routing).map(
([key, value]) => [key.trim(), value] as const,
);
for (const [segment, element] of pairs) {
const makePairs = (subject: Routing, parent?: string) =>
toPairs(subject).map(([segment, item]) => {
if (segment.includes("/")) {
throw new RoutingError(
`The entry '${segment}' must avoid having slashes — use nesting instead.`,
);
}
const path = `${parentPath || ""}${segment ? `/${segment}` : ""}`;
const trimmed = segment.trim();
return [`${parent || ""}${trimmed ? `/${trimmed}` : ""}`, item] as const;
});

export const walkRouting = ({
routing,
onEndpoint,
onStatic,
}: RoutingWalkerParams) => {
const stack = makePairs(routing);
while (stack.length) {
const [path, element] = stack.shift()!;
if (element instanceof AbstractEndpoint) {
const methods = element.getMethods() || ["get"];
for (const method of methods) onEndpoint(element, path, method);
Expand All @@ -49,7 +53,7 @@ export const walkRouting = ({
onEndpoint(endpoint, path, method, siblingMethods);
}
} else {
walkRouting({ onEndpoint, onStatic, routing: element, parentPath: path });
stack.push(...makePairs(element, path));
}
}
};
63 changes: 31 additions & 32 deletions tests/bench/experiment.bench.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
import assert from "node:assert/strict";
import { bench } from "vitest";
import { RoutingError } from "../../src";
import { retrieveUserEndpoint } from "../../example/endpoints/retrieve-user";
import { DependsOnMethod } from "../../src";
import { walkRouting } from "../../src/routing-walker";

describe.each([false, true])("Experiment for errors %s", (ass) => {
const notAss = !ass;
const routing = {
a: {
b: {
c: {
d: {
e: {
f: {
g: {
h: {
i: {
j: new DependsOnMethod({
post: retrieveUserEndpoint,
}),
},
},
},
},
},
},
},
},
k: { l: {} },
m: {},
},
};

bench("assert with text", () => {
try {
assert(ass, "text");
} catch {}
});

bench("assert with Error", () => {
try {
assert(ass, new Error());
} catch {}
});

bench("assert with custom error", () => {
try {
assert(ass, new RoutingError());
} catch {}
});

bench("throwing Error", () => {
try {
if (notAss) throw new Error();
} catch {}
});

bench("throwing custom error", () => {
try {
if (notAss) throw new RoutingError();
} catch {}
describe("Experiment for routing walker", () => {
bench("featured", () => {
walkRouting({ routing, onEndpoint: vi.fn() });
});
});
Loading