From 6bd178b7ab7eea46179e7d41543a2579df5f1807 Mon Sep 17 00:00:00 2001 From: naorpeled Date: Sat, 27 Jun 2026 00:12:07 +0300 Subject: [PATCH 1/3] Add benchmark suite comparing lambda-api to other Lambda frameworks (#34) In-process micro-benchmarks that invoke each framework's aws-lambda handler in the same Node VM with identical synthetic API Gateway (v1 + v2) events, measuring framework overhead with mitata. A correctness gate validates every cell before timing. Frameworks: baseline (raw handler), lambda-api, @vendia/serverless-express, @fastify/aws-lambda, hono. Scenarios: get-json, path-param, post-json, routing-50, not-found. Lives in an isolated benchmarks/ package so the root library keeps its zero-dependency policy; excluded from the npm tarball by the files whitelist. A manual/release GitHub workflow refreshes the README Benchmarks section (marker-delimited, idempotent, with a per-release history row) on each published release. --- .eslintignore | 3 +- .github/workflows/benchmark.yml | 79 + .prettierignore | 3 +- README.md | 66 + benchmarks/.gitignore | 3 + benchmarks/README.md | 132 ++ benchmarks/frameworks/baseline.js | 67 + benchmarks/frameworks/fastify.js | 34 + benchmarks/frameworks/hono.js | 46 + benchmarks/frameworks/lambda-api.js | 32 + benchmarks/frameworks/serverless-express.js | 42 + benchmarks/lib/events.js | 107 ++ benchmarks/lib/scenarios.js | 60 + benchmarks/lib/table.js | 116 ++ benchmarks/lib/update-readme.js | 156 ++ benchmarks/lib/validate.js | 49 + benchmarks/package-lock.json | 1494 +++++++++++++++++++ benchmarks/package.json | 23 + benchmarks/results/.gitkeep | 0 benchmarks/run.js | 178 +++ 20 files changed, 2688 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/benchmark.yml create mode 100644 benchmarks/.gitignore create mode 100644 benchmarks/README.md create mode 100644 benchmarks/frameworks/baseline.js create mode 100644 benchmarks/frameworks/fastify.js create mode 100644 benchmarks/frameworks/hono.js create mode 100644 benchmarks/frameworks/lambda-api.js create mode 100644 benchmarks/frameworks/serverless-express.js create mode 100644 benchmarks/lib/events.js create mode 100644 benchmarks/lib/scenarios.js create mode 100644 benchmarks/lib/table.js create mode 100644 benchmarks/lib/update-readme.js create mode 100644 benchmarks/lib/validate.js create mode 100644 benchmarks/package-lock.json create mode 100644 benchmarks/package.json create mode 100644 benchmarks/results/.gitkeep create mode 100644 benchmarks/run.js diff --git a/.eslintignore b/.eslintignore index 628e08b..adadeb8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,4 +2,5 @@ coverage node_modules __tests__ *.test.js -dist \ No newline at end of file +dist +benchmarks \ No newline at end of file diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 0000000..7214836 --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,79 @@ +name: 'Benchmarks' + +# Perf numbers are noisy on shared runners, so benchmarks never gate PRs. They run on each +# published release (and on demand) to refresh the Benchmarks section of the README. +on: + release: + types: [published] + workflow_dispatch: + inputs: + version: + description: 'Version label to record (defaults to package.json version)' + required: false + type: string + commit: + description: 'Commit the refreshed README back to the default branch' + required: false + default: true + type: boolean + +permissions: + contents: write + +jobs: + benchmark: + name: 'Run benchmarks (Node 20)' + runs-on: ubuntu-latest + steps: + - name: Checkout default branch + uses: actions/checkout@v4 + with: + ref: ${{ github.event.repository.default_branch }} + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Resolve version label + id: ver + run: | + RAW="${{ github.event.release.tag_name || inputs.version }}" + echo "value=${RAW#v}" >> "$GITHUB_OUTPUT" + + - name: Install benchmark dependencies + working-directory: benchmarks + run: npm ci + + - name: Run benchmarks and refresh README + working-directory: benchmarks + env: + LAMBDA_API_VERSION: ${{ steps.ver.outputs.value }} + run: node run.js --md results/RESULTS.md --json results/raw.json --update-readme + + # Keep the committed README Prettier-clean so the push doesn't break main's CI. + - name: Format README + run: npx --yes prettier@2 --write README.md + + - name: Upload raw results + uses: actions/upload-artifact@v4 + with: + name: benchmark-results + path: benchmarks/results/ + + - name: Add results to job summary + run: cat benchmarks/results/RESULTS.md >> "$GITHUB_STEP_SUMMARY" + + - name: Commit refreshed README + if: ${{ github.event_name == 'release' || inputs.commit }} + run: | + if git diff --quiet -- README.md; then + echo 'README.md unchanged — nothing to commit' + exit 0 + fi + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + git add README.md + git commit -m 'docs: update benchmark results [skip ci]' + git push origin HEAD:${{ github.event.repository.default_branch }} diff --git a/.prettierignore b/.prettierignore index 628e08b..adadeb8 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,4 +2,5 @@ coverage node_modules __tests__ *.test.js -dist \ No newline at end of file +dist +benchmarks \ No newline at end of file diff --git a/README.md b/README.md index 5fe5b88..706e58d 100644 --- a/README.md +++ b/README.md @@ -1523,3 +1523,69 @@ Contributions, ideas and bug reports are welcome and greatly appreciated. Please ## Are you using Lambda API? If you're using Lambda API and finding it useful, hit me up on [Twitter](https://twitter.com/jeremy_daly) or email me at contact[at]jeremydaly.com. I'd love to hear your stories, ideas, and even your complaints! + + + +## Benchmarks + +In-process micro-benchmarks of lambda-api against other AWS Lambda web frameworks. The numbers measure **framework overhead only** (event → route → middleware → response, in a single Node VM) — not end-to-end Lambda timings. Absolute values vary by machine, so compare the **relative** ranking rather than the raw ops/sec. See [`benchmarks/`](./benchmarks) for the methodology and how to reproduce. + +_Generated 2026-06-26 21:07:24 UTC · lambda-api v0.0.0-development · Node 20.19.5 · Apple M4 Max (16 cores) · darwin/arm64_ + +#### API Gateway REST (v1) — throughput (ops/sec, higher is better) + +| Framework | get-json | path-param | post-json | routing-50 | not-found | +| ------------------------------ | --------- | ---------- | --------- | ---------- | --------- | +| baseline | 5,765,495 | 5,514,860 | 2,561,648 | 4,530,735 | 4,646,303 | +| lambda-api `0.0.0-development` | 215,921 | 200,518 | 191,027 | 198,443 | 81,754 | +| fastify `5.8.5` | 54,972 | 52,420 | 13,758 | 56,234 | 49,800 | +| hono `4.12.27` | 48,833 | 55,054 | 36,955 | 59,643 | 61,384 | +| serverless-express `4.22.2` | 25,569 | 27,466 | 15,432 | 25,636 | 27,325 | + +
API Gateway REST (v1) — latency (avg / p99, µs, lower is better) + +| Framework | get-json | path-param | post-json | routing-50 | not-found | +| ------------------------------ | ----------- | ----------- | ----------- | ----------- | ----------- | +| baseline | 0.17 / 0.22 | 0.18 / 0.25 | 0.39 / 0.50 | 0.22 / 0.28 | 0.22 / 0.27 | +| lambda-api `0.0.0-development` | 4.63 / 4.84 | 4.99 / 5.24 | 5.23 / 5.80 | 5.04 / 5.25 | 12.2 / 12.3 | +| fastify `5.8.5` | 18.2 / 68.8 | 19.1 / 20.7 | 72.7 / 151 | 17.8 / 23.4 | 20.1 / 19.7 | +| hono `4.12.27` | 20.5 / 118 | 18.2 / 20.8 | 27.1 / 27.4 | 16.8 / 18.0 | 16.3 / 17.5 | +| serverless-express `4.22.2` | 39.1 / 126 | 36.4 / 38.2 | 64.8 / 194 | 39.0 / 103 | 36.6 / 37.7 | + +
+ +#### API Gateway HTTP (v2) — throughput (ops/sec, higher is better) + +| Framework | get-json | path-param | post-json | routing-50 | not-found | +| ------------------------------ | --------- | ---------- | --------- | ---------- | --------- | +| baseline | 4,838,095 | 4,425,059 | 2,283,297 | 3,864,414 | 4,039,412 | +| lambda-api `0.0.0-development` | 196,317 | 190,996 | 185,950 | 187,732 | 79,295 | +| hono `4.12.27` | 70,609 | 67,280 | 41,784 | 67,375 | 68,399 | +| fastify `5.8.5` | 44,946 | 39,743 | 14,657 | 56,339 | 39,648 | +| serverless-express `4.22.2` | 27,309 | 24,706 | 17,450 | 25,505 | 28,701 | + +
API Gateway HTTP (v2) — latency (avg / p99, µs, lower is better) + +| Framework | get-json | path-param | post-json | routing-50 | not-found | +| ------------------------------ | ----------- | ----------- | ----------- | ----------- | ----------- | +| baseline | 0.21 / 0.44 | 0.23 / 0.42 | 0.44 / 0.69 | 0.26 / 0.47 | 0.25 / 0.50 | +| lambda-api `0.0.0-development` | 5.09 / 5.28 | 5.24 / 5.41 | 5.38 / 5.56 | 5.33 / 5.62 | 12.6 / 12.8 | +| hono `4.12.27` | 14.2 / 15.2 | 14.9 / 16.3 | 23.9 / 25.6 | 14.8 / 15.9 | 14.6 / 15.9 | +| fastify `5.8.5` | 22.2 / 23.4 | 25.2 / 30.4 | 68.2 / 130 | 17.7 / 23.0 | 25.2 / 28.2 | +| serverless-express `4.22.2` | 36.6 / 39.2 | 40.5 / 53.5 | 57.3 / 58.8 | 39.2 / 40.7 | 34.8 / 37.0 | + +
+ +#### History + +Throughput for the `get-json` scenario on V2 events (ops/sec), one row per release: + + + +| version | date | node | baseline | lambda-api | fastify | hono | express | +| ----------------- | ---------- | ------- | --------- | ---------- | ------- | ------ | ------- | +| 0.0.0-development | 2026-06-26 | 20.19.5 | 4,838,095 | 196,317 | 44,946 | 70,609 | 27,309 | + + + + diff --git a/benchmarks/.gitignore b/benchmarks/.gitignore new file mode 100644 index 0000000..219bdb2 --- /dev/null +++ b/benchmarks/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +results/*.json +results/*.md diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 0000000..bc43e16 --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,132 @@ +# lambda-api benchmarks + +Benchmark suite for [issue #34](https://github.com/jeremydaly/lambda-api/issues/34): +a repeatable comparison of **lambda-api** against other popular frameworks running on AWS +Lambda. + +This is an isolated package — its (heavy) comparison-framework dependencies live here and +never touch the zero-dependency root library. It is excluded from the published npm tarball +by the root `package.json` `files` whitelist. + +```bash +cd benchmarks +npm install +npm run bench # print results to stdout +npm run bench:md # also write results/RESULTS.md +npm run bench:release # write md + json and refresh the README Benchmarks section +``` + +## What this measures + +Each framework's compiled `aws-lambda` handler is invoked **in-process** — in the same Node +VM — with identical synthetic API Gateway events. We measure the **framework overhead** of a +request: parse the event → match a route → run middleware → serialize the response. That is +the work the maintainer clocked at ~0.68 ms per request, and the thing this suite resolves. + +For every `(framework × scenario × event format)` cell, the handler is first run once and its +response is checked by a **correctness gate** (`lib/validate.js`) — status code, and body +fields where applicable. Only cells that pass are timed, so we never publish a number for a +framework that is silently 404-ing, throwing, or returning the wrong shape. + +Measurement uses [mitata](https://github.com/evanwashere/mitata), which calibrates clock +overhead, warms up the JIT, and auto-tunes iteration counts for sub-microsecond accuracy. + +## Why in-process, and not LocalStack or a real Lambda deploy + +The signal we care about is sub-millisecond. LocalStack and real Lambda wrap that signal in: + +- Docker / Firecracker container startup and the Node runtime bootstrap (cold start), +- the Lambda Runtime API request loop, +- network latency to the function URL / API Gateway. + +That is **tens of milliseconds of noise** — 10–100× the thing we are trying to compare. Worse, +LocalStack would be benchmarking the _emulator_, not the framework, and shared CI runners make +absolute numbers non-reproducible. So the in-process harness is the right primary tool: it +isolates framework overhead, is deterministic, needs no Docker, and runs in seconds. + +End-to-end Lambda timings (cold/warm `Duration`, `Init Duration`) are a **different, valid** +question — see the [real-Lambda appendix](#appendix-end-to-end-real-lambda-numbers) below — but +they answer "how fast is my whole deployment", not "how much overhead does the framework add". + +## Frameworks compared + +| key | package | adapter | +| -------------------- | --------------------------------------- | ---------------------- | +| `baseline` | — (hand-written handler) | none — the lower bound | +| `lambda-api` | this repo (loaded from the working tree)| native `api.run()` | +| `serverless-express` | `express` + `@vendia/serverless-express`| `serverlessExpress()` | +| `fastify` | `fastify` + `@fastify/aws-lambda` | `awsLambdaFastify()` | +| `hono` | `hono` | `hono/aws-lambda` | + +`baseline` is a raw handler with zero routing abstraction; every other framework's overhead is +read as the gap above it. + +## Scenarios + +Each framework registers the **same** canonical routes, configured as minimally and equally as +possible (JSON responses, no extra middleware). All scenarios run against both **API Gateway +REST (v1)** and **HTTP API (v2)** events. + +| id | request | checks | +| ------------ | ------------------------------------ | ----------------- | +| `get-json` | `GET /` | 200, `{hello}` | +| `path-param` | `GET /users/42` | 200, `{id:'42'}` | +| `post-json` | `POST /users` with a JSON body | 200, echoes body | +| `routing-50` | `GET /r49/x` (50 routes registered) | 200 — routing cost| +| `not-found` | `GET /does-not-exist` | 404 | + +## Fairness notes + +- Frameworks are configured minimally and equivalently — the goal is to compare core overhead, + not feature sets. Defaults still differ (e.g. lambda-api computes an `ETag` and handles + serialization itself; Express needs an explicit `express.json()` and a 404 handler), and those + differences are part of what the numbers reflect. Read the `frameworks/*.js` to see exactly how + each is set up. +- Absolute ops/sec depend on the machine. **Compare the relative ranking**, not raw numbers. +- All frameworks run in one process by default. Pass `--framework ` to run a single one in + its own process — used for published numbers — to avoid cross-framework JIT interference. + +## CLI + +``` +node run.js [options] + + --framework run a single framework (baseline | lambda-api | serverless-express + | fastify | hono); also gives clean per-process JIT isolation + --md write the markdown report to a file (relative to benchmarks/) + --json dump raw per-cell stats as JSON + --update-readme refresh the Benchmarks section in ../README.md (full run only) +``` + +The version label recorded in the README history can be overridden with the +`LAMBDA_API_VERSION` environment variable (the release workflow sets this from the release tag). + +## How the README stays up to date + +`.github/workflows/benchmark.yml` runs on every published release (and via manual dispatch). It +runs the suite on Node 20, calls `--update-readme`, Prettier-formats the README, commits it back +to the default branch, and uploads `results/` as an artifact. The Benchmarks section lives +between `` / `` markers and is regenerated in +place, with one history row appended per release. Move that marker block anywhere in the README +once; future runs respect its position. + +## Adding a framework + +1. Create `frameworks/.js` exporting `{ name, version, build }`, where `build()` returns + (or resolves to) an async `(event, context) => apiGatewayResponse` handler with the canonical + routes registered. Use `await import()` for ESM-only packages (see `frameworks/hono.js`). +2. Add `` to `ALL_FRAMEWORKS` in `run.js`. +3. Add its dependency to `package.json` and re-run `npm install`. + +## Appendix: end-to-end (real Lambda) numbers + +To measure cold/warm start and total billed duration — which include infrastructure, not just +framework overhead — deploy each `frameworks/*.js` handler behind API Gateway with AWS SAM or the +Serverless Framework, then: + +- drive warm throughput with a load tool (e.g. `autocannon` / `artillery`) against the API URL, or +- loop `aws lambda invoke` for controlled single invocations, +- and read `Duration` / `Init Duration` from the CloudWatch `REPORT` log lines. + +These are deliberately **not** part of the in-process suite: they answer a different question and +are not reproducible on shared CI. Label any such results clearly as end-to-end, infra-inclusive. diff --git a/benchmarks/frameworks/baseline.js b/benchmarks/frameworks/baseline.js new file mode 100644 index 0000000..7bcac97 --- /dev/null +++ b/benchmarks/frameworks/baseline.js @@ -0,0 +1,67 @@ +'use strict'; + +/** + * Baseline: a hand-written Lambda handler with zero framework abstraction. + * + * Establishes the theoretical lower bound for request handling — event parse, a manual + * route match, JSON serialize. Every framework's overhead is meaningfully read as the gap + * above this floor. + * + * @author Benchmark suite for lambda-api (issue #34) + * @license MIT + */ + +const { ROUTE_COUNT } = require('../lib/scenarios'); + +function pathOf(event) { + return event.rawPath || event.path; +} + +function methodOf(event) { + return event.httpMethod || (event.requestContext && event.requestContext.http && event.requestContext.http.method); +} + +// Emit the response shape lambda-api / the adapters produce for each event format. +function reply(event, statusCode, payload) { + const body = JSON.stringify(payload); + if (event.version === '2.0') { + return { statusCode, headers: { 'content-type': 'application/json' }, body, isBase64Encoded: false }; + } + return { + statusCode, + multiValueHeaders: { 'content-type': ['application/json'] }, + body, + isBase64Encoded: false + }; +} + +const USER_RE = /^\/users\/([^/]+)$/; +const ROUTE_RE = /^\/r(\d+)\/([^/]+)$/; + +function build() { + return async (event) => { + const path = pathOf(event); + const method = methodOf(event); + + if (method === 'GET' && path === '/') return reply(event, 200, { hello: 'world' }); + + if (method === 'POST' && path === '/users') { + const parsed = event.body ? JSON.parse(event.body) : {}; + return reply(event, 200, { created: parsed }); + } + + let match; + if (method === 'GET' && (match = USER_RE.exec(path))) { + return reply(event, 200, { id: match[1] }); + } + + if (method === 'GET' && (match = ROUTE_RE.exec(path))) { + const i = Number(match[1]); + if (i >= 0 && i < ROUTE_COUNT) return reply(event, 200, { i }); + } + + return reply(event, 404, { error: 'Not Found' }); + }; +} + +module.exports = { name: 'baseline', version: '-', build }; diff --git a/benchmarks/frameworks/fastify.js b/benchmarks/frameworks/fastify.js new file mode 100644 index 0000000..a0ef78f --- /dev/null +++ b/benchmarks/frameworks/fastify.js @@ -0,0 +1,34 @@ +'use strict'; + +/** + * Fastify (via @fastify/aws-lambda) adapter for the benchmark suite. + * + * @author Benchmark suite for lambda-api (issue #34) + * @license MIT + */ + +const Fastify = require('fastify'); +const awsLambdaFastify = require('@fastify/aws-lambda'); +const pkg = require('fastify/package.json'); +const { ROUTE_COUNT } = require('../lib/scenarios'); + +async function build() { + const app = Fastify(); + + app.get('/', async () => ({ hello: 'world' })); + app.get('/users/:id', async (req) => ({ id: req.params.id })); + app.post('/users', async (req) => ({ created: req.body })); + + for (let i = 0; i < ROUTE_COUNT; i++) { + const index = i; + app.get(`/r${i}/:p`, async () => ({ i: index })); + } + + const proxy = awsLambdaFastify(app); + // Build the route tree before timing so the first measured call isn't paying init cost. + await app.ready(); + + return (event, context) => proxy(event, context); +} + +module.exports = { name: 'fastify', version: pkg.version, build }; diff --git a/benchmarks/frameworks/hono.js b/benchmarks/frameworks/hono.js new file mode 100644 index 0000000..008fdf8 --- /dev/null +++ b/benchmarks/frameworks/hono.js @@ -0,0 +1,46 @@ +'use strict'; + +/** + * Hono (via hono/aws-lambda) adapter for the benchmark suite. + * + * Hono ships as ESM-only, so it is loaded through a dynamic import() inside an async + * build() — keeping the rest of the suite plain CommonJS. The runner awaits build(). + * + * @author Benchmark suite for lambda-api (issue #34) + * @license MIT + */ + +const fs = require('fs'); +const path = require('path'); +const { ROUTE_COUNT } = require('../lib/scenarios'); + +// hono's `exports` map blocks `require('hono/package.json')`, so read it from disk directly. +function honoVersion() { + try { + const pkgPath = path.join(__dirname, '..', 'node_modules', 'hono', 'package.json'); + return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version; + } catch (err) { + return 'unknown'; + } +} + +async function build() { + const { Hono } = await import('hono'); + const { handle } = await import('hono/aws-lambda'); + + const app = new Hono(); + + app.get('/', (c) => c.json({ hello: 'world' })); + app.get('/users/:id', (c) => c.json({ id: c.req.param('id') })); + app.post('/users', async (c) => c.json({ created: await c.req.json() })); + + for (let i = 0; i < ROUTE_COUNT; i++) { + const index = i; + app.get(`/r${i}/:p`, (c) => c.json({ i: index })); + } + + const handler = handle(app); + return (event, context) => handler(event, context); +} + +module.exports = { name: 'hono', version: honoVersion(), build }; diff --git a/benchmarks/frameworks/lambda-api.js b/benchmarks/frameworks/lambda-api.js new file mode 100644 index 0000000..7ea1429 --- /dev/null +++ b/benchmarks/frameworks/lambda-api.js @@ -0,0 +1,32 @@ +'use strict'; + +/** + * lambda-api adapter for the benchmark suite. + * + * Loaded directly from the repository working tree (../../), so `npm run bench` always + * measures the local source — exactly what a maintainer iterating on performance wants. + * + * @author Benchmark suite for lambda-api (issue #34) + * @license MIT + */ + +const createAPI = require('../../'); +const pkg = require('../../package.json'); +const { ROUTE_COUNT } = require('../lib/scenarios'); + +function build() { + const api = createAPI(); + + api.get('/', (req, res) => res.json({ hello: 'world' })); + api.get('/users/:id', (req, res) => res.json({ id: req.params.id })); + api.post('/users', (req, res) => res.json({ created: req.body })); + + for (let i = 0; i < ROUTE_COUNT; i++) { + const index = i; + api.get(`/r${i}/:p`, (req, res) => res.json({ i: index })); + } + + return (event, context) => api.run(event, context); +} + +module.exports = { name: 'lambda-api', version: pkg.version, build }; diff --git a/benchmarks/frameworks/serverless-express.js b/benchmarks/frameworks/serverless-express.js new file mode 100644 index 0000000..5adb9b0 --- /dev/null +++ b/benchmarks/frameworks/serverless-express.js @@ -0,0 +1,42 @@ +'use strict'; + +/** + * Express (via @vendia/serverless-express) adapter for the benchmark suite. + * + * Represents the common "port my Express app to Lambda" path. Configured minimally — + * only the JSON body parser middleware — to keep the comparison fair. + * + * @author Benchmark suite for lambda-api (issue #34) + * @license MIT + */ + +const express = require('express'); +const serverlessExpress = require('@vendia/serverless-express'); +const pkg = require('express/package.json'); +const { ROUTE_COUNT } = require('../lib/scenarios'); + +function build() { + const app = express(); + app.use(express.json()); + + app.get('/', (req, res) => res.json({ hello: 'world' })); + app.get('/users/:id', (req, res) => res.json({ id: req.params.id })); + app.post('/users', (req, res) => res.json({ created: req.body })); + + for (let i = 0; i < ROUTE_COUNT; i++) { + const index = i; + app.get(`/r${i}/:p`, (req, res) => res.json({ i: index })); + } + + // Explicit 404 — every Express-on-Lambda app needs one, and it keeps unmatched routes off + // Express's finalhandler path, which `on-finished` can't attach to serverless-express's + // mock socket (it would otherwise 500). Mirrors the built-in 404 of the other frameworks. + app.use((req, res) => res.status(404).json({ error: 'Not Found' })); + + const handler = serverlessExpress({ app }); + + // @vendia/serverless-express v4 returns a promise when invoked without a callback. + return (event, context) => handler(event, context); +} + +module.exports = { name: 'serverless-express', version: pkg.version, build }; diff --git a/benchmarks/lib/events.js b/benchmarks/lib/events.js new file mode 100644 index 0000000..c52a5c8 --- /dev/null +++ b/benchmarks/lib/events.js @@ -0,0 +1,107 @@ +'use strict'; + +/** + * Synthetic API Gateway event builders for the benchmark suite. + * + * Seeded from the real shapes in __tests__/sample-event-apigateway-v{1,2}.json but + * trimmed to the fields that lambda-api and the comparison adapters actually read, so + * every framework performs equivalent work. The method / path / body are overridden per + * scenario; everything else is realistic boilerplate. + * + * @author Benchmark suite for lambda-api (issue #34) + * @license MIT + */ + +const V1_HEADERS = { + 'content-type': 'application/json', + accept: 'application/json', + host: 'wt6mne2s9k.execute-api.us-west-2.amazonaws.com', + 'user-agent': 'lambda-api-benchmarks', + 'x-forwarded-for': '192.168.100.1', + 'x-forwarded-port': '443', + 'x-forwarded-proto': 'https' +}; + +const V2_HEADERS = { + 'content-type': 'application/json', + accept: 'application/json', + host: 'id.execute-api.us-east-1.amazonaws.com', + 'user-agent': 'lambda-api-benchmarks', + 'x-forwarded-for': '192.168.100.1', + 'x-forwarded-port': '443', + 'x-forwarded-proto': 'https' +}; + +function serializeBody(body) { + if (body === null || body === undefined) return null; + return typeof body === 'string' ? body : JSON.stringify(body); +} + +/** + * Build an API Gateway REST (v1 / proxy) event. + * @param {{ method?: string, path?: string, body?: any }} opts + */ +function apiGatewayV1({ method = 'GET', path = '/', body = null } = {}) { + const multiValueHeaders = {}; + for (const key of Object.keys(V1_HEADERS)) multiValueHeaders[key] = [V1_HEADERS[key]]; + + return { + resource: path, + path, + httpMethod: method, + headers: { ...V1_HEADERS }, + multiValueHeaders, + queryStringParameters: null, + multiValueQueryStringParameters: null, + pathParameters: null, + stageVariables: null, + requestContext: { + accountId: '123456789012', + stage: 'test', + httpMethod: method, + path, + resourcePath: path, + identity: { sourceIp: '192.168.100.12' }, + requestId: 'bench-request-id', + apiId: 'wt6mne2s9k' + }, + body: serializeBody(body), + isBase64Encoded: false + }; +} + +/** + * Build an API Gateway HTTP (v2) event. + * @param {{ method?: string, path?: string, body?: any }} opts + */ +function apiGatewayV2({ method = 'GET', path = '/', body = null } = {}) { + return { + version: '2.0', + routeKey: '$default', + rawPath: path, + rawQueryString: '', + headers: { ...V2_HEADERS }, + requestContext: { + accountId: '123456789012', + apiId: 'api-id', + domainName: 'id.execute-api.us-east-1.amazonaws.com', + domainPrefix: 'id', + http: { + method, + path, + protocol: 'HTTP/1.1', + sourceIp: '192.168.100.12', + userAgent: 'lambda-api-benchmarks' + }, + requestId: 'bench-request-id', + routeKey: '$default', + stage: '$default', + time: '12/Mar/2020:19:03:58 +0000', + timeEpoch: 1583348638390 + }, + body: serializeBody(body), + isBase64Encoded: false + }; +} + +module.exports = { apiGatewayV1, apiGatewayV2 }; diff --git a/benchmarks/lib/scenarios.js b/benchmarks/lib/scenarios.js new file mode 100644 index 0000000..1619b96 --- /dev/null +++ b/benchmarks/lib/scenarios.js @@ -0,0 +1,60 @@ +'use strict'; + +/** + * Shared benchmark scenarios. Every framework registers the same canonical routes + * (see frameworks/*.js) and is exercised with the same set of requests, against both + * API Gateway v1 and v2 events. + * + * `expect` is the correctness gate (lib/validate.js): a handler must produce this exact + * status (and matching body fields, when given) before it is timed — so we never publish + * numbers for a framework that is silently 404-ing, throwing, or returning the wrong shape. + * + * The 50 `r{i}/:p` routes (registered by each framework) make `routing-50` a meaningful + * test of routing cost as the route table grows. + * + * @author Benchmark suite for lambda-api (issue #34) + * @license MIT + */ + +const ROUTE_COUNT = 50; + +const scenarios = [ + { + id: 'get-json', + description: 'simplest GET returning a small JSON object', + method: 'GET', + path: '/', + expect: { status: 200, body: { hello: 'world' } } + }, + { + id: 'path-param', + description: 'GET with a single path parameter', + method: 'GET', + path: '/users/42', + expect: { status: 200, body: { id: '42' } } + }, + { + id: 'post-json', + description: 'POST that parses a JSON body and echoes it', + method: 'POST', + path: '/users', + body: { name: 'ada' }, + expect: { status: 200, body: { created: { name: 'ada' } } } + }, + { + id: 'routing-50', + description: 'routing cost with 50 registered routes (hits the last one)', + method: 'GET', + path: '/r' + (ROUTE_COUNT - 1) + '/x', + expect: { status: 200, body: { i: ROUTE_COUNT - 1 } } + }, + { + id: 'not-found', + description: 'unmatched route returns 404', + method: 'GET', + path: '/does-not-exist', + expect: { status: 404 } + } +]; + +module.exports = { scenarios, ROUTE_COUNT }; diff --git a/benchmarks/lib/table.js b/benchmarks/lib/table.js new file mode 100644 index 0000000..c195fa6 --- /dev/null +++ b/benchmarks/lib/table.js @@ -0,0 +1,116 @@ +'use strict'; + +/** + * Renders benchmark results into GitHub-flavored markdown. + * + * Layout: one throughput table per event format (rows = framework, columns = scenario), + * each followed by a collapsible latency table (avg / p99). Rows are sorted by the + * `get-json` throughput, descending. + * + * @author Benchmark suite for lambda-api (issue #34) + * @license MIT + */ + +const { scenarios } = require('./scenarios'); + +const FORMAT_LABELS = { + v1: 'API Gateway REST (v1)', + v2: 'API Gateway HTTP (v2)' +}; + +function opsPerSec(stats) { + return stats && stats.avg ? 1e9 / stats.avg : 0; +} + +function fmtOps(n) { + if (!n) return 'n/a'; + return Math.round(n).toLocaleString('en-US'); +} + +function fmtUs(ns) { + if (!ns) return 'n/a'; + const us = ns / 1000; + if (us >= 100) return us.toFixed(0); + if (us >= 10) return us.toFixed(1); + return us.toFixed(2); +} + +function cell(results, framework, format, scenarioId) { + return results.find( + (r) => r.framework === framework && r.format === format && r.scenario === scenarioId + ); +} + +// Unique framework metadata ({ name, version }) in the order first seen. +function frameworkMeta(results) { + const seen = new Map(); + for (const r of results) { + if (!seen.has(r.framework)) seen.set(r.framework, { name: r.framework, version: r.version }); + } + return [...seen.values()]; +} + +function label(meta) { + return meta.version && meta.version !== '-' ? `${meta.name} \`${meta.version}\`` : meta.name; +} + +function toMarkdown(header, rows) { + const lines = []; + lines.push(`| ${header.join(' | ')} |`); + lines.push(`| ${header.map(() => '---').join(' | ')} |`); + for (const row of rows) lines.push(`| ${row.join(' | ')} |`); + return lines.join('\n'); +} + +function buildTable(results, frameworks, format, render) { + const ordered = [...frameworks].sort((a, b) => { + const ca = cell(results, a.name, format, 'get-json'); + const cb = cell(results, b.name, format, 'get-json'); + return opsPerSec(cb && cb.stats) - opsPerSec(ca && ca.stats); + }); + + const header = ['Framework', ...scenarios.map((s) => s.id)]; + const rows = ordered.map((meta) => { + const cells = scenarios.map((s) => { + const c = cell(results, meta.name, format, s.id); + return c && c.ok && c.stats ? render(c.stats) : 'n/a'; + }); + return [label(meta), ...cells]; + }); + return toMarkdown(header, rows); +} + +function envHeader(env) { + return ( + `_Generated ${env.date} · lambda-api v${env.lambdaApiVersion} · ` + + `Node ${env.node} · ${env.cpu} · ${env.platform}/${env.arch}_` + ); +} + +/** + * @param {Array} results - the runner's result rows + * @param {object} env - environment metadata from the runner + * @param {string[]} formats - event format ids to render (default ['v1','v2']) + * @returns {string} markdown + */ +function renderTables(results, env, formats = ['v1', 'v2']) { + const frameworks = frameworkMeta(results); + const out = [envHeader(env), '']; + + for (const format of formats) { + out.push(`#### ${FORMAT_LABELS[format] || format} — throughput (ops/sec, higher is better)`); + out.push(''); + out.push(buildTable(results, frameworks, format, (s) => fmtOps(opsPerSec(s)))); + out.push(''); + out.push(`
${FORMAT_LABELS[format] || format} — latency (avg / p99, µs, lower is better)`); + out.push(''); + out.push(buildTable(results, frameworks, format, (s) => `${fmtUs(s.avg)} / ${fmtUs(s.p99)}`)); + out.push(''); + out.push('
'); + out.push(''); + } + + return out.join('\n').trim() + '\n'; +} + +module.exports = { renderTables, opsPerSec, fmtOps, fmtUs, FORMAT_LABELS }; diff --git a/benchmarks/lib/update-readme.js b/benchmarks/lib/update-readme.js new file mode 100644 index 0000000..0b9fb21 --- /dev/null +++ b/benchmarks/lib/update-readme.js @@ -0,0 +1,156 @@ +'use strict'; + +/** + * Idempotently writes the Benchmarks section into the root README.md. + * + * The whole section lives between BENCHMARKS:START / BENCHMARKS:END markers and is + * regenerated in place on every run (so the README — already large — never accumulates + * duplicate sections). Inside it, a compact History table keeps one row per lambda-api + * version: a new version appends a row; re-running for the same version replaces its row. + * If the markers are absent, the section is appended once at the end of the file. + * + * @author Benchmark suite for lambda-api (issue #34) + * @license MIT + */ + +const fs = require('fs'); +const { renderTables, opsPerSec, fmtOps } = require('./table'); + +const START = ''; +const END = ''; +const H_START = ''; +const H_END = ''; + +// Representative cell used for the compact history table. +const HISTORY_FORMAT = 'v2'; +const HISTORY_SCENARIO = 'get-json'; +const HISTORY_FRAMEWORKS = [ + { name: 'baseline', header: 'baseline' }, + { name: 'lambda-api', header: 'lambda-api' }, + { name: 'fastify', header: 'fastify' }, + { name: 'hono', header: 'hono' }, + { name: 'serverless-express', header: 'express' } +]; + +function rowCells(line) { + return line + .split('|') + .slice(1, -1) + .map((c) => c.trim()); +} + +function stripVersion(v) { + return String(v).replace(/`/g, '').trim(); +} + +function extractHistoryRows(readme) { + const s = readme.indexOf(H_START); + const e = readme.indexOf(H_END); + if (s === -1 || e === -1 || e < s) return []; + const block = readme.slice(s + H_START.length, e); + const lines = block + .split('\n') + .map((l) => l.trim()) + .filter((l) => l.startsWith('|')); + // [0] = header, [1] = separator, rest = data rows + return lines + .slice(2) + .filter((l) => !/^\|\s*-/.test(l)) + .map(rowCells); +} + +function opsFor(results, framework) { + const c = results.find( + (r) => r.framework === framework && r.format === HISTORY_FORMAT && r.scenario === HISTORY_SCENARIO + ); + return c && c.ok && c.stats ? fmtOps(opsPerSec(c.stats)) : 'n/a'; +} + +function buildHistoryRow(results, env) { + return [ + env.lambdaApiVersion, + env.dateShort, + env.node, + ...HISTORY_FRAMEWORKS.map((f) => opsFor(results, f.name)) + ]; +} + +function renderHistoryTable(prevRows, current) { + const header = ['version', 'date', 'node', ...HISTORY_FRAMEWORKS.map((f) => f.header)]; + const rows = []; + let replaced = false; + for (const r of prevRows) { + if (stripVersion(r[0]) === stripVersion(current[0])) { + rows.push(current); + replaced = true; + } else { + rows.push(r); + } + } + if (!replaced) rows.push(current); + + const lines = [ + H_START, + `| ${header.join(' | ')} |`, + `| ${header.map(() => '---').join(' | ')} |`, + ...rows.map((r) => `| ${r.join(' | ')} |`), + H_END + ]; + return lines.join('\n'); +} + +function buildSection(results, env, historyTable) { + return [ + START, + '## Benchmarks', + '', + 'In-process micro-benchmarks of lambda-api against other AWS Lambda web frameworks. ' + + 'The numbers measure **framework overhead only** (event → route → middleware → response, ' + + 'in a single Node VM) — not end-to-end Lambda timings. Absolute values vary by machine, so ' + + 'compare the **relative** ranking rather than the raw ops/sec. See ' + + '[`benchmarks/`](./benchmarks) for the methodology and how to reproduce.', + '', + renderTables(results, env).trim(), + '', + '#### History', + '', + `Throughput for the \`${HISTORY_SCENARIO}\` scenario on ${HISTORY_FORMAT.toUpperCase()} events (ops/sec), one row per release:`, + '', + historyTable, + '', + END + ].join('\n'); +} + +/** + * @param {{ readmePath: string, results: Array, env: object }} args + * @returns {{ changed: boolean, action: 'replaced'|'appended' }} + */ +function updateReadme({ readmePath, results, env }) { + const existing = fs.existsSync(readmePath) ? fs.readFileSync(readmePath, 'utf8') : ''; + + const prevRows = extractHistoryRows(existing); + const current = buildHistoryRow(results, env); + const historyTable = renderHistoryTable(prevRows, current); + const section = buildSection(results, env, historyTable); + + const s = existing.indexOf(START); + const e = existing.indexOf(END); + + let next; + let action; + if (s !== -1 && e !== -1 && e > s) { + next = existing.slice(0, s) + section + existing.slice(e + END.length); + action = 'replaced'; + } else { + const prefix = existing.length ? existing.replace(/\s*$/, '') + '\n\n' : ''; + next = prefix + section + '\n'; + action = 'appended'; + } + + const changed = next !== existing; + if (changed) fs.writeFileSync(readmePath, next); + return { changed, action }; +} + +module.exports = { updateReadme, START, END }; diff --git a/benchmarks/lib/validate.js b/benchmarks/lib/validate.js new file mode 100644 index 0000000..0f83324 --- /dev/null +++ b/benchmarks/lib/validate.js @@ -0,0 +1,49 @@ +'use strict'; + +/** + * Correctness gate. Runs once per (framework, scenario, event format) BEFORE timing. + * + * Guarantees we never benchmark a handler that is silently returning the wrong status, + * throwing, or emitting the wrong body — a classic way framework comparisons become + * dishonest. Throws a descriptive Error on any mismatch; the runner catches it, marks + * that cell as failed, and excludes it from the results (rather than crashing the suite). + * + * @author Benchmark suite for lambda-api (issue #34) + * @license MIT + */ + +/** + * @param {object} response - the Lambda proxy response returned by the handler + * @param {{ status: number, body?: object }} expect - expected status and (optional) body fields + * @param {string} ctx - "framework/format/scenario" label for error messages + */ +function validate(response, expect, ctx) { + if (!response || typeof response !== 'object') { + throw new Error(`${ctx}: handler returned ${typeof response}, expected a response object`); + } + + if (response.statusCode !== expect.status) { + throw new Error(`${ctx}: expected statusCode ${expect.status}, got ${response.statusCode}`); + } + + if (expect.body) { + let parsed; + try { + parsed = JSON.parse(response.body); + } catch (err) { + throw new Error(`${ctx}: response body is not valid JSON (${err.message}): ${response.body}`); + } + + for (const key of Object.keys(expect.body)) { + const got = JSON.stringify(parsed[key]); + const want = JSON.stringify(expect.body[key]); + if (got !== want) { + throw new Error(`${ctx}: body.${key} expected ${want}, got ${got}`); + } + } + } + + return true; +} + +module.exports = validate; diff --git a/benchmarks/package-lock.json b/benchmarks/package-lock.json new file mode 100644 index 0000000..5dabca6 --- /dev/null +++ b/benchmarks/package-lock.json @@ -0,0 +1,1494 @@ +{ + "name": "lambda-api-benchmarks", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lambda-api-benchmarks", + "version": "0.0.0", + "dependencies": { + "@fastify/aws-lambda": "^6.4.0", + "@vendia/serverless-express": "^4.12.6", + "express": "^4.21.2", + "fastify": "^5.8.5", + "hono": "^4.12.27", + "mitata": "^1.0.34" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@codegenie/serverless-express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@codegenie/serverless-express/-/serverless-express-4.17.1.tgz", + "integrity": "sha512-B/4RRtVK2iAp5ho+qoUFxUeMaWCgSP+hNrdLJV3DWKZ1E9n9oLKdsJ6W9LQekZsD8rGD15hDadIKWKKw5i3f3A==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", + "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/aws-lambda": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@fastify/aws-lambda/-/aws-lambda-6.4.0.tgz", + "integrity": "sha512-7iCx4Bl+SoeeRP0ALv1Rg4AR63yBW8XxgQiQ4OrTJApUObMzMtN1VPFtnoAqGhtpruAeVUTxgGpsW1hasYxU8Q==", + "license": "MIT" + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz", + "integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^6.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", + "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@vendia/serverless-express": { + "version": "4.12.6", + "resolved": "https://registry.npmjs.org/@vendia/serverless-express/-/serverless-express-4.12.6.tgz", + "integrity": "sha512-ePsIPk3VQwgm5nh/JGBtTKQs5ZOF7REjHxC+PKk/CHvhlKQkJuUU365uPOlxuLJhC+BAefDznDRReWxpnKjmYg==", + "license": "Apache-2.0", + "dependencies": { + "@codegenie/serverless-express": "^4.12.5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.2.0.tgz", + "integrity": "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.4.0.tgz", + "integrity": "sha512-ibRCQ0GZKJIQ+P3Et1h0LhPgp3PMTYk0MH8O+kW3lNYsvmaQww5Nn3f1jf73Q0jR1Yz3a1CDP4/NZD3vOajWJQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.8.5", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.8.5.tgz", + "integrity": "sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^6.0.0", + "find-my-way": "^9.0.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-my-way": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.6.0.tgz", + "integrity": "sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.27", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.27.tgz", + "integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mitata": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/mitata/-/mitata-1.0.34.tgz", + "integrity": "sha512-Mc3zrtNBKIMeHSCQ0XqRLo1vbdIx1wvFV9c8NJAiyho6AjNfMY8bVhbS12bwciUdd1t4rj8099CH3N3NFahaUA==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "license": "MIT" + }, + "node_modules/toad-cache": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.1.tgz", + "integrity": "sha512-5DXWzE4Vz7xNHsv+xQ+MGfJYyC78Aok3tEr0MNwHoRf7vZnga1mQXZ4/Nsodld4VR6Wd+VhfmqnNrsRJyYPfrQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/benchmarks/package.json b/benchmarks/package.json new file mode 100644 index 0000000..acae80b --- /dev/null +++ b/benchmarks/package.json @@ -0,0 +1,23 @@ +{ + "name": "lambda-api-benchmarks", + "version": "0.0.0", + "private": true, + "description": "In-process micro-benchmarks comparing lambda-api against other AWS Lambda web frameworks (issue #34)", + "scripts": { + "bench": "node run.js", + "bench:md": "node run.js --md results/RESULTS.md", + "bench:json": "node run.js --json results/raw.json", + "bench:release": "node run.js --md results/RESULTS.md --json results/raw.json --update-readme" + }, + "engines": { + "node": ">=18" + }, + "dependencies": { + "@fastify/aws-lambda": "^6.4.0", + "@vendia/serverless-express": "^4.12.6", + "express": "^4.21.2", + "fastify": "^5.8.5", + "hono": "^4.12.27", + "mitata": "^1.0.34" + } +} diff --git a/benchmarks/results/.gitkeep b/benchmarks/results/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/benchmarks/run.js b/benchmarks/run.js new file mode 100644 index 0000000..02b0d25 --- /dev/null +++ b/benchmarks/run.js @@ -0,0 +1,178 @@ +'use strict'; + +/** + * lambda-api benchmark runner (issue #34). + * + * Invokes each framework's compiled aws-lambda handler IN-PROCESS with identical synthetic + * API Gateway events and measures framework overhead with mitata. This deliberately avoids + * LocalStack / a real Lambda deploy: those wrap the sub-millisecond work we care about in + * Docker, the runtime bootstrap and network latency — tens of milliseconds of noise that + * would swamp the signal and measure the emulator, not the framework. See README.md. + * + * Usage: + * node run.js run every framework, print tables to stdout + * node run.js --framework lambda-api run a single framework (clean JIT isolation) + * node run.js --md results/RESULTS.md write the markdown report to a file + * node run.js --json results/raw.json dump raw per-cell stats (machine-readable) + * node run.js --update-readme refresh the Benchmarks section in ../README.md + * + * @author Benchmark suite for lambda-api (issue #34) + * @license MIT + */ + +const os = require('os'); +const fs = require('fs'); +const path = require('path'); + +const { apiGatewayV1, apiGatewayV2 } = require('./lib/events'); +const { scenarios } = require('./lib/scenarios'); +const validate = require('./lib/validate'); +const { renderTables } = require('./lib/table'); +const { updateReadme } = require('./lib/update-readme'); + +const ALL_FRAMEWORKS = ['baseline', 'lambda-api', 'serverless-express', 'fastify', 'hono']; + +const FORMATS = [ + { id: 'v1', build: apiGatewayV1 }, + { id: 'v2', build: apiGatewayV2 } +]; + +// Minimal but realistic Lambda context. Some adapters read getRemainingTimeInMillis(). +const CONTEXT = { + awsRequestId: 'benchmark-request', + functionName: 'lambda-api-benchmark', + functionVersion: '$LATEST', + memoryLimitInMB: '1024', + callbackWaitsForEmptyEventLoop: false, + getRemainingTimeInMillis: () => 30000 +}; + +function parseArgs(argv) { + const opts = { md: null, json: null, updateReadme: false, framework: null }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--md') opts.md = argv[++i]; + else if (a === '--json') opts.json = argv[++i]; + else if (a === '--update-readme') opts.updateReadme = true; + else if (a === '--framework') opts.framework = argv[++i]; + else if (a === '--help' || a === '-h') opts.help = true; + else throw new Error(`Unknown argument: ${a}`); + } + return opts; +} + +function collectEnv() { + const cpus = os.cpus(); + const now = new Date(); + return { + lambdaApiVersion: process.env.LAMBDA_API_VERSION || require('../package.json').version, + node: process.version.replace(/^v/, ''), + platform: process.platform, + arch: process.arch, + cpu: cpus && cpus.length ? `${cpus[0].model.trim()} (${cpus.length} cores)` : 'unknown', + date: now.toISOString().replace('T', ' ').slice(0, 19) + ' UTC', + dateShort: now.toISOString().slice(0, 10) + }; +} + +function eventFor(format, scenario) { + return format.build({ method: scenario.method, path: scenario.path, body: scenario.body || null }); +} + +async function main() { + const opts = parseArgs(process.argv.slice(2)); + if (opts.help) { + console.log(fs.readFileSync(__filename, 'utf8').split('\n').slice(6, 24).join('\n')); + return; + } + + const names = opts.framework ? [opts.framework] : ALL_FRAMEWORKS; + const env = collectEnv(); + + // mitata is ESM-only; load it from CommonJS via dynamic import. + const { measure } = await import('mitata'); + + console.error(`\nlambda-api benchmarks — Node ${env.node} on ${env.cpu}`); + console.error(`frameworks: ${names.join(', ')}\n`); + + const results = []; + + for (const name of names) { + const fw = require(`./frameworks/${name}`); + const handler = await fw.build(); + console.error(`▸ ${name}${fw.version !== '-' ? ' ' + fw.version : ''}`); + + for (const format of FORMATS) { + for (const scenario of scenarios) { + const event = eventFor(format, scenario); + const ctx = `${name}/${format.id}/${scenario.id}`; + + // Correctness gate first — never time a handler that fails the contract. + let ok = true; + try { + const res = await handler(event, CONTEXT); + validate(res, scenario.expect, ctx); + } catch (err) { + ok = false; + console.error(` ! skipped ${format.id}/${scenario.id}: ${err.message}`); + } + + let stats = null; + if (ok) { + // mitata detects the returned Promise and awaits it; handles warmup + batching. + stats = await measure(() => handler(event, CONTEXT)); + const ops = Math.round(1e9 / stats.avg).toLocaleString('en-US'); + console.error(` ✓ ${format.id}/${scenario.id}: ${ops} ops/sec`); + } + + results.push({ + framework: name, + version: fw.version, + format: format.id, + scenario: scenario.id, + ok, + stats: stats && { + avg: stats.avg, + min: stats.min, + max: stats.max, + p50: stats.p50, + p75: stats.p75, + p99: stats.p99 + } + }); + } + } + } + + const markdown = renderTables(results, env); + console.log('\n' + markdown); + + if (opts.md) { + const dest = path.resolve(__dirname, opts.md); + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.writeFileSync(dest, `# lambda-api benchmark results\n\n${markdown}`); + console.error(`\nwrote ${opts.md}`); + } + + if (opts.json) { + const dest = path.resolve(__dirname, opts.json); + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.writeFileSync(dest, JSON.stringify({ env, results }, null, 2)); + console.error(`wrote ${opts.json}`); + } + + if (opts.updateReadme) { + if (opts.framework) { + console.error('refusing --update-readme with --framework (partial results)'); + } else { + const readmePath = path.resolve(__dirname, '..', 'README.md'); + const { changed, action } = updateReadme({ readmePath, results, env }); + console.error(`README.md ${changed ? action : 'unchanged'}`); + } + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); From 912beb53b8723be03efc1cd8b4c455e111e4186e Mon Sep 17 00:00:00 2001 From: naorpeled Date: Sat, 27 Jun 2026 00:16:03 +0300 Subject: [PATCH 2/3] Drop misused @author/@license banner from benchmark file headers The @author field held the project description instead of an author name, which read oddly in every file. The descriptive block comment is the useful part; the boilerplate banner added nothing to these new files. --- benchmarks/frameworks/baseline.js | 3 --- benchmarks/frameworks/fastify.js | 3 --- benchmarks/frameworks/hono.js | 3 --- benchmarks/frameworks/lambda-api.js | 3 --- benchmarks/frameworks/serverless-express.js | 3 --- benchmarks/lib/events.js | 3 --- benchmarks/lib/scenarios.js | 3 --- benchmarks/lib/table.js | 3 --- benchmarks/lib/update-readme.js | 3 --- benchmarks/lib/validate.js | 3 --- benchmarks/run.js | 3 --- 11 files changed, 33 deletions(-) diff --git a/benchmarks/frameworks/baseline.js b/benchmarks/frameworks/baseline.js index 7bcac97..52aca77 100644 --- a/benchmarks/frameworks/baseline.js +++ b/benchmarks/frameworks/baseline.js @@ -6,9 +6,6 @@ * Establishes the theoretical lower bound for request handling — event parse, a manual * route match, JSON serialize. Every framework's overhead is meaningfully read as the gap * above this floor. - * - * @author Benchmark suite for lambda-api (issue #34) - * @license MIT */ const { ROUTE_COUNT } = require('../lib/scenarios'); diff --git a/benchmarks/frameworks/fastify.js b/benchmarks/frameworks/fastify.js index a0ef78f..579a772 100644 --- a/benchmarks/frameworks/fastify.js +++ b/benchmarks/frameworks/fastify.js @@ -2,9 +2,6 @@ /** * Fastify (via @fastify/aws-lambda) adapter for the benchmark suite. - * - * @author Benchmark suite for lambda-api (issue #34) - * @license MIT */ const Fastify = require('fastify'); diff --git a/benchmarks/frameworks/hono.js b/benchmarks/frameworks/hono.js index 008fdf8..c972d11 100644 --- a/benchmarks/frameworks/hono.js +++ b/benchmarks/frameworks/hono.js @@ -5,9 +5,6 @@ * * Hono ships as ESM-only, so it is loaded through a dynamic import() inside an async * build() — keeping the rest of the suite plain CommonJS. The runner awaits build(). - * - * @author Benchmark suite for lambda-api (issue #34) - * @license MIT */ const fs = require('fs'); diff --git a/benchmarks/frameworks/lambda-api.js b/benchmarks/frameworks/lambda-api.js index 7ea1429..96ed48e 100644 --- a/benchmarks/frameworks/lambda-api.js +++ b/benchmarks/frameworks/lambda-api.js @@ -5,9 +5,6 @@ * * Loaded directly from the repository working tree (../../), so `npm run bench` always * measures the local source — exactly what a maintainer iterating on performance wants. - * - * @author Benchmark suite for lambda-api (issue #34) - * @license MIT */ const createAPI = require('../../'); diff --git a/benchmarks/frameworks/serverless-express.js b/benchmarks/frameworks/serverless-express.js index 5adb9b0..8d5779c 100644 --- a/benchmarks/frameworks/serverless-express.js +++ b/benchmarks/frameworks/serverless-express.js @@ -5,9 +5,6 @@ * * Represents the common "port my Express app to Lambda" path. Configured minimally — * only the JSON body parser middleware — to keep the comparison fair. - * - * @author Benchmark suite for lambda-api (issue #34) - * @license MIT */ const express = require('express'); diff --git a/benchmarks/lib/events.js b/benchmarks/lib/events.js index c52a5c8..906065a 100644 --- a/benchmarks/lib/events.js +++ b/benchmarks/lib/events.js @@ -7,9 +7,6 @@ * trimmed to the fields that lambda-api and the comparison adapters actually read, so * every framework performs equivalent work. The method / path / body are overridden per * scenario; everything else is realistic boilerplate. - * - * @author Benchmark suite for lambda-api (issue #34) - * @license MIT */ const V1_HEADERS = { diff --git a/benchmarks/lib/scenarios.js b/benchmarks/lib/scenarios.js index 1619b96..742e3af 100644 --- a/benchmarks/lib/scenarios.js +++ b/benchmarks/lib/scenarios.js @@ -11,9 +11,6 @@ * * The 50 `r{i}/:p` routes (registered by each framework) make `routing-50` a meaningful * test of routing cost as the route table grows. - * - * @author Benchmark suite for lambda-api (issue #34) - * @license MIT */ const ROUTE_COUNT = 50; diff --git a/benchmarks/lib/table.js b/benchmarks/lib/table.js index c195fa6..7155e3b 100644 --- a/benchmarks/lib/table.js +++ b/benchmarks/lib/table.js @@ -6,9 +6,6 @@ * Layout: one throughput table per event format (rows = framework, columns = scenario), * each followed by a collapsible latency table (avg / p99). Rows are sorted by the * `get-json` throughput, descending. - * - * @author Benchmark suite for lambda-api (issue #34) - * @license MIT */ const { scenarios } = require('./scenarios'); diff --git a/benchmarks/lib/update-readme.js b/benchmarks/lib/update-readme.js index 0b9fb21..04b96f7 100644 --- a/benchmarks/lib/update-readme.js +++ b/benchmarks/lib/update-readme.js @@ -8,9 +8,6 @@ * duplicate sections). Inside it, a compact History table keeps one row per lambda-api * version: a new version appends a row; re-running for the same version replaces its row. * If the markers are absent, the section is appended once at the end of the file. - * - * @author Benchmark suite for lambda-api (issue #34) - * @license MIT */ const fs = require('fs'); diff --git a/benchmarks/lib/validate.js b/benchmarks/lib/validate.js index 0f83324..a68eeee 100644 --- a/benchmarks/lib/validate.js +++ b/benchmarks/lib/validate.js @@ -7,9 +7,6 @@ * throwing, or emitting the wrong body — a classic way framework comparisons become * dishonest. Throws a descriptive Error on any mismatch; the runner catches it, marks * that cell as failed, and excludes it from the results (rather than crashing the suite). - * - * @author Benchmark suite for lambda-api (issue #34) - * @license MIT */ /** diff --git a/benchmarks/run.js b/benchmarks/run.js index 02b0d25..367490a 100644 --- a/benchmarks/run.js +++ b/benchmarks/run.js @@ -15,9 +15,6 @@ * node run.js --md results/RESULTS.md write the markdown report to a file * node run.js --json results/raw.json dump raw per-cell stats (machine-readable) * node run.js --update-readme refresh the Benchmarks section in ../README.md - * - * @author Benchmark suite for lambda-api (issue #34) - * @license MIT */ const os = require('os'); From 632b3b81900f52f5775c3180ab441bc84031ad75 Mon Sep 17 00:00:00 2001 From: naorpeled Date: Sat, 27 Jun 2026 00:26:17 +0300 Subject: [PATCH 3/3] Add Middy to the benchmark comparison; drop redundant issue #34 references - Add a middy framework (@middy/core + http-router + http-json-body-parser + http-error-handler) to the comparison set and README history. Middy is a middleware engine, so it's wired with the official router/body-parser/error handler and otherwise kept a thin layer; documented in the fairness notes. - Bump the benchmarks Node floor to >=20 (Middy 6 requires it; matches the workflow and Fastify 5). - Remove the redundant 'issue #34' mentions from the benchmark README, run.js, and package.json description. --- README.md | 52 +++++++++++---------- benchmarks/README.md | 13 +++--- benchmarks/frameworks/middy.js | 59 ++++++++++++++++++++++++ benchmarks/lib/update-readme.js | 1 + benchmarks/package-lock.json | 80 ++++++++++++++++++++++++++++++++- benchmarks/package.json | 8 +++- benchmarks/run.js | 4 +- 7 files changed, 182 insertions(+), 35 deletions(-) create mode 100644 benchmarks/frameworks/middy.js diff --git a/README.md b/README.md index 706e58d..7e1bd30 100644 --- a/README.md +++ b/README.md @@ -1530,27 +1530,29 @@ If you're using Lambda API and finding it useful, hit me up on [Twitter](https:/ In-process micro-benchmarks of lambda-api against other AWS Lambda web frameworks. The numbers measure **framework overhead only** (event → route → middleware → response, in a single Node VM) — not end-to-end Lambda timings. Absolute values vary by machine, so compare the **relative** ranking rather than the raw ops/sec. See [`benchmarks/`](./benchmarks) for the methodology and how to reproduce. -_Generated 2026-06-26 21:07:24 UTC · lambda-api v0.0.0-development · Node 20.19.5 · Apple M4 Max (16 cores) · darwin/arm64_ +_Generated 2026-06-26 21:24:41 UTC · lambda-api v0.0.0-development · Node 20.19.5 · Apple M4 Max (16 cores) · darwin/arm64_ #### API Gateway REST (v1) — throughput (ops/sec, higher is better) | Framework | get-json | path-param | post-json | routing-50 | not-found | | ------------------------------ | --------- | ---------- | --------- | ---------- | --------- | -| baseline | 5,765,495 | 5,514,860 | 2,561,648 | 4,530,735 | 4,646,303 | -| lambda-api `0.0.0-development` | 215,921 | 200,518 | 191,027 | 198,443 | 81,754 | -| fastify `5.8.5` | 54,972 | 52,420 | 13,758 | 56,234 | 49,800 | -| hono `4.12.27` | 48,833 | 55,054 | 36,955 | 59,643 | 61,384 | -| serverless-express `4.22.2` | 25,569 | 27,466 | 15,432 | 25,636 | 27,325 | +| baseline | 5,654,874 | 5,513,012 | 2,560,533 | 4,493,436 | 4,620,989 | +| middy `6.4.5` | 1,056,927 | 811,301 | 62,540 | 433,185 | 155,744 | +| lambda-api `0.0.0-development` | 217,700 | 202,224 | 194,819 | 199,565 | 91,296 | +| hono `4.12.27` | 58,370 | 57,559 | 38,469 | 60,441 | 61,732 | +| fastify `5.8.5` | 51,240 | 52,873 | 13,598 | 56,465 | 48,915 | +| serverless-express `4.22.2` | 26,732 | 28,082 | 15,997 | 26,940 | 28,643 |
API Gateway REST (v1) — latency (avg / p99, µs, lower is better) | Framework | get-json | path-param | post-json | routing-50 | not-found | | ------------------------------ | ----------- | ----------- | ----------- | ----------- | ----------- | -| baseline | 0.17 / 0.22 | 0.18 / 0.25 | 0.39 / 0.50 | 0.22 / 0.28 | 0.22 / 0.27 | -| lambda-api `0.0.0-development` | 4.63 / 4.84 | 4.99 / 5.24 | 5.23 / 5.80 | 5.04 / 5.25 | 12.2 / 12.3 | -| fastify `5.8.5` | 18.2 / 68.8 | 19.1 / 20.7 | 72.7 / 151 | 17.8 / 23.4 | 20.1 / 19.7 | -| hono `4.12.27` | 20.5 / 118 | 18.2 / 20.8 | 27.1 / 27.4 | 16.8 / 18.0 | 16.3 / 17.5 | -| serverless-express `4.22.2` | 39.1 / 126 | 36.4 / 38.2 | 64.8 / 194 | 39.0 / 103 | 36.6 / 37.7 | +| baseline | 0.18 / 0.23 | 0.18 / 0.25 | 0.39 / 0.46 | 0.22 / 0.28 | 0.22 / 0.27 | +| middy `6.4.5` | 0.95 / 2.29 | 1.23 / 1.41 | 16.0 / 16.3 | 2.31 / 2.45 | 6.42 / 6.56 | +| lambda-api `0.0.0-development` | 4.59 / 4.74 | 4.95 / 5.14 | 5.13 / 5.28 | 5.01 / 5.63 | 11.0 / 11.0 | +| hono `4.12.27` | 17.1 / 69.7 | 17.4 / 18.7 | 26.0 / 30.2 | 16.5 / 17.3 | 16.2 / 18.1 | +| fastify `5.8.5` | 19.5 / 95.6 | 18.9 / 20.4 | 73.5 / 152 | 17.7 / 23.4 | 20.4 / 20.4 | +| serverless-express `4.22.2` | 37.4 / 118 | 35.6 / 36.8 | 62.5 / 195 | 37.1 / 99.0 | 34.9 / 36.7 |
@@ -1558,21 +1560,23 @@ _Generated 2026-06-26 21:07:24 UTC · lambda-api v0.0.0-development · Node 20.1 | Framework | get-json | path-param | post-json | routing-50 | not-found | | ------------------------------ | --------- | ---------- | --------- | ---------- | --------- | -| baseline | 4,838,095 | 4,425,059 | 2,283,297 | 3,864,414 | 4,039,412 | -| lambda-api `0.0.0-development` | 196,317 | 190,996 | 185,950 | 187,732 | 79,295 | -| hono `4.12.27` | 70,609 | 67,280 | 41,784 | 67,375 | 68,399 | -| fastify `5.8.5` | 44,946 | 39,743 | 14,657 | 56,339 | 39,648 | -| serverless-express `4.22.2` | 27,309 | 24,706 | 17,450 | 25,505 | 28,701 | +| baseline | 4,830,598 | 4,424,847 | 2,237,039 | 3,946,604 | 3,994,042 | +| middy `6.4.5` | 1,032,952 | 791,846 | 55,802 | 372,581 | 148,189 | +| lambda-api `0.0.0-development` | 201,969 | 187,395 | 183,887 | 195,975 | 88,721 | +| hono `4.12.27` | 70,859 | 62,001 | 40,860 | 65,895 | 67,978 | +| fastify `5.8.5` | 45,114 | 38,232 | 12,973 | 45,687 | 49,536 | +| serverless-express `4.22.2` | 26,837 | 27,753 | 17,533 | 25,565 | 29,008 |
API Gateway HTTP (v2) — latency (avg / p99, µs, lower is better) | Framework | get-json | path-param | post-json | routing-50 | not-found | | ------------------------------ | ----------- | ----------- | ----------- | ----------- | ----------- | -| baseline | 0.21 / 0.44 | 0.23 / 0.42 | 0.44 / 0.69 | 0.26 / 0.47 | 0.25 / 0.50 | -| lambda-api `0.0.0-development` | 5.09 / 5.28 | 5.24 / 5.41 | 5.38 / 5.56 | 5.33 / 5.62 | 12.6 / 12.8 | -| hono `4.12.27` | 14.2 / 15.2 | 14.9 / 16.3 | 23.9 / 25.6 | 14.8 / 15.9 | 14.6 / 15.9 | -| fastify `5.8.5` | 22.2 / 23.4 | 25.2 / 30.4 | 68.2 / 130 | 17.7 / 23.0 | 25.2 / 28.2 | -| serverless-express `4.22.2` | 36.6 / 39.2 | 40.5 / 53.5 | 57.3 / 58.8 | 39.2 / 40.7 | 34.8 / 37.0 | +| baseline | 0.21 / 0.47 | 0.23 / 0.44 | 0.45 / 0.71 | 0.25 / 0.48 | 0.25 / 0.51 | +| middy `6.4.5` | 0.97 / 1.17 | 1.26 / 1.38 | 17.9 / 21.8 | 2.68 / 3.70 | 6.75 / 7.37 | +| lambda-api `0.0.0-development` | 4.95 / 5.32 | 5.34 / 5.57 | 5.44 / 5.87 | 5.10 / 5.30 | 11.3 / 11.3 | +| hono `4.12.27` | 14.1 / 14.2 | 16.1 / 19.9 | 24.5 / 26.9 | 15.2 / 16.4 | 14.7 / 15.2 | +| fastify `5.8.5` | 22.2 / 20.0 | 26.2 / 30.9 | 77.1 / 248 | 21.9 / 30.3 | 20.2 / 25.6 | +| serverless-express `4.22.2` | 37.3 / 46.0 | 36.0 / 38.1 | 57.0 / 144 | 39.1 / 40.9 | 34.5 / 35.5 |
@@ -1582,9 +1586,9 @@ Throughput for the `get-json` scenario on V2 events (ops/sec), one row per relea -| version | date | node | baseline | lambda-api | fastify | hono | express | -| ----------------- | ---------- | ------- | --------- | ---------- | ------- | ------ | ------- | -| 0.0.0-development | 2026-06-26 | 20.19.5 | 4,838,095 | 196,317 | 44,946 | 70,609 | 27,309 | +| version | date | node | baseline | lambda-api | fastify | hono | middy | express | +| ----------------- | ---------- | ------- | --------- | ---------- | ------- | ------ | --------- | ------- | +| 0.0.0-development | 2026-06-26 | 20.19.5 | 4,830,598 | 201,969 | 45,114 | 70,859 | 1,032,952 | 26,837 | diff --git a/benchmarks/README.md b/benchmarks/README.md index bc43e16..7a7132c 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -1,7 +1,6 @@ # lambda-api benchmarks -Benchmark suite for [issue #34](https://github.com/jeremydaly/lambda-api/issues/34): -a repeatable comparison of **lambda-api** against other popular frameworks running on AWS +A repeatable comparison of **lambda-api** against other popular frameworks running on AWS Lambda. This is an isolated package — its (heavy) comparison-framework dependencies live here and @@ -57,6 +56,7 @@ they answer "how fast is my whole deployment", not "how much overhead does the f | `serverless-express` | `express` + `@vendia/serverless-express`| `serverlessExpress()` | | `fastify` | `fastify` + `@fastify/aws-lambda` | `awsLambdaFastify()` | | `hono` | `hono` | `hono/aws-lambda` | +| `middy` | `@middy/core` + `@middy/http-router` | `httpRouterHandler()` | `baseline` is a raw handler with zero routing abstraction; every other framework's overhead is read as the gap above it. @@ -79,9 +79,10 @@ REST (v1)** and **HTTP API (v2)** events. - Frameworks are configured minimally and equivalently — the goal is to compare core overhead, not feature sets. Defaults still differ (e.g. lambda-api computes an `ETag` and handles - serialization itself; Express needs an explicit `express.json()` and a 404 handler), and those - differences are part of what the numbers reflect. Read the `frameworks/*.js` to see exactly how - each is set up. + serialization itself; Express needs an explicit `express.json()` and a 404 handler; Middy is a + middleware engine wired with only `http-router` + a per-route JSON body parser + an error + handler, and is otherwise a very thin layer), and those differences are part of what the numbers + reflect. Read the `frameworks/*.js` to see exactly how each is set up. - Absolute ops/sec depend on the machine. **Compare the relative ranking**, not raw numbers. - All frameworks run in one process by default. Pass `--framework ` to run a single one in its own process — used for published numbers — to avoid cross-framework JIT interference. @@ -92,7 +93,7 @@ REST (v1)** and **HTTP API (v2)** events. node run.js [options] --framework run a single framework (baseline | lambda-api | serverless-express - | fastify | hono); also gives clean per-process JIT isolation + | fastify | hono | middy); also gives clean per-process JIT isolation --md write the markdown report to a file (relative to benchmarks/) --json dump raw per-cell stats as JSON --update-readme refresh the Benchmarks section in ../README.md (full run only) diff --git a/benchmarks/frameworks/middy.js b/benchmarks/frameworks/middy.js new file mode 100644 index 0000000..67be243 --- /dev/null +++ b/benchmarks/frameworks/middy.js @@ -0,0 +1,59 @@ +'use strict'; + +/** + * Middy (@middy/core) adapter for the benchmark suite. + * + * Middy is a middleware engine, not a router, so a fair comparison adds the official + * http-router (routing + path params), http-json-body-parser (the POST body), and + * http-error-handler (the 404). These packages are ESM-only, so they are loaded through a + * dynamic import() inside an async build() — the runner awaits build(). + */ + +const fs = require('fs'); +const path = require('path'); +const { ROUTE_COUNT } = require('../lib/scenarios'); + +function middyVersion() { + try { + const pkgPath = path.join(__dirname, '..', 'node_modules', '@middy', 'core', 'package.json'); + return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version; + } catch (err) { + return 'unknown'; + } +} + +async function build() { + const middy = (await import('@middy/core')).default; + const httpRouterHandler = (await import('@middy/http-router')).default; + const httpJsonBodyParser = (await import('@middy/http-json-body-parser')).default; + const httpErrorHandler = (await import('@middy/http-error-handler')).default; + + const json = (statusCode, payload) => ({ statusCode, body: JSON.stringify(payload) }); + + const routes = [ + { method: 'GET', path: '/', handler: async () => json(200, { hello: 'world' }) }, + { + method: 'GET', + path: '/users/{id}', + handler: async (event) => json(200, { id: event.pathParameters.id }) + }, + { + method: 'POST', + path: '/users', + handler: middy(async (event) => json(200, { created: event.body })).use(httpJsonBodyParser()) + } + ]; + + for (let i = 0; i < ROUTE_COUNT; i++) { + const index = i; + routes.push({ method: 'GET', path: `/r${i}/{p}`, handler: async () => json(200, { i: index }) }); + } + + // logger:false — http-router throws a 404 for unmatched routes; without this the + // not-found scenario would spam stderr on every timed iteration. + const handler = middy(httpRouterHandler(routes)).use(httpErrorHandler({ logger: false })); + + return (event, context) => handler(event, context); +} + +module.exports = { name: 'middy', version: middyVersion(), build }; diff --git a/benchmarks/lib/update-readme.js b/benchmarks/lib/update-readme.js index 04b96f7..4d90c00 100644 --- a/benchmarks/lib/update-readme.js +++ b/benchmarks/lib/update-readme.js @@ -26,6 +26,7 @@ const HISTORY_FRAMEWORKS = [ { name: 'lambda-api', header: 'lambda-api' }, { name: 'fastify', header: 'fastify' }, { name: 'hono', header: 'hono' }, + { name: 'middy', header: 'middy' }, { name: 'serverless-express', header: 'express' } ]; diff --git a/benchmarks/package-lock.json b/benchmarks/package-lock.json index 5dabca6..1106ca7 100644 --- a/benchmarks/package-lock.json +++ b/benchmarks/package-lock.json @@ -9,6 +9,10 @@ "version": "0.0.0", "dependencies": { "@fastify/aws-lambda": "^6.4.0", + "@middy/core": "^6.4.3", + "@middy/http-error-handler": "^6.4.3", + "@middy/http-json-body-parser": "^6.4.3", + "@middy/http-router": "^6.4.3", "@vendia/serverless-express": "^4.12.6", "express": "^4.21.2", "fastify": "^5.8.5", @@ -16,7 +20,7 @@ "mitata": "^1.0.34" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/@codegenie/serverless-express": { @@ -145,6 +149,80 @@ "ipaddr.js": "^2.1.0" } }, + "node_modules/@middy/core": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/@middy/core/-/core-6.4.5.tgz", + "integrity": "sha512-qRGCslDHjMr08fywcfVbWR9qpx16vGD481i9GpX3r5efi8Arjp/44JTjfeJkJJxvIb/8/+E9MLvU86+3oe1oJQ==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/willfarrell" + } + }, + "node_modules/@middy/http-error-handler": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/@middy/http-error-handler/-/http-error-handler-6.4.5.tgz", + "integrity": "sha512-0El5exXIl5QW47kQQTx1lVk0dZC961hGK6TVd+jtQAv9tpN6ibZQaRI7CaL8b+bmAgsdsxZQmEqoLOBhhW5gQQ==", + "license": "MIT", + "dependencies": { + "@middy/util": "6.4.5" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/willfarrell" + } + }, + "node_modules/@middy/http-json-body-parser": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/@middy/http-json-body-parser/-/http-json-body-parser-6.4.5.tgz", + "integrity": "sha512-laK78DjphVJTDNPYFy2v7B6Tj0Pby96+HBxJ3AE9De1WcfKkeFbII+K0WR2KJND8+61fzHPWnPCGoRWB7AD3RQ==", + "license": "MIT", + "dependencies": { + "@middy/util": "6.4.5" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/willfarrell" + } + }, + "node_modules/@middy/http-router": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/@middy/http-router/-/http-router-6.4.5.tgz", + "integrity": "sha512-hYaZgNu53Po0VAfMlnvrHXoYLXRVUTrEJ/0FlY7RS3EqBp+hpTibNT2N7icc8M/FewqAFAWBvI4CLINrzS3xgA==", + "license": "MIT", + "dependencies": { + "@middy/util": "6.4.5" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/willfarrell" + } + }, + "node_modules/@middy/util": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/@middy/util/-/util-6.4.5.tgz", + "integrity": "sha512-170ml5Q8QIHjric+ynwmDoa1sbcaMkUJumpUNxubqzStN7rEebVHnzXLQBwvSAhp7Fhcja7q2JLo0yjHpyPO4Q==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/willfarrell" + } + }, "node_modules/@pinojs/redact": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", diff --git a/benchmarks/package.json b/benchmarks/package.json index acae80b..7b399ff 100644 --- a/benchmarks/package.json +++ b/benchmarks/package.json @@ -2,7 +2,7 @@ "name": "lambda-api-benchmarks", "version": "0.0.0", "private": true, - "description": "In-process micro-benchmarks comparing lambda-api against other AWS Lambda web frameworks (issue #34)", + "description": "In-process micro-benchmarks comparing lambda-api against other AWS Lambda web frameworks", "scripts": { "bench": "node run.js", "bench:md": "node run.js --md results/RESULTS.md", @@ -10,10 +10,14 @@ "bench:release": "node run.js --md results/RESULTS.md --json results/raw.json --update-readme" }, "engines": { - "node": ">=18" + "node": ">=20" }, "dependencies": { "@fastify/aws-lambda": "^6.4.0", + "@middy/core": "^6.4.3", + "@middy/http-error-handler": "^6.4.3", + "@middy/http-json-body-parser": "^6.4.3", + "@middy/http-router": "^6.4.3", "@vendia/serverless-express": "^4.12.6", "express": "^4.21.2", "fastify": "^5.8.5", diff --git a/benchmarks/run.js b/benchmarks/run.js index 367490a..12fd0ca 100644 --- a/benchmarks/run.js +++ b/benchmarks/run.js @@ -1,7 +1,7 @@ 'use strict'; /** - * lambda-api benchmark runner (issue #34). + * lambda-api benchmark runner. * * Invokes each framework's compiled aws-lambda handler IN-PROCESS with identical synthetic * API Gateway events and measures framework overhead with mitata. This deliberately avoids @@ -27,7 +27,7 @@ const validate = require('./lib/validate'); const { renderTables } = require('./lib/table'); const { updateReadme } = require('./lib/update-readme'); -const ALL_FRAMEWORKS = ['baseline', 'lambda-api', 'serverless-express', 'fastify', 'hono']; +const ALL_FRAMEWORKS = ['baseline', 'lambda-api', 'serverless-express', 'fastify', 'hono', 'middy']; const FORMATS = [ { id: 'v1', build: apiGatewayV1 },