From c72718a69df91d0d47826e42578ec34790cab6fd Mon Sep 17 00:00:00 2001 From: coo1estguy <70365929+coolestcodeguy@users.noreply.github.com> Date: Fri, 25 Sep 2026 00:41:02 +0530 Subject: [PATCH] feat(profiling): add containerized load, CPU scaling, and distributed budget test suite - Provide Docker Compose test harness with CPU and memory resource limits\n- Add single-node CPU scaling benchmark suite (0.5 to 4.0 vCPU)\n- Add distributed multi-node scaling and budget benchmarks (, , /mo)\n- Support partitioned service topologies (dedicated API, pipeline worker, and delivery worker instances)\n- Implement configurable provider latency simulation with randomized network jitter\n- Support dynamic client and worker concurrency tuning --- profiling/Dockerfile | 24 + profiling/README.md | 83 + profiling/docker-compose.distributed.yml | 99 + profiling/docker-compose.yml | 48 + profiling/package-lock.json | 2720 +++++++++++++++++++ profiling/package.json | 25 + profiling/src/server.ts | 107 + profiling/tests/distributed-load.test.ts | 376 +++ profiling/tests/distributed-scaling.test.ts | 190 ++ profiling/tests/load.test.ts | 340 +++ profiling/tests/money-scaling.test.ts | 214 ++ profiling/tests/scaling.test.ts | 312 +++ profiling/tsconfig.json | 12 + 13 files changed, 4550 insertions(+) create mode 100644 profiling/Dockerfile create mode 100644 profiling/README.md create mode 100644 profiling/docker-compose.distributed.yml create mode 100644 profiling/docker-compose.yml create mode 100644 profiling/package-lock.json create mode 100644 profiling/package.json create mode 100644 profiling/src/server.ts create mode 100644 profiling/tests/distributed-load.test.ts create mode 100644 profiling/tests/distributed-scaling.test.ts create mode 100644 profiling/tests/load.test.ts create mode 100644 profiling/tests/money-scaling.test.ts create mode 100644 profiling/tests/scaling.test.ts create mode 100644 profiling/tsconfig.json diff --git a/profiling/Dockerfile b/profiling/Dockerfile new file mode 100644 index 0000000..21043f7 --- /dev/null +++ b/profiling/Dockerfile @@ -0,0 +1,24 @@ +FROM node:22-alpine + +WORKDIR /app + +# Copy root build configurations, sources, packages, and prebuilt dist +COPY package*.json tsconfig.json tsdown.config.ts ./ +COPY packages/ ./packages/ +COPY src/ ./src/ +COPY dist/ ./dist/ + +RUN npm install --ignore-scripts + +# Setup profiling app +WORKDIR /app/profiling +COPY profiling/package*.json profiling/tsconfig.json ./ +RUN npm install + +COPY profiling/src/ ./src/ + +EXPOSE 3000 + +CMD ["npm", "start"] + + diff --git a/profiling/README.md b/profiling/README.md new file mode 100644 index 0000000..ca849a2 --- /dev/null +++ b/profiling/README.md @@ -0,0 +1,83 @@ +# Notifkit Profiling & Benchmarking Suite + +This folder provides a realistic, container-isolated profiling and benchmarking environment for Notifkit. + +--- + +## ๐Ÿ—๏ธ Architecture & Resource Constraints + +The environment simulates a standard small production node (e.g. $15/mo cloud VM): + +- **Server**: 1.0 vCPU, 1 GB RAM (`notifkit` standalone orchestrator) +- **PostgreSQL 15**: 0.5 vCPU, 512 MB RAM +- **Redis 7**: 0.5 vCPU, 256 MB RAM + +--- + +## ๐Ÿš€ Quick Start + +### 1. Install Dependencies + +```bash +cd profiling +npm install +``` + +### 2. Run the Load Test + +```bash +npm run test:load +``` + +The load test will: + +1. Boot the isolated 3-container stack (`postgres`, `redis`, `server`) via `testcontainers`. +2. Run database schema migrations and seed project/template data. +3. Fire 50 concurrent virtual users generating notifications against `/v1/notify`. +4. Stream and buffer container logs until 100% of messages are delivered to the transport. +5. Output detailed throughput, p95, and p99 percentiles for both API ingestion and background delivery. +6. Automatically clean up and shut down the containers. + +--- + +### 3. Run the CPU Scaling Benchmark + +Evaluate server performance scaling from 0.5 vCPU up to 4.0 vCPUs: + +```bash +npm run test:scaling +``` + +#### CLI Flags & Customization: + +```bash +# Rapid test (5s duration per tier) +npm run test:scaling -- --quick + +# Specify custom CPU tiers +npm run test:scaling -- --cpus=0.5,1.0,2.0,4.0 + +# Customize duration and concurrency +npm run test:scaling -- --duration=15 --concurrency=100 +``` + +The scaling benchmark produces an aggregated comparative table with: + +- Ingestion Throughput (`req/sec`) +- Active Delivery Throughput (`msgs/sec`) +- Wall Drain Throughput (`msgs/sec`) +- API & Delivery Latencies (`p50`, `p95`, `p99`) +- Speedup vs Baseline & Diminishing Returns Analysis + +--- + +## โž• Adding New Profiling Tests + +To add a new benchmark (e.g. batch delivery, quiet hours queuing, multi-channel fallback): + +1. Create a new test file under `tests/` (e.g. `tests/batch.test.ts`). +2. Add a corresponding script to `package.json`: + ```json + "test:batch": "tsx tests/batch.test.ts" + ``` +3. Run `npm run test:batch`. diff --git a/profiling/docker-compose.distributed.yml b/profiling/docker-compose.distributed.yml new file mode 100644 index 0000000..9b4e6e9 --- /dev/null +++ b/profiling/docker-compose.distributed.yml @@ -0,0 +1,99 @@ +services: + db: + image: postgres:15-alpine + environment: + POSTGRES_DB: notifkit + POSTGRES_USER: notifkit + POSTGRES_PASSWORD: password + ports: + - "35432:5432" + deploy: + resources: + limits: + cpus: "${DB_CPUS:-1.0}" + memory: "${DB_MEMORY:-1G}" + + redis: + image: redis:7-alpine + ports: + - "36379:6379" + deploy: + resources: + limits: + cpus: "${REDIS_CPUS:-1.5}" + memory: "${REDIS_MEMORY:-1G}" + + server1: + build: + context: .. + dockerfile: profiling/Dockerfile + environment: + DATABASE_URL: postgres://notifkit:password@db:5432/notifkit + REDIS_URL: redis://redis:6379 + ADMIN_API_KEY: perf-admin-key + LOG_LEVEL: warn + HOST: 0.0.0.0 + WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-100} + DB_MAX_CONNECTIONS: ${DB_MAX_CONNECTIONS:-5} + PROVIDER_LATENCY_MS: ${PROVIDER_LATENCY_MS:-0} + SERVICES: ${SERVER1_SERVICES:-all} + ports: + - "35678:3000" + depends_on: + - db + - redis + deploy: + resources: + limits: + cpus: "${SERVER_CPUS:-1.0}" + memory: "${SERVER_MEMORY:-1G}" + + server2: + build: + context: .. + dockerfile: profiling/Dockerfile + environment: + DATABASE_URL: postgres://notifkit:password@db:5432/notifkit + REDIS_URL: redis://redis:6379 + ADMIN_API_KEY: perf-admin-key + LOG_LEVEL: warn + HOST: 0.0.0.0 + WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-100} + DB_MAX_CONNECTIONS: ${DB_MAX_CONNECTIONS:-5} + PROVIDER_LATENCY_MS: ${PROVIDER_LATENCY_MS:-0} + SERVICES: ${SERVER2_SERVICES:-all} + ports: + - "35679:3000" + depends_on: + - db + - redis + deploy: + resources: + limits: + cpus: "${SERVER_CPUS:-1.0}" + memory: "${SERVER_MEMORY:-1G}" + + server3: + build: + context: .. + dockerfile: profiling/Dockerfile + environment: + DATABASE_URL: postgres://notifkit:password@db:5432/notifkit + REDIS_URL: redis://redis:6379 + ADMIN_API_KEY: perf-admin-key + LOG_LEVEL: warn + HOST: 0.0.0.0 + WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-100} + DB_MAX_CONNECTIONS: ${DB_MAX_CONNECTIONS:-5} + PROVIDER_LATENCY_MS: ${PROVIDER_LATENCY_MS:-0} + SERVICES: ${SERVER3_SERVICES:-all} + ports: + - "35680:3000" + depends_on: + - db + - redis + deploy: + resources: + limits: + cpus: "${SERVER_CPUS:-1.0}" + memory: "${SERVER_MEMORY:-1G}" diff --git a/profiling/docker-compose.yml b/profiling/docker-compose.yml new file mode 100644 index 0000000..34b50be --- /dev/null +++ b/profiling/docker-compose.yml @@ -0,0 +1,48 @@ +services: + db: + image: postgres:15-alpine + environment: + POSTGRES_DB: notifkit + POSTGRES_USER: notifkit + POSTGRES_PASSWORD: password + ports: + - "35432:5432" + deploy: + resources: + limits: + cpus: "${DB_CPUS:-0.5}" + memory: "${DB_MEMORY:-512M}" + + redis: + image: redis:7-alpine + ports: + - "36379:6379" + deploy: + resources: + limits: + cpus: "${REDIS_CPUS:-0.5}" + memory: "${REDIS_MEMORY:-256M}" + + server: + build: + context: .. + dockerfile: profiling/Dockerfile + environment: + DATABASE_URL: postgres://notifkit:password@db:5432/notifkit + REDIS_URL: redis://redis:6379 + ADMIN_API_KEY: perf-admin-key + LOG_LEVEL: warn + HOST: 0.0.0.0 + WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-50} + DB_MAX_CONNECTIONS: ${DB_MAX_CONNECTIONS:-20} + PROVIDER_LATENCY_MS: ${PROVIDER_LATENCY_MS:-0} + ports: + - "35678:3000" + depends_on: + - db + - redis + deploy: + resources: + limits: + cpus: "${SERVER_CPUS:-1.0}" + memory: "${SERVER_MEMORY:-1G}" diff --git a/profiling/package-lock.json b/profiling/package-lock.json new file mode 100644 index 0000000..dfc9955 --- /dev/null +++ b/profiling/package-lock.json @@ -0,0 +1,2720 @@ +{ + "name": "notifkit-profiling", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "notifkit-profiling", + "version": "1.0.0", + "dependencies": { + "@notifkit/provider-console": "file:../packages/provider-console", + "notifkit": "file:.." + }, + "devDependencies": { + "@types/node": "^22.0.0", + "testcontainers": "^10.18.0", + "tsx": "^4.19.3", + "typescript": "^5.8.3" + } + }, + "..": { + "version": "0.1.10", + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "ai": "^7.0.37", + "dotenv": "^16.4.5", + "drizzle-orm": "^0.45.2", + "drizzle-zod": "^0.8.3", + "ioredis": "^5.4.1", + "pino": "^9.2.0", + "pino-pretty": "^11.0.0", + "postgres": "^3.4.4", + "prom-client": "^15.1.3", + "zod": "^3.23.8" + }, + "bin": { + "notifkit-create-admin": "scripts/create-admin.mjs", + "notifkit-create-project": "scripts/create-project.mjs" + }, + "devDependencies": { + "@commitlint/cli": "^19.3.0", + "@commitlint/config-conventional": "^19.2.2", + "@testcontainers/postgresql": "^12.0.4", + "@testcontainers/redis": "^12.0.4", + "@types/node": "^22.0.0", + "@types/supertest": "^7.2.1", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "@vitest/coverage-v8": "^2.0.0", + "drizzle-kit": "^0.31.10", + "eslint": "^9.0.0", + "husky": "^9.0.11", + "lint-staged": "^15.2.5", + "prettier": "^3.3.0", + "supertest": "^7.2.2", + "testcontainers": "^12.0.4", + "tsdown": "^0.22.14", + "typescript": "^5.8.3", + "vitest": "^2.0.0" + }, + "engines": { + "node": ">=22.0.0", + "npm": ">=10.0.0" + } + }, + "../packages/provider-console": { + "name": "@notifkit/provider-console", + "version": "0.1.1", + "license": "MIT", + "devDependencies": { + "notifkit": "file:../../", + "typescript": "^5.8.3" + }, + "peerDependencies": { + "notifkit": "^0.1.0" + } + }, + "node_modules/@balena/dockerignore": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", + "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.5", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.5.tgz", + "integrity": "sha512-7VZM+SVdEcUUqSQeNI3zM8Qs/BhQKZndPo2h5VkYkAM8Iz0wJIa8mKV5ekQGqG8UUsnkQ0NMxIxwkIHYvj0qOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@notifkit/provider-console": { + "resolved": "../packages/provider-console", + "link": true + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@types/docker-modem": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", + "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/dockerode": { + "version": "3.3.47", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.47.tgz", + "integrity": "sha512-ShM1mz7rCjdssXt7Xz0u1/R2BJC7piWa3SJpUBiVjCf2A3XNn4cP6pUVaD8bLanpPVVn4IKzJuw3dOvkJ8IbYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/docker-modem": "*", + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/node": { + "version": "22.20.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.4.tgz", + "integrity": "sha512-zJRE40jpHtKqE/C4fgHrAKQLJuSpzEnP9ff9Y7YtoR3Wd2pwqzlekDeEuUQXjRd+QCYnVnNwuJYmhdk9XV8gvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/ssh2": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.6.tgz", + "integrity": "sha512-oGdxhBqcRTwSTKFm+9EiKzkNVYRLEFkcW44lhguvBalGJbWfGnDt/ezwSUZc+SF9m9bMc3VyklNAtp7zICjS5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2-streams": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.13.tgz", + "integrity": "sha512-faHyY3brO9oLEA0QlcO8N2wT7R0+1sHWZvQ+y3rMLwdY1ZyS1z0W3t65j9PqT4HmQ6ALzNe7RZlNuCNE0wBSWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/b4a": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", + "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", + "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.1.tgz", + "integrity": "sha512-N1nnXdHZAOSstz0XiHikGS4HGMH4CnSwhqWdGQQMqqdvp4Jybm9sE3R1WVnpWVd4SFkc8ryPDBLViNLwiEqECg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.28.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", + "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.4", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", + "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz", + "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "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/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "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", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "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", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buildcheck": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", + "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/byline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", + "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/docker-compose": { + "version": "0.24.8", + "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.24.8.tgz", + "integrity": "sha512-plizRs/Vf15H+GCVxq2EUvyPK7ei9b/cVesHvjnX4xaXjM9spHe2Ytq0BitndFgvTJ3E3NljPNUEl7BAN43iZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "yaml": "^2.2.2" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/docker-modem": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.7.tgz", + "integrity": "sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^1.15.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/docker-modem/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/dockerode": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.12.tgz", + "integrity": "sha512-/bCZd6KlGcjZO8Buqmi/vXuqEGVEZ0PNjx/biBNqJD3MhK9DmdiAuKxqfNhflgDESDIiBz3qF+0e55+CpnrUcw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "@grpc/grpc-js": "^1.11.1", + "@grpc/proto-loader": "^0.7.13", + "docker-modem": "^5.0.7", + "protobufjs": "^7.3.2", + "tar-fs": "^2.1.4", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/dockerode/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/dockerode/node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/dockerode/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-port": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.2.0.tgz", + "integrity": "sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "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": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/nan": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.29.0.tgz", + "integrity": "sha512-GlGk3HIvitbvs+LT3g6XUP1kpirKNvmDFwF/bmo6XNWSb/eYEs/O4bfgIEIXCZ+lIOTS5xNwDvSGMw6FJdAhtA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/notifkit": { + "resolved": "..", + "link": true + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/properties-reader": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/properties-reader/-/properties-reader-2.3.0.tgz", + "integrity": "sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/properties?sponsor=1" + } + }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "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==", + "dev": true, + "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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ssh-remote-port-forward": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/ssh-remote-port-forward/-/ssh-remote-port-forward-1.0.4.tgz", + "integrity": "sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ssh2": "^0.5.48", + "ssh2": "^1.4.0" + } + }, + "node_modules/ssh-remote-port-forward/node_modules/@types/ssh2": { + "version": "0.5.52", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.52.tgz", + "integrity": "sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2-streams": "*" + } + }, + "node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, + "node_modules/streamx": { + "version": "2.28.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", + "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", + "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", + "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/testcontainers": { + "version": "10.28.0", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.28.0.tgz", + "integrity": "sha512-1fKrRRCsgAQNkarjHCMKzBKXSJFmzNTiTbhb5E/j5hflRXChEtHvkefjaHlgkNUjfw92/Dq8LTgwQn6RDBFbMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "@types/dockerode": "^3.3.35", + "archiver": "^7.0.1", + "async-lock": "^1.4.1", + "byline": "^5.0.0", + "debug": "^4.3.5", + "docker-compose": "^0.24.8", + "dockerode": "^4.0.5", + "get-port": "^7.1.0", + "proper-lockfile": "^4.1.2", + "properties-reader": "^2.3.0", + "ssh-remote-port-forward": "^1.0.4", + "tar-fs": "^3.0.7", + "tmp": "^0.2.3", + "undici": "^5.29.0" + } + }, + "node_modules/testcontainers/node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tsx": { + "version": "4.23.15", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz", + "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + } + } +} diff --git a/profiling/package.json b/profiling/package.json new file mode 100644 index 0000000..67db1fd --- /dev/null +++ b/profiling/package.json @@ -0,0 +1,25 @@ +{ + "name": "notifkit-profiling", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "start": "tsx src/server.ts", + "test:load": "tsx tests/load.test.ts", + "test:scaling": "tsx tests/scaling.test.ts", + "test:distributed": "tsx tests/distributed-scaling.test.ts", + "test:money": "tsx tests/money-scaling.test.ts", + "test:cpu": "tsx tests/scaling.test.ts", + "test": "tsx tests/load.test.ts" + }, + "dependencies": { + "@notifkit/provider-console": "file:../packages/provider-console", + "notifkit": "file:.." + }, + "devDependencies": { + "@types/node": "^22.0.0", + "testcontainers": "^10.18.0", + "tsx": "^4.19.3", + "typescript": "^5.8.3" + } +} diff --git a/profiling/src/server.ts b/profiling/src/server.ts new file mode 100644 index 0000000..8d0bfed --- /dev/null +++ b/profiling/src/server.ts @@ -0,0 +1,107 @@ +import { + NotifkitServer, + type Transport, + type NotificationDispatchedPayload, + type DeliveryResult, +} from "notifkit"; + +const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000; +const workerConcurrency = process.env.WORKER_CONCURRENCY + ? parseInt(process.env.WORKER_CONCURRENCY, 10) + : 50; +function parseLatency(val?: string): { min: number; max: number } { + if (!val) return { min: 0, max: 0 }; + if (val.includes("-")) { + const [minStr, maxStr] = val.split("-"); + const min = parseInt(minStr!, 10) || 0; + const max = parseInt(maxStr!, 10) || min; + return { min, max }; + } + const exact = parseInt(val, 10) || 0; + return { min: exact, max: exact }; +} + +const latencyRange = parseLatency(process.env.PROVIDER_LATENCY_MS); + +class ProfilingTransport implements Transport { + readonly channel = "email"; + readonly limits = { limit: 1_000_000, windowSeconds: 1 }; + + async send(task: NotificationDispatchedPayload): Promise { + if (latencyRange.max > 0) { + const delay = + latencyRange.min === latencyRange.max + ? latencyRange.min + : Math.floor(Math.random() * (latencyRange.max - latencyRange.min + 1)) + + latencyRange.min; + await new Promise((resolve) => setTimeout(resolve, delay)); + } + + const providerMessageId = `prof-${Date.now()}`; + const requestTime = (task.templateVariables as Record | undefined) + ?.requestTime; + const latency = typeof requestTime === "number" ? Date.now() - requestTime : 0; + + console.log( + JSON.stringify({ + taskId: task.taskId, + channel: this.channel, + destination: task.destination, + providerMessageId, + latency, + msg: "push delivered (console transport)", + }), + ); + + return { success: true, providerMessageId }; + } +} + +import http from "node:http"; + +const rawServices = process.env.SERVICES?.trim(); +const services: ( + "api" | "enricher" | "engine" | "delivery" | "scheduler" | "ai" | "workflow" | "events" +)[] = + rawServices && rawServices !== "all" + ? (rawServices.split(",").map((s) => s.trim()) as ( + "api" | "enricher" | "engine" | "delivery" | "scheduler" + )[]) + : ["api", "enricher", "engine", "delivery", "scheduler"]; + +const server = new NotifkitServer({ + services, + redisUrl: process.env.REDIS_URL || "redis://localhost:6379", + databaseUrl: process.env.DATABASE_URL || "postgres://notifkit:password@localhost:5432/notifkit", + logLevel: (process.env.LOG_LEVEL as "info" | "warn" | "error" | "debug") || "info", + autoMigrate: false, + port: PORT, + workerConcurrency, + providers: [new ProfilingTransport()], +}); + +server + .start() + .then(() => { + console.log( + `๐Ÿš€ Profiling Server started on port ${PORT} [services: ${services.join(",")}] (concurrency: ${workerConcurrency})`, + ); + + // If API is not enabled on this worker node, expose a lightweight health responder so orchestration succeeds + if (!services.includes("api")) { + const healthServer = http.createServer((req, res) => { + if (req.url === "/health") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ status: "ok", services, isWorkerOnly: true })); + } else { + res.writeHead(404); + res.end(); + } + }); + healthServer.listen(PORT, "0.0.0.0"); + } + }) + .catch((err) => { + console.error("Failed to start profiling server:", err); + process.exit(1); + }); diff --git a/profiling/tests/distributed-load.test.ts b/profiling/tests/distributed-load.test.ts new file mode 100644 index 0000000..5071a26 --- /dev/null +++ b/profiling/tests/distributed-load.test.ts @@ -0,0 +1,376 @@ +import { DockerComposeEnvironment } from "testcontainers"; +import { createDatabase, runMigrations } from "notifkit"; +import { setGlobalDispatcher, Agent } from "undici"; + +// Configure high-concurrency connection pool for client benchmark +setGlobalDispatcher( + new Agent({ + connections: 300, + pipelining: 1, + keepAliveTimeout: 30000, + keepAliveMaxTimeout: 60000, + }), +); + +export interface DistributedLoadTestOptions { + serverCount?: number; + durationSeconds?: number; + concurrency?: number; + adminApiKey?: string; + dbPort?: number; + serverCpus?: string; + serverMemory?: string; + workerConcurrency?: number; + dbCpus?: string; + dbMemory?: string; + redisCpus?: string; + redisMemory?: string; + dbMaxConnections?: number; + providerLatencyMs?: number | string; + serverServices?: string[]; + quiet?: boolean; +} + +export interface DistributedBenchmarkResult { + serverCount: number; + serverCpus: string; + serverMemory: string; + workerConcurrency: number; + durationSec: number; + concurrency: number; + requestsSent: number; + successCount: number; + failCount: number; + ingestionThroughput: number; + firstDelivOffset: number; + lastDelivOffset: number; + deliverySpanSec: number; + activeDeliveryRate: number; + totalDeliveryRate: number; + apiP50: number; + apiP95: number; + apiP99: number; + deliveryP50: number; + deliveryP95: number; + deliveryP99: number; + totalDelivered: number; +} + +export async function runDistributedBenchmark( + options: DistributedLoadTestOptions = {}, +): Promise { + const serverCount = options.serverCount ?? 2; + const durationSec = options.durationSeconds ?? 10; + const concurrency = options.concurrency ?? serverCount * 75; + const adminApiKey = options.adminApiKey ?? "perf-admin-key"; + const dbPort = options.dbPort ?? 35432; + const serverCpus = options.serverCpus ?? "1.0"; + const serverMemory = options.serverMemory ?? "1G"; + const workerConcurrency = options.workerConcurrency ?? 100; + const providerLatency = options.providerLatencyMs ?? process.env.PROVIDER_LATENCY_MS ?? "200-300"; + + const composeEnv: Record = { + SERVER_CPUS: serverCpus, + SERVER_MEMORY: serverMemory, + WORKER_CONCURRENCY: String(workerConcurrency), + PROVIDER_LATENCY_MS: String(providerLatency), + }; + if (options.dbCpus) composeEnv.DB_CPUS = options.dbCpus; + if (options.dbMemory) composeEnv.DB_MEMORY = options.dbMemory; + if (options.redisCpus) composeEnv.REDIS_CPUS = options.redisCpus; + if (options.redisMemory) composeEnv.REDIS_MEMORY = options.redisMemory; + if (options.dbMaxConnections) composeEnv.DB_MAX_CONNECTIONS = String(options.dbMaxConnections); + + if (options.serverServices) { + options.serverServices.forEach((svc, idx) => { + composeEnv[`SERVER${idx + 1}_SERVICES`] = svc; + }); + } + + const services = [ + "db", + "redis", + ...Array.from({ length: serverCount }, (_, i) => `server${i + 1}`), + ]; + + console.log( + `๐Ÿš€ Booting distributed profiling environment (${serverCount} Server Instances: ${serverCpus} vCPU each, ${serverMemory} RAM | DB: ${composeEnv.DB_CPUS ?? "2.0"} vCPU | Redis: ${composeEnv.REDIS_CPUS ?? "1.5"} vCPU | Provider Latency: ${providerLatency}ms)...`, + ); + + const environment = await new DockerComposeEnvironment(".", "docker-compose.distributed.yml") + .withEnvironment(composeEnv) + .withBuild() + .up(services); + + const serverPorts = Array.from({ length: serverCount }, (_, i) => 35678 + i); + const serverUrls = serverPorts.map((p) => `http://localhost:${p}`); + const dbUrl = `postgres://notifkit:password@localhost:${dbPort}/notifkit`; + + const apiLatencies: number[] = []; + const deliveryLatencies: number[] = []; + + console.log(`โณ Waiting for ${serverCount} server instances health checks...`); + const apiUrls: string[] = []; + for (const url of serverUrls) { + let ready = false; + while (!ready) { + try { + const res = await fetch(`${url}/health`); + if (res.ok) { + ready = true; + const body = (await res.json().catch(() => ({}))) as { isWorkerOnly?: boolean }; + if (!body.isWorkerOnly) { + apiUrls.push(url); + } + } + } catch { + await new Promise((r) => setTimeout(r, 400)); + } + } + } + + const targetApiUrls = apiUrls.length > 0 ? apiUrls : [serverUrls[0]!]; + + let firstDeliveryTimestamp: number | null = null; + let lastDeliveryTimestamp: number | null = null; + + // Hook into container logs of all active servers + for (let i = 1; i <= serverCount; i++) { + const serverContainer = environment.getContainer(`server${i}-1`); + const stream = await serverContainer.logs(); + let logBuffer = ""; + stream.on("data", (chunk) => { + logBuffer += chunk.toString(); + const lines = logBuffer.split("\n"); + logBuffer = lines.pop() || ""; + for (const line of lines) { + if (line.includes("push delivered (console transport)")) { + const now = Date.now(); + if (firstDeliveryTimestamp === null) { + firstDeliveryTimestamp = now; + } + lastDeliveryTimestamp = now; + + try { + const parsed = JSON.parse(line); + deliveryLatencies.push(typeof parsed.latency === "number" ? parsed.latency : 0); + } catch { + deliveryLatencies.push(0); + } + } + } + }); + } + + console.log("๐Ÿ› ๏ธ Running database migrations..."); + const dbData = createDatabase({ url: dbUrl }); + await runMigrations(dbData.db); + + console.log("๐Ÿ—๏ธ Provisioning benchmark project and templates..."); + const projectRes = await fetch(`${targetApiUrls[0]}/v1/projects`, { + method: "POST", + headers: { + Authorization: `Bearer ${adminApiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ name: "Profiling Project" }), + }); + const project = await projectRes.json(); + const projectApiKey = project.apiKey; + + await dbData.sql`UPDATE projects SET rate_limit_rpm = 10000000`; + + await fetch(`${targetApiUrls[0]}/v1/templates`, { + method: "PUT", + headers: { + Authorization: `Bearer ${projectApiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + templates: [ + { + id: "perf-email", + channel: "email", + topic: ["transactional"], + content: { + subject: "Perf Test Email", + html: "

Hello {{name}}

", + }, + }, + ], + }), + }); + + await fetch(`${targetApiUrls[0]}/v1/users`, { + method: "POST", + headers: { + Authorization: `Bearer ${projectApiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + id: "perf-user-1", + email: ["perf@example.com"], + }), + }); + + console.log( + `\n๐Ÿ”ฅ Starting Distributed Load Generation: ${concurrency} virtual users across ${targetApiUrls.length} API ingress node(s) for ${durationSec}s`, + ); + + let requestsSent = 0; + let successCount = 0; + let failCount = 0; + + const startTime = Date.now(); + const endTime = startTime + durationSec * 1000; + + async function worker(workerIndex: number) { + const targetUrl = targetApiUrls[workerIndex % targetApiUrls.length]!; + while (Date.now() < endTime) { + requestsSent++; + try { + const reqStart = Date.now(); + const res = await fetch(`${targetUrl}/v1/notify`, { + method: "POST", + headers: { + Authorization: `Bearer ${projectApiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + user: "perf-user-1", + template: "perf-email", + channels: ["email"], + priority: "critical", + data: { name: "Load Tester", requestTime: reqStart }, + }), + }); + + apiLatencies.push(Date.now() - reqStart); + + if (res.status === 202) { + successCount++; + } else { + failCount++; + } + } catch { + failCount++; + } + } + } + + const workers = Array.from({ length: concurrency }).map((_, i) => worker(i)); + await Promise.all(workers); + + const activeWindowSec = durationSec; + const ingestionThroughput = (requestsSent / activeWindowSec).toFixed(2); + + console.log( + `\nโณ Load generation finished (${requestsSent} sent in ${activeWindowSec}s window, ${successCount} accepted). Awaiting queue flush...`, + ); + + const flushStart = Date.now(); + let lastLogged = 0; + + while (deliveryLatencies.length < successCount) { + if (Date.now() - flushStart > 600000) { + console.log( + `โš ๏ธ Flush timeout! Delivered ${deliveryLatencies.length}/${successCount} messages.`, + ); + break; + } + if (Date.now() - lastLogged > 2000) { + lastLogged = Date.now(); + const pct = ((deliveryLatencies.length / successCount) * 100).toFixed(1); + const rate = (deliveryLatencies.length / ((Date.now() - startTime) / 1000)).toFixed(0); + console.log( + `โณ Flushing: [${deliveryLatencies.length} / ${successCount}] (${pct}%) ~ ${rate} delivered/sec`, + ); + } + await new Promise((r) => setTimeout(r, 200)); + } + + const totalElapsed = (Date.now() - startTime) / 1000; + const firstDelivOffset = firstDeliveryTimestamp ? (firstDeliveryTimestamp - startTime) / 1000 : 0; + const lastDelivOffset = lastDeliveryTimestamp + ? (lastDeliveryTimestamp - startTime) / 1000 + : totalElapsed; + const deliverySpanSec = + firstDeliveryTimestamp && + lastDeliveryTimestamp && + lastDeliveryTimestamp > firstDeliveryTimestamp + ? (lastDeliveryTimestamp - firstDeliveryTimestamp) / 1000 + : totalElapsed; + + const activeDeliveryRate = (deliveryLatencies.length / deliverySpanSec).toFixed(2); + const totalDeliveryRate = (deliveryLatencies.length / totalElapsed).toFixed(2); + + function percentile(arr: number[], p: number) { + if (arr.length === 0) return 0; + const sorted = [...arr].sort((a, b) => a - b); + const idx = Math.ceil((p / 100) * sorted.length) - 1; + return sorted[idx]; + } + + console.log("\n================================="); + console.log(" ๐Ÿ“Š LOAD TEST RESULTS ๐Ÿ“Š "); + console.log("================================="); + console.log(`Topology: ${serverCount} Distributed Servers`); + console.log(`Generation Window: ${activeWindowSec}s (${concurrency} virtual users)`); + console.log(`Total Ingestion: ${requestsSent} requests (${ingestionThroughput} req/sec)`); + console.log( + `Accepted (202): ${successCount} (${((successCount / requestsSent) * 100).toFixed(1)}%)`, + ); + console.log(`Failed / Dropped: ${failCount}`); + console.log("---------------------------------"); + console.log(`First Msg Delivered: +${firstDelivOffset.toFixed(2)}s from test start`); + console.log(`Last Msg Delivered: +${lastDelivOffset.toFixed(2)}s from test start`); + console.log(`Delivery Span (1->N): ${deliverySpanSec.toFixed(2)}s`); + console.log(`Active Delivery Rate: ${activeDeliveryRate} msgs/sec`); + console.log( + `Total Wall Drain Rate:${totalDeliveryRate} msgs/sec (${totalElapsed.toFixed(2)}s total)`, + ); + console.log("---------------------------------"); + const apiP50 = percentile(apiLatencies, 50); + const apiP95 = percentile(apiLatencies, 95); + const apiP99 = percentile(apiLatencies, 99); + const deliveryP50 = percentile(deliveryLatencies, 50); + const deliveryP95 = percentile(deliveryLatencies, 95); + const deliveryP99 = percentile(deliveryLatencies, 99); + + console.log(`API Latency: p50: ${apiP50}ms | p95: ${apiP95}ms | p99: ${apiP99}ms`); + console.log( + `Delivery Latency: p50: ${deliveryP50}ms | p95: ${deliveryP95}ms | p99: ${deliveryP99}ms`, + ); + console.log(`Total Delivered: ${deliveryLatencies.length} / ${successCount}`); + console.log("=================================\n"); + + console.log("๐Ÿงน Tearing down test environment..."); + await dbData.sql.end(); + await environment.down(); + console.log("โœ… Benchmark completed successfully."); + + return { + serverCount, + serverCpus, + serverMemory, + workerConcurrency, + durationSec: activeWindowSec, + concurrency, + requestsSent, + successCount, + failCount, + ingestionThroughput: Number(ingestionThroughput), + firstDelivOffset: Number(firstDelivOffset.toFixed(2)), + lastDelivOffset: Number(lastDelivOffset.toFixed(2)), + deliverySpanSec: Number(deliverySpanSec.toFixed(2)), + activeDeliveryRate: Number(activeDeliveryRate), + totalDeliveryRate: Number(totalDeliveryRate), + apiP50, + apiP95, + apiP99, + deliveryP50, + deliveryP95, + deliveryP99, + totalDelivered: deliveryLatencies.length, + }; +} diff --git a/profiling/tests/distributed-scaling.test.ts b/profiling/tests/distributed-scaling.test.ts new file mode 100644 index 0000000..4a6baad --- /dev/null +++ b/profiling/tests/distributed-scaling.test.ts @@ -0,0 +1,190 @@ +import { + runDistributedBenchmark, + type DistributedBenchmarkResult, +} from "./distributed-load.test.js"; + +export interface DistributedTier { + serverCount: number; + serverCpus: string; + serverMemory: string; + dbCpus: string; + dbMemory: string; + redisCpus: string; + redisMemory: string; + workerConcurrency: number; + label: string; +} + +const DEFAULT_DISTRIBUTED_TIERS: DistributedTier[] = [ + { + serverCount: 1, + serverCpus: "1.0", + serverMemory: "1G", + dbCpus: "1.0", + dbMemory: "1G", + redisCpus: "0.5", + redisMemory: "512M", + workerConcurrency: 50, + label: "1 Server (1.0 vCPU)", + }, + { + serverCount: 2, + serverCpus: "1.0", + serverMemory: "1G", + dbCpus: "1.0", + dbMemory: "1G", + redisCpus: "0.5", + redisMemory: "512M", + workerConcurrency: 50, + label: "2 Distributed Servers (2x 1.0 vCPU)", + }, + { + serverCount: 3, + serverCpus: "1.0", + serverMemory: "1G", + dbCpus: "1.0", + dbMemory: "1G", + redisCpus: "0.5", + redisMemory: "512M", + workerConcurrency: 50, + label: "3 Distributed Servers (3x 1.0 vCPU)", + }, +]; + +function parseArgs() { + const args = process.argv.slice(2); + let durationSeconds = 10; + let customConcurrency: number | null = null; + let tiers = DEFAULT_DISTRIBUTED_TIERS; + + for (const arg of args) { + if (arg.startsWith("--duration=")) { + durationSeconds = parseInt(arg.split("=")[1]!, 10); + } else if (arg.startsWith("--concurrency=")) { + customConcurrency = parseInt(arg.split("=")[1]!, 10); + } else if (arg === "--quick") { + durationSeconds = 5; + } else if (arg.startsWith("--servers=")) { + const counts = arg + .split("=")[1]! + .split(",") + .map((s) => parseInt(s.trim(), 10)); + tiers = counts.map((count) => ({ + serverCount: count, + serverCpus: "1.0", + serverMemory: "1G", + dbCpus: "2.0", + dbMemory: "2G", + redisCpus: "1.5", + redisMemory: "1G", + workerConcurrency: 100, + label: `${count} Server${count > 1 ? "s" : ""} (${count}x 1.0 vCPU)`, + })); + } + } + + return { durationSeconds, customConcurrency, tiers }; +} + +function printSummaryTable( + results: Array<{ tier: DistributedTier; res: DistributedBenchmarkResult }>, +) { + console.log("\n" + "=".repeat(140)); + console.log(" ๐Ÿ“Š NOTIFKIT DISTRIBUTED SCALING BENCHMARK SUMMARY ๐Ÿ“Š"); + console.log("=".repeat(140)); + + const headers = [ + "Cluster Topology", + "Clients", + "Total vCPU", + "Ingestion", + "Active Deliv", + "Wall Drain", + "API p95", + "Deliv p95", + "Speedup", + ]; + + const colWidths = [36, 9, 12, 14, 15, 14, 11, 12, 10]; + + const formatRow = (cols: string[]) => + cols.map((col, idx) => col.padEnd(colWidths[idx]!)).join(" | "); + + console.log(formatRow(headers)); + console.log("-".repeat(140)); + + const baselineDelivery = results[0]?.res.activeDeliveryRate || 1; + + for (const { tier, res } of results) { + const totalCpu = `${(tier.serverCount * parseFloat(tier.serverCpus)).toFixed(1)} vCPU`; + const speedup = (res.activeDeliveryRate / baselineDelivery).toFixed(2) + "x"; + + const row = [ + tier.label, + `${res.concurrency} VUs`, + totalCpu, + `${res.ingestionThroughput} req/s`, + `${res.activeDeliveryRate} msg/s`, + `${res.totalDeliveryRate} msg/s`, + `${res.apiP95}ms`, + `${res.deliveryP95}ms`, + speedup, + ]; + console.log(formatRow(row)); + } + + console.log("=".repeat(140) + "\n"); +} + +async function main() { + const { durationSeconds, customConcurrency, tiers } = parseArgs(); + + console.log("================================================================================"); + console.log(" ๐Ÿš€ STARTING NOTIFKIT DISTRIBUTED HORIZONTAL SCALING BENCHMARK "); + console.log("================================================================================"); + console.log(`Window: ${durationSeconds}s per tier | Auto Client Concurrency: 150 VUs/server`); + console.log(`Tiers to test: ${tiers.map((t) => t.label).join(" โžœ ")}\n`); + + const results: Array<{ tier: DistributedTier; res: DistributedBenchmarkResult }> = []; + + for (let i = 0; i < tiers.length; i++) { + const tier = tiers[i]!; + const tierConcurrency = customConcurrency ?? tier.serverCount * 150; + console.log( + `\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>`, + ); + console.log( + `[Tier ${i + 1}/${tiers.length}] Benchmarking ${tier.label} (${tierConcurrency} Virtual Users)`, + ); + console.log( + `>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n`, + ); + + const res = await runDistributedBenchmark({ + serverCount: tier.serverCount, + serverCpus: tier.serverCpus, + serverMemory: tier.serverMemory, + dbCpus: tier.dbCpus, + dbMemory: tier.dbMemory, + redisCpus: tier.redisCpus, + redisMemory: tier.redisMemory, + workerConcurrency: tier.workerConcurrency, + durationSeconds, + concurrency: tierConcurrency, + }); + + results.push({ tier, res }); + + if (i < tiers.length - 1) { + console.log("โณ Cooling down for 4s before next tier..."); + await new Promise((r) => setTimeout(r, 4000)); + } + } + + printSummaryTable(results); +} + +main().catch((err) => { + console.error("Distributed benchmark failed:", err); + process.exit(1); +}); diff --git a/profiling/tests/load.test.ts b/profiling/tests/load.test.ts new file mode 100644 index 0000000..c2384bc --- /dev/null +++ b/profiling/tests/load.test.ts @@ -0,0 +1,340 @@ +import { DockerComposeEnvironment } from "testcontainers"; +import { createDatabase, runMigrations } from "notifkit"; + +export interface LoadTestOptions { + durationSeconds?: number; + concurrency?: number; + adminApiKey?: string; + port?: number; + dbPort?: number; + serverCpus?: string; + serverMemory?: string; + workerConcurrency?: number; + dbCpus?: string; + dbMemory?: string; + redisCpus?: string; + redisMemory?: string; + providerLatencyMs?: number | string; + quiet?: boolean; +} + +export interface BenchmarkResult { + serverCpus: string; + serverMemory: string; + workerConcurrency: number; + durationSec: number; + concurrency: number; + requestsSent: number; + successCount: number; + failCount: number; + ingestionThroughput: number; + firstDelivOffset: number; + lastDelivOffset: number; + deliverySpanSec: number; + activeDeliveryRate: number; + totalDeliveryRate: number; + apiP50: number; + apiP95: number; + apiP99: number; + deliveryP50: number; + deliveryP95: number; + deliveryP99: number; + totalDelivered: number; +} + +export async function runLoadBenchmark(options: LoadTestOptions = {}): Promise { + const durationSec = options.durationSeconds ?? 10; + const concurrency = options.concurrency ?? 50; + const adminApiKey = options.adminApiKey ?? "perf-admin-key"; + const port = options.port ?? 35678; + const dbPort = options.dbPort ?? 35432; + const serverCpus = options.serverCpus ?? "1.0"; + const serverMemory = options.serverMemory ?? "1G"; + const workerConcurrency = options.workerConcurrency ?? 50; + const providerLatency = options.providerLatencyMs ?? process.env.PROVIDER_LATENCY_MS ?? 0; + + const composeEnv: Record = { + SERVER_CPUS: serverCpus, + SERVER_MEMORY: serverMemory, + WORKER_CONCURRENCY: String(workerConcurrency), + PROVIDER_LATENCY_MS: String(providerLatency), + }; + if (options.dbCpus) composeEnv.DB_CPUS = options.dbCpus; + if (options.dbMemory) composeEnv.DB_MEMORY = options.dbMemory; + if (options.redisCpus) composeEnv.REDIS_CPUS = options.redisCpus; + if (options.redisMemory) composeEnv.REDIS_MEMORY = options.redisMemory; + + console.log( + `๐Ÿš€ Booting profiling environment (Server: ${serverCpus} vCPU, ${serverMemory} RAM | DB: ${composeEnv.DB_CPUS ?? "0.5"} vCPU, ${composeEnv.DB_MEMORY ?? "512M"} | Redis: ${composeEnv.REDIS_CPUS ?? "0.5"} vCPU, ${composeEnv.REDIS_MEMORY ?? "256M"}${providerLatency && providerLatency !== "0" ? ` | Provider Latency: ${providerLatency}ms` : ""})...`, + ); + const environment = await new DockerComposeEnvironment(".", "docker-compose.yml") + .withEnvironment(composeEnv) + .withBuild() + .up(); + + const baseUrl = `http://localhost:${port}`; + const dbUrl = `postgres://notifkit:password@localhost:${dbPort}/notifkit`; + + const apiLatencies: number[] = []; + const deliveryLatencies: number[] = []; + + console.log("โณ Waiting for API health check..."); + let ready = false; + while (!ready) { + try { + const res = await fetch(`${baseUrl}/health`); + if (res.ok) ready = true; + } catch { + await new Promise((r) => setTimeout(r, 500)); + } + } + + let firstDeliveryTimestamp: number | null = null; + let lastDeliveryTimestamp: number | null = null; + + // Hook into container logs to capture delivery latency + const serverContainer = environment.getContainer("server-1"); + const stream = await serverContainer.logs(); + let logBuffer = ""; + stream.on("data", (chunk) => { + logBuffer += chunk.toString(); + const lines = logBuffer.split("\n"); + logBuffer = lines.pop() || ""; + for (const line of lines) { + if (line.includes("push delivered (console transport)")) { + const now = Date.now(); + if (firstDeliveryTimestamp === null) { + firstDeliveryTimestamp = now; + } + lastDeliveryTimestamp = now; + + try { + const parsed = JSON.parse(line); + deliveryLatencies.push(typeof parsed.latency === "number" ? parsed.latency : 0); + } catch { + deliveryLatencies.push(0); + } + } + } + }); + + console.log("๐Ÿ› ๏ธ Running database migrations..."); + const dbData = createDatabase({ url: dbUrl }); + await runMigrations(dbData.db); + + console.log("๐Ÿ—๏ธ Provisioning benchmark project and templates..."); + const projectRes = await fetch(`${baseUrl}/v1/projects`, { + method: "POST", + headers: { + Authorization: `Bearer ${adminApiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ name: "Profiling Project" }), + }); + const project = await projectRes.json(); + const projectApiKey = project.apiKey; + + // Uncap API rate limits for the project + await dbData.sql`UPDATE projects SET rate_limit_rpm = 1000000`; + + await fetch(`${baseUrl}/v1/templates`, { + method: "PUT", + headers: { + Authorization: `Bearer ${projectApiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + templates: [ + { + id: "perf-email", + channel: "email", + topic: ["transactional"], + content: { + subject: "Perf Test Email", + html: "

Hello {{name}}

", + }, + }, + ], + }), + }); + + await fetch(`${baseUrl}/v1/users`, { + method: "POST", + headers: { + Authorization: `Bearer ${projectApiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + id: "perf-user-1", + email: ["perf@example.com"], + }), + }); + + console.log(`\n๐Ÿ”ฅ Starting Load Generation: ${concurrency} virtual users for ${durationSec}s`); + + let requestsSent = 0; + let successCount = 0; + let failCount = 0; + + const startTime = Date.now(); + const endTime = startTime + durationSec * 1000; + + async function worker() { + while (Date.now() < endTime) { + requestsSent++; + try { + const reqStart = Date.now(); + const res = await fetch(`${baseUrl}/v1/notify`, { + method: "POST", + headers: { + Authorization: `Bearer ${projectApiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + user: "perf-user-1", + template: "perf-email", + channels: ["email"], + priority: "critical", + data: { name: "Load Tester", requestTime: reqStart }, + }), + }); + + apiLatencies.push(Date.now() - reqStart); + + if (res.status === 202) { + successCount++; + } else { + failCount++; + } + } catch { + failCount++; + } + } + } + + const workers = Array.from({ length: concurrency }).map(() => worker()); + await Promise.all(workers); + + const _genWallTime = (Date.now() - startTime) / 1000; + const activeWindowSec = durationSec; + const ingestionThroughput = (requestsSent / activeWindowSec).toFixed(2); + + console.log( + `\nโณ Load generation finished (${requestsSent} sent in ${activeWindowSec}s window, ${successCount} accepted). Awaiting queue flush...`, + ); + const flushStart = Date.now(); + let lastLogged = 0; + + while (deliveryLatencies.length < successCount) { + if (Date.now() - flushStart > 600000) { + console.log( + `โš ๏ธ Flush timeout! Delivered ${deliveryLatencies.length}/${successCount} messages.`, + ); + break; + } + if (Date.now() - lastLogged > 2000) { + lastLogged = Date.now(); + const pct = ((deliveryLatencies.length / successCount) * 100).toFixed(1); + const rate = (deliveryLatencies.length / ((Date.now() - startTime) / 1000)).toFixed(0); + console.log( + `โณ Flushing: [${deliveryLatencies.length} / ${successCount}] (${pct}%) ~ ${rate} delivered/sec`, + ); + } + await new Promise((r) => setTimeout(r, 200)); + } + + const totalElapsed = (Date.now() - startTime) / 1000; + const firstDelivOffset = firstDeliveryTimestamp ? (firstDeliveryTimestamp - startTime) / 1000 : 0; + const lastDelivOffset = lastDeliveryTimestamp + ? (lastDeliveryTimestamp - startTime) / 1000 + : totalElapsed; + const deliverySpanSec = + firstDeliveryTimestamp && + lastDeliveryTimestamp && + lastDeliveryTimestamp > firstDeliveryTimestamp + ? (lastDeliveryTimestamp - firstDeliveryTimestamp) / 1000 + : totalElapsed; + + const activeDeliveryRate = (deliveryLatencies.length / deliverySpanSec).toFixed(2); + const totalDeliveryRate = (deliveryLatencies.length / totalElapsed).toFixed(2); + + function percentile(arr: number[], p: number) { + if (arr.length === 0) return 0; + const sorted = [...arr].sort((a, b) => a - b); + const idx = Math.ceil((p / 100) * sorted.length) - 1; + return sorted[idx]; + } + + console.log("\n================================="); + console.log(" ๐Ÿ“Š LOAD TEST RESULTS ๐Ÿ“Š "); + console.log("================================="); + console.log(`Generation Window: ${activeWindowSec}s (${concurrency} virtual users)`); + console.log(`Total Ingestion: ${requestsSent} requests (${ingestionThroughput} req/sec)`); + console.log( + `Accepted (202): ${successCount} (${((successCount / requestsSent) * 100).toFixed(1)}%)`, + ); + console.log(`Failed / Dropped: ${failCount}`); + console.log("---------------------------------"); + console.log(`First Msg Delivered: +${firstDelivOffset.toFixed(2)}s from test start`); + console.log(`Last Msg Delivered: +${lastDelivOffset.toFixed(2)}s from test start`); + console.log(`Delivery Span (1->N): ${deliverySpanSec.toFixed(2)}s`); + console.log(`Active Delivery Rate: ${activeDeliveryRate} msgs/sec`); + console.log( + `Total Wall Drain Rate:${totalDeliveryRate} msgs/sec (${totalElapsed.toFixed(2)}s total)`, + ); + console.log("---------------------------------"); + const apiP50 = percentile(apiLatencies, 50); + const apiP95 = percentile(apiLatencies, 95); + const apiP99 = percentile(apiLatencies, 99); + const deliveryP50 = percentile(deliveryLatencies, 50); + const deliveryP95 = percentile(deliveryLatencies, 95); + const deliveryP99 = percentile(deliveryLatencies, 99); + + console.log(`API Latency: p50: ${apiP50}ms | p95: ${apiP95}ms | p99: ${apiP99}ms`); + console.log( + `Delivery Latency: p50: ${deliveryP50}ms | p95: ${deliveryP95}ms | p99: ${deliveryP99}ms`, + ); + console.log(`Total Delivered: ${deliveryLatencies.length} / ${successCount}`); + console.log("=================================\n"); + + console.log("๐Ÿงน Tearing down test environment..."); + await dbData.sql.end(); + await environment.down(); + console.log("โœ… Benchmark completed successfully."); + + return { + serverCpus, + serverMemory, + workerConcurrency, + durationSec: activeWindowSec, + concurrency, + requestsSent, + successCount, + failCount, + ingestionThroughput: Number(ingestionThroughput), + firstDelivOffset: Number(firstDelivOffset.toFixed(2)), + lastDelivOffset: Number(lastDelivOffset.toFixed(2)), + deliverySpanSec: Number(deliverySpanSec.toFixed(2)), + activeDeliveryRate: Number(activeDeliveryRate), + totalDeliveryRate: Number(totalDeliveryRate), + apiP50, + apiP95, + apiP99, + deliveryP50, + deliveryP95, + deliveryP99, + totalDelivered: deliveryLatencies.length, + }; +} + +// Execute when run directly via CLI +const isDirectRun = + process.argv[1] && + (process.argv[1].endsWith("load.test.ts") || process.argv[1].endsWith("load.test.js")); + +if (isDirectRun) { + runLoadBenchmark().catch((err) => { + console.error("Benchmark failed:", err); + process.exit(1); + }); +} diff --git a/profiling/tests/money-scaling.test.ts b/profiling/tests/money-scaling.test.ts new file mode 100644 index 0000000..ed74886 --- /dev/null +++ b/profiling/tests/money-scaling.test.ts @@ -0,0 +1,214 @@ +import { + runDistributedBenchmark, + type DistributedBenchmarkResult, +} from "./distributed-load.test.js"; + +export interface BudgetTier { + budget: string; + monthlyCost: number; + serverCount: number; + serverCpus: string; + serverMemory: string; + dbCpus: string; + dbMemory: string; + redisCpus: string; + redisMemory: string; + workerConcurrency: number; + dbMaxConnections: number; + concurrency: number; + description: string; + serverServices?: string[]; +} + +const DEFAULT_BUDGET_TIERS: BudgetTier[] = [ + { + budget: "$20/mo", + monthlyCost: 20, + serverCount: 1, + serverCpus: "1.0", + serverMemory: "1G", + dbCpus: "1.0", + dbMemory: "1G", + redisCpus: "0.5", + redisMemory: "512M", + workerConcurrency: 100, + dbMaxConnections: 5, + concurrency: 150, + description: "1x Monolith (API + Pipeline + Delivery)", + serverServices: ["api,enricher,engine,delivery,scheduler"], + }, + { + budget: "$40/mo", + monthlyCost: 40, + serverCount: 2, + serverCpus: "1.0", + serverMemory: "1G", + dbCpus: "2.0", + dbMemory: "2G", + redisCpus: "1.0", + redisMemory: "1G", + workerConcurrency: 200, + dbMaxConnections: 8, + concurrency: 300, + description: "1x API + 1x Worker (Enricher/Engine/Delivery)", + serverServices: ["api", "enricher,engine,delivery,scheduler"], + }, + { + budget: "$60/mo", + monthlyCost: 60, + serverCount: 3, + serverCpus: "1.0", + serverMemory: "1G", + dbCpus: "3.0", + dbMemory: "3G", + redisCpus: "1.5", + redisMemory: "1G", + workerConcurrency: 300, + dbMaxConnections: 10, + concurrency: 300, + description: "1x API + 1x Pipeline (Engine/Enrich) + 1x Delivery", + serverServices: ["api", "enricher,engine,scheduler", "delivery"], + }, +]; + +function parseArgs() { + const args = process.argv.slice(2); + let durationSeconds = 10; + let providerLatencyMs: number | string | undefined = process.env.PROVIDER_LATENCY_MS || "60-150"; + let tiers = DEFAULT_BUDGET_TIERS; + + for (const arg of args) { + if (arg.startsWith("--duration=")) { + durationSeconds = parseInt(arg.split("=")[1]!, 10); + } else if (arg === "--quick") { + durationSeconds = 5; + } else if (arg.startsWith("--latency=")) { + const val = arg.split("=")[1]!; + providerLatencyMs = val.includes("-") ? val : parseInt(val, 10); + } else if (arg.startsWith("--workers=")) { + const val = parseInt(arg.split("=")[1]!, 10); + tiers = tiers.map((t) => ({ ...t, workerConcurrency: val })); + } else if (arg.startsWith("--tiers=")) { + const requested = arg + .split("=")[1]! + .split(",") + .map((s) => s.trim()); + tiers = DEFAULT_BUDGET_TIERS.filter((t) => requested.includes(t.budget.replace("/mo", ""))); + } + } + + return { durationSeconds, providerLatencyMs, tiers }; +} + +function printSummaryTable(results: Array<{ tier: BudgetTier; res: DistributedBenchmarkResult }>) { + console.log("\n" + "=".repeat(150)); + console.log( + " ๐Ÿ’ฐ NOTIFKIT MONEY / BUDGET SCALING BENCHMARK SUMMARY ๐Ÿ’ฐ", + ); + console.log("=".repeat(150)); + + const headers = [ + "Budget Tier", + "Architecture Topology", + "Clients", + "Ingestion", + "Active Deliv", + "Wall Drain", + "API p95", + "Deliv p95", + "Cost Efficiency", + ]; + + const colWidths = [13, 44, 9, 14, 15, 14, 10, 11, 15]; + + const formatRow = (cols: string[]) => + cols.map((col, idx) => col.padEnd(colWidths[idx]!)).join(" | "); + + console.log(formatRow(headers)); + console.log("-".repeat(150)); + + for (const { tier, res } of results) { + const msgsPerDollar = (res.activeDeliveryRate / tier.monthlyCost).toFixed(1) + " msg/s/$"; + + const row = [ + tier.budget, + tier.description, + `${res.concurrency} VUs`, + `${res.ingestionThroughput} req/s`, + `${res.activeDeliveryRate} msg/s`, + `${res.totalDeliveryRate} msg/s`, + `${res.apiP95}ms`, + `${res.deliveryP95}ms`, + msgsPerDollar, + ]; + console.log(formatRow(row)); + } + + console.log("=".repeat(150) + "\n"); +} + +async function main() { + const { durationSeconds, providerLatencyMs, tiers } = parseArgs(); + + console.log( + "==========================================================================================", + ); + console.log( + " ๐Ÿ’ฐ STARTING NOTIFKIT MONEY / BUDGET SCALING BENCHMARK ", + ); + console.log( + "==========================================================================================", + ); + console.log(`Window: ${durationSeconds}s per tier | Measuring: $20/mo โžœ $40/mo โžœ $60/mo`); + console.log(`Tiers to test: ${tiers.map((t) => t.budget).join(" โžœ ")}`); + if (providerLatencyMs && providerLatencyMs !== "0") { + console.log(`Mock Provider Latency: ${providerLatencyMs}ms`); + } + console.log( + "==========================================================================================\n", + ); + + const results: Array<{ tier: BudgetTier; res: DistributedBenchmarkResult }> = []; + + for (let i = 0; i < tiers.length; i++) { + const tier = tiers[i]!; + console.log( + `\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>`, + ); + console.log(`[Tier ${i + 1}/${tiers.length}] Benchmarking Budget Tier: ${tier.budget}`); + console.log(`Topology: ${tier.description} (${tier.concurrency} Virtual Users)`); + console.log( + `>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n`, + ); + + const res = await runDistributedBenchmark({ + serverCount: tier.serverCount, + serverCpus: tier.serverCpus, + serverMemory: tier.serverMemory, + dbCpus: tier.dbCpus, + dbMemory: tier.dbMemory, + redisCpus: tier.redisCpus, + redisMemory: tier.redisMemory, + workerConcurrency: tier.workerConcurrency, + dbMaxConnections: tier.dbMaxConnections, + serverServices: tier.serverServices, + providerLatencyMs, + durationSeconds, + concurrency: tier.concurrency, + }); + + results.push({ tier, res }); + + if (i < tiers.length - 1) { + console.log("โณ Cooling down for 4s before next tier..."); + await new Promise((r) => setTimeout(r, 4000)); + } + } + + printSummaryTable(results); +} + +main().catch((err) => { + console.error("Money scaling benchmark failed:", err); + process.exit(1); +}); diff --git a/profiling/tests/scaling.test.ts b/profiling/tests/scaling.test.ts new file mode 100644 index 0000000..b280a17 --- /dev/null +++ b/profiling/tests/scaling.test.ts @@ -0,0 +1,312 @@ +import { runLoadBenchmark, type BenchmarkResult } from "./load.test.js"; + +export interface CpuScalingTier { + cpus: string; + memory: string; + dbCpus: string; + dbMemory: string; + redisCpus: string; + redisMemory: string; + workerConcurrency?: number; + label: string; +} + +export interface ScalingBenchmarkOptions { + tiers?: CpuScalingTier[]; + durationSeconds?: number; + concurrency?: number; + providerLatencyMs?: number | string; +} + +const DEFAULT_TIERS: CpuScalingTier[] = [ + { + cpus: "0.5", + memory: "1G", + dbCpus: "0.5", + dbMemory: "512M", + redisCpus: "0.5", + redisMemory: "256M", + workerConcurrency: 25, + label: "0.5 vCPU", + }, + { + cpus: "1.0", + memory: "2G", + dbCpus: "1.0", + dbMemory: "1G", + redisCpus: "0.5", + redisMemory: "512M", + workerConcurrency: 50, + label: "1.0 vCPU", + }, + { + cpus: "2.0", + memory: "4G", + dbCpus: "1.5", + dbMemory: "2G", + redisCpus: "1.0", + redisMemory: "1G", + workerConcurrency: 100, + label: "2.0 vCPU", + }, + { + cpus: "4.0", + memory: "6G", + dbCpus: "2.0", + dbMemory: "3G", + redisCpus: "1.0", + redisMemory: "1G", + workerConcurrency: 200, + label: "4.0 vCPU", + }, +]; + +function parseArgs(): ScalingBenchmarkOptions { + const args = process.argv.slice(2); + let durationSeconds = 10; + let concurrency = 50; + let tiers = DEFAULT_TIERS; + let providerLatencyMs: number | string | undefined = process.env.PROVIDER_LATENCY_MS || "200-300"; + + for (const arg of args) { + if (arg.startsWith("--duration=")) { + durationSeconds = parseInt(arg.split("=")[1]!, 10); + } else if (arg.startsWith("--concurrency=")) { + concurrency = parseInt(arg.split("=")[1]!, 10); + } else if (arg.startsWith("--latency=")) { + const val = arg.split("=")[1]!; + providerLatencyMs = val.includes("-") ? val : parseInt(val, 10); + } else if (arg === "--quick") { + durationSeconds = 5; + } else if (arg.startsWith("--cpus=")) { + const cpusList = arg + .split("=")[1]! + .split(",") + .map((s) => s.trim()); + tiers = cpusList.map((c) => { + const num = parseFloat(c); + let mem = "1G"; + let dbC = "0.5"; + let dbM = "512M"; + let redC = "0.5"; + let redM = "256M"; + + if (num >= 4) { + mem = "6G"; + dbC = "2.0"; + dbM = "3G"; + redC = "1.0"; + redM = "1G"; + } else if (num >= 2) { + mem = "4G"; + dbC = "1.5"; + dbM = "2G"; + redC = "1.0"; + redM = "1G"; + } else if (num >= 1) { + mem = "2G"; + dbC = "1.0"; + dbM = "1G"; + redC = "0.5"; + redM = "512M"; + } + + return { + cpus: c, + memory: mem, + dbCpus: dbC, + dbMemory: dbM, + redisCpus: redC, + redisMemory: redM, + label: `${c} vCPU`, + }; + }); + } + } + + return { tiers, durationSeconds, concurrency, providerLatencyMs }; +} + +export async function runCpuScalingBenchmark(options: ScalingBenchmarkOptions = {}) { + const tiers = options.tiers ?? DEFAULT_TIERS; + const durationSeconds = options.durationSeconds ?? 10; + const concurrency = options.concurrency ?? 50; + const providerLatencyMs = options.providerLatencyMs ?? "200-300"; + + console.log( + "\n==========================================================================================", + ); + console.log(" ๐Ÿš€ STARTING NOTIFKIT CPU SCALING BENCHMARK SUITE"); + console.log( + "==========================================================================================", + ); + console.log(`Evaluated Tiers: ${tiers.map((t) => t.label).join(", ")}`); + console.log(`Test Duration/Tier: ${durationSeconds}s`); + console.log(`Virtual Users: ${concurrency}`); + if (providerLatencyMs && providerLatencyMs !== "0" && providerLatencyMs !== 0) { + console.log(`Mock Provider Latency: ${providerLatencyMs}ms`); + } + console.log( + "==========================================================================================\n", + ); + + const results: { tier: CpuScalingTier; result: BenchmarkResult }[] = []; + + for (let i = 0; i < tiers.length; i++) { + const tier = tiers[i]!; + console.log( + `\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>`, + ); + console.log( + `[Tier ${i + 1}/${tiers.length}] Benchmarking ${tier.label} (${tier.memory} RAM | DB: ${tier.dbCpus} CPU, ${tier.dbMemory} | Redis: ${tier.redisCpus} CPU, ${tier.redisMemory})`, + ); + console.log( + `>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n`, + ); + + try { + const result = await runLoadBenchmark({ + serverCpus: tier.cpus, + serverMemory: tier.memory, + dbCpus: tier.dbCpus, + dbMemory: tier.dbMemory, + redisCpus: tier.redisCpus, + redisMemory: tier.redisMemory, + workerConcurrency: tier.workerConcurrency, + providerLatencyMs, + durationSeconds, + concurrency, + }); + results.push({ tier, result }); + } catch (err) { + console.error(`โŒ Benchmark failed for tier ${tier.label}:`, err); + } + + if (i < tiers.length - 1) { + console.log(`\nโณ Cooling down for 3s before next tier...\n`); + await new Promise((r) => setTimeout(r, 3000)); + } + } + + // Print comparative summary table + printScalingSummary(results); +} + +function printScalingSummary(results: { tier: CpuScalingTier; result: BenchmarkResult }[]) { + if (results.length === 0) { + console.log("\nโŒ No benchmark results collected."); + return; + } + + const baselineDelivery = results[0]?.result.activeDeliveryRate || 1; + + console.log("\n"); + console.log( + "===============================================================================================================================================================", + ); + console.log( + " ๐Ÿ“Š NOTIFKIT SERVER CPU SCALING BENCHMARK SUMMARY ๐Ÿ“Š", + ); + console.log( + "===============================================================================================================================================================", + ); + console.log( + [ + "Server Tier".padEnd(12), + "Server RAM".padEnd(10), + "DB (CPU/RAM)".padEnd(14), + "Redis (CPU/RAM)".padEnd(15), + "Ingestion".padStart(13), + "Active Deliv".padStart(13), + "Wall Drain".padStart(12), + "API p95".padStart(9), + "Deliv p95".padStart(11), + "1st Msg".padStart(9), + "Speedup".padStart(10), + ].join(" | "), + ); + console.log("-".repeat(159)); + + for (const { tier, result } of results) { + const speedup = (result.activeDeliveryRate / baselineDelivery).toFixed(2) + "x"; + const dbSpec = `${tier.dbCpus}c / ${tier.dbMemory}`; + const redisSpec = `${tier.redisCpus}c / ${tier.redisMemory}`; + + console.log( + [ + tier.label.padEnd(12), + tier.memory.padEnd(10), + dbSpec.padEnd(14), + redisSpec.padEnd(15), + `${result.ingestionThroughput.toFixed(1)} req/s`.padStart(13), + `${result.activeDeliveryRate.toFixed(1)} msg/s`.padStart(13), + `${result.totalDeliveryRate.toFixed(1)} msg/s`.padStart(12), + `${result.apiP95}ms`.padStart(9), + `${result.deliveryP95}ms`.padStart(11), + `+${result.firstDelivOffset.toFixed(2)}s`.padStart(9), + speedup.padStart(10), + ].join(" | "), + ); + } + + console.log( + "===============================================================================================================================================================\n", + ); + + // Summary takeaways + console.log("๐Ÿ“Œ SCALING ANALYSIS & TAKEAWAYS:"); + for (let i = 0; i < results.length; i++) { + const curr = results[i]!; + if (i === 0) { + console.log( + ` โ€ข Baseline (${curr.tier.label}): ${curr.result.activeDeliveryRate.toFixed(1)} msgs/sec delivery, ${curr.result.ingestionThroughput.toFixed(1)} req/sec ingestion.`, + ); + } else { + const prev = results[i - 1]!; + const ingGain = ( + ((curr.result.ingestionThroughput - prev.result.ingestionThroughput) / + prev.result.ingestionThroughput) * + 100 + ).toFixed(1); + const delivGain = ( + ((curr.result.activeDeliveryRate - prev.result.activeDeliveryRate) / + prev.result.activeDeliveryRate) * + 100 + ).toFixed(1); + + console.log( + ` โ€ข ${prev.tier.label} โž” ${curr.tier.label}: Ingestion ${Number(ingGain) >= 0 ? "+" : ""}${ingGain}%, Active Delivery ${Number(delivGain) >= 0 ? "+" : ""}${delivGain}%.`, + ); + } + } + + // Detect plateauing + if (results.length >= 3) { + const last = results[results.length - 1]!.result.activeDeliveryRate; + const secondLast = results[results.length - 2]!.result.activeDeliveryRate; + const diff = ((last - secondLast) / secondLast) * 100; + if (diff < 15) { + console.log( + `\nโš ๏ธ Noticeable throughput plateau between ${results[results.length - 2]!.tier.label} and ${results[results.length - 1]!.tier.label} (+${diff.toFixed(1)}%).`, + ); + console.log( + ` Because Notifkit runs as a single Node.js event loop, adding vCPUs beyond 1-2 cores primarily benefits V8 GC/crypto threads. Core delivery remains limited by single-thread concurrency and batch timer intervals (e.g. 100ms batch flush).`, + ); + } + } + + console.log("\n"); +} + +// Auto-execute if run from CLI +const isDirectRun = + process.argv[1] && + (process.argv[1].endsWith("scaling.test.ts") || process.argv[1].endsWith("scaling.test.js")); + +if (isDirectRun) { + const options = parseArgs(); + runCpuScalingBenchmark(options).catch((err) => { + console.error("Scaling benchmark failed:", err); + process.exit(1); + }); +} diff --git a/profiling/tsconfig.json b/profiling/tsconfig.json new file mode 100644 index 0000000..c55f02e --- /dev/null +++ b/profiling/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*", "tests/**/*"] +}