From ceeecf3937af3be2fb42dea6c543c7de729167b9 Mon Sep 17 00:00:00 2001 From: coo1estguy <70365929+coolestcodeguy@users.noreply.github.com> Date: Sat, 26 Sep 2026 16:49:23 +0530 Subject: [PATCH] refactor(profiling): name service containers and update dependencies - Upgrade zod to ^4.6.5 along with compatible dependency updates across ai, dotenv, drizzle-orm, ioredis, pino, postgres, and dev tools. - Adjust tests/workers-ai.test.ts mock UUIDs to adhere to RFC 4122 v4 validation in Zod 4. - Update tests/http.test.ts validation message expectations for Zod 4. - Replace generic server container names with role-based definitions (api-server-1, api-server-2, enricher-worker, engine-worker, delivery-worker). - Add serverNodes option in distributed profiling harness to route and health-check designated worker and API nodes. - Add multi-run statistical metrics (mean, median, min, max, stdDev) reporting to money-scaling benchmark runner. --- package-lock.json | 398 ++++++++++++---------- package.json | 28 +- profiling/docker-compose.distributed.yml | 315 ++++++++++++++++- profiling/docker-compose.yml | 1 + profiling/src/server.ts | 83 +++-- profiling/tests/distributed-load.test.ts | 90 ++++- profiling/tests/money-scaling.test.ts | 409 ++++++++++++++++++----- tests/http.test.ts | 4 +- tests/workers-ai.test.ts | 30 +- 9 files changed, 1001 insertions(+), 357 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4fa3fb4..ff433f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,16 +13,16 @@ "packages/*" ], "dependencies": { - "ai": "^7.0.37", - "dotenv": "^16.4.5", - "drizzle-orm": "^0.45.2", + "ai": "^7.0.116", + "dotenv": "^18.0.4", + "drizzle-orm": "^0.45.3", "drizzle-zod": "^0.8.3", - "ioredis": "^5.4.1", - "pino": "^9.2.0", - "pino-pretty": "^11.0.0", - "postgres": "^3.4.4", + "ioredis": "^6.0.0", + "pino": "^10.3.1", + "pino-pretty": "^13.1.3", + "postgres": "^3.4.9", "prom-client": "^15.1.3", - "zod": "^3.23.8" + "zod": "^4.6.5" }, "bin": { "notifkit-create-admin": "scripts/create-admin.mjs", @@ -35,15 +35,15 @@ "@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", + "@typescript-eslint/eslint-plugin": "^8.70.1", + "@typescript-eslint/parser": "^8.70.1", "@vitest/coverage-v8": "^2.0.0", - "drizzle-kit": "^0.31.10", + "drizzle-kit": "^0.31.11", "eslint": "^9.0.0", "husky": "^9.0.11", - "lint-staged": "^15.2.5", - "prettier": "^3.3.0", - "supertest": "^7.2.2", + "lint-staged": "^15.5.2", + "prettier": "^3.9.9", + "supertest": "^7.3.0", "testcontainers": "^12.0.4", "tsdown": "^0.22.14", "typescript": "^5.8.3", @@ -55,13 +55,13 @@ } }, "node_modules/@ai-sdk/gateway": { - "version": "4.0.28", - "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-4.0.28.tgz", - "integrity": "sha512-ee9TsNO3mkgHDWmTmJ8Fvltr6PFh52zhrV2+FaKJY3F3iu7kWZi5tCRJCR1HVhhlpj6lHniUb28nLQdPHkA/1Q==", + "version": "4.0.94", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-4.0.94.tgz", + "integrity": "sha512-hcp7GLnMynH6NfMnemgvW9acn7j3WsUe/dHFzX3/fhPit08xhm1aYmDGHMiIT6YwsXVBxkit9TriKb2SGAOg+Q==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider": "4.0.3", - "@ai-sdk/provider-utils": "5.0.12", + "@ai-sdk/provider": "4.0.18", + "@ai-sdk/provider-utils": "5.0.49", "@vercel/oidc": "3.2.0" }, "engines": { @@ -72,9 +72,9 @@ } }, "node_modules/@ai-sdk/provider": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.3.tgz", - "integrity": "sha512-e0CpNWJUY7OxAFAnCZkw+ri9QOHWwTs1tXP42782KFGCU07qt8NiXCrCVowyCB5dP2r5/Uls+g2oPd8kOJn9dw==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.18.tgz", + "integrity": "sha512-+GZJIgz1jk86pwEbb3f1BD2bdoSKyWE4Jg4YUc7NMnMozbWemSKYZcw2F4nMaO5qwsL5A8RmioAKW81YktRpIQ==", "license": "Apache-2.0", "dependencies": { "json-schema": "^0.4.0" @@ -84,15 +84,16 @@ } }, "node_modules/@ai-sdk/provider-utils": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-5.0.12.tgz", - "integrity": "sha512-bbhlOgHeYwrIGheLkM6fhS8hVger8uFPmcOLg+kxc9EFh7y30XYorWhthlYAgpadO3SJhFZrIcEknN7qEqEVvA==", + "version": "5.0.49", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-5.0.49.tgz", + "integrity": "sha512-T+/H8DCvqJoCqLhltVatS7ffA779Iyk/ZZrgNXkrgv51E0n2Cd6eIdCmtwhCGIKqtbTtwCvTmQdAMv5Z8rOGHQ==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider": "4.0.3", + "@ai-sdk/provider": "4.0.18", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", - "eventsource-parser": "^3.0.8" + "eventsource-parser": "^3.0.8", + "undici": "^7.29.0" }, "engines": { "node": ">=22" @@ -1534,9 +1535,9 @@ } }, "node_modules/@ioredis/commands": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.10.0.tgz", - "integrity": "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-2.0.0.tgz", + "integrity": "sha512-vrx0AE/T0h7cRZwfo1M39Cr+ZhZrkf0V8mQN75wucKCxCLD9l/VX6no3gFvrLqD1IlG/1LtzWovqEw3t0Vr9zg==", "license": "MIT" }, "node_modules/@isaacs/cliui": { @@ -2775,17 +2776,17 @@ "optional": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz", - "integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==", + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.1.tgz", + "integrity": "sha512-nDNrUQ/4ruSNYbu749TRY7cfrzPtoLHEXSNBI8aaNY32LlZCajixqRf3FqcKC4p5Cam4VOHYx/t+i5+nKXvrqA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.64.0", - "@typescript-eslint/type-utils": "8.64.0", - "@typescript-eslint/utils": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0", + "@typescript-eslint/scope-manager": "8.70.1", + "@typescript-eslint/type-utils": "8.70.1", + "@typescript-eslint/utils": "8.70.1", + "@typescript-eslint/visitor-keys": "8.70.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -2798,22 +2799,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.64.0", + "@typescript-eslint/parser": "^8.70.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz", - "integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==", + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.70.1.tgz", + "integrity": "sha512-nO974WLllwhSFWQXnMLj6nDGa8f0khKEz1JzpPJ1u7Vm/4X1X6ZHajpoknU4bb41vJyMB0HHVyS2GqdhWfIXZw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.64.0", - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/typescript-estree": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0", + "@typescript-eslint/scope-manager": "8.70.1", + "@typescript-eslint/types": "8.70.1", + "@typescript-eslint/typescript-estree": "8.70.1", + "@typescript-eslint/visitor-keys": "8.70.1", "debug": "^4.4.3" }, "engines": { @@ -2829,14 +2830,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz", - "integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==", + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.70.1.tgz", + "integrity": "sha512-62xOgboPfwc3/IgPSX/W6oQR3ZbF04194FPGUGH8HL8iLFHbt/456/8Ph1wLNUgVF+s94FlHoipBsz+v7+LMnA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.64.0", - "@typescript-eslint/types": "^8.64.0", + "@typescript-eslint/tsconfig-utils": "^8.70.1", + "@typescript-eslint/types": "^8.70.1", "debug": "^4.4.3" }, "engines": { @@ -2851,14 +2852,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz", - "integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==", + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.70.1.tgz", + "integrity": "sha512-Pa0EeSeAusQc1WbjQMac+YfenewYTBu0KjgYvkUKwhXaHUKbFog23Dm/rp0DX/6tyYOQ3Xl1a+3EcFNZynGHCw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0" + "@typescript-eslint/types": "8.70.1", + "@typescript-eslint/visitor-keys": "8.70.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2869,9 +2870,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz", - "integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==", + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.1.tgz", + "integrity": "sha512-jumze1fPI+sDOaM2TWGQdn39PDxTr7TZGeuyLkAbNyx2vtMT3uRnVKChN0hfht5V2TugphJzF6bYXvBcE09qqg==", "dev": true, "license": "MIT", "engines": { @@ -2886,15 +2887,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz", - "integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==", + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.70.1.tgz", + "integrity": "sha512-7zKTnyvaVWqzLZHPFQtX1hVHqgkMC+WebPWakNCSyrQVbIP1AM0L0TlBZtACldIRb6PptI8Odk+jyZ5kP3B1VA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/typescript-estree": "8.64.0", - "@typescript-eslint/utils": "8.64.0", + "@typescript-eslint/types": "8.70.1", + "@typescript-eslint/typescript-estree": "8.70.1", + "@typescript-eslint/utils": "8.70.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -2911,9 +2912,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz", - "integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==", + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.1.tgz", + "integrity": "sha512-Dm1ypdhhrGCTyyehxElhgJ6kgk8MVCv5qXdoOVqPr1uqk42jX8KjrZqhROvdShczA8qrDoYiOWn1ykWlx2k81Q==", "dev": true, "license": "MIT", "engines": { @@ -2925,16 +2926,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz", - "integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==", + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.1.tgz", + "integrity": "sha512-TU8PwyGN0PQJUcE96mw8eCQ44SmxGdQlJmlWakHaHQ15eIuuvye5yNtmh/i6oS88jzXVQB71xdNkbkB/fMwL0g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.64.0", - "@typescript-eslint/tsconfig-utils": "8.64.0", - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0", + "@typescript-eslint/project-service": "8.70.1", + "@typescript-eslint/tsconfig-utils": "8.70.1", + "@typescript-eslint/types": "8.70.1", + "@typescript-eslint/visitor-keys": "8.70.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -2953,16 +2954,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz", - "integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==", + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.70.1.tgz", + "integrity": "sha512-Esgul8MsnKnRLdYU2Eb2cRV9bS5HJYtKj1ByJnOzzG2M58DGdSUQ1jUuILxipqcpB2h9WLrbD5GijIWUjX/Tqw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.64.0", - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/typescript-estree": "8.64.0" + "@typescript-eslint/scope-manager": "8.70.1", + "@typescript-eslint/types": "8.70.1", + "@typescript-eslint/typescript-estree": "8.70.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2977,13 +2978,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz", - "integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==", + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.1.tgz", + "integrity": "sha512-Vwj9lUIW5Xq3wQ9w6gv3R86g1hMK8f2zNOdGTAgeXUMMXFK78G9ruCjjqutHMNJc0+CH7LYRnHeUB9IT8wFmcw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/types": "8.70.1", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -3501,6 +3502,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "devOptional": true, "license": "MIT", "dependencies": { "event-target-shim": "^5.0.0" @@ -3580,14 +3582,14 @@ } }, "node_modules/ai": { - "version": "7.0.37", - "resolved": "https://registry.npmjs.org/ai/-/ai-7.0.37.tgz", - "integrity": "sha512-stF+SEQJgKY3Qfe3FwNzqUrehHviOp2l7LemoI8YMOa0Zk5PxKsRNgUk3cHXz0RAue9RRyCAis2fz6LSCP6EKw==", + "version": "7.0.116", + "resolved": "https://registry.npmjs.org/ai/-/ai-7.0.116.tgz", + "integrity": "sha512-gmkVGPzTNPcJixBiG9zUvP3gmvwko85dHiKrxFKK0zcVJIIu1FP6YDo3jna+ZrC2twMAsUPLr5htCwZjCINH3Q==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/gateway": "4.0.28", - "@ai-sdk/provider": "4.0.3", - "@ai-sdk/provider-utils": "5.0.12" + "@ai-sdk/gateway": "4.0.94", + "@ai-sdk/provider": "4.0.18", + "@ai-sdk/provider-utils": "5.0.49" }, "engines": { "node": ">=22" @@ -4168,6 +4170,7 @@ "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", @@ -5033,10 +5036,13 @@ } }, "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.4.tgz", + "integrity": "sha512-jGt9uI5BIxpVO15kvy+iEFqNAwxYClKWnkptLjSNPnFLC003Imd0ofr3aEVsbpHdSBdSsbcApNIlz4nbyGMwdA==", "license": "BSD-2-Clause", + "bin": { + "dotenv": "dist/index.cjs" + }, "engines": { "node": ">=12" }, @@ -5045,9 +5051,9 @@ } }, "node_modules/drizzle-kit": { - "version": "0.31.10", - "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz", - "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==", + "version": "0.31.11", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.11.tgz", + "integrity": "sha512-YCYqxTLIB2OCqf6w9/Vef13baBVbwQIPcbT4Y56AfO6B9ajZhDhD8Jkveg/hJvT/iuMloKD/IYYkyMMNhr7Kqg==", "dev": true, "license": "MIT", "dependencies": { @@ -5545,9 +5551,9 @@ } }, "node_modules/drizzle-orm": { - "version": "0.45.2", - "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", - "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", + "version": "0.45.3", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.3.tgz", + "integrity": "sha512-CAloER21cDdcgZ1OmjrZX82EeRNsdP+y0onX/eyoYDaYzki/adrAa91lzM5jWh7zZPK4qUbw/6LGv9J+A+uizA==", "license": "Apache-2.0", "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", @@ -5556,6 +5562,7 @@ "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", + "@netlify/db": ">=0.4.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", @@ -5598,6 +5605,9 @@ "@neondatabase/serverless": { "optional": true }, + "@netlify/db": { + "optional": true + }, "@op-engineering/op-sqlite": { "optional": true }, @@ -6258,6 +6268,7 @@ "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==", + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -6274,6 +6285,7 @@ "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" @@ -6451,9 +6463,9 @@ "license": "MIT" }, "node_modules/fast-copy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", - "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.1.1.tgz", + "integrity": "sha512-A4QTJmuiztpGtr6AMeJts9R4hbj2ZBUwtOaKrG6rw2y7t6+IaJKjz5M3XDs8BUznxDH43FVc6A0y/gWlMl4UtA==", "license": "MIT" }, "node_modules/fast-deep-equal": { @@ -6928,9 +6940,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", - "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", "dev": true, "license": "MIT", "engines": { @@ -7584,6 +7596,7 @@ "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", @@ -7688,21 +7701,20 @@ } }, "node_modules/ioredis": { - "version": "5.11.1", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.11.1.tgz", - "integrity": "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-6.0.0.tgz", + "integrity": "sha512-f+Dtubxfpf6KYFq7WVXJoOLn0bk4TJrMrN9SzeE+jrWrCWj7XX3fA6vkryafhADX+GMymRxgDJDOI33COkJc0w==", "license": "MIT", "dependencies": { - "@ioredis/commands": "1.10.0", + "@ioredis/commands": "2.0.0", "cluster-key-slot": "1.1.1", "debug": "4.4.3", "denque": "2.1.0", "redis-errors": "1.2.0", - "redis-parser": "3.0.0", "standard-as-callback": "2.1.0" }, "engines": { - "node": ">=12.22.0" + "node": ">=20.0.0" }, "funding": { "type": "opencollective", @@ -9150,9 +9162,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { @@ -9176,75 +9188,70 @@ } }, "node_modules/pino": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", - "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", "license": "MIT", "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", + "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", - "thread-stream": "^3.0.0" + "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "node_modules/pino-abstract-transport": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", - "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", "license": "MIT", "dependencies": { "split2": "^4.0.0" } }, "node_modules/pino-pretty": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-11.3.0.tgz", - "integrity": "sha512-oXwn7ICywaZPHmu3epHGU2oJX4nPmKvHvB/bwrJHlGcbEWaVcotkpyVHMKLKmiVryWYByNp0jpgAcXpFJDXJzA==", + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", + "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", "license": "MIT", "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", - "fast-copy": "^3.0.2", + "fast-copy": "^4.0.0", "fast-safe-stringify": "^2.1.1", "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", + "pino-abstract-transport": "^3.0.0", "pump": "^3.0.0", - "readable-stream": "^4.0.0", - "secure-json-parse": "^2.4.0", + "secure-json-parse": "^4.0.0", "sonic-boom": "^4.0.1", - "strip-json-comments": "^3.1.1" + "strip-json-comments": "^5.0.2" }, "bin": { "pino-pretty": "bin.js" } }, - "node_modules/pino-pretty/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==", + "node_modules/pino-pretty/node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", "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": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/pino-std-serializers": { @@ -9321,9 +9328,9 @@ } }, "node_modules/prettier": { - "version": "3.9.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz", - "integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==", + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz", + "integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==", "dev": true, "license": "MIT", "bin": { @@ -9340,6 +9347,7 @@ "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" @@ -9372,6 +9380,7 @@ "version": "15.1.3", "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz", "integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==", + "deprecated": "prom-client has been replaced by @prometheus-io/client", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.4.0", @@ -9647,18 +9656,6 @@ "node": ">=4" } }, - "node_modules/redis-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", - "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", - "license": "MIT", - "dependencies": { - "redis-errors": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -9996,9 +9993,19 @@ "license": "BSD-3-Clause" }, "node_modules/secure-json-parse": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", - "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "BSD-3-Clause" }, "node_modules/semver": { @@ -10387,6 +10394,7 @@ "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==", + "devOptional": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -10533,6 +10541,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10616,9 +10625,9 @@ } }, "node_modules/supertest": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", - "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.3.0.tgz", + "integrity": "sha512-UwwmWq3xLhyU96c521wYNaBg7IqX78Wpj4FKs6Katp6vdklX42zU5ESvX3KeZRLFyO44RwFp4yvxYMNZzCcX9g==", "dev": true, "license": "MIT", "dependencies": { @@ -10773,6 +10782,16 @@ "undici": "^8.5.0" } }, + "node_modules/testcontainers/node_modules/undici": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.11.2.tgz", + "integrity": "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, "node_modules/text-decoder": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", @@ -10797,14 +10816,23 @@ } }, "node_modules/thread-stream": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz", - "integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", "license": "MIT", "dependencies": { - "real-require": "^0.2.0" + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" } }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "license": "MIT" + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -10820,9 +10848,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", - "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", "dev": true, "license": "MIT", "engines": { @@ -11678,13 +11706,12 @@ } }, "node_modules/undici": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.7.0.tgz", - "integrity": "sha512-N7iQtfyLhIMOFgQubvmLV26svHpO0bqKnAiWotTQCVKCmWrcGbBotPuW1x+xwYZ2VHdSTVUfPQQnlEt1/LouTQ==", - "dev": true, + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", + "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", "license": "MIT", "engines": { - "node": ">=22.19.0" + "node": ">=20.18.1" } }, "node_modules/undici-types": { @@ -12611,9 +12638,9 @@ } }, "node_modules/yaml": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "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": { @@ -12813,9 +12840,9 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -12850,6 +12877,15 @@ "node": ">=22.0.0" } }, + "packages/mcp/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "packages/provider-console": { "name": "@notifkit/provider-console", "version": "0.1.1", diff --git a/package.json b/package.json index 124d0e4..0a88caa 100644 --- a/package.json +++ b/package.json @@ -72,16 +72,16 @@ "prepublishOnly": "npm run typecheck && npm run build && npm run build:dashboard" }, "dependencies": { - "ai": "^7.0.37", - "dotenv": "^16.4.5", - "drizzle-orm": "^0.45.2", + "ai": "^7.0.116", + "dotenv": "^18.0.4", + "drizzle-orm": "^0.45.3", "drizzle-zod": "^0.8.3", - "ioredis": "^5.4.1", - "pino": "^9.2.0", - "pino-pretty": "^11.0.0", - "postgres": "^3.4.4", + "ioredis": "^6.0.0", + "pino": "^10.3.1", + "pino-pretty": "^13.1.3", + "postgres": "^3.4.9", "prom-client": "^15.1.3", - "zod": "^3.23.8" + "zod": "^4.6.5" }, "devDependencies": { "@commitlint/cli": "^19.3.0", @@ -90,15 +90,15 @@ "@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", + "@typescript-eslint/eslint-plugin": "^8.70.1", + "@typescript-eslint/parser": "^8.70.1", "@vitest/coverage-v8": "^2.0.0", - "drizzle-kit": "^0.31.10", + "drizzle-kit": "^0.31.11", "eslint": "^9.0.0", "husky": "^9.0.11", - "lint-staged": "^15.2.5", - "prettier": "^3.3.0", - "supertest": "^7.2.2", + "lint-staged": "^15.5.2", + "prettier": "^3.9.9", + "supertest": "^7.3.0", "testcontainers": "^12.0.4", "tsdown": "^0.22.14", "typescript": "^5.8.3", diff --git a/profiling/docker-compose.distributed.yml b/profiling/docker-compose.distributed.yml index c3ceed3..3c1232a 100644 --- a/profiling/docker-compose.distributed.yml +++ b/profiling/docker-compose.distributed.yml @@ -23,6 +23,244 @@ services: cpus: "${REDIS_CPUS:-1.5}" memory: "${REDIS_MEMORY:-1G}" + # ── Descriptive Role-Based Services ────────────────────────────────────────── + + api-server-1: + 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} + RATE_LIMIT_PER_HOUR: ${RATE_LIMIT_PER_HOUR:-10000000} + SERVICES: ${API_SERVER_1_SERVICES:-api} + ports: + - "35678:3000" + depends_on: + - db + - redis + deploy: + resources: + limits: + cpus: "${SERVER_CPUS:-2.0}" + memory: "${SERVER_MEMORY:-2G}" + + api-server-2: + 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} + RATE_LIMIT_PER_HOUR: ${RATE_LIMIT_PER_HOUR:-10000000} + SERVICES: ${API_SERVER_2_SERVICES:-api} + ports: + - "35679:3000" + depends_on: + - db + - redis + deploy: + resources: + limits: + cpus: "${SERVER_CPUS:-2.0}" + memory: "${SERVER_MEMORY:-2G}" + + pipeline-worker: + 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} + RATE_LIMIT_PER_HOUR: ${RATE_LIMIT_PER_HOUR:-10000000} + SERVICES: ${PIPELINE_WORKER_SERVICES:-enricher,engine,scheduler} + ports: + - "35680:3000" + depends_on: + - db + - redis + deploy: + resources: + limits: + cpus: "${SERVER_CPUS:-2.0}" + memory: "${SERVER_MEMORY:-2G}" + + delivery-worker: + 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} + RATE_LIMIT_PER_HOUR: ${RATE_LIMIT_PER_HOUR:-10000000} + SERVICES: ${DELIVERY_WORKER_SERVICES:-delivery} + ports: + - "35681:3000" + depends_on: + - db + - redis + deploy: + resources: + limits: + cpus: "${SERVER_CPUS:-2.0}" + memory: "${SERVER_MEMORY:-2G}" + + scheduler-worker: + 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} + RATE_LIMIT_PER_HOUR: ${RATE_LIMIT_PER_HOUR:-10000000} + SERVICES: ${SCHEDULER_WORKER_SERVICES:-scheduler} + ports: + - "35682:3000" + depends_on: + - db + - redis + deploy: + resources: + limits: + cpus: "${SERVER_CPUS:-2.0}" + memory: "${SERVER_MEMORY:-2G}" + + enricher-worker: + 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} + RATE_LIMIT_PER_HOUR: ${RATE_LIMIT_PER_HOUR:-10000000} + SERVICES: ${ENRICHER_WORKER_SERVICES:-enricher} + ports: + - "35683:3000" + depends_on: + - db + - redis + deploy: + resources: + limits: + cpus: "${SERVER_CPUS:-2.0}" + memory: "${SERVER_MEMORY:-2G}" + + engine-worker: + 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} + RATE_LIMIT_PER_HOUR: ${RATE_LIMIT_PER_HOUR:-10000000} + SERVICES: ${ENGINE_WORKER_SERVICES:-engine} + ports: + - "35684:3000" + depends_on: + - db + - redis + deploy: + resources: + limits: + cpus: "${SERVER_CPUS:-2.0}" + memory: "${SERVER_MEMORY:-2G}" + + monolith-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:-100} + DB_MAX_CONNECTIONS: ${DB_MAX_CONNECTIONS:-5} + PROVIDER_LATENCY_MS: ${PROVIDER_LATENCY_MS:-0} + RATE_LIMIT_PER_HOUR: ${RATE_LIMIT_PER_HOUR:-10000000} + SERVICES: ${MONOLITH_SERVER_SERVICES:-all} + ports: + - "35678:3000" + depends_on: + - db + - redis + deploy: + resources: + limits: + cpus: "${SERVER_CPUS:-2.0}" + memory: "${SERVER_MEMORY:-2G}" + + worker-node: + 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} + RATE_LIMIT_PER_HOUR: ${RATE_LIMIT_PER_HOUR:-10000000} + SERVICES: ${WORKER_NODE_SERVICES:-enricher,engine,delivery,scheduler} + ports: + - "35679:3000" + depends_on: + - db + - redis + deploy: + resources: + limits: + cpus: "${SERVER_CPUS:-2.0}" + memory: "${SERVER_MEMORY:-2G}" + + # ── Generic Server Definitions (Backwards Compatibility) ───────────────────── + server1: build: context: .. @@ -36,6 +274,7 @@ services: WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-100} DB_MAX_CONNECTIONS: ${DB_MAX_CONNECTIONS:-5} PROVIDER_LATENCY_MS: ${PROVIDER_LATENCY_MS:-0} + RATE_LIMIT_PER_HOUR: ${RATE_LIMIT_PER_HOUR:-10000000} SERVICES: ${SERVER1_SERVICES:-all} ports: - "35678:3000" @@ -45,8 +284,8 @@ services: deploy: resources: limits: - cpus: "${SERVER_CPUS:-1.0}" - memory: "${SERVER_MEMORY:-1G}" + cpus: "${SERVER_CPUS:-2.0}" + memory: "${SERVER_MEMORY:-2G}" server2: build: @@ -61,6 +300,7 @@ services: WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-100} DB_MAX_CONNECTIONS: ${DB_MAX_CONNECTIONS:-5} PROVIDER_LATENCY_MS: ${PROVIDER_LATENCY_MS:-0} + RATE_LIMIT_PER_HOUR: ${RATE_LIMIT_PER_HOUR:-10000000} SERVICES: ${SERVER2_SERVICES:-all} ports: - "35679:3000" @@ -70,8 +310,8 @@ services: deploy: resources: limits: - cpus: "${SERVER_CPUS:-1.0}" - memory: "${SERVER_MEMORY:-1G}" + cpus: "${SERVER_CPUS:-2.0}" + memory: "${SERVER_MEMORY:-2G}" server3: build: @@ -86,6 +326,7 @@ services: WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-100} DB_MAX_CONNECTIONS: ${DB_MAX_CONNECTIONS:-5} PROVIDER_LATENCY_MS: ${PROVIDER_LATENCY_MS:-0} + RATE_LIMIT_PER_HOUR: ${RATE_LIMIT_PER_HOUR:-10000000} SERVICES: ${SERVER3_SERVICES:-all} ports: - "35680:3000" @@ -95,8 +336,8 @@ services: deploy: resources: limits: - cpus: "${SERVER_CPUS:-1.0}" - memory: "${SERVER_MEMORY:-1G}" + cpus: "${SERVER_CPUS:-2.0}" + memory: "${SERVER_MEMORY:-2G}" server4: build: @@ -111,6 +352,7 @@ services: WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-100} DB_MAX_CONNECTIONS: ${DB_MAX_CONNECTIONS:-5} PROVIDER_LATENCY_MS: ${PROVIDER_LATENCY_MS:-0} + RATE_LIMIT_PER_HOUR: ${RATE_LIMIT_PER_HOUR:-10000000} SERVICES: ${SERVER4_SERVICES:-all} ports: - "35681:3000" @@ -120,8 +362,8 @@ services: deploy: resources: limits: - cpus: "${SERVER_CPUS:-1.0}" - memory: "${SERVER_MEMORY:-1G}" + cpus: "${SERVER_CPUS:-2.0}" + memory: "${SERVER_MEMORY:-2G}" server5: build: @@ -136,6 +378,7 @@ services: WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-100} DB_MAX_CONNECTIONS: ${DB_MAX_CONNECTIONS:-5} PROVIDER_LATENCY_MS: ${PROVIDER_LATENCY_MS:-0} + RATE_LIMIT_PER_HOUR: ${RATE_LIMIT_PER_HOUR:-10000000} SERVICES: ${SERVER5_SERVICES:-all} ports: - "35682:3000" @@ -145,5 +388,57 @@ services: deploy: resources: limits: - cpus: "${SERVER_CPUS:-1.0}" - memory: "${SERVER_MEMORY:-1G}" + cpus: "${SERVER_CPUS:-2.0}" + memory: "${SERVER_MEMORY:-2G}" + + server6: + 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} + RATE_LIMIT_PER_HOUR: ${RATE_LIMIT_PER_HOUR:-10000000} + SERVICES: ${SERVER6_SERVICES:-all} + ports: + - "35683:3000" + depends_on: + - db + - redis + deploy: + resources: + limits: + cpus: "${SERVER_CPUS:-2.0}" + memory: "${SERVER_MEMORY:-2G}" + + server7: + 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} + RATE_LIMIT_PER_HOUR: ${RATE_LIMIT_PER_HOUR:-10000000} + SERVICES: ${SERVER7_SERVICES:-all} + ports: + - "35684:3000" + depends_on: + - db + - redis + deploy: + resources: + limits: + cpus: "${SERVER_CPUS:-2.0}" + memory: "${SERVER_MEMORY:-2G}" diff --git a/profiling/docker-compose.yml b/profiling/docker-compose.yml index 34b50be..051b672 100644 --- a/profiling/docker-compose.yml +++ b/profiling/docker-compose.yml @@ -36,6 +36,7 @@ services: WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-50} DB_MAX_CONNECTIONS: ${DB_MAX_CONNECTIONS:-20} PROVIDER_LATENCY_MS: ${PROVIDER_LATENCY_MS:-0} + RATE_LIMIT_PER_HOUR: ${RATE_LIMIT_PER_HOUR:-10000000} ports: - "35678:3000" depends_on: diff --git a/profiling/src/server.ts b/profiling/src/server.ts index 8d0bfed..9fdbfbb 100644 --- a/profiling/src/server.ts +++ b/profiling/src/server.ts @@ -58,6 +58,7 @@ class ProfilingTransport implements Transport { } import http from "node:http"; +import cluster from "node:cluster"; const rawServices = process.env.SERVICES?.trim(); const services: ( @@ -69,39 +70,53 @@ const services: ( )[]) : ["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()], -}); +const isApiOnly = services.length === 1 && services[0] === "api"; +const clusterWorkers = isApiOnly ? 2 : 1; -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); +if (clusterWorkers > 1 && cluster.isPrimary) { + console.log(`⚡ Forking ${clusterWorkers} API worker processes on port ${PORT}...`); + for (let i = 0; i < clusterWorkers; i++) { + cluster.fork(); + } + cluster.on("exit", (w) => { + console.warn(`Worker ${w.process.pid} exited, spawning replacement...`); + cluster.fork(); }); +} else { + 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(",")}] (pid: ${process.pid})`, + ); + + // 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 index 5071a26..174e17c 100644 --- a/profiling/tests/distributed-load.test.ts +++ b/profiling/tests/distributed-load.test.ts @@ -5,7 +5,7 @@ import { setGlobalDispatcher, Agent } from "undici"; // Configure high-concurrency connection pool for client benchmark setGlobalDispatcher( new Agent({ - connections: 300, + connections: 2000, pipelining: 1, keepAliveTimeout: 30000, keepAliveMaxTimeout: 60000, @@ -16,6 +16,7 @@ export interface DistributedLoadTestOptions { serverCount?: number; durationSeconds?: number; concurrency?: number; + servicesCount?: number; adminApiKey?: string; dbPort?: number; serverCpus?: string; @@ -28,6 +29,7 @@ export interface DistributedLoadTestOptions { dbMaxConnections?: number; providerLatencyMs?: number | string; serverServices?: string[]; + serverNodes?: Array<{ name: string; port: number; services?: string }>; quiet?: boolean; } @@ -38,6 +40,7 @@ export interface DistributedBenchmarkResult { workerConcurrency: number; durationSec: number; concurrency: number; + servicesCount?: number; requestsSent: number; successCount: number; failCount: number; @@ -87,14 +90,29 @@ export async function runDistributedBenchmark( }); } - const services = [ - "db", - "redis", - ...Array.from({ length: serverCount }, (_, i) => `server${i + 1}`), - ]; + // If explicit serverNodes are provided, use their service names and custom env vars + const nodes = + options.serverNodes && options.serverNodes.length > 0 + ? options.serverNodes + : Array.from({ length: serverCount }, (_, i) => ({ + name: `server${i + 1}`, + port: 35678 + i, + services: options.serverServices?.[i], + })); + + for (const node of nodes) { + if (node.services) { + const envKey = `${node.name.toUpperCase().replace(/-/g, "_")}_SERVICES`; + composeEnv[envKey] = node.services; + } + } + + const effectiveServerCount = nodes.length; + + const services = ["db", "redis", ...nodes.map((n) => n.name)]; 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)...`, + `🚀 Booting distributed profiling environment (${effectiveServerCount} Server Instances: [${nodes.map((n) => n.name).join(", ")}] | ${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") @@ -102,14 +120,13 @@ export async function runDistributedBenchmark( .withBuild() .up(services); - const serverPorts = Array.from({ length: serverCount }, (_, i) => 35678 + i); - const serverUrls = serverPorts.map((p) => `http://localhost:${p}`); + const serverUrls = nodes.map((n) => `http://localhost:${n.port}`); const dbUrl = `postgres://notifkit:password@localhost:${dbPort}/notifkit`; const apiLatencies: number[] = []; const deliveryLatencies: number[] = []; - console.log(`⏳ Waiting for ${serverCount} server instances health checks...`); + console.log(`⏳ Waiting for ${effectiveServerCount} server instances health checks...`); const apiUrls: string[] = []; for (const url of serverUrls) { let ready = false; @@ -135,8 +152,8 @@ export async function runDistributedBenchmark( 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`); + for (const node of nodes) { + const serverContainer = environment.getContainer(`${node.name}-1`); const stream = await serverContainer.logs(); let logBuffer = ""; stream.on("data", (chunk) => { @@ -213,8 +230,28 @@ export async function runDistributedBenchmark( }), }); + const MICROSERVICES = [ + { name: "order-service", priority: "critical", event: "order_confirmation" }, + { name: "payments-service", priority: "critical", event: "payment_receipt" }, + { name: "billing-service", priority: "normal", event: "invoice_ready" }, + { name: "auth-service", priority: "critical", event: "password_reset" }, + { name: "shipping-service", priority: "normal", event: "tracking_update" }, + { name: "account-service", priority: "normal", event: "security_alert" }, + { name: "inventory-service", priority: "low", event: "stock_replenished" }, + { name: "fraud-service", priority: "critical", event: "suspicious_login" }, + { name: "marketing-service", priority: "low", event: "promotional_blast" }, + { name: "support-service", priority: "normal", event: "ticket_reply" }, + ]; + + const activeServicesCount = options.servicesCount ?? (concurrency <= 10 ? concurrency : 10); + const activeServices = MICROSERVICES.slice( + 0, + Math.min(activeServicesCount, MICROSERVICES.length), + ); + const workersPerService = Math.max(1, Math.round(concurrency / activeServices.length)); + console.log( - `\n🔥 Starting Distributed Load Generation: ${concurrency} virtual users across ${targetApiUrls.length} API ingress node(s) for ${durationSec}s`, + `\n🔥 Starting Distributed Load Generation: ${activeServices.length} microservices blasting concurrently (${concurrency} parallel workers, ~${workersPerService} workers/service) across ${targetApiUrls.length} API ingress node(s) for ${durationSec}s`, ); let requestsSent = 0; @@ -224,8 +261,19 @@ export async function runDistributedBenchmark( const startTime = Date.now(); const endTime = startTime + durationSec * 1000; + const progressInterval = setInterval(() => { + const elapsedSec = ((Date.now() - startTime) / 1000).toFixed(0); + const remainingSec = Math.max(0, Math.round((endTime - Date.now()) / 1000)); + const curReqRate = (requestsSent / Math.max(1, Number(elapsedSec))).toFixed(1); + const curDeliv = deliveryLatencies.length; + console.log( + `⚡ Blasting: ${elapsedSec}s elapsed (${remainingSec}s remaining) | Sent: ${requestsSent} (${curReqRate} req/s) | Delivered so far: ${curDeliv} msgs`, + ); + }, 10000); + async function worker(workerIndex: number) { const targetUrl = targetApiUrls[workerIndex % targetApiUrls.length]!; + const svc = activeServices[workerIndex % activeServices.length]!; while (Date.now() < endTime) { requestsSent++; try { @@ -240,8 +288,13 @@ export async function runDistributedBenchmark( user: "perf-user-1", template: "perf-email", channels: ["email"], - priority: "critical", - data: { name: "Load Tester", requestTime: reqStart }, + priority: svc.priority, + data: { + name: "Load Tester", + service: svc.name, + event: svc.event, + requestTime: reqStart, + }, }), }); @@ -260,6 +313,7 @@ export async function runDistributedBenchmark( const workers = Array.from({ length: concurrency }).map((_, i) => worker(i)); await Promise.all(workers); + clearInterval(progressInterval); const activeWindowSec = durationSec; const ingestionThroughput = (requestsSent / activeWindowSec).toFixed(2); @@ -270,9 +324,10 @@ export async function runDistributedBenchmark( const flushStart = Date.now(); let lastLogged = 0; + const flushTimeoutMs = Math.max(1800000, durationSec * 3000); while (deliveryLatencies.length < successCount) { - if (Date.now() - flushStart > 600000) { + if (Date.now() - flushStart > flushTimeoutMs) { console.log( `⚠️ Flush timeout! Delivered ${deliveryLatencies.length}/${successCount} messages.`, ); @@ -315,7 +370,7 @@ export async function runDistributedBenchmark( console.log(" 📊 LOAD TEST RESULTS 📊 "); console.log("================================="); console.log(`Topology: ${serverCount} Distributed Servers`); - console.log(`Generation Window: ${activeWindowSec}s (${concurrency} virtual users)`); + console.log(`Generation Window: ${activeWindowSec}s (${concurrency} microservice callers)`); console.log(`Total Ingestion: ${requestsSent} requests (${ingestionThroughput} req/sec)`); console.log( `Accepted (202): ${successCount} (${((successCount / requestsSent) * 100).toFixed(1)}%)`, @@ -356,6 +411,7 @@ export async function runDistributedBenchmark( workerConcurrency, durationSec: activeWindowSec, concurrency, + servicesCount: activeServices.length, requestsSent, successCount, failCount, diff --git a/profiling/tests/money-scaling.test.ts b/profiling/tests/money-scaling.test.ts index 46b77d7..af07659 100644 --- a/profiling/tests/money-scaling.test.ts +++ b/profiling/tests/money-scaling.test.ts @@ -16,8 +16,10 @@ export interface BudgetTier { workerConcurrency: number; dbMaxConnections: number; concurrency: number; + servicesCount?: number; description: string; serverServices?: string[]; + serverNodes?: Array<{ name: string; port: number; services?: string }>; awsTopology?: string; awsCostDetails?: string; } @@ -27,110 +29,191 @@ const DEFAULT_BUDGET_TIERS: BudgetTier[] = [ budget: "$20/mo", monthlyCost: 20, serverCount: 1, - serverCpus: "1.0", - serverMemory: "1G", + serverCpus: "1.5", + serverMemory: "1.5G", dbCpus: "0.5", dbMemory: "512M", redisCpus: "0.5", redisMemory: "256M", - workerConcurrency: 100, + workerConcurrency: 80, dbMaxConnections: 5, - concurrency: 150, + concurrency: 100, + servicesCount: 5, description: "1x Monolith on EC2 t4g.small", awsTopology: "1x EC2 t4g.small (Monolith)", awsCostDetails: "EC2 t4g.small (2 vCPU/2G: $12.26) + 30GB gp3 ($2.40) + IPv4/Net ($5.34) = ~$20.00/mo", serverServices: ["api,enricher,engine,delivery,scheduler"], + serverNodes: [ + { name: "monolith-server", port: 35678, services: "api,enricher,engine,delivery,scheduler" }, + ], }, { budget: "$40/mo", monthlyCost: 40, serverCount: 2, - serverCpus: "1.0", - serverMemory: "1G", + serverCpus: "2.0", + serverMemory: "2G", dbCpus: "1.0", dbMemory: "1G", redisCpus: "0.5", redisMemory: "512M", - workerConcurrency: 200, + workerConcurrency: 160, dbMaxConnections: 8, - concurrency: 300, + concurrency: 180, + servicesCount: 6, description: "1x API + 1x Worker (RDS db.t4g.micro + ElastiCache)", awsTopology: "1x EC2 t4g.small + RDS db.t4g.micro + ElastiCache t4g.micro", awsCostDetails: "EC2 t4g.small ($13.86) + RDS db.t4g.micro ($13.98) + ElastiCache Valkey ($9.34) + Net ($2.82) = ~$40.00/mo", serverServices: ["api", "enricher,engine,delivery,scheduler"], + serverNodes: [ + { name: "api-server-1", port: 35678, services: "api" }, + { name: "worker-node", port: 35679, services: "enricher,engine,delivery,scheduler" }, + ], }, { budget: "$60/mo", monthlyCost: 60, serverCount: 3, - serverCpus: "1.0", - serverMemory: "1G", + serverCpus: "2.0", + serverMemory: "2G", dbCpus: "1.0", dbMemory: "1G", redisCpus: "1.0", redisMemory: "1G", - workerConcurrency: 300, + workerConcurrency: 220, dbMaxConnections: 10, - concurrency: 300, + concurrency: 240, + servicesCount: 8, description: "1x API + 1x Pipeline + 1x Delivery (Decoupled Compute)", awsTopology: "2x EC2 t4g.small + RDS db.t4g.micro + ElastiCache t4g.micro + ALB", awsCostDetails: "2x EC2 t4g.small ($27.72) + RDS db.t4g.micro ($13.98) + ElastiCache Valkey ($9.34) + ALB/Net ($8.96) = ~$60.00/mo", serverServices: ["api", "enricher,engine,scheduler", "delivery"], + serverNodes: [ + { name: "api-server-1", port: 35678, services: "api" }, + { name: "pipeline-worker", port: 35680, services: "enricher,engine,scheduler" }, + { name: "delivery-worker", port: 35681, services: "delivery" }, + ], }, { budget: "$80/mo", monthlyCost: 80, serverCount: 3, - serverCpus: "1.0", - serverMemory: "1G", + serverCpus: "2.0", + serverMemory: "2G", dbCpus: "2.0", dbMemory: "2G", redisCpus: "1.0", redisMemory: "1G", - workerConcurrency: 400, + workerConcurrency: 280, dbMaxConnections: 15, - concurrency: 400, + concurrency: 280, + servicesCount: 8, description: "1x API + 1x Pipeline + 1x Delivery (RDS db.t4g.small 2G)", awsTopology: "3x EC2 t4g.small + RDS db.t4g.small (2G) + ElastiCache t4g.micro", awsCostDetails: "3x EC2 t4g.small ($41.58) + RDS db.t4g.small ($27.70) + ElastiCache Valkey ($9.34) + Net ($1.38) = ~$80.00/mo", serverServices: ["api", "enricher,engine,scheduler", "delivery"], + serverNodes: [ + { name: "api-server-1", port: 35678, services: "api" }, + { name: "pipeline-worker", port: 35680, services: "enricher,engine,scheduler" }, + { name: "delivery-worker", port: 35681, services: "delivery" }, + ], }, { budget: "$100/mo", monthlyCost: 100, serverCount: 4, - serverCpus: "1.0", - serverMemory: "1G", + serverCpus: "2.0", + serverMemory: "2G", dbCpus: "2.0", dbMemory: "2G", redisCpus: "1.5", redisMemory: "1.5G", - workerConcurrency: 450, + workerConcurrency: 400, dbMaxConnections: 20, - concurrency: 500, - description: "1x API + 1x Pipeline + 2x Delivery (2x Scaled Workers)", + concurrency: 350, + servicesCount: 10, + description: "1x API + 1x Enricher + 1x Engine + 1x Delivery (Decoupled Pipeline)", awsTopology: "4x EC2 t4g.small + RDS db.t4g.small + ElastiCache cache.t4g.small", awsCostDetails: "4x EC2 t4g.small ($53.84) + RDS db.t4g.small ($28.27) + ElastiCache t4g.small ($18.69) = ~$100.80/mo", serverServices: ["api", "enricher,scheduler", "engine", "delivery"], + serverNodes: [ + { name: "api-server-1", port: 35678, services: "api" }, + { name: "enricher-worker", port: 35683, services: "enricher,scheduler" }, + { name: "engine-worker", port: 35684, services: "engine" }, + { name: "delivery-worker", port: 35681, services: "delivery" }, + ], }, ]; +import fs from "node:fs"; +import path from "node:path"; + +export interface MetricStats { + mean: number; + median: number; + min: number; + max: number; + stdDev: number; +} + +export interface TierAggregatedStats { + tier: BudgetTier; + runs: DistributedBenchmarkResult[]; + ingestionThroughput: MetricStats; + activeDeliveryRate: MetricStats; + totalDeliveryRate: MetricStats; + apiP50: MetricStats; + apiP95: MetricStats; + apiP99: MetricStats; + deliveryP50: MetricStats; + deliveryP95: MetricStats; + deliveryP99: MetricStats; + costEfficiency: MetricStats; +} + +export function calculateStats(values: number[]): MetricStats { + if (values.length === 0) { + return { mean: 0, median: 0, min: 0, max: 0, stdDev: 0 }; + } + const sorted = [...values].sort((a, b) => a - b); + const sum = sorted.reduce((acc, v) => acc + v, 0); + const mean = sum / sorted.length; + const mid = Math.floor(sorted.length / 2); + const median = sorted.length % 2 !== 0 ? sorted[mid]! : (sorted[mid - 1]! + sorted[mid]!) / 2; + const min = sorted[0]!; + const max = sorted[sorted.length - 1]!; + const variance = sorted.reduce((acc, v) => acc + Math.pow(v - mean, 2), 0) / sorted.length; + const stdDev = Math.sqrt(variance); + + return { + mean: Number(mean.toFixed(2)), + median: Number(median.toFixed(2)), + min: Number(min.toFixed(2)), + max: Number(max.toFixed(2)), + stdDev: Number(stdDev.toFixed(2)), + }; +} + function parseArgs() { const args = process.argv.slice(2); - let durationSeconds = 10; - let providerLatencyMs: number | string | undefined = process.env.PROVIDER_LATENCY_MS || "60-150"; + let durationSeconds = 600; // 10 minutes default + let runs = 3; // 3 runs default + let providerLatencyMs: number | string | undefined = process.env.PROVIDER_LATENCY_MS || "150-250"; let tiers = DEFAULT_BUDGET_TIERS; for (const arg of args) { if (arg.startsWith("--duration=")) { durationSeconds = parseInt(arg.split("=")[1]!, 10); + } else if (arg.startsWith("--runs=") || arg.startsWith("--iterations=")) { + runs = parseInt(arg.split("=")[1]!, 10); } else if (arg === "--quick") { durationSeconds = 5; + runs = 1; } else if (arg.startsWith("--latency=")) { const val = arg.split("=")[1]!; providerLatencyMs = val.includes("-") ? val : parseInt(val, 10); @@ -150,67 +233,173 @@ function parseArgs() { } } - return { durationSeconds, providerLatencyMs, tiers }; + return { durationSeconds, runs, providerLatencyMs, tiers }; } -function printSummaryTable(results: Array<{ tier: BudgetTier; res: DistributedBenchmarkResult }>) { +function printTierBreakdown(stat: TierAggregatedStats, runsCount: number) { console.log("\n" + "=".repeat(170)); console.log( - " 💰 NOTIFKIT AWS BUDGET SCALING BENCHMARK SUMMARY 💰", + ` 📊 TIER STATISTICAL BREAKDOWN: ${stat.tier.budget} [${stat.tier.description}] across ${runsCount} Runs (${stat.runs[0]?.durationSec ?? 600}s each)`, ); console.log("=".repeat(170)); const headers = [ - "Budget Tier", - "AWS Architecture & Topology", - "Clients", - "Ingestion", - "Active Deliv", - "Wall Drain", - "API p95", - "Deliv p95", - "Cost Efficiency", + "Metric", + ...stat.runs.map((_, i) => `Run ${i + 1}`), + "Avg (Mean)", + "Median", + "Min", + "Max", + "StdDev", ]; - const colWidths = [13, 62, 9, 14, 15, 14, 10, 11, 15]; + const colWidths = [32, ...stat.runs.map(() => 14), 14, 14, 14, 14, 12]; + + const formatRow = (cols: string[]) => + cols.map((col, idx) => col.padEnd(colWidths[idx] ?? 14)).join(" | "); + + console.log(formatRow(headers)); + console.log("-".repeat(170)); + + const rows: Array<{ label: string; values: number[]; s: MetricStats; unit: string }> = [ + { + label: "Ingestion Throughput", + values: stat.runs.map((r) => r.ingestionThroughput), + s: stat.ingestionThroughput, + unit: " req/s", + }, + { + label: "Active Delivery Rate", + values: stat.runs.map((r) => r.activeDeliveryRate), + s: stat.activeDeliveryRate, + unit: " msg/s", + }, + { + label: "Wall Drain Rate", + values: stat.runs.map((r) => r.totalDeliveryRate), + s: stat.totalDeliveryRate, + unit: " msg/s", + }, + { + label: "API Latency p50", + values: stat.runs.map((r) => r.apiP50), + s: stat.apiP50, + unit: "ms", + }, + { + label: "API Latency p95", + values: stat.runs.map((r) => r.apiP95), + s: stat.apiP95, + unit: "ms", + }, + { + label: "API Latency p99", + values: stat.runs.map((r) => r.apiP99), + s: stat.apiP99, + unit: "ms", + }, + { + label: "Delivery Latency p50", + values: stat.runs.map((r) => r.deliveryP50), + s: stat.deliveryP50, + unit: "ms", + }, + { + label: "Delivery Latency p95", + values: stat.runs.map((r) => r.deliveryP95), + s: stat.deliveryP95, + unit: "ms", + }, + { + label: "Delivery Latency p99", + values: stat.runs.map((r) => r.deliveryP99), + s: stat.deliveryP99, + unit: "ms", + }, + { + label: "Cost Efficiency", + values: stat.runs.map((r) => + Number((r.activeDeliveryRate / stat.tier.monthlyCost).toFixed(2)), + ), + s: stat.costEfficiency, + unit: " msg/s/$", + }, + ]; + + for (const row of rows) { + const cols = [ + row.label, + ...row.values.map((v) => `${v}${row.unit}`), + `${row.s.mean}${row.unit}`, + `${row.s.median}${row.unit}`, + `${row.s.min}${row.unit}`, + `${row.s.max}${row.unit}`, + `${row.s.stdDev}`, + ]; + console.log(formatRow(cols)); + } + + console.log("=".repeat(170) + "\n"); +} + +function printSummaryTable(allStats: TierAggregatedStats[], runsCount: number) { + console.log("\n" + "=".repeat(215)); + console.log( + ` 💰 NOTIFKIT AWS BUDGET SCALING BENCHMARK SUMMARY (${runsCount} RUNS AGGREGATED) 💰`, + ); + console.log("=".repeat(215)); + + const headers = [ + "Budget Tier", + "AWS Architecture & Topology", + "Ingestion (Avg ± Std)", + "Active Deliv (Avg / Med / Max)", + "Wall Drain (Avg)", + "API p95 (Avg / Med)", + "Deliv p95 (Avg / Med)", + "Cost Eff (Avg)", + ]; + + const colWidths = [13, 56, 26, 34, 18, 22, 24, 16]; const formatRow = (cols: string[]) => cols.map((col, idx) => col.padEnd(colWidths[idx]!)).join(" | "); console.log(formatRow(headers)); - console.log("-".repeat(170)); - - for (const { tier, res } of results) { - const msgsPerDollar = (res.activeDeliveryRate / tier.monthlyCost).toFixed(1) + " msg/s/$"; + console.log("-".repeat(215)); + for (const stat of allStats) { 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, + stat.tier.budget, + stat.tier.description, + `${stat.ingestionThroughput.mean} ± ${stat.ingestionThroughput.stdDev} r/s`, + `${stat.activeDeliveryRate.mean} / ${stat.activeDeliveryRate.median} / ${stat.activeDeliveryRate.max} m/s`, + `${stat.totalDeliveryRate.mean} msg/s`, + `${stat.apiP95.mean}ms / ${stat.apiP95.median}ms`, + `${stat.deliveryP95.mean}ms / ${stat.deliveryP95.median}ms`, + `${stat.costEfficiency.mean} msg/s/$`, ]; console.log(formatRow(row)); } - console.log("=".repeat(170)); + console.log("=".repeat(215)); - console.log("\n" + "-".repeat(170)); - console.log(" 📋 AWS MONTHLY INFRASTRUCTURE PRICING BREAKDOWN"); - console.log("-".repeat(170)); - for (const { tier } of results) { - console.log(` • ${tier.budget.padEnd(8)} [${tier.awsTopology ?? tier.description}]:`); - console.log(` ${tier.awsCostDetails ?? "On-demand AWS components"}`); + console.log("\n" + "-".repeat(215)); + console.log( + " 📋 AWS MONTHLY INFRASTRUCTURE PRICING BREAKDOWN", + ); + console.log("-".repeat(215)); + for (const stat of allStats) { + console.log( + ` • ${stat.tier.budget.padEnd(8)} [${stat.tier.awsTopology ?? stat.tier.description}]:`, + ); + console.log(` ${stat.tier.awsCostDetails ?? "On-demand AWS components"}`); } - console.log("-".repeat(170) + "\n"); + console.log("-".repeat(215) + "\n"); } async function main() { - const { durationSeconds, providerLatencyMs, tiers } = parseArgs(); + const { durationSeconds, runs, providerLatencyMs, tiers } = parseArgs(); console.log( "==========================================================================================", @@ -222,7 +411,7 @@ async function main() { "==========================================================================================", ); console.log( - `Window: ${durationSeconds}s per tier | Measuring: ${DEFAULT_BUDGET_TIERS.map((t) => t.budget).join(" ➜ ")} (AWS Actual Pricing)`, + `Window: ${durationSeconds}s (${(durationSeconds / 60).toFixed(1)} mins) per run | Runs: ${runs} | Total Runs: ${tiers.length * runs}`, ); console.log(`Tiers to test: ${tiers.map((t) => t.budget).join(" ➜ ")}`); if (providerLatencyMs && providerLatencyMs !== "0") { @@ -232,44 +421,96 @@ async function main() { "==========================================================================================\n", ); - const results: Array<{ tier: BudgetTier; res: DistributedBenchmarkResult }> = []; + const allStats: TierAggregatedStats[] = []; + + for (let tIdx = 0; tIdx < tiers.length; tIdx++) { + const tier = tiers[tIdx]!; + const tierRuns: 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( + `[Tier ${tIdx + 1}/${tiers.length}] Budget Tier: ${tier.budget} — Running ${runs} iteration(s)`, + ); + console.log( + `Topology: ${tier.description} (${tier.servicesCount ?? 10} Microservices, ${tier.concurrency} In-Flight Workers)`, + ); 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, - }); + for (let rIdx = 0; rIdx < runs; rIdx++) { + console.log( + `\n--- [Tier ${tier.budget}] Run ${rIdx + 1} of ${runs} (Duration: ${durationSeconds}s) ---`, + ); - results.push({ tier, res }); + 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, + serverNodes: tier.serverNodes, + providerLatencyMs, + durationSeconds, + concurrency: tier.concurrency, + servicesCount: tier.servicesCount, + }); - if (i < tiers.length - 1) { - console.log("⏳ Cooling down for 4s before next tier..."); - await new Promise((r) => setTimeout(r, 4000)); + tierRuns.push(res); + + if (rIdx < runs - 1) { + console.log("⏳ Cooling down for 5s before next run..."); + await new Promise((resolve) => setTimeout(resolve, 5000)); + } + } + + const tierStats: TierAggregatedStats = { + tier, + runs: tierRuns, + ingestionThroughput: calculateStats(tierRuns.map((r) => r.ingestionThroughput)), + activeDeliveryRate: calculateStats(tierRuns.map((r) => r.activeDeliveryRate)), + totalDeliveryRate: calculateStats(tierRuns.map((r) => r.totalDeliveryRate)), + apiP50: calculateStats(tierRuns.map((r) => r.apiP50)), + apiP95: calculateStats(tierRuns.map((r) => r.apiP95)), + apiP99: calculateStats(tierRuns.map((r) => r.apiP99)), + deliveryP50: calculateStats(tierRuns.map((r) => r.deliveryP50)), + deliveryP95: calculateStats(tierRuns.map((r) => r.deliveryP95)), + deliveryP99: calculateStats(tierRuns.map((r) => r.deliveryP99)), + costEfficiency: calculateStats( + tierRuns.map((r) => Number((r.activeDeliveryRate / tier.monthlyCost).toFixed(2))), + ), + }; + + allStats.push(tierStats); + printTierBreakdown(tierStats, runs); + + if (tIdx < tiers.length - 1) { + console.log("⏳ Cooling down for 6s before next tier..."); + await new Promise((resolve) => setTimeout(resolve, 6000)); } } - printSummaryTable(results); + printSummaryTable(allStats, runs); + + // Persist results to JSON + try { + const resultsDir = path.resolve(process.cwd(), "results"); + if (!fs.existsSync(resultsDir)) { + fs.mkdirSync(resultsDir, { recursive: true }); + } + const outputPath = path.join(resultsDir, `money-scaling-${Date.now()}.json`); + fs.writeFileSync(outputPath, JSON.stringify(allStats, null, 2), "utf-8"); + console.log(`📁 Detailed statistical benchmark results saved to: ${outputPath}`); + } catch (err) { + console.warn("⚠️ Failed to persist results to JSON:", err); + } } main().catch((err) => { diff --git a/tests/http.test.ts b/tests/http.test.ts index fad58a8..69633c3 100644 --- a/tests/http.test.ts +++ b/tests/http.test.ts @@ -171,8 +171,8 @@ describe("HTTP Helpers (src/services/api/http.ts)", () => { const parsedBody = JSON.parse(res.body); expect(parsedBody.error).toBe("validation_error"); expect(parsedBody.issues).toEqual([ - { path: "user.email", message: "Invalid email" }, - { path: "user.age", message: "Number must be greater than or equal to 18" }, + { path: "user.email", message: "Invalid email address" }, + { path: "user.age", message: "Too small: expected number to be >=18" }, ]); } }); diff --git a/tests/workers-ai.test.ts b/tests/workers-ai.test.ts index a7eba4c..c8b0a85 100644 --- a/tests/workers-ai.test.ts +++ b/tests/workers-ai.test.ts @@ -81,8 +81,8 @@ describe("AiWorker Edge Cases", () => { event: buildStreamEvent( "notification.ai_pending", { - projectId: "00000000-0000-0000-0000-000000000001", - enrichedEventId: "00000000-0000-0000-0000-000000000002", + projectId: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", + enrichedEventId: "b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22", recipientId: "usr1", channel: "email", priority: "normal", @@ -105,8 +105,8 @@ describe("AiWorker Edge Cases", () => { event: buildStreamEvent( "notification.ai_pending", { - projectId: "00000000-0000-0000-0000-000000000001", - enrichedEventId: "00000000-0000-0000-0000-000000000002", + projectId: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", + enrichedEventId: "b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22", recipientId: "usr1", channel: "email", priority: "normal", @@ -130,8 +130,8 @@ describe("AiWorker Edge Cases", () => { event: buildStreamEvent( "notification.ai_pending", { - projectId: "00000000-0000-0000-0000-000000000001", - enrichedEventId: "00000000-0000-0000-0000-000000000002", + projectId: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", + enrichedEventId: "b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22", recipientId: "usr1", channel: "email", priority: "normal", @@ -157,8 +157,8 @@ describe("AiWorker Edge Cases", () => { event: buildStreamEvent( "notification.ai_pending", { - projectId: "00000000-0000-0000-0000-000000000001", - enrichedEventId: "00000000-0000-0000-0000-000000000002", + projectId: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", + enrichedEventId: "b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22", recipientId: "usr1", channel: "email", priority: "normal", @@ -174,7 +174,7 @@ describe("AiWorker Edge Cases", () => { await worker.process(msg); expect(globalEmitter.emit).toHaveBeenCalledWith( "notification:failed", - "00000000-0000-0000-0000-000000000002", + "b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22", "Invalid prompt", "email", ); @@ -191,8 +191,8 @@ describe("AiWorker Edge Cases", () => { event: buildStreamEvent( "notification.ai_pending", { - projectId: "00000000-0000-0000-0000-000000000001", - enrichedEventId: "00000000-0000-0000-0000-000000000002", + projectId: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", + enrichedEventId: "b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22", recipientId: "usr1", channel: "email", priority: "normal", @@ -215,8 +215,8 @@ describe("AiWorker Edge Cases", () => { event: buildStreamEvent( "notification.ai_pending", { - projectId: "00000000-0000-0000-0000-000000000001", - enrichedEventId: "00000000-0000-0000-0000-000000000002", + projectId: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", + enrichedEventId: "b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22", recipientId: "usr1", channel: "push", priority: "normal", @@ -241,8 +241,8 @@ describe("AiWorker Edge Cases", () => { event: buildStreamEvent( "notification.ai_pending", { - projectId: "00000000-0000-0000-0000-000000000001", - enrichedEventId: "00000000-0000-0000-0000-000000000002", + projectId: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", + enrichedEventId: "b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22", recipientId: "usr1", channel: "push", priority: "normal",