diff --git a/package-lock.json b/package-lock.json index bddeb36..7dbf4d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,37 +9,36 @@ "version": "2.0.5", "license": "GPL-3.0-only", "dependencies": { - "ioredis": "^5.6.1" + "ioredis": "^5.7.0" }, "devDependencies": { - "@eslint/js": "^9.30.0", + "@eslint/js": "^9.33.0", "@types/chai": "^5.2.2", "@types/eslint__eslintrc": "^2.1.2", "@types/mocha": "^10.0.0", "@types/mock-require": "^3.0.0", - "@types/node": "^24.0.8", + "@types/node": "^24.2.1", "@types/sinon": "^17.0.4", "@types/yargs": "^17.0.33", - "@typescript-eslint/eslint-plugin": "^8.35.1", - "@typescript-eslint/parser": "^8.35.1", - "@typescript-eslint/typescript-estree": "^8.35.1", - "chai": "^5.2.0", - "codeclimate-test-reporter": "^0.5.1", - "coveralls-next": "^4.2.1", - "eslint": "^9.30.0", - "eslint-plugin-jsdoc": "^51.3.1", + "@typescript-eslint/eslint-plugin": "^8.39.0", + "@typescript-eslint/parser": "^8.39.0", + "@typescript-eslint/typescript-estree": "^8.39.0", + "chai": "^5.2.1", + "coveralls-next": "^5.0.0", + "eslint": "^9.33.0", + "eslint-plugin-jsdoc": "^52.0.4", "mocha": "^11.7.1", "mocha-lcov-reporter": "^1.3.0", "mock-require": "^3.0.3", "nyc": "^17.1.0", - "open": "^10.1.2", + "open": "^10.2.0", "reflect-metadata": "^0.2.2", "sinon": "^21.0.0", "source-map-support": "^0.5.21", "ts-node": "^10.9.2", - "typedoc": "^0.28.7", - "typescript": "^5.8.3", - "yargs": "^17.7.2" + "typedoc": "^0.28.9", + "typescript": "^5.9.2", + "yargs": "^18.0.0" } }, "node_modules/@ampproject/remapping": { @@ -433,9 +432,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -443,9 +442,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -514,9 +513,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.30.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.1.tgz", - "integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", "dev": true, "license": "MIT", "engines": { @@ -537,43 +536,30 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", - "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.1", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@gerrit0/mini-shiki": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.7.0.tgz", - "integrity": "sha512-7iY9wg4FWXmeoFJpUL2u+tsmh0d0jcEJHAIzVxl3TG4KL493JNnisdLAILZ77zcD+z3J0keEXZ+lFzUgzQzPDg==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.9.2.tgz", + "integrity": "sha512-Tvsj+AOO4Z8xLRJK900WkyfxHsZQu+Zm1//oT1w443PO6RiYMoq/4NGOhaNuZoUMYsjKIAPVQ6eOFMddj6yphQ==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/engine-oniguruma": "^3.7.0", - "@shikijs/langs": "^3.7.0", - "@shikijs/themes": "^3.7.0", - "@shikijs/types": "^3.7.0", + "@shikijs/engine-oniguruma": "^3.9.2", + "@shikijs/langs": "^3.9.2", + "@shikijs/themes": "^3.9.2", + "@shikijs/types": "^3.9.2", "@shikijs/vscode-textmate": "^10.0.2" } }, @@ -644,9 +630,9 @@ } }, "node_modules/@ioredis/commands": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", - "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.0.tgz", + "integrity": "sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==", "license": "MIT" }, "node_modules/@isaacs/cliui": { @@ -873,40 +859,40 @@ } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.7.0.tgz", - "integrity": "sha512-5BxcD6LjVWsGu4xyaBC5bu8LdNgPCVBnAkWTtOCs/CZxcB22L8rcoWfv7Hh/3WooVjBZmFtyxhgvkQFedPGnFw==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.9.2.tgz", + "integrity": "sha512-Vn/w5oyQ6TUgTVDIC/BrpXwIlfK6V6kGWDVVz2eRkF2v13YoENUvaNwxMsQU/t6oCuZKzqp9vqtEtEzKl9VegA==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.7.0", + "@shikijs/types": "3.9.2", "@shikijs/vscode-textmate": "^10.0.2" } }, "node_modules/@shikijs/langs": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.7.0.tgz", - "integrity": "sha512-1zYtdfXLr9xDKLTGy5kb7O0zDQsxXiIsw1iIBcNOO8Yi5/Y1qDbJ+0VsFoqTlzdmneO8Ij35g7QKF8kcLyznCQ==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.9.2.tgz", + "integrity": "sha512-X1Q6wRRQXY7HqAuX3I8WjMscjeGjqXCg/Sve7J2GWFORXkSrXud23UECqTBIdCSNKJioFtmUGJQNKtlMMZMn0w==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.7.0" + "@shikijs/types": "3.9.2" } }, "node_modules/@shikijs/themes": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.7.0.tgz", - "integrity": "sha512-VJx8497iZPy5zLiiCTSIaOChIcKQwR0FebwE9S3rcN0+J/GTWwQ1v/bqhTbpbY3zybPKeO8wdammqkpXc4NVjQ==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.9.2.tgz", + "integrity": "sha512-6z5lBPBMRfLyyEsgf6uJDHPa6NAGVzFJqH4EAZ+03+7sedYir2yJBRu2uPZOKmj43GyhVHWHvyduLDAwJQfDjA==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.7.0" + "@shikijs/types": "3.9.2" } }, "node_modules/@shikijs/types": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.7.0.tgz", - "integrity": "sha512-MGaLeaRlSWpnP0XSAum3kP3a8vtcTsITqoEPYdt3lQG3YCdQH4DnEhodkYcNMcU0uW0RffhoD1O3e0vG5eSBBg==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.9.2.tgz", + "integrity": "sha512-/M5L0Uc2ljyn2jKvj4Yiah7ow/W+DJSglVafvWAJ/b8AZDeeRAdMu3c2riDzB7N42VD+jSnWxeP9AKtd4TfYVw==", "dev": true, "license": "MIT", "dependencies": { @@ -1068,13 +1054,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.0.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz", - "integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==", + "version": "24.2.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", + "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.8.0" + "undici-types": "~7.10.0" } }, "node_modules/@types/sinon": { @@ -1119,17 +1105,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz", - "integrity": "sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", + "integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.35.1", - "@typescript-eslint/type-utils": "8.35.1", - "@typescript-eslint/utils": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/type-utils": "8.39.0", + "@typescript-eslint/utils": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1143,22 +1129,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.35.1", + "@typescript-eslint/parser": "^8.39.0", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.1.tgz", - "integrity": "sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz", + "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.35.1", - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/typescript-estree": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4" }, "engines": { @@ -1170,18 +1156,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.1.tgz", - "integrity": "sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz", + "integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.35.1", - "@typescript-eslint/types": "^8.35.1", + "@typescript-eslint/tsconfig-utils": "^8.39.0", + "@typescript-eslint/types": "^8.39.0", "debug": "^4.3.4" }, "engines": { @@ -1192,18 +1178,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.1.tgz", - "integrity": "sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz", + "integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1" + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1214,9 +1200,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.1.tgz", - "integrity": "sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz", + "integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==", "dev": true, "license": "MIT", "engines": { @@ -1227,18 +1213,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.1.tgz", - "integrity": "sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz", + "integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.35.1", - "@typescript-eslint/utils": "8.35.1", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/utils": "8.39.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1251,13 +1238,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.1.tgz", - "integrity": "sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz", + "integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==", "dev": true, "license": "MIT", "engines": { @@ -1269,16 +1256,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.1.tgz", - "integrity": "sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz", + "integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.35.1", - "@typescript-eslint/tsconfig-utils": "8.35.1", - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1", + "@typescript-eslint/project-service": "8.39.0", + "@typescript-eslint/tsconfig-utils": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1294,20 +1281,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.1.tgz", - "integrity": "sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz", + "integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.35.1", - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/typescript-estree": "8.35.1" + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1318,17 +1305,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.1.tgz", - "integrity": "sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz", + "integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/types": "8.39.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -1492,26 +1479,6 @@ "dev": true, "license": "Python-2.0" }, - "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/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1522,37 +1489,6 @@ "node": ">=12" } }, - "node_modules/async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", - "dev": true, - "license": "MIT" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", - "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", - "dev": true, - "license": "MIT" - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1560,16 +1496,6 @@ "dev": true, "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/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -1713,17 +1639,10 @@ ], "license": "CC-BY-4.0" }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/chai": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", - "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", + "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", "dev": true, "license": "MIT", "dependencies": { @@ -1734,7 +1653,7 @@ "pathval": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/chalk": { @@ -1791,78 +1710,71 @@ } }, "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==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, "license": "ISC", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=12" + "node": ">=20" } }, - "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==", + "node_modules/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "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==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "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" + "node": ">=18" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" @@ -1877,26 +1789,6 @@ "node": ">=0.10.0" } }, - "node_modules/codeclimate-test-reporter": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/codeclimate-test-reporter/-/codeclimate-test-reporter-0.5.1.tgz", - "integrity": "sha512-XCzmc8dH+R4orK11BCg5pBbXc35abxq9sept4YvUFRkFl9zb9MIVRrCKENe6U1TKAMTgvGJmrYyHn0y2lerpmg==", - "deprecated": "codeclimate-test-reporter has been deprecated in favor of our new unified test-reporter. Please visit https://docs.codeclimate.com/docs/configuring-test-coverage for details on setting up the new test-reporter.", - "dev": true, - "license": "MIT", - "dependencies": { - "async": "~1.5.2", - "commander": "2.9.0", - "lcov-parse": "0.0.10", - "request": "~2.88.0" - }, - "bin": { - "codeclimate-test-reporter": "bin/codeclimate.js" - }, - "engines": { - "node": ">= 4" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1917,32 +1809,6 @@ "dev": true, "license": "MIT" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", - "integrity": "sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-readlink": ">= 1.0.0" - }, - "engines": { - "node": ">= 0.6.x" - } - }, "node_modules/comment-parser": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", @@ -1974,21 +1840,13 @@ "dev": true, "license": "MIT" }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "license": "MIT" - }, "node_modules/coveralls-next": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/coveralls-next/-/coveralls-next-4.2.1.tgz", - "integrity": "sha512-O/SBGZsCryt+6Q3NuJHENyQYaucTEV9qp0KGaed+y42PUh+GuF949LRLHKZbxWwOIc1tV8bJRIVWlfbZ8etEwQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/coveralls-next/-/coveralls-next-5.0.0.tgz", + "integrity": "sha512-RCj6Oflf6iQtN3Q5b0SSemEbQBzeBjQlLUrc3bfNECTy83hMJA9krdNZ5GTRm7Jpbyo92yKUbQDP5FYlWcL5sA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "form-data": "4.0.0", "js-yaml": "4.1.0", "lcov-parse": "1.0.0", "log-driver": "1.2.7", @@ -1998,7 +1856,7 @@ "coveralls": "bin/coveralls.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/coveralls-next/node_modules/lcov-parse": { @@ -2033,19 +1891,6 @@ "node": ">= 8" } }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -2149,16 +1994,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", @@ -2185,17 +2020,6 @@ "dev": true, "license": "MIT" }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, "node_modules/electron-to-chromium": { "version": "1.5.178", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.178.tgz", @@ -2254,20 +2078,20 @@ } }, "node_modules/eslint": { - "version": "9.30.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz", - "integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", + "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.14.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.30.1", - "@eslint/plugin-kit": "^0.3.1", + "@eslint/js": "9.33.0", + "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -2315,9 +2139,9 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "51.3.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-51.3.2.tgz", - "integrity": "sha512-sBmS2MoxbUuKE1wMn/jeHitlCwdk3jAkkpdo3TNA5qGADjiow9D5z/zJ3XScScDsNI2fzZJsmCyf5rc12oRbUA==", + "version": "52.0.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-52.0.4.tgz", + "integrity": "sha512-be5OzGlLExvcK13Il3noU7/v7WmAQGenTmCaBKf1pwVtPOb6X+PGFVnJad0QhMj4KKf45XjE4hbsBxv25q1fTg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2507,23 +2331,6 @@ "node": ">=0.10.0" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true, - "license": "MIT" - }, - "node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2694,31 +2501,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fromentries": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", @@ -2764,6 +2546,19 @@ "dev": true, "license": "ISC" }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -2774,16 +2569,6 @@ "node": ">=8.0.0" } }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - } - }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -2838,13 +2623,6 @@ "dev": true, "license": "ISC" }, - "node_modules/graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==", - "dev": true, - "license": "MIT" - }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -2852,31 +2630,6 @@ "dev": true, "license": "MIT" }, - "node_modules/har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "deprecated": "this library is no longer supported", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2921,22 +2674,6 @@ "dev": true, "license": "MIT" }, - "node_modules/http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - } - }, "node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -3004,12 +2741,12 @@ "license": "ISC" }, "node_modules/ioredis": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz", - "integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.7.0.tgz", + "integrity": "sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==", "license": "MIT", "dependencies": { - "@ioredis/commands": "^1.1.1", + "@ioredis/commands": "^1.3.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", @@ -3181,13 +2918,6 @@ "dev": true, "license": "ISC" }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", - "dev": true, - "license": "MIT" - }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -3342,13 +3072,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", - "dev": true, - "license": "MIT" - }, "node_modules/jsdoc-type-pratt-parser": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz", @@ -3379,13 +3102,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "dev": true, - "license": "(AFL-2.1 OR BSD-3-Clause)" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -3400,13 +3116,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, - "license": "ISC" - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3420,22 +3129,6 @@ "node": ">=6" } }, - "node_modules/jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3446,13 +3139,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/lcov-parse": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", - "integrity": "sha512-YsL0D4QF/vNlNcHPXM832si9d2ROryFQ4r4JvcfMIiUYr1f6WULuO75YCtxNu4P+XMRHz0SfUc524+c+U3G5kg==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3660,29 +3346,6 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -3765,6 +3428,76 @@ "node": ">= 0.6.0" } }, + "node_modules/mocha/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/mocha/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/mocha/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/mocha/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/mocha/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/mocha/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/mocha/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -3781,6 +3514,43 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/mocha/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/mocha/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "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/mock-require": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-3.0.3.tgz", @@ -4128,16 +3898,6 @@ "node": ">=6" } }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4149,16 +3909,16 @@ } }, "node_modules/open": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", - "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", "dev": true, "license": "MIT", "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", - "is-wsl": "^3.1.0" + "wsl-utils": "^0.1.0" }, "engines": { "node": ">=18" @@ -4357,13 +4117,6 @@ "node": ">= 14.16" } }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", - "dev": true, - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4476,19 +4229,6 @@ "node": ">=8" } }, - "node_modules/psl": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", - "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "funding": { - "url": "https://github.com/sponsors/lupomontero" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4509,16 +4249,6 @@ "node": ">=6" } }, - "node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.6" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4612,65 +4342,6 @@ "dev": true, "license": "ISC" }, - "node_modules/request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/request/node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/request/node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "bin/uuid" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4830,13 +4501,6 @@ ], "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/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -5013,32 +4677,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/sshpk": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", - "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -5259,20 +4897,6 @@ "node": ">=8.0" } }, - "node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -5340,26 +4964,6 @@ "node": ">=0.3.1" } }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "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/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5404,13 +5008,13 @@ } }, "node_modules/typedoc": { - "version": "0.28.7", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.7.tgz", - "integrity": "sha512-lpz0Oxl6aidFkmS90VQDQjk/Qf2iw0IUvFqirdONBdj7jPSN9mGXhy66BcGNDxx5ZMyKKiBVAREvPEzT6Uxipw==", + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.9.tgz", + "integrity": "sha512-aw45vwtwOl3QkUAmWCnLV9QW1xY+FSX2zzlit4MAfE99wX+Jij4ycnpbAWgBXsRrxmfs9LaYktg/eX5Bpthd3g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@gerrit0/mini-shiki": "^3.7.0", + "@gerrit0/mini-shiki": "^3.9.0", "lunr": "^2.3.9", "markdown-it": "^14.1.0", "minimatch": "^9.0.5", @@ -5424,13 +5028,13 @@ "pnpm": ">= 10" }, "peerDependencies": { - "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x" + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x" } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5449,9 +5053,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", "dev": true, "license": "MIT" }, @@ -5513,21 +5117,6 @@ "dev": true, "license": "MIT" }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5690,6 +5279,22 @@ "dev": true, "license": "ISC" }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -5721,22 +5326,21 @@ } }, "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", "dev": true, "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^7.2.0", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { @@ -5791,20 +5395,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "dev": true, "license": "MIT" }, @@ -5819,31 +5413,31 @@ } }, "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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "node_modules/yargs/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, + "license": "ISC", "engines": { - "node": ">=8" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yn": { diff --git a/package.json b/package.json index 0da7309..2a158ae 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,9 @@ "json-message" ], "scripts": { + "benchmark": "node benchmark -c $(( $(nproc) - 2 )) -m 100000", "prepare": "./node_modules/.bin/tsc", - "test": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha && ./node_modules/.bin/nyc report --reporter=text-lcov && npm run test-coverage", + "test": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha && ./node_modules/.bin/nyc report --reporter=text-lcov", "test-fast": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha && /usr/bin/env node -e \"import('open').then(open => open.default('file://`pwd`/coverage/index.html', { wait: false }))\"", "test-local": "export COVERALLS_REPO_TOKEN=$IMQ_COVERALLS_TOKEN && npm test && /usr/bin/env node -e \"import('open').then(open => open.default('https://coveralls.io/github/imqueue/imq', { wait: false }))\"", "test-dev": "npm run test && npm run clean-js && npm run clean-typedefs && npm run clean-maps", @@ -37,37 +38,36 @@ "author": "imqueue.com (https://imqueue.com)", "license": "GPL-3.0-only", "dependencies": { - "ioredis": "^5.6.1" + "ioredis": "^5.7.0" }, "devDependencies": { - "@eslint/js": "^9.30.0", + "@eslint/js": "^9.33.0", "@types/chai": "^5.2.2", "@types/eslint__eslintrc": "^2.1.2", "@types/mocha": "^10.0.0", "@types/mock-require": "^3.0.0", - "@types/node": "^24.0.8", + "@types/node": "^24.2.1", "@types/sinon": "^17.0.4", "@types/yargs": "^17.0.33", - "@typescript-eslint/eslint-plugin": "^8.35.1", - "@typescript-eslint/parser": "^8.35.1", - "@typescript-eslint/typescript-estree": "^8.35.1", - "chai": "^5.2.0", - "codeclimate-test-reporter": "^0.5.1", - "coveralls-next": "^4.2.1", - "eslint": "^9.30.0", - "eslint-plugin-jsdoc": "^51.3.1", + "@typescript-eslint/eslint-plugin": "^8.39.0", + "@typescript-eslint/parser": "^8.39.0", + "@typescript-eslint/typescript-estree": "^8.39.0", + "chai": "^5.2.1", + "coveralls-next": "^5.0.0", + "eslint": "^9.33.0", + "eslint-plugin-jsdoc": "^52.0.4", "mocha": "^11.7.1", "mocha-lcov-reporter": "^1.3.0", "mock-require": "^3.0.3", "nyc": "^17.1.0", - "open": "^10.1.2", + "open": "^10.2.0", "reflect-metadata": "^0.2.2", "sinon": "^21.0.0", "source-map-support": "^0.5.21", "ts-node": "^10.9.2", - "typedoc": "^0.28.7", - "typescript": "^5.8.3", - "yargs": "^17.7.2" + "typedoc": "^0.28.9", + "typescript": "^5.9.2", + "yargs": "^18.0.0" }, "main": "index.js", "typescript": { diff --git a/src/UDPClusterManager.ts b/src/UDPClusterManager.ts index 2e67343..644d52c 100644 --- a/src/UDPClusterManager.ts +++ b/src/UDPClusterManager.ts @@ -368,7 +368,10 @@ export class UDPClusterManager extends ClusterManager { if (typeof socket.close === 'function') { socket.removeAllListeners(); socket.close(() => { - socket?.unref(); + // unref may be missing or not a function on mocked sockets + if (socket && typeof (socket as any).unref === 'function') { + socket.unref(); + } if ( socketKey diff --git a/test/ClusterManager.spec.ts b/test/ClusterManager.spec.ts new file mode 100644 index 0000000..895e9fc --- /dev/null +++ b/test/ClusterManager.spec.ts @@ -0,0 +1,54 @@ +/*! + * ClusterManager additional tests + * + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { ClusterManager, InitializedCluster } from '../src/ClusterManager'; + +class TestClusterManager extends ClusterManager { + public destroyed = false; + public constructor() { super(); } + public async destroy(): Promise { + this.destroyed = true; + } +} + +describe('ClusterManager.remove()', () => { + it('should call destroy when the last cluster is removed and destroy=true', async () => { + const cm = new TestClusterManager(); + const cluster: InitializedCluster = cm.init({ + add: () => undefined, + remove: () => undefined, + find: () => undefined, + }); + + // sanity: one cluster registered + expect((cm as any).clusters.length).to.equal(1); + const spy = sinon.spy(cm, 'destroy'); + + await cm.remove(cluster, true); + + expect(spy.calledOnce).to.be.true; + expect((cm as any).clusters.length).to.equal(0); + expect(cm.destroyed).to.be.true; + }); + + it('should not call destroy when destroy=false', async () => { + const cm = new TestClusterManager(); + const cluster: InitializedCluster = cm.init({ + add: () => undefined, + remove: () => undefined, + find: () => undefined, + }); + + const spy = sinon.spy(cm, 'destroy'); + await cm.remove(cluster.id, false); + + expect(spy.called).to.be.false; + expect((cm as any).clusters.length).to.equal(0); + }); +}); diff --git a/test/ClusteredRedisQueue.addServer.defaultInit.spec.ts b/test/ClusteredRedisQueue.addServer.defaultInit.spec.ts new file mode 100644 index 0000000..c70dd47 --- /dev/null +++ b/test/ClusteredRedisQueue.addServer.defaultInit.spec.ts @@ -0,0 +1,34 @@ +/*! + * ClusteredRedisQueue.addServerWithQueueInitializing default param branch + */ +import './mocks'; +import { expect } from 'chai'; +import { ClusteredRedisQueue } from '../src'; + +describe('ClusteredRedisQueue.addServerWithQueueInitializing() default param', () => { + it('should use default initializeQueue=true when second param omitted', async () => { + const cq: any = new ClusteredRedisQueue('CQ-Default', { + logger: console, + cluster: [{ host: '127.0.0.1', port: 6379 }], + }); + // prevent any actual start/subscription side-effects + (cq as any).state.started = false; + (cq as any).state.subscription = null; + + const server = { host: '192.168.0.1', port: 6380 }; + const initializedSpy = new Promise((resolve) => { + cq['clusterEmitter'].once('initialized', () => resolve()); + }); + + // Call without the second argument to hit default "true" branch + (cq as any).addServerWithQueueInitializing(server); + + await initializedSpy; // should emit initialized when default is true + + // Ensure the server added and queue length updated + expect((cq as any).servers.some((s: any) => s.host === server.host && s.port === server.port)).to.equal(true); + expect((cq as any).queueLength).to.equal((cq as any).imqs.length); + + await cq.destroy(); + }); +}); diff --git a/test/ClusteredRedisQueue.addServer.noInit.spec.ts b/test/ClusteredRedisQueue.addServer.noInit.spec.ts new file mode 100644 index 0000000..8666387 --- /dev/null +++ b/test/ClusteredRedisQueue.addServer.noInit.spec.ts @@ -0,0 +1,32 @@ +/*! + * Cover ClusteredRedisQueue.addServerWithQueueInitializing with initializeQueue=false + */ +import './mocks'; +import { expect } from 'chai'; +import { ClusteredRedisQueue } from '../src'; +import { ClusterManager } from '../src/ClusterManager'; + +const server = { host: '127.0.0.1', port: 6380 }; + +describe('ClusteredRedisQueue.addServerWithQueueInitializing(false)', () => { + it('should add server without initializing queue and not emit initialized', async () => { + const manager = new (ClusterManager as any)(); + const cq: any = new ClusteredRedisQueue('NoInit', { clusterManagers: [manager] }); + + let initializedCalled = false; + (cq as any).clusterEmitter.on('initialized', () => { initializedCalled = true; }); + + // call private method via any to cover branch + (cq as any).addServerWithQueueInitializing(server, false); + + // should have server and imq added + expect(cq.servers.length).to.be.greaterThan(0); + expect(cq.imqs.length).to.be.greaterThan(0); + // queueLength updated + expect(cq.queueLength).to.equal(cq.imqs.length); + // initialized not emitted + expect(initializedCalled).to.equal(false); + + await cq.destroy(); + }); +}); diff --git a/test/ClusteredRedisQueue.extra.spec.ts b/test/ClusteredRedisQueue.extra.spec.ts new file mode 100644 index 0000000..358d319 --- /dev/null +++ b/test/ClusteredRedisQueue.extra.spec.ts @@ -0,0 +1,48 @@ +/*! + * Additional tests for ClusteredRedisQueue event emitter proxy methods + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { ClusteredRedisQueue } from '../src'; +import { ClusterManager } from '../src/ClusterManager'; + +const clusterConfig = { + cluster: [ + { host: '127.0.0.1', port: 6379 }, + ], +}; + +describe('ClusteredRedisQueue - EventEmitter proxy methods', () => { + it('should cover rawListeners/getMaxListeners/eventNames/listenerCount/emit', async () => { + const clusterManager = new (ClusterManager as any)(); + const cq: any = new ClusteredRedisQueue('ProxyQueue', { + clusterManagers: [clusterManager], + }); + + // add underlying server and listener + cq.addServer(clusterConfig.cluster[0]); + const handler = sinon.spy(); + cq.imqs[0].on('test', handler); + + // set max listeners across emitters and verify getMaxListeners uses templateEmitter + cq.setMaxListeners(20); + expect(cq.getMaxListeners()).to.equal(20); + + // collect raw listeners + const raw = cq.rawListeners('test'); + expect(raw.length).to.be.greaterThan(0); + + // event names come from underlying imq + const names = cq.eventNames(); + expect(names).to.be.an('array'); + expect(names.map(String)).to.include('test'); + + // listener count is aggregated via templateEmitter method applied on imq[0] + expect(cq.listenerCount('test')).to.equal(1); + + // emit should return true + expect(cq.emit('test', 1, 2, 3)).to.equal(true); + expect(handler.calledOnce).to.be.true; + }); +}); diff --git a/test/ClusteredRedisQueue.initialize.spec.ts b/test/ClusteredRedisQueue.initialize.spec.ts new file mode 100644 index 0000000..90520e8 --- /dev/null +++ b/test/ClusteredRedisQueue.initialize.spec.ts @@ -0,0 +1,37 @@ +/*! + * Additional tests for ClusteredRedisQueue.initializeQueue branches + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { ClusteredRedisQueue, RedisQueue } from '../src'; +import { ClusterManager } from '../src/ClusterManager'; + +describe('ClusteredRedisQueue.initializeQueue()', () => { + it('should call imq.start when started and imq.subscribe when subscription is set', async () => { + const startStub = sinon.stub(RedisQueue.prototype as any, 'start').resolves(undefined); + const subscribeStub = sinon.stub(RedisQueue.prototype as any, 'subscribe').resolves(); + + const clusterManager = new (ClusterManager as any)(); + const cq: any = new ClusteredRedisQueue('InitCover', { clusterManagers: [clusterManager] }); + + // mark started and set subscription using public APIs + await cq.start(); + const channel = 'X'; + const handler = () => undefined; + await cq.subscribe(channel, handler); + + // adding a server triggers initializeQueue which should call start and subscribe + cq.addServer({ host: '127.0.0.1', port: 6453 }); + + // allow promises to resolve + await new Promise(res => setTimeout(res, 0)); + + expect(startStub.called).to.be.true; + expect(subscribeStub.called).to.be.true; + + startStub.restore(); + subscribeStub.restore(); + await cq.destroy(); + }); +}); diff --git a/test/ClusteredRedisQueue.matchServers.spec.ts b/test/ClusteredRedisQueue.matchServers.spec.ts new file mode 100644 index 0000000..adf16c2 --- /dev/null +++ b/test/ClusteredRedisQueue.matchServers.spec.ts @@ -0,0 +1,34 @@ +/*! + * Tests for ClusteredRedisQueue.matchServers combinations + */ +import './mocks'; +import { expect } from 'chai'; +import { ClusteredRedisQueue } from '../src'; + +// Access private static via casting +const match = (ClusteredRedisQueue as any).matchServers as ( + source: any, target: any, strict?: boolean +) => boolean; + +describe('ClusteredRedisQueue.matchServers()', () => { + it('should return sameAddress when no ids provided', () => { + expect(match({ host: 'h', port: 1 }, { host: 'h', port: 1 })).to.be.true; + expect(match({ host: 'h', port: 1 }, { host: 'h', port: 2 })).to.be.false; + }); + + it('should use strict logic when strict=true', () => { + // same id and same address -> true + expect(match({ id: 'a', host: 'h', port: 1 }, { id: 'a', host: 'h', port: 1 }, true)).to.be.true; + // same id but different address -> false + expect(match({ id: 'a', host: 'h', port: 1 }, { id: 'a', host: 'h', port: 2 }, true)).to.be.false; + // different id but same address -> false + expect(match({ id: 'a', host: 'h', port: 1 }, { id: 'b', host: 'h', port: 1 }, true)).to.be.false; + }); + + it('should use relaxed logic when strict=false', () => { + // id matches -> true even if address differs + expect(match({ id: 'a', host: 'h', port: 1 }, { id: 'a', host: 'h', port: 2 }, false)).to.be.true; + // address matches -> true even if id differs + expect(match({ id: 'a', host: 'h', port: 1 }, { id: 'b', host: 'h', port: 1 }, false)).to.be.true; + }); +}); diff --git a/test/ClusteredRedisQueue.ts b/test/ClusteredRedisQueue.ts index dbd60ac..3d37e8e 100644 --- a/test/ClusteredRedisQueue.ts +++ b/test/ClusteredRedisQueue.ts @@ -21,7 +21,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import * as mocks from './mocks'; +import { logger } from './mocks'; import { expect } from 'chai'; import * as sinon from 'sinon'; import { ClusteredRedisQueue } from '../src'; @@ -30,7 +30,7 @@ import { ClusterManager } from '../src/ClusterManager'; process.setMaxListeners(100); const clusterConfig = { - logger: mocks.logger, + logger, cluster: [{ host: '127.0.0.1', port: 7777 @@ -163,14 +163,14 @@ describe('ClusteredRedisQueue', function() { 'TestClusteredQueueOne', { clusterManagers: [clusterManager], - logger: mocks.logger, + logger, }, ); const cqTwo: any = new ClusteredRedisQueue( 'TestClusteredQueueTwo', { clusterManagers: [clusterManager], - logger: mocks.logger, + logger, }, ); const message = { 'hello': 'world' }; @@ -238,7 +238,7 @@ describe('ClusteredRedisQueue', function() { 'TestClusteredQueue', { clusterManagers: [clusterManager], - logger: mocks.logger, + logger, }, ); const channel = 'TestChannel'; diff --git a/test/IMessageQueue.EventEmitter.spec.ts b/test/IMessageQueue.EventEmitter.spec.ts new file mode 100644 index 0000000..b1fe92f --- /dev/null +++ b/test/IMessageQueue.EventEmitter.spec.ts @@ -0,0 +1,41 @@ +/*! + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. + */ +import './mocks'; +import { expect } from 'chai'; +import { EventEmitter as IMQEventEmitter } from '../src'; +import { EventEmitter as NodeEventEmitter } from 'events'; + +// This test ensures the re-exported EventEmitter from IMessageQueue.ts is exercised +// to cover the function counted by nyc/istanbul for that re-export. +describe('IMessageQueue EventEmitter re-export', () => { + it('should re-export Node.js EventEmitter and be usable', () => { + // Ensure it is the same constructor + expect(IMQEventEmitter).to.equal(NodeEventEmitter); + + // And it works as expected when instantiated + const ee = new IMQEventEmitter(); + let called = 0; + ee.on('ping', () => { called++; }); + ee.emit('ping'); + expect(called).to.equal(1); + }); +}); diff --git a/test/RedisQueue.cleanup.catch.spec.ts b/test/RedisQueue.cleanup.catch.spec.ts new file mode 100644 index 0000000..8066318 --- /dev/null +++ b/test/RedisQueue.cleanup.catch.spec.ts @@ -0,0 +1,41 @@ +/*! + * Additional RedisQueue tests: processCleanup catch branch + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { RedisQueue } from '../src'; +import { logger as testLogger } from './mocks'; + +function makeLogger() { + return { + log: (..._args: any[]) => undefined, + info: (..._args: any[]) => undefined, + warn: (..._args: any[]) => undefined, + error: (..._args: any[]) => undefined, + } as any; +} + +describe('RedisQueue.processCleanup catch path', function() { + this.timeout(10000); + + it('should log a warning when processCleanup throws', async () => { + const logger = makeLogger(); + const warnSpy = sinon.spy(logger, 'warn'); + const rq: any = new RedisQueue('CleanupCatch', { + logger, + cleanup: true, + }); + + await rq.start(); + // Stub writer.client to throw to hit the catch branch + const stub = sinon.stub(rq.writer, 'client').throws(new Error('LIST failed')); + + await rq.processCleanup(); + + expect(warnSpy.called).to.be.true; + + stub.restore(); + await rq.destroy(); + }); +}); diff --git a/test/RedisQueue.connect.fallbacks.spec.ts b/test/RedisQueue.connect.fallbacks.spec.ts new file mode 100644 index 0000000..60cb217 --- /dev/null +++ b/test/RedisQueue.connect.fallbacks.spec.ts @@ -0,0 +1,40 @@ +/*! + * Additional RedisQueue tests: connect() option fallbacks branches + */ +import './mocks'; +import { expect } from 'chai'; +import { RedisQueue, IMQMode } from '../src'; + +function makeLogger() { + return { + log: (..._args: any[]) => undefined, + info: (..._args: any[]) => undefined, + warn: (..._args: any[]) => undefined, + error: (..._args: any[]) => undefined, + } as any; +} + +describe('RedisQueue.connect() option fallbacks', function() { + this.timeout(10000); + + it('should use fallback values when falsy options are provided', async () => { + const logger = makeLogger(); + // Intentionally provide falsy values to trigger `||` fallbacks in connect() + const rq: any = new RedisQueue('ConnFallbacks', { + logger, + port: 0 as unknown as number, // falsy to trigger 6379 fallback + host: '' as unknown as string, // falsy to trigger 'localhost' fallback + prefix: '' as unknown as string, // falsy to trigger '' fallback in connectionName + cleanup: false, + }, IMQMode.BOTH); + + await rq.start(); + + // Basic sanity: writer/reader/watcher are created + expect(Boolean(rq.writer)).to.equal(true); + expect(Boolean(rq.reader)).to.equal(true); + expect(Boolean(rq.watcher)).to.equal(true); + + await rq.destroy(); + }); +}); diff --git a/test/RedisQueue.processCleanup.clientsFilter.spec.ts b/test/RedisQueue.processCleanup.clientsFilter.spec.ts new file mode 100644 index 0000000..98bde70 --- /dev/null +++ b/test/RedisQueue.processCleanup.clientsFilter.spec.ts @@ -0,0 +1,53 @@ +/*! + * Additional RedisQueue tests: processCleanup connectedKeys filter branches + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { RedisQueue, uuid } from '../src'; + +describe('RedisQueue.processCleanup connectedKeys RX/filter combinations', function() { + this.timeout(5000); + + it('should handle RX_CLIENT_TEST true but filter false case (exclude unmatched prefix)', async () => { + const name = `PCleanRX_${uuid()}`; + const rq: any = new RedisQueue(name, { + logger: console, + cleanup: true, + prefix: 'imqA', + cleanupFilter: '*', + }); + + await rq.start(); + + const writer: any = rq.writer; + + // Stub client('LIST') to include a writer channel with a different prefix, + // so RX_CLIENT_TEST.test(name) is true but filter.test(name) is false. + const clientStub = sinon.stub(writer, 'client'); + clientStub.callsFake(async (cmd: string) => { + if (cmd === 'LIST') { + return [ + 'id=1 name=imqZ:Other:writer:pid:1:host:x', // RX true, filter false + 'id=2 name=imqA:Other:subscription:pid:1:host:x', // RX false, filter true + ].join('\n'); + } + return true as any; + }); + + // Return no keys on SCAN to avoid deletions and just walk the branch + const scanStub = sinon.stub(writer, 'scan'); + scanStub.resolves(['0', []] as any); + + const delSpy = sinon.spy(writer, 'del'); + + await rq.processCleanup(); + + expect(delSpy.called).to.equal(false); + + clientStub.restore(); + scanStub.restore(); + delSpy.restore(); + await rq.destroy(); + }); +}); diff --git a/test/RedisQueue.processCleanup.extra.spec.ts b/test/RedisQueue.processCleanup.extra.spec.ts new file mode 100644 index 0000000..761a27f --- /dev/null +++ b/test/RedisQueue.processCleanup.extra.spec.ts @@ -0,0 +1,38 @@ +/*! + * Additional RedisQueue tests for processCleanup branches + */ +import './mocks'; +import { expect } from 'chai'; +import { RedisQueue, uuid } from '../src'; +import { RedisClientMock } from './mocks'; + +describe('RedisQueue.processCleanup extra branches', function() { + this.timeout(5000); + + it('should remove scanned keys that do not match any connectedKey (different prefix)', async () => { + const name = uuid(); + const rq: any = new RedisQueue(name, { + logger: console, + cleanup: true, + prefix: 'imqX', + cleanupFilter: '*', + }); + + // start to create reader/writer/watcher with connection names + await rq.start(); + + // Create an orphan worker key with a different prefix so it won't include any connectedKey + const orphanKey = 'imqY:orphan:worker:someuuid:123456'; + (RedisClientMock as any).__queues__[orphanKey] = ['payload']; + + // Sanity: ensure the key is present before cleanup + expect((RedisClientMock as any).__queues__[orphanKey]).to.be.ok; + + await rq.processCleanup(); + + // The orphan key should be deleted by cleanup (true branch of keysToRemove filter) + expect((RedisClientMock as any).__queues__[orphanKey]).to.be.undefined; + + await rq.destroy(); + }); +}); diff --git a/test/RedisQueue.processCleanup.multiscan.spec.ts b/test/RedisQueue.processCleanup.multiscan.spec.ts new file mode 100644 index 0000000..db7825c --- /dev/null +++ b/test/RedisQueue.processCleanup.multiscan.spec.ts @@ -0,0 +1,42 @@ +/*! + * Additional RedisQueue tests for processCleanup branches: multi-scan and no-deletion path + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { RedisQueue, uuid } from '../src'; + +describe('RedisQueue.processCleanup multi-scan/no-delete branches', function() { + this.timeout(5000); + + it('should handle multi-page SCAN (cursor != "0" first) and avoid deletion when keys belong to connected clients', async () => { + const name = `PClean_${uuid()}`; + const rq: any = new RedisQueue(name, { + logger: console, + cleanup: true, + prefix: 'imq', + cleanupFilter: '*', + }); + + await rq.start(); + + const writer: any = rq.writer; + + // Stub scan to first return non-zero cursor with undefined keys (to exercise `|| []`), + // then return zero cursor with keys that include connectedKey (so no removal happens). + const scanStub = sinon.stub(writer, 'scan'); + scanStub.onCall(0).resolves(['1', undefined] as any); + scanStub.onCall(1).resolves(['0', [`imq:${name}:reader:pid:123`]] as any); + + const delSpy = sinon.spy(writer, 'del'); + + await rq.processCleanup(); + + // del should not be called because keysToRemove.length === 0 + expect(delSpy.called).to.equal(false); + + scanStub.restore(); + delSpy.restore(); + await rq.destroy(); + }); +}); diff --git a/test/RedisQueue.processCleanup.nullmatch.spec.ts b/test/RedisQueue.processCleanup.nullmatch.spec.ts new file mode 100644 index 0000000..e90b32e --- /dev/null +++ b/test/RedisQueue.processCleanup.nullmatch.spec.ts @@ -0,0 +1,49 @@ +/*! + * Additional RedisQueue tests for processCleanup branches: clients.match null and cleanupFilter falsy + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { RedisQueue, uuid } from '../src'; + +describe('RedisQueue.processCleanup null-match and falsy cleanupFilter', function() { + this.timeout(5000); + + it('should handle clients.match returning null and cleanupFilter as falsy (\'\')', async () => { + const name = `PCleanNull_${uuid()}`; + const rq: any = new RedisQueue(name, { + logger: console, + cleanup: true, + prefix: 'imq', + cleanupFilter: '', // falsy to exercise "|| '*'" in both RegExp and SCAN MATCH + }); + + await rq.start(); + + const writer: any = rq.writer; + + // Force clients.match(...) to return null by stubbing client('LIST') to return a string without 'name=' + const clientStub = sinon.stub(writer, 'client'); + clientStub.callsFake(async (cmd: string) => { + if (cmd === 'LIST') { + return 'id=1 flags=x'; // no 'name=' + } + return true as any; + }); + + // Ensure SCAN returns no keys, to avoid deletions and just cover the branch paths + const scanStub = sinon.stub(writer, 'scan'); + scanStub.resolves(['0', []] as any); + + const delSpy = sinon.spy(writer, 'del'); + + await rq.processCleanup(); + + expect(delSpy.called).to.equal(false); + + clientStub.restore(); + scanStub.restore(); + delSpy.restore(); + await rq.destroy(); + }); +}); diff --git a/test/RedisQueue.processDelayed.catch.spec.ts b/test/RedisQueue.processDelayed.catch.spec.ts new file mode 100644 index 0000000..82fa5ca --- /dev/null +++ b/test/RedisQueue.processDelayed.catch.spec.ts @@ -0,0 +1,46 @@ +/*! + * Additional RedisQueue tests: processDelayed() catch branch + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { RedisQueue } from '../src'; + +function makeLogger() { + return { + log: (..._args: any[]) => undefined, + info: (..._args: any[]) => undefined, + warn: (..._args: any[]) => undefined, + error: (..._args: any[]) => undefined, + } as any; +} + +describe('RedisQueue.processDelayed extra branches', function() { + this.timeout(10000); + + it('should emit error when evalsha throws', async () => { + const logger = makeLogger(); + const rq: any = new RedisQueue('ProcessDelayedCatch', { logger }); + await rq.start(); + + // Force checksum to exist to enter evalsha path + rq.scripts.moveDelayed.checksum = 'deadbeef'; + + const err = new Error('evalsha failed'); + const emitErrorStub = sinon.stub((RedisQueue as any).prototype, 'emitError'); + + // Temporarily drop writer to force a synchronous error in processDelayed + const originalWriter = rq.writer; + rq['writer'] = undefined; + + await rq['processDelayed'](rq.key); + + expect(emitErrorStub.called).to.be.true; + expect(emitErrorStub.firstCall.args[0]).to.equal('OnProcessDelayed'); + + // Restore writer and cleanup + rq['writer'] = originalWriter; + emitErrorStub.restore(); + await rq.destroy(); + }); +}); diff --git a/test/RedisQueue.publish.spec.ts b/test/RedisQueue.publish.spec.ts new file mode 100644 index 0000000..b4f48d7 --- /dev/null +++ b/test/RedisQueue.publish.spec.ts @@ -0,0 +1,70 @@ +/*! + * Additional RedisQueue tests: publish() branches + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { RedisQueue, IMQMode } from '../src'; + +function makeLogger() { + return { + log: (..._args: any[]) => undefined, + info: (..._args: any[]) => undefined, + warn: (..._args: any[]) => undefined, + error: (..._args: any[]) => undefined, + } as any; +} + +describe('RedisQueue.publish()', function() { + this.timeout(10000); + + it('should throw when writer is not connected', async () => { + const logger = makeLogger(); + const rq: any = new RedisQueue('PubNoWriter', { logger }, IMQMode.PUBLISHER); + + let thrown: any; + try { + await rq.publish({ a: 1 }); + } catch (err) { + thrown = err; + } + + expect(thrown).to.be.instanceof(TypeError); + expect(`${thrown}`).to.include('Writer is not connected'); + + await rq.destroy().catch(() => undefined); + }); + + it('should publish to default channel when writer is connected', async () => { + const logger = makeLogger(); + const rq: any = new RedisQueue('PubDefault', { logger }, IMQMode.PUBLISHER); + await rq.start(); + + const pubSpy = sinon.spy((rq as any).writer, 'publish'); + await rq.publish({ hello: 'world' }); + + expect(pubSpy.called).to.equal(true); + const [channel, msg] = pubSpy.getCall(0).args; + expect(channel).to.equal('imq:PubDefault'); + expect(() => JSON.parse(msg)).not.to.throw(); + + pubSpy.restore(); + await rq.destroy().catch(() => undefined); + }); + + it('should publish to provided toName channel when given', async () => { + const logger = makeLogger(); + const rq: any = new RedisQueue('PubOther', { logger }, IMQMode.PUBLISHER); + await rq.start(); + + const pubSpy = sinon.spy((rq as any).writer, 'publish'); + await rq.publish({ t: true }, 'OtherChannel'); + + expect(pubSpy.called).to.equal(true); + const [channel] = pubSpy.getCall(0).args; + expect(channel).to.equal('imq:OtherChannel'); + + pubSpy.restore(); + await rq.destroy().catch(() => undefined); + }); +}); diff --git a/test/RedisQueue.send.extra.branches.spec.ts b/test/RedisQueue.send.extra.branches.spec.ts new file mode 100644 index 0000000..36da896 --- /dev/null +++ b/test/RedisQueue.send.extra.branches.spec.ts @@ -0,0 +1,41 @@ +/*! + * Additional RedisQueue tests: send() extra branches + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { RedisQueue, IMQMode } from '../src'; +import { logger as testLogger } from './mocks'; + +function makeLogger() { + return { + log: (..._args: any[]) => undefined, + info: (..._args: any[]) => undefined, + warn: (..._args: any[]) => undefined, + error: (..._args: any[]) => undefined, + } as any; +} + +describe('RedisQueue.send() extra branches', function() { + this.timeout(10000); + + it('should throw when writer is still uninitialized after start()', async () => { + const logger = makeLogger(); + const rq: any = new RedisQueue('SendNoWriter', { logger }, IMQMode.PUBLISHER); + // Force start to be a no-op so writer remains undefined + const startStub = sinon.stub(rq, 'start').resolves(rq); + + let thrown: any; + try { + await rq.send('AnyQueue', { test: true }); + } catch (err) { + thrown = err; + } + + expect(thrown).to.be.instanceof(TypeError); + expect(`${thrown}`).to.include('unable to initialize queue'); + + startStub.restore(); + await rq.destroy().catch(() => undefined); + }); +}); diff --git a/test/RedisQueue.send.worker.mode.spec.ts b/test/RedisQueue.send.worker.mode.spec.ts new file mode 100644 index 0000000..213e987 --- /dev/null +++ b/test/RedisQueue.send.worker.mode.spec.ts @@ -0,0 +1,36 @@ +/*! + * Additional RedisQueue tests: send() worker-only mode error + */ +import './mocks'; +import { expect } from 'chai'; +import { RedisQueue, IMQMode } from '../src'; + +function makeLogger() { + return { + log: (..._args: any[]) => undefined, + info: (..._args: any[]) => undefined, + warn: (..._args: any[]) => undefined, + error: (..._args: any[]) => undefined, + } as any; +} + +describe('RedisQueue.send() worker-only mode', function() { + this.timeout(10000); + + it('should throw when called in WORKER only mode', async () => { + const logger = makeLogger(); + const rq: any = new RedisQueue('WorkerOnly', { logger }, IMQMode.WORKER); + + let thrown: any; + try { + await rq.send('AnyQueue', { test: true }); + } catch (err) { + thrown = err; + } + + expect(thrown).to.be.instanceof(TypeError); + expect(`${thrown}`).to.include('WORKER only mode'); + + await rq.destroy().catch(() => undefined); + }); +}); diff --git a/test/RedisQueue.unsubscribe.spec.ts b/test/RedisQueue.unsubscribe.spec.ts new file mode 100644 index 0000000..a5af0e8 --- /dev/null +++ b/test/RedisQueue.unsubscribe.spec.ts @@ -0,0 +1,53 @@ +/*! + * Additional RedisQueue tests: unsubscribe() cleanup path + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { RedisQueue } from '../src'; + +function makeLogger() { + return { + log: (..._args: any[]) => undefined, + info: (..._args: any[]) => undefined, + warn: (..._args: any[]) => undefined, + error: (..._args: any[]) => undefined, + } as any; +} + +describe('RedisQueue.unsubscribe()', function() { + this.timeout(10000); + + it('should cleanup subscription channel when present', async () => { + const logger = makeLogger(); + const rq: any = new RedisQueue('SubUnsub', { logger }); + await rq.start(); + + const handler = sinon.spy(); + await rq.subscribe('SubUnsub', handler); + + expect(rq.subscription).to.be.ok; + expect(rq.subscriptionName).to.equal('SubUnsub'); + + const unsubSpy = sinon.spy(rq.subscription, 'unsubscribe'); + const ralSpy = sinon.spy(rq.subscription, 'removeAllListeners'); + const disconnectSpy = sinon.spy(rq.subscription, 'disconnect'); + const quitSpy = sinon.spy(rq.subscription, 'quit'); + + await rq.unsubscribe(); + + expect(unsubSpy.calledOnce).to.equal(true); + expect(ralSpy.calledOnce).to.equal(true); + expect(disconnectSpy.calledOnce).to.equal(true); + expect(quitSpy.calledOnce).to.equal(true); + expect(rq.subscription).to.equal(undefined); + expect(rq.subscriptionName).to.equal(undefined); + + unsubSpy.restore(); + ralSpy.restore(); + disconnectSpy.restore(); + quitSpy.restore(); + + await rq.destroy().catch(() => undefined); + }); +}); diff --git a/test/UDPClusterManager.destroySocket.spec.ts b/test/UDPClusterManager.destroySocket.spec.ts new file mode 100644 index 0000000..022b5d6 --- /dev/null +++ b/test/UDPClusterManager.destroySocket.spec.ts @@ -0,0 +1,51 @@ +/*! + * UDPClusterManager.destroySocket() branch coverage tests + */ +import './mocks'; +import { expect } from 'chai'; +import { UDPClusterManager } from '../src'; + +describe('UDPClusterManager.destroySocket()', () => { + it('should resolve when socket has no close() function', async () => { + const destroy = (UDPClusterManager as any).destroySocket as Function; + const fakeSocket: any = { /* no close, no removeAllListeners */ }; + + await destroy('0.0.0.0:63000', fakeSocket); + }); + + it('should reject when removeAllListeners throws inside try-block', async () => { + const destroy = (UDPClusterManager as any).destroySocket as Function; + const fakeSocket: any = { + removeAllListeners: () => { throw new Error('boom'); }, + close: (cb: Function) => cb && cb(), + }; + + let thrown = null as any; + try { + await destroy('1.1.1.1:63000', fakeSocket); + } catch (e) { + thrown = e; + } + expect(thrown).to.be.instanceOf(Error); + expect((thrown as Error).message).to.equal('boom'); + }); + + it('should remove socket entry and unref after successful close()', async () => { + const destroy = (UDPClusterManager as any).destroySocket as Function; + const sockets = (UDPClusterManager as any).sockets as Record; + const key = '9.9.9.9:65000'; + + let unrefCalled = false; + const fakeSocket: any = { + removeAllListeners: () => {}, + close: (cb: Function) => cb && cb(), + unref: () => { unrefCalled = true; }, + }; + + sockets[key] = fakeSocket; + await destroy(key, fakeSocket); + + expect(unrefCalled).to.equal(true); + expect(sockets[key]).to.be.undefined; + }); +}); diff --git a/test/UDPClusterManager.extra.branches.spec.ts b/test/UDPClusterManager.extra.branches.spec.ts new file mode 100644 index 0000000..a862231 --- /dev/null +++ b/test/UDPClusterManager.extra.branches.spec.ts @@ -0,0 +1,89 @@ +/*! + * Additional branch coverage for UDPClusterManager + */ +import './mocks'; +import { expect } from 'chai'; +import { UDPClusterManager } from '../src'; + +describe('UDPClusterManager additional branches', () => { + describe('processMessageOnCluster added-path', () => { + const processMessageOnCluster = (UDPClusterManager as any).processMessageOnCluster as any; + + it('should call serverAliveWait when server is added and found (added truthy)', async () => { + const calls: any[] = []; + const addedServer: any = { id: 'id', host: '127.0.0.1', port: 6379 }; + const cluster: any = { + add: (message: any) => { calls.push(['add', message]); }, + find: (message: any, strict?: boolean) => strict ? addedServer : undefined, + }; + const original = (UDPClusterManager as any).serverAliveWait; + let waited = false; + (UDPClusterManager as any).serverAliveWait = (...args: any[]) => { + waited = true; + }; + + processMessageOnCluster(cluster, { id: 'id', name: 'n', type: 'up', host: 'h', port: 1, timeout: 0 }, 5); + + // allow microtask queue + await new Promise(res => setTimeout(res, 0)); + + expect(waited).to.equal(true); + // restore + (UDPClusterManager as any).serverAliveWait = original; + }); + + it('should not call serverAliveWait when added not found', async () => { + const cluster: any = { + add: (_: any) => undefined, + find: (_: any, __?: boolean) => undefined, + }; + const original = (UDPClusterManager as any).serverAliveWait; + let waited = false; + (UDPClusterManager as any).serverAliveWait = () => { waited = true; }; + + processMessageOnCluster(cluster, { id: 'id', name: 'n', type: 'up', host: 'h', port: 1, timeout: 0 }, 5); + await new Promise(res => setTimeout(res, 0)); + + expect(waited).to.equal(false); + (UDPClusterManager as any).serverAliveWait = original; + }); + }); + + describe('serverAliveWait branches', () => { + const serverAliveWait = (UDPClusterManager as any).serverAliveWait as any; + + it('should return early when computed timeout is <= 0', () => { + const cluster: any = { find: () => ({}) }; + const server: any = {}; + + serverAliveWait(cluster, server, 0); // no message and correction 0 => timeout 0 + + expect(server.timer).to.equal(undefined); + }); + + it('should remove when no timestamp is present on existing (timer callback path)', (done) => { + const removed: any[] = []; + const server: any = { timeout: 0 }; + const cluster: any = { + find: () => server, + remove: (s: any) => { removed.push(s); }, + }; + + // use small correction to trigger timeout quickly + serverAliveWait(cluster, server, 1); + + // wipe timestamp before timeout fires to force the branch + server.timestamp = undefined; + + setTimeout(() => { + try { + expect(removed.length).to.equal(1); + expect(server.timer).to.equal(undefined); + done(); + } catch (e) { + done(e as any); + } + }, 10); + }); + }); +}); diff --git a/test/UDPClusterManager.free.spec.ts b/test/UDPClusterManager.free.spec.ts new file mode 100644 index 0000000..160d8e8 --- /dev/null +++ b/test/UDPClusterManager.free.spec.ts @@ -0,0 +1,27 @@ +/*! + * UDPClusterManager.free() coverage test + */ +import './mocks'; +import { expect } from 'chai'; + +describe('UDPClusterManager.free()', () => { + it('should destroy all sockets via destroySocket and clear sockets map', async () => { + const { UDPClusterManager } = await import('../src'); + const sockets = (UDPClusterManager as any).sockets as Record; + // prepare two mock sockets + sockets['0.0.0.0:5555'] = { + removeAllListeners: () => {}, + close: (cb: Function) => cb(), + unref: () => {}, + }; + sockets['0.0.0.0:6666'] = { + removeAllListeners: () => {}, + close: (cb: Function) => cb(), + unref: () => {}, + }; + + await (UDPClusterManager as any).free(); + + expect(Object.keys((UDPClusterManager as any).sockets)).to.have.length(0); + }); +}); diff --git a/test/UDPClusterManager.missing.branches.spec.ts b/test/UDPClusterManager.missing.branches.spec.ts new file mode 100644 index 0000000..b090f10 --- /dev/null +++ b/test/UDPClusterManager.missing.branches.spec.ts @@ -0,0 +1,63 @@ +/*! + * UDPClusterManager missing branches coverage + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { UDPClusterManager } from '../src'; + +describe('UDPClusterManager - cover remaining branches', () => { + it('serverAliveWait should handle existing.timeout falsy (|| 0) path and remove on timeout', async () => { + // Arrange cluster stub + const server: any = { host: 'h', port: 1, timer: undefined, timeout: undefined, timestamp: undefined }; + const cluster: any = { + find: sinon.stub().callsFake((_s: any, _strict?: boolean) => server), + remove: sinon.stub(), + }; + + // Use fake timers to control setTimeout and Date + const clock = sinon.useFakeTimers(); + try { + // make timestamp truthy + clock.tick(1); + // Alive correction > 0 ensures timer is scheduled even if timeout is falsy + (UDPClusterManager as any).serverAliveWait(cluster, server, 1); + // Advance time to trigger setTimeout callback and make delta >= currentTimeout (1ms) + clock.tick(2); + + expect(cluster.remove.called).to.equal(true); + } finally { + clock.restore(); + } + }); + + it('destroySocket should call socket.unref() when socket is present', async () => { + // Prepare fake socket with unref + const unref = sinon.spy(); + const removeAll = sinon.spy(); + const sock: any = { + removeAllListeners: removeAll, + close: (cb: (err?: any) => void) => cb(), + unref, + }; + const key = 'test-key'; + (UDPClusterManager as any).sockets[key] = sock; + await (UDPClusterManager as any).destroySocket(key, sock); + expect(unref.called).to.equal(true); + expect((UDPClusterManager as any).sockets[key]).to.equal(undefined); + }); + + it('destroySocket should work when socket.unref() is absent (optional chaining negative branch)', async () => { + const removeAll = sinon.spy(); + const sock: any = { + removeAllListeners: removeAll, + close: (cb: (err?: any) => void) => cb(), + // no unref method + }; + const key = 'test-key-2'; + (UDPClusterManager as any).sockets[key] = sock; + await (UDPClusterManager as any).destroySocket(key, sock); + // should not throw, sockets map cleaned + expect((UDPClusterManager as any).sockets[key]).to.equal(undefined); + }); +}); diff --git a/test/UDPClusterManager.parseAndStart.spec.ts b/test/UDPClusterManager.parseAndStart.spec.ts new file mode 100644 index 0000000..0e5434a --- /dev/null +++ b/test/UDPClusterManager.parseAndStart.spec.ts @@ -0,0 +1,38 @@ +/*! + * UDPClusterManager parseBroadcastedMessage and startListening branch tests + */ +import './mocks'; +import { expect } from 'chai'; +import { UDPClusterManager } from '../src'; + +/** + * Covers default parameters in startListening(options = {}) and + * default destructuring for address = '' and timeout = '0' in + * parseBroadcastedMessage(). + */ +describe('UDPClusterManager parse/start branches', () => { + it('parseBroadcastedMessage: should apply defaults for empty address and timeout', () => { + const parse = (UDPClusterManager as any).parseBroadcastedMessage as Function; + const buf = Buffer.from(['name', 'id', 'UP'].join('\t')); + const msg = parse(buf); + expect(msg).to.include({ name: 'name', id: 'id', type: 'up' }); + // address default => '' leads to host '' and port NaN + expect(msg.host).to.equal(''); + expect(Number.isNaN(msg.port)).to.equal(true); + // timeout default '0' => 0 ms + expect(msg.timeout).to.equal(0); + }); + + it('startListening: should call listenBroadcastedMessages when called without options', () => { + const mgr: any = new (UDPClusterManager as any)(); + let called = false; + const original = mgr.listenBroadcastedMessages; + mgr.listenBroadcastedMessages = (..._args: any[]) => { called = true; }; + try { + (mgr as any).startListening(); + expect(called).to.equal(true); + } finally { + mgr.listenBroadcastedMessages = original; + } + }); +}); diff --git a/test/UDPClusterManager.selectNetworkInterface.spec.ts b/test/UDPClusterManager.selectNetworkInterface.spec.ts new file mode 100644 index 0000000..aad8390 --- /dev/null +++ b/test/UDPClusterManager.selectNetworkInterface.spec.ts @@ -0,0 +1,46 @@ +/*! + * UDPClusterManager.selectNetworkInterface() branch coverage tests + */ +import './mocks'; +import { expect } from 'chai'; +import * as mock from 'mock-require'; + +describe('UDPClusterManager.selectNetworkInterface()', () => { + it('should return default when broadcastAddress is undefined', async () => { + const { UDPClusterManager } = await import('../src'); + const select = (UDPClusterManager as any).selectNetworkInterface as Function; + const res = select({}); + expect(res).to.equal('0.0.0.0'); + }); + + it('should return default when broadcastAddress equals limitedBroadcastAddress', async () => { + const { UDPClusterManager } = await import('../src'); + const select = (UDPClusterManager as any).selectNetworkInterface as Function; + const res = select({ broadcastAddress: '127.0.0.255', limitedBroadcastAddress: '127.0.0.255' }); + expect(res).to.equal('0.0.0.0'); + }); + + it('should continue on undefined interface entry and still select matching address', () => { + // Re-mock os.networkInterfaces to include an undefined entry + const os = require('node:os'); + const networkInterfaces = () => ({ bad: undefined, lo: [{ address: '127.0.0.1', family: 'IPv4' }] }); + mock.stop('os'); + mock('os', Object.assign({}, os, { networkInterfaces })); + // Re-require the module to capture new binding + const { UDPClusterManager } = mock.reRequire('../src/UDPClusterManager'); + const res = (UDPClusterManager as any).selectNetworkInterface({ broadcastAddress: '127.0.0.255', limitedBroadcastAddress: '255.255.255.255' }); + expect(res).to.equal('127.0.0.1'); + + // restore to base mocks for other tests + mock.stop('os'); + mock.reRequire('./mocks/os'); + mock.reRequire('../src/UDPClusterManager'); + }); + + it('should select matching interface address when not equal to limited broadcast', async () => { + const { UDPClusterManager } = await import('../src'); + const select = (UDPClusterManager as any).selectNetworkInterface as Function; + const res = select({ broadcastAddress: '127.0.0.255', limitedBroadcastAddress: '255.255.255.255' }); + expect(res).to.equal('127.0.0.1'); + }); +}); diff --git a/test/UDPClusterManager.serverAliveWait.truthyTimeout.spec.ts b/test/UDPClusterManager.serverAliveWait.truthyTimeout.spec.ts new file mode 100644 index 0000000..82c8dac --- /dev/null +++ b/test/UDPClusterManager.serverAliveWait.truthyTimeout.spec.ts @@ -0,0 +1,25 @@ +/*! + * UDPClusterManager serverAliveWait: cover currentTimeout left side (existing.timeout truthy) + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { UDPClusterManager } from '../src'; + +describe('UDPClusterManager.serverAliveWait truthy timeout', () => { + it('should use existing.timeout (truthy) in currentTimeout and remove on expiry', async () => { + const server: any = { host: 'h', port: 1, timer: undefined, timeout: 2, timestamp: undefined }; + const cluster: any = { + find: sinon.stub().callsFake((_s: any, _strict?: boolean) => server), + remove: sinon.stub(), + }; + const clock = sinon.useFakeTimers(); + try { + (UDPClusterManager as any).serverAliveWait(cluster, server, 0); + clock.tick(3); + expect(cluster.remove.called).to.equal(true); + } finally { + clock.restore(); + } + }); +}); diff --git a/test/UDPClusterManager.ts b/test/UDPClusterManager.ts index 249e690..b3fb8f3 100644 --- a/test/UDPClusterManager.ts +++ b/test/UDPClusterManager.ts @@ -21,11 +21,11 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ +import './mocks'; import { expect } from 'chai'; import { UDPClusterManager } from '../src'; import * as sinon from 'sinon'; import { Socket } from 'dgram'; -import * as os from 'os'; const testMessageUp = 'name\tid\tup\taddress\ttimeout'; const testMessageDown = 'name\tid\tdown\taddress\ttimeout'; diff --git a/test/copyEventEmitter.ts b/test/copyEventEmitter.ts index 95faeb0..99a50de 100644 --- a/test/copyEventEmitter.ts +++ b/test/copyEventEmitter.ts @@ -19,6 +19,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ +import './mocks'; import { EventEmitter } from 'events'; import { expect } from 'chai'; import { copyEventEmitter } from '../src'; @@ -130,4 +131,111 @@ describe('copyEventEmitter()', function() { expect(target.listenerCount(eventName)).to.be.equal(1); }); + it('should handle onceWrapper-like listener with falsy listener property', () => { + const source = new EventEmitter(); + const target = new EventEmitter(); + + // Create a mock listener that looks like onceWrapper and has a falsy listener property + const mockListener: any = function() {}; + mockListener.listener = 0; // falsy value present + const originalInspect = require('util').inspect; + require('util').inspect = (obj: any) => { + if (obj === mockListener) { + return 'function onceWrapper() { ... }'; + } + return originalInspect(obj); + }; + + source.on(eventName, mockListener as any); + copyEventEmitter(source, target); + + // Restore original inspect + require('util').inspect = originalInspect; + + expect(target.listenerCount(eventName)).to.be.equal(1); + }); + + it('should handle onceWrapper-like listener with undefined listener property', () => { + const source = new EventEmitter(); + const target = new EventEmitter(); + + const mockListener: any = function() {}; + mockListener.listener = undefined; // explicitly undefined + const originalInspect = require('util').inspect; + require('util').inspect = (obj: any) => { + if (obj === mockListener) { + return 'function onceWrapper() { ... }'; + } + return originalInspect(obj); + }; + + source.on(eventName, mockListener as any); + copyEventEmitter(source, target); + + // Restore original inspect + require('util').inspect = originalInspect; + + expect(target.listenerCount(eventName)).to.be.equal(1); + }); + + it('should handle onceWrapper-like listener with truthy listener property', () => { + const source = new EventEmitter(); + const target = new EventEmitter(); + + // Create a mock listener that looks like onceWrapper and has a truthy listener property + let called = 0; + const realListener = () => { called++; }; + const mockListener: any = function() {}; + mockListener.listener = realListener; // truthy function + + const originalInspect = require('util').inspect; + require('util').inspect = (obj: any) => { + if (obj === mockListener) { + return 'function onceWrapper() { ... }'; + } + return originalInspect(obj); + }; + + source.on(eventName, mockListener as any); + copyEventEmitter(source, target); + + // Restore original inspect + require('util').inspect = originalInspect; + + // Ensure the listener was attached via once() and is callable exactly once + expect(target.listenerCount(eventName)).to.be.equal(1); + target.emit(eventName); + target.emit(eventName); + expect(called).to.equal(1); + }); + + it('should handle onceWrapper path when originalListener is undefined', () => { + const source: any = { + eventNames: () => [eventName], + rawListeners: () => [undefined], + getMaxListeners: () => 0, + setMaxListeners: () => {}, + }; + const onceCalls: any[] = []; + const target: any = { + once: (ev: any, listener: any) => { onceCalls.push([ev, listener]); }, + on: () => {}, + }; + const originalInspect = require('util').inspect; + require('util').inspect = (obj: any) => { + if (typeof obj === 'undefined') { + return 'function onceWrapper() { ... }'; + } + return originalInspect(obj); + }; + + copyEventEmitter(source as any, target as any); + + // Restore original inspect + require('util').inspect = originalInspect; + + expect(onceCalls.length).to.equal(1); + expect(onceCalls[0][0]).to.equal(eventName); + expect(onceCalls[0][1]).to.equal(undefined); + }); }); diff --git a/test/profile.decorator.branches.spec.ts b/test/profile.decorator.branches.spec.ts new file mode 100644 index 0000000..aa9c08a --- /dev/null +++ b/test/profile.decorator.branches.spec.ts @@ -0,0 +1,34 @@ +/*! + * Additional tests to cover remaining branches in profile decorator + */ +import './mocks'; +import { expect } from 'chai'; +import { profile, LogLevel } from '..'; + +// Note: We intentionally call decorated methods without a "this" context +// to exercise the (this || target) branches inside the decorator wrapper. + +describe('profile decorator extra branches', () => { + it('should return early via original.apply(target, ...) when both debug flags are false and this is undefined', () => { + class T1 { + @profile({ enableDebugTime: false, enableDebugArgs: false, logLevel: LogLevel.LOG }) + public m(...args: any[]) { return args; } + } + const o = new T1(); + const fn = Object.getPrototypeOf(o).m as Function; // wrapper + const res = fn.call(undefined, 1, 2, 3); + expect(res).to.deep.equal([1, 2, 3]); + }); + + it('should execute debug path with (this || target) picking target and logLevel fallback to IMQ_LOG_LEVEL', () => { + class T2 { + // no logger on prototype; calling with undefined this picks target + @profile({ enableDebugTime: true, enableDebugArgs: true, logLevel: undefined as any }) + public m(..._args: any[]) { /* noop */ } + } + const o = new T2(); + const fn = Object.getPrototypeOf(o).m as Function; // wrapper + // provide serializable args to avoid logger.error path when logger is undefined + expect(() => fn.call(undefined, 1, { a: 2 }, 'x')).to.not.throw(); + }); +}); diff --git a/test/profile.more.branches.spec.ts b/test/profile.more.branches.spec.ts new file mode 100644 index 0000000..80f1672 --- /dev/null +++ b/test/profile.more.branches.spec.ts @@ -0,0 +1,57 @@ +/*! + * Additional profile.ts branch coverage tests + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import * as mock from 'mock-require'; + +// We re-require the module inside tests to pick up env changes when needed + +describe('profile.ts additional branches', () => { + afterEach(() => { + mock.stopAll(); + delete (process as any).env.IMQ_LOG_TIME_FORMAT; + }); + + it('logDebugInfo: should not attempt to call missing log method (no-op path)', () => { + const { logDebugInfo, LogLevel } = mock.reRequire('../src/profile'); + const fakeLogger: any = { + // intentionally no 'log' or 'info' method for selected level + error: sinon.spy(), + }; + const options = { + debugTime: true, + debugArgs: true, + className: 'X', + args: [1, { a: 2 }], + methodName: 'm', + start: (process.hrtime as any).bigint(), + logger: fakeLogger, + logLevel: LogLevel.LOG, + }; + expect(() => logDebugInfo(options)).to.not.throw(); + // ensures error not called due to serialization success + expect(fakeLogger.error.called).to.equal(false); + }); + + it('logDebugInfo: should call logger.error on JSON.stringify error (BigInt arg)', () => { + const { logDebugInfo, LogLevel } = mock.reRequire('../src/profile'); + const fakeLogger: any = { + error: sinon.spy(), + }; + const args = [BigInt(1)]; // JSON.stringify throws on BigInt + const options = { + debugTime: false, + debugArgs: true, + className: 'Y', + args, + methodName: 'n', + start: (process.hrtime as any).bigint(), + logger: fakeLogger, + logLevel: LogLevel.INFO, + }; + logDebugInfo(options); + expect(fakeLogger.error.calledOnce).to.equal(true); + }); +}); diff --git a/test/profile.rejection.spec.ts b/test/profile.rejection.spec.ts new file mode 100644 index 0000000..bc4b7f0 --- /dev/null +++ b/test/profile.rejection.spec.ts @@ -0,0 +1,33 @@ +/*! + * Additional profile tests for async rejection catch path + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { profile } from '..'; +import * as core from '..'; + +class RejectingClass { + public logger: any = { info: () => undefined, error: () => undefined }; + + @profile({ enableDebugTime: true, enableDebugArgs: true }) + public async willReject(): Promise { + return Promise.reject(new Error('boom')); + } +} + +describe('profile() async rejection path', () => { + it('should log via logger when async method rejects', async () => { + const logger = { info: sinon.spy(), error: () => undefined } as any; + const obj = new RejectingClass(); + obj.logger = logger; + try { + await obj.willReject(); + } catch (e) { + // expected + } + // allow microtask queue + await new Promise(res => setTimeout(res, 0)); + expect(logger.info.called).to.be.true; + }); +}); diff --git a/test/profile.ts b/test/profile.ts index c77312e..8ad7433 100644 --- a/test/profile.ts +++ b/test/profile.ts @@ -21,6 +21,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ +import './mocks'; import { expect } from 'chai'; import * as sinon from 'sinon'; import * as mock from 'mock-require'; @@ -201,6 +202,13 @@ describe('profile()', function() { expect(error.notCalled).to.be.true; }); + it('should not log when logger method is missing', () => { + const { logDebugInfo } = mock.reRequire('../src/profile'); + const dummyLogger: any = { error: logger.error.bind(logger) }; + logDebugInfo({ ...baseOptions, logger: dummyLogger, logLevel: 'nonexistent' as any }); + expect(log.notCalled).to.be.true; + }); + it('should handle JSON.stringify errors', () => { const { logDebugInfo } = mock.reRequire('../src/profile'); const badJson = { toJSON: () => { throw new Error('bad json'); } }; diff --git a/test/promisify.ts b/test/promisify.ts index a14fb4f..86eea56 100644 --- a/test/promisify.ts +++ b/test/promisify.ts @@ -21,6 +21,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ +import './mocks'; import { expect } from 'chai'; import * as sinon from 'sinon'; import { promisify } from '..'; diff --git a/test/uuid.ts b/test/uuid.ts index 37ca78c..7543f65 100644 --- a/test/uuid.ts +++ b/test/uuid.ts @@ -21,6 +21,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ +import './mocks'; import { expect } from 'chai'; import { uuid } from '..';