Skip to content
Closed
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
43 changes: 43 additions & 0 deletions .github/workflows/codspeed.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: "Benchmarks"
on:
push:
branches:
- "main"
pull_request:
types: ["opened", "reopened", "synchronize"]
branches:
- "main"
workflow_dispatch: {}
permissions: {}
concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
cancel-in-progress: true
jobs:
benchmarks:
name: "Run benchmarks"
runs-on: "ubuntu-latest"
timeout-minutes: 30
permissions:
contents: "read"
id-token: "write"
steps:
- name: "Checkout"
uses: "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd" # v6.0.2
with:
ref: "${{ github.ref }}"
fetch-depth: 1
persist-credentials: false
- name: "Setup Node.js"
uses: "actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020" # v4.4.0
with:
node-version: "22"
- name: "Install pnpm"
run: "corepack enable && corepack prepare"
- name: "Install dependencies"
run: "pnpm install --frozen-lockfile --ignore-scripts"
- name: "Run benchmarks"
uses: "CodSpeedHQ/action@3194d9a39c4d46684cb44bf7207fc56626aad8fd" # v4.15.1
with:
mode: "simulation"
run: "pnpm exec vitest bench --config vitest.bench.config.mts --run"
working-directory: "packages/node"
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
[![CI status][status-badge]][status-dashboard]
[![Test coverage][coverage-badge]][coverage-dashboard]
[![Supply-chain score][socket-badge]][socket-dashboard]
[![CodSpeed][codspeed-badge]][codspeed-dashboard]

[github-badge]: https://img.shields.io/github/stars/vadimpiven/node_reqwest?style=flat&logo=github
[github-repo]: https://github.com/vadimpiven/node_reqwest
Expand All @@ -20,6 +21,8 @@
[coverage-dashboard]: https://app.codecov.io/gh/vadimpiven/node_reqwest/tree/main
[socket-badge]: https://badge.socket.dev/npm/package/node-reqwest
[socket-dashboard]: https://socket.dev/npm/package/node-reqwest
[codspeed-badge]: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json
[codspeed-dashboard]: https://codspeed.io/vadimpiven/node_reqwest?utm_source=badge

[![Open in GitHub Codespaces][codespace-badge]][codespace-action]

Expand Down
74 changes: 2 additions & 72 deletions packages/node/export/agent.ts
Original file line number Diff line number Diff line change
@@ -1,78 +1,8 @@
import type Stream from "node:stream";
import { Dispatcher, type FormData, Response } from "undici";
import { Dispatcher } from "undici";
import { Addon } from "./addon.ts";
import type { AgentCreationOptions, AgentDispatchOptions, AgentInstance } from "./addon-def.ts";
import type { Agent as AgentDef, AgentOptions } from "./agent-def.ts";

function normalizePem(pem?: string | Buffer | (string | Buffer)[]): string[] {
if (!pem) {
return [];
}

if (Array.isArray(pem)) {
return pem.flatMap(normalizePem);
}

return [Buffer.isBuffer(pem) ? pem.toString() : pem];
}

type HeaderValue = string | string[] | number | undefined;

function normalizeHeaders(
headers?: Record<string, HeaderValue> | Iterable<[string, HeaderValue]> | string[] | null,
): Record<string, string> {
if (!headers) {
return {};
}

const result: Record<string, string> = {};
const add = (key: string, value: HeaderValue): void => {
if (value === undefined || value === null) {
return;
}
const k = key.toLowerCase();
const v = Array.isArray(value) ? value.join(", ") : String(value);
if (!v) {
return;
}
const existing = result[k];
result[k] = existing ? `${existing}, ${v}` : v;
};

if (Array.isArray(headers)) {
for (let i = 0; i < headers.length; i += 2) {
const key = headers[i];
if (key !== undefined) {
add(key, headers[i + 1]);
}
}
} else if (Symbol.iterator in headers) {
for (const [key, value] of headers) {
add(key, value);
}
} else {
for (const [key, value] of Object.entries(headers)) {
add(key, value);
}
}

return result;
}

function normalizeBody(
body?: string | Buffer | Uint8Array | FormData | Stream.Readable | null,
): ReadableStreamBYOBReader | null {
if (!body) {
return null;
}

const response = new Response(body);
if (!response.body) {
return null;
}

return response.body.getReader({ mode: "byob" });
}
import { normalizeBody, normalizeHeaders, normalizePem } from "./normalize.ts";

class AgentImpl extends Dispatcher {
#agent: AgentInstance;
Expand Down
83 changes: 83 additions & 0 deletions packages/node/export/normalize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: Apache-2.0 OR MIT

import type Stream from "node:stream";
import { type FormData, Response } from "undici";

/**
* Normalizes PEM certificate input into an array of strings.
*/
export function normalizePem(pem?: string | Buffer | (string | Buffer)[]): string[] {
if (!pem) {
return [];
}

if (Array.isArray(pem)) {
return pem.flatMap(normalizePem);
}

return [Buffer.isBuffer(pem) ? pem.toString() : pem];
}

export type HeaderValue = string | string[] | number | undefined;

/**
* Normalizes various header formats into a flat record of lowercase key-value pairs.
*/
export function normalizeHeaders(
headers?: Record<string, HeaderValue> | Iterable<[string, HeaderValue]> | string[] | null,
): Record<string, string> {
if (!headers) {
return {};
}

const result: Record<string, string> = {};
const add = (key: string, value: HeaderValue): void => {
if (value === undefined || value === null) {
return;
}
const k = key.toLowerCase();
const v = Array.isArray(value) ? value.join(", ") : String(value);
if (!v) {
return;
}
const existing = result[k];
result[k] = existing ? `${existing}, ${v}` : v;
};

if (Array.isArray(headers)) {
for (let i = 0; i < headers.length; i += 2) {
const key = headers[i];
if (key !== undefined) {
add(key, headers[i + 1]);
}
}
} else if (Symbol.iterator in headers) {
for (const [key, value] of headers) {
add(key, value);
}
} else {
for (const [key, value] of Object.entries(headers)) {
add(key, value);
}
}

return result;
}

/**
* Normalizes various body types into a ReadableStreamBYOBReader.
*/
export function normalizeBody(
body?: string | Buffer | Uint8Array | FormData | Stream.Readable | null,
): ReadableStreamBYOBReader | null {
if (!body) {
return null;
}

const response = new Response(body);
if (!response.body) {
return null;
}

return response.body.getReader({ mode: "byob" });
}
4 changes: 3 additions & 1 deletion packages/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,15 @@
"ci-test:rust": "pnpm run test:nextest -r --locked && pnpm run test:rustdoc -r --locked",
"ci-test": "pnpm run test:ts && pnpm run ci-test:rust",
"pack-addon": "slsa pack dist/node_reqwest-v{version}-{platform}-{arch}.node.gz",
"postinstall": "slsa wget"
"postinstall": "slsa wget",
"bench": "vitest bench --config vitest.bench.config.mts"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The local bench script omits --run, so pnpm bench starts vitest in watch mode and never exits. The CI workflow explicitly adds --run; the local script should match to avoid accidental hangs in automation and to produce a deterministic one-shot run when invoked manually.

Suggested change
"bench": "vitest bench --config vitest.bench.config.mts"
"bench": "vitest bench --config vitest.bench.config.mts --run"
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/node/package.json
Line: 70

Comment:
The local `bench` script omits `--run`, so `pnpm bench` starts vitest in watch mode and never exits. The CI workflow explicitly adds `--run`; the local script should match to avoid accidental hangs in automation and to produce a deterministic one-shot run when invoked manually.

```suggestion
    "bench": "vitest bench --config vitest.bench.config.mts --run"
```

How can I resolve this? If you propose a fix, please make it concise.

},
"dependencies": {
"node-addon-slsa": "1.0.0"
},
"devDependencies": {
"@codecov/vite-plugin": "catalog:",
"@codspeed/vitest-plugin": "^5.4.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Every other dev dependency in this file uses the catalog: specifier; @codspeed/vitest-plugin uses a bare semver range instead. Using catalog: keeps the version centralised in the workspace catalog and consistent with the project's conventions.

Suggested change
"@codspeed/vitest-plugin": "^5.4.0",
"@codspeed/vitest-plugin": "catalog:",
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/node/package.json
Line: 77

Comment:
Every other dev dependency in this file uses the `catalog:` specifier; `@codspeed/vitest-plugin` uses a bare semver range instead. Using `catalog:` keeps the version centralised in the workspace catalog and consistent with the project's conventions.

```suggestion
    "@codspeed/vitest-plugin": "catalog:",
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

"@playwright/test": "catalog:",
"@types/node": "catalog:",
"@vitest/coverage-istanbul": "catalog:",
Expand Down
26 changes: 26 additions & 0 deletions packages/node/tests/bench/normalize-body.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: Apache-2.0 OR MIT

import { bench, describe } from "vitest";
import { normalizeBody } from "../../export/normalize.ts";

describe("normalizeBody", () => {
bench("null input", () => {
normalizeBody(null);
});

bench("string body", () => {
normalizeBody('{"key":"value"}');
});

bench("Buffer body", () => {
normalizeBody(Buffer.from("request body content"));
});

bench("Uint8Array body", () => {
normalizeBody(new Uint8Array([72, 101, 108, 108, 111]));
});

bench("large string body", () => {
normalizeBody(JSON.stringify({ data: "x".repeat(1024) }));
});
});
Comment on lines +6 to +26

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Fixture data created inside bench callbacks inflates benchmark numbers. Buffer.from(), new Uint8Array(), JSON.stringify(), and especially "x".repeat(1024) all run on every iteration, so measured timings include allocation and serialization cost rather than just normalizeBody performance. normalize-pem.bench.ts correctly hoists fixtures to module level — the same pattern should be applied here.

Suggested change
describe("normalizeBody", () => {
bench("null input", () => {
normalizeBody(null);
});
bench("string body", () => {
normalizeBody('{"key":"value"}');
});
bench("Buffer body", () => {
normalizeBody(Buffer.from("request body content"));
});
bench("Uint8Array body", () => {
normalizeBody(new Uint8Array([72, 101, 108, 108, 111]));
});
bench("large string body", () => {
normalizeBody(JSON.stringify({ data: "x".repeat(1024) }));
});
});
const sampleBuffer = Buffer.from("request body content");
const sampleUint8Array = new Uint8Array([72, 101, 108, 108, 111]);
const largeStringBody = JSON.stringify({ data: "x".repeat(1024) });
describe("normalizeBody", () => {
bench("null input", () => {
normalizeBody(null);
});
bench("string body", () => {
normalizeBody('{"key":"value"}');
});
bench("Buffer body", () => {
normalizeBody(sampleBuffer);
});
bench("Uint8Array body", () => {
normalizeBody(sampleUint8Array);
});
bench("large string body", () => {
normalizeBody(largeStringBody);
});
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/node/tests/bench/normalize-body.bench.ts
Line: 6-26

Comment:
Fixture data created inside bench callbacks inflates benchmark numbers. `Buffer.from()`, `new Uint8Array()`, `JSON.stringify()`, and especially `"x".repeat(1024)` all run on every iteration, so measured timings include allocation and serialization cost rather than just `normalizeBody` performance. `normalize-pem.bench.ts` correctly hoists fixtures to module level — the same pattern should be applied here.

```suggestion
const sampleBuffer = Buffer.from("request body content");
const sampleUint8Array = new Uint8Array([72, 101, 108, 108, 111]);
const largeStringBody = JSON.stringify({ data: "x".repeat(1024) });

describe("normalizeBody", () => {
  bench("null input", () => {
    normalizeBody(null);
  });

  bench("string body", () => {
    normalizeBody('{"key":"value"}');
  });

  bench("Buffer body", () => {
    normalizeBody(sampleBuffer);
  });

  bench("Uint8Array body", () => {
    normalizeBody(sampleUint8Array);
  });

  bench("large string body", () => {
    normalizeBody(largeStringBody);
  });
});
```

How can I resolve this? If you propose a fix, please make it concise.

70 changes: 70 additions & 0 deletions packages/node/tests/bench/normalize-headers.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: Apache-2.0 OR MIT

import { bench, describe } from "vitest";
import { normalizeHeaders } from "../../export/normalize.ts";

describe("normalizeHeaders", () => {
bench("empty input", () => {
normalizeHeaders();
});

bench("object with string values", () => {
normalizeHeaders({
"Content-Type": "application/json",
Accept: "text/html",
Authorization: "Bearer token123",
});
});

bench("object with array values", () => {
normalizeHeaders({
"Accept-Encoding": ["gzip", "deflate", "br"],
"Cache-Control": ["no-cache", "no-store"],
"X-Custom": ["value1", "value2", "value3"],
});
});

bench("object with many headers", () => {
normalizeHeaders({
"Content-Type": "application/json",
Accept: "text/html",
Authorization: "Bearer token123",
"Accept-Encoding": "gzip",
"Cache-Control": "no-cache",
"X-Request-Id": "abc-123",
"X-Forwarded-For": "127.0.0.1",
"X-Forwarded-Proto": "https",
"User-Agent": "node-reqwest/1.0",
Cookie: "session=abc; theme=dark",
});
});

bench("flat string array pairs", () => {
normalizeHeaders([
"Content-Type",
"application/json",
"Accept",
"text/html",
"Authorization",
"Bearer token123",
]);
});

bench("Map iterable", () => {
const headers = new Map<string, string>([
["Content-Type", "application/json"],
["Accept", "text/html"],
["Authorization", "Bearer token123"],
]);
normalizeHeaders(headers);
});
Comment on lines +53 to +60

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The Map is constructed inside the bench callback, so every iteration measures both Map construction and normalizeHeaders. Hoisting it to module level (like the fixtures in normalize-pem.bench.ts) isolates the function under test.

Suggested change
bench("Map iterable", () => {
const headers = new Map<string, string>([
["Content-Type", "application/json"],
["Accept", "text/html"],
["Authorization", "Bearer token123"],
]);
normalizeHeaders(headers);
});
const sampleMap = new Map<string, string>([
["Content-Type", "application/json"],
["Accept", "text/html"],
["Authorization", "Bearer token123"],
]);
bench("Map iterable", () => {
normalizeHeaders(sampleMap);
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/node/tests/bench/normalize-headers.bench.ts
Line: 53-60

Comment:
The `Map` is constructed inside the bench callback, so every iteration measures both `Map` construction and `normalizeHeaders`. Hoisting it to module level (like the fixtures in `normalize-pem.bench.ts`) isolates the function under test.

```suggestion
const sampleMap = new Map<string, string>([
  ["Content-Type", "application/json"],
  ["Accept", "text/html"],
  ["Authorization", "Bearer token123"],
]);

  bench("Map iterable", () => {
    normalizeHeaders(sampleMap);
  });
```

How can I resolve this? If you propose a fix, please make it concise.


bench("object with mixed value types", () => {
normalizeHeaders({
"Content-Type": "application/json",
"Content-Length": 42,
"X-Optional": undefined,
Accept: ["text/html", "application/json"],
});
});
});
38 changes: 38 additions & 0 deletions packages/node/tests/bench/normalize-pem.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: Apache-2.0 OR MIT

import { bench, describe } from "vitest";
import { normalizePem } from "../../export/normalize.ts";

const samplePem = `-----BEGIN CERTIFICATE-----
MIICpDCCAYwCCQDU+PQ4F6a9WjANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls
b2NhbGhvc3QwHhcNMjQwMTAxMDAwMDAwWhcNMjUwMTAxMDAwMDAwWjAUMRIwEAYD
VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7
-----END CERTIFICATE-----`;

const samplePemBuffer = Buffer.from(samplePem);

describe("normalizePem", () => {
bench("undefined input", () => {
normalizePem();
});

bench("single string", () => {
normalizePem(samplePem);
});

bench("single Buffer", () => {
normalizePem(samplePemBuffer);
});

bench("array of strings", () => {
normalizePem([samplePem, samplePem, samplePem]);
});

bench("array of Buffers", () => {
normalizePem([samplePemBuffer, samplePemBuffer, samplePemBuffer]);
});

bench("mixed array", () => {
normalizePem([samplePem, samplePemBuffer, samplePem, samplePemBuffer]);
});
});
11 changes: 11 additions & 0 deletions packages/node/vitest.bench.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import codspeed from "@codspeed/vitest-plugin";
import { defineConfig } from "vitest/config";

export default defineConfig({
plugins: [codspeed()],
test: {
benchmark: {
include: ["tests/bench/**/*.bench.ts"],
},
},
});
Loading
Loading