diff --git a/README.md b/README.md index 7daca4f..b11ca5f 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,5 @@ # I Message Queue (@imqueue/core) -[![Build Status](https://img.shields.io/github/actions/workflow/status/imqueue/core/build.yml)](https://github.com/imqueue/core) -[![codebeat badge](https://codebeat.co/badges/b7685cb5-b290-47de-80e1-bde3e0582355)](https://codebeat.co/projects/github-com-imqueue-core-master) -[![Coverage Status](https://coveralls.io/repos/github/imqueue/core/badge.svg?branch=master)](https://coveralls.io/github/imqueue/core?branch=master) -[![Known Vulnerabilities](https://snyk.io/test/github/imqueue/core/badge.svg?targetFile=package.json)](https://snyk.io/test/github/imqueue/core?targetFile=package.json) [![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://rawgit.com/imqueue/core/master/LICENSE) Simple JSON-based messaging queue for inter service communication diff --git a/package-lock.json b/package-lock.json index 9972530..c5a9f71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,59 +1,44 @@ { "name": "@imqueue/core", - "version": "2.0.2", + "version": "2.0.26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "2.0.2", + "version": "2.0.26", "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" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" + "typedoc": "^0.28.9", + "typescript": "^5.9.2", + "yargs": "^18.0.0" } }, "node_modules/@babel/code-frame": { @@ -72,9 +57,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", "dev": true, "license": "MIT", "engines": { @@ -82,22 +67,22 @@ } }, "node_modules/@babel/core": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -130,14 +115,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -198,15 +183,15 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -226,9 +211,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -246,27 +231,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", - "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.27.6" + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -291,18 +276,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", + "@babel/types": "^7.28.5", "debug": "^4.3.1" }, "engines": { @@ -310,14 +295,14 @@ } }, "node_modules/@babel/types": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz", - "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -365,9 +350,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { @@ -384,9 +369,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -394,13 +379,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -433,19 +418,22 @@ } }, "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.4.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz", + "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "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.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -514,9 +502,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.38.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", + "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", "dev": true, "license": "MIT", "engines": { @@ -527,9 +515,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -537,43 +525,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.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.1", + "@eslint/core": "^0.16.0", "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.14.0", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.14.0.tgz", + "integrity": "sha512-c5X8fwPLOtUS8TVdqhynz9iV0GlOtFUT1ppXYzUUlEXe4kbZ/mvMT8wXoT8kCwUka+zsiloq7sD3pZ3+QVTuNQ==", "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.14.0", + "@shikijs/langs": "^3.14.0", + "@shikijs/themes": "^3.14.0", + "@shikijs/types": "^3.14.0", "@shikijs/vscode-textmate": "^10.0.2" } }, @@ -588,33 +563,19 @@ } }, "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -644,9 +605,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.4.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz", + "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==", "license": "MIT" }, "node_modules/@isaacs/cliui": { @@ -785,9 +746,9 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { @@ -795,6 +756,17 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -806,16 +778,16 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -873,40 +845,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.14.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.14.0.tgz", + "integrity": "sha512-TNcYTYMbJyy+ZjzWtt0bG5y4YyMIWC2nyePz+CFMWqm+HnZZyy9SWMgo8Z6KBJVIZnx8XUXS8U2afO6Y0g1Oug==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.7.0", + "@shikijs/types": "3.14.0", "@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.14.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.14.0.tgz", + "integrity": "sha512-DIB2EQY7yPX1/ZH7lMcwrK5pl+ZkP/xoSpUzg9YC8R+evRCCiSQ7yyrvEyBsMnfZq4eBzLzBlugMyTAf13+pzg==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.7.0" + "@shikijs/types": "3.14.0" } }, "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.14.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.14.0.tgz", + "integrity": "sha512-fAo/OnfWckNmv4uBoUu6dSlkcBc+SA1xzj5oUSaz5z3KqHtEbUypg/9xxgJARtM6+7RVm0Q6Xnty41xA1ma1IA==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.7.0" + "@shikijs/types": "3.14.0" } }, "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.14.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.14.0.tgz", + "integrity": "sha512-bQGgC6vrY8U/9ObG1Z/vTro+uclbjjD/uG58RvfxKZVD5p9Yc1ka3tVyEFy7BNJLzxuWyHH5NWynP9zZZS59eQ==", "dev": true, "license": "MIT", "dependencies": { @@ -942,14 +914,13 @@ } }, "node_modules/@sinonjs/samsam": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", - "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", + "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.1", - "lodash.get": "^4.4.2", "type-detect": "^4.1.0" } }, @@ -992,13 +963,14 @@ "license": "MIT" }, "node_modules/@types/chai": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", - "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { - "@types/deep-eql": "*" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, "node_modules/@types/deep-eql": { @@ -1068,13 +1040,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.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", + "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.8.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/sinon": { @@ -1088,9 +1060,9 @@ } }, "node_modules/@types/sinonjs__fake-timers": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", - "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-15.0.1.tgz", + "integrity": "sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==", "dev": true, "license": "MIT" }, @@ -1102,9 +1074,9 @@ "license": "MIT" }, "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "version": "17.0.34", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.34.tgz", + "integrity": "sha512-KExbHVa92aJpw9WDQvzBaGVE2/Pz+pLZQloT2hjL8IqsZnV62rlPOYvNnLmf/L2dyllfVUOVBj64M0z/46eR2A==", "dev": true, "license": "MIT", "dependencies": { @@ -1119,17 +1091,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.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", + "integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==", "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.46.2", + "@typescript-eslint/type-utils": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1143,22 +1115,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.35.1", + "@typescript-eslint/parser": "^8.46.2", "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.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz", + "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "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.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", "debug": "^4.3.4" }, "engines": { @@ -1170,18 +1142,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.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz", + "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.35.1", - "@typescript-eslint/types": "^8.35.1", + "@typescript-eslint/tsconfig-utils": "^8.46.2", + "@typescript-eslint/types": "^8.46.2", "debug": "^4.3.4" }, "engines": { @@ -1192,18 +1164,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.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz", + "integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1" + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1214,9 +1186,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.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz", + "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==", "dev": true, "license": "MIT", "engines": { @@ -1227,18 +1199,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.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz", + "integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.35.1", - "@typescript-eslint/utils": "8.35.1", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/utils": "8.46.2", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1251,13 +1224,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.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz", + "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==", "dev": true, "license": "MIT", "engines": { @@ -1269,16 +1242,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.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz", + "integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==", "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.46.2", + "@typescript-eslint/tsconfig-utils": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1294,20 +1267,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.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz", + "integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==", "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.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1318,17 +1291,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.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz", + "integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/types": "8.46.2", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -1420,9 +1393,9 @@ } }, "node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { @@ -1492,26 +1465,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 +1475,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,14 +1482,14 @@ "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==", + "node_modules/baseline-browser-mapping": { + "version": "2.8.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.20.tgz", + "integrity": "sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ==", "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tweetnacl": "^0.14.3" + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" } }, "node_modules/brace-expansion": { @@ -1601,9 +1523,9 @@ "license": "ISC" }, "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", "dev": true, "funding": [ { @@ -1621,10 +1543,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" @@ -1693,9 +1616,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001726", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz", - "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==", + "version": "1.0.30001751", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", + "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", "dev": true, "funding": [ { @@ -1713,17 +1636,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.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "dev": true, "license": "MIT", "dependencies": { @@ -1734,7 +1650,7 @@ "pathval": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/chalk": { @@ -1791,78 +1707,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.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { - "node": ">=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.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "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.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "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 +1786,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 +1806,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 +1837,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,17 +1853,7 @@ "coveralls": "bin/coveralls.js" }, "engines": { - "node": ">=16" - } - }, - "node_modules/coveralls-next/node_modules/lcov-parse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", - "integrity": "sha512-aprLII/vPzuQvYZnDRU78Fns9I2Ag3gi4Ipga/hxnVMCZC8DnR2nI7XBqrPoywGfxqIx/DgarGvDJZAD3YBTgQ==", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "lcov-parse": "bin/cli.js" + "node": ">=18" } }, "node_modules/create-require": { @@ -2033,23 +1878,10 @@ "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", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2149,16 +1981,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,21 +2007,10 @@ "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", - "integrity": "sha512-wObbz/ar3Bc6e4X5vf0iO8xTN8YAjN/tgiAOJLr7yjYFtP9wAjq8Mb5h0yn6kResir+VYx2DXBj9NNobs0ETSA==", + "version": "1.5.241", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.241.tgz", + "integrity": "sha512-ILMvKX/ZV5WIJzzdtuHg8xquk2y0BOGlFOxBVwTpbiXqWIH0hamG45ddU4R3PQ0gYu+xgo0vdHXHli9sHIGb4w==", "dev": true, "license": "ISC" }, @@ -2254,25 +2065,24 @@ } }, "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.38.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", + "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.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-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.1", + "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.30.1", - "@eslint/plugin-kit": "^0.3.1", + "@eslint/js": "9.38.0", + "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", @@ -2315,9 +2125,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 +2317,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 +2487,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 +2532,19 @@ "dev": true, "license": "ISC" }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "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 +2555,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 +2609,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,39 +2616,14 @@ "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==", + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "license": "ISC", + "license": "MIT", "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", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" + "node": ">=8" } }, "node_modules/hasha": { @@ -2921,22 +2660,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 +2727,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.8.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz", + "integrity": "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==", "license": "MIT", "dependencies": { - "@ioredis/commands": "^1.1.1", + "@ioredis/commands": "1.4.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", @@ -3105,6 +2828,16 @@ "node": ">=0.12.0" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -3181,13 +2914,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", @@ -3293,9 +3019,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -3342,13 +3068,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 +3098,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 +3112,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 +3125,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", @@ -3447,11 +3136,14 @@ } }, "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==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", + "integrity": "sha512-aprLII/vPzuQvYZnDRU78Fns9I2Ag3gi4Ipga/hxnVMCZC8DnR2nI7XBqrPoywGfxqIx/DgarGvDJZAD3YBTgQ==", "dev": true, - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "bin": { + "lcov-parse": "bin/cli.js" + } }, "node_modules/levn": { "version": "0.4.1", @@ -3506,14 +3198,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", @@ -3555,9 +3239,9 @@ } }, "node_modules/loupe": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", - "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", "dev": true, "license": "MIT" }, @@ -3660,29 +3344,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", @@ -3720,9 +3381,9 @@ } }, "node_modules/mocha": { - "version": "11.7.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.1.tgz", - "integrity": "sha512-5EK+Cty6KheMS/YLPPMJC64g5V61gIR25KsRItHw6x4hEKT6Njp1n9LOlH4gpevuwMVS66SXaBBpg+RWZkza4A==", + "version": "11.7.4", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.4.tgz", + "integrity": "sha512-1jYAaY8x0kAZ0XszLWu14pzsf4KV740Gld4HXkhNTXwcHx4AUEDkPzgEHg9CM5dVcW+zv036tjpsEbLraPJj4w==", "dev": true, "license": "MIT", "dependencies": { @@ -3734,6 +3395,7 @@ "find-up": "^5.0.0", "glob": "^10.4.5", "he": "^1.2.0", + "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "log-symbols": "^4.1.0", "minimatch": "^9.0.5", @@ -3765,6 +3427,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 +3513,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", @@ -3822,9 +3591,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.26", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", + "integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==", "dev": true, "license": "MIT" }, @@ -4128,16 +3897,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 +3908,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 +4116,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 +4228,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 +4248,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 +4341,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", @@ -4773,9 +4443,9 @@ } }, "node_modules/run-applescript": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", - "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", "dev": true, "license": "MIT", "engines": { @@ -4830,17 +4500,10 @@ ], "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", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -5000,9 +4663,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.21", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", - "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", "dev": true, "license": "CC0-1.0" }, @@ -5013,32 +4676,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", @@ -5110,9 +4747,9 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, "license": "MIT", "dependencies": { @@ -5259,20 +4896,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 +4963,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,17 +5007,17 @@ } }, "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.14", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.14.tgz", + "integrity": "sha512-ftJYPvpVfQvFzpkoSfHLkJybdA/geDJ8BGQt/ZnkkhnBYoYW6lBgPQXu6vqLxO4X75dA55hX8Af847H5KXlEFA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@gerrit0/mini-shiki": "^3.7.0", + "@gerrit0/mini-shiki": "^3.12.0", "lunr": "^2.3.9", "markdown-it": "^14.1.0", "minimatch": "^9.0.5", - "yaml": "^2.8.0" + "yaml": "^2.8.1" }, "bin": { "typedoc": "bin/typedoc" @@ -5424,13 +5027,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.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5449,16 +5052,16 @@ "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.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", "dev": true, "funding": [ { @@ -5513,21 +5116,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", @@ -5562,9 +5150,9 @@ } }, "node_modules/workerpool": { - "version": "9.3.3", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.3.tgz", - "integrity": "sha512-slxCaKbYjEdFT/o2rH9xS1hf4uRDch1w7Uo+apxhZ+sf/1d9e0ZVkn42kPNGP2dgjIx6YFvSevj0zHvbWe2jdw==", + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", "dev": true, "license": "Apache-2.0" }, @@ -5651,9 +5239,9 @@ } }, "node_modules/wrap-ansi/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==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { @@ -5690,6 +5278,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", @@ -5708,9 +5312,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "dev": true, "license": "ISC", "bin": { @@ -5721,22 +5325,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 +5394,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.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "dev": true, "license": "MIT" }, @@ -5819,31 +5412,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 38659c4..d41118a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "2.0.2", + "version": "2.0.26", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", @@ -11,9 +11,10 @@ "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-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": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha --exit --timeout 10000 && ./node_modules/.bin/nyc report --reporter=text-lcov", + "test-fast": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha --exit --timeout 10000 && /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", "test-coverage": "cat ./coverage/lcov.info | CODECLIMATE_API_HOST=https://codebeat.co/webhooks/code_coverage CODECLIMATE_REPO_TOKEN=85bb2a18-4ebb-4e48-a2ce-92b7bf438b1a ./node_modules/.bin/codeclimate-test-reporter", @@ -37,37 +38,36 @@ "author": "imqueue.com (https://imqueue.com)", "license": "GPL-3.0-only", "dependencies": { - "ioredis": "^5.6.1" + "ioredis": "^5.8.2" }, "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": { @@ -80,7 +80,9 @@ ], "recursive": true, "bail": true, - "full-trace": true + "full-trace": true, + "exit": true, + "timeout": 10000 }, "nyc": { "check-coverage": false, diff --git a/src/ClusterManager.ts b/src/ClusterManager.ts index 80236d3..86bbab5 100644 --- a/src/ClusterManager.ts +++ b/src/ClusterManager.ts @@ -22,21 +22,58 @@ * to get commercial licensing options. */ import { IMessageQueueConnection, IServerInput } from './IMessageQueue'; +import { uuid } from './uuid'; export interface ICluster { - add: (server: IServerInput) => void; + add: (server: IServerInput) => T; remove: (server: IServerInput) => void; find: ( server: IServerInput, ) => T | undefined; } +export interface InitializedCluster extends ICluster { + id: string; +} + export abstract class ClusterManager { - protected clusters: ICluster[] = []; + protected clusters: InitializedCluster[] = []; protected constructor() {} - public init(cluster: ICluster): void { - this.clusters.push(cluster); + public init(cluster: ICluster): InitializedCluster { + const initializedCluster = Object.assign( + cluster, + { id: uuid() }, + ) as InitializedCluster; + + this.clusters.push(initializedCluster); + + return initializedCluster; + } + + public async anyCluster( + fn: (cluster: InitializedCluster) => Promise | void, + ): Promise { + await Promise.all(this.clusters.map(cluster => fn(cluster))); } + + public async remove( + cluster: string | InitializedCluster, + destroy: boolean = true, + ): Promise { + const id = typeof cluster === 'string' ? cluster : cluster.id; + + this.clusters = this.clusters.filter(cluster => cluster.id !== id); + + if ( + this.clusters.length === 0 + && destroy + && typeof this.destroy === 'function' + ) { + await this.destroy(); + } + } + + public abstract destroy(): Promise; } diff --git a/src/ClusteredRedisQueue.ts b/src/ClusteredRedisQueue.ts index e51b562..739535e 100644 --- a/src/ClusteredRedisQueue.ts +++ b/src/ClusteredRedisQueue.ts @@ -23,19 +23,20 @@ */ import { EventEmitter } from 'events'; import { - DEFAULT_IMQ_OPTIONS, buildOptions, + copyEventEmitter, + DEFAULT_IMQ_OPTIONS, + EventMap, ILogger, IMessageQueue, IMessageQueueConnection, IMQMode, IMQOptions, + IServerInput, JsonObject, RedisQueue, - EventMap, - IServerInput, - copyEventEmitter, } from '.'; +import { InitializedCluster } from './ClusterManager'; interface ClusterServer extends IMessageQueueConnection { imq?: RedisQueue; @@ -107,7 +108,7 @@ export class ClusteredRedisQueue implements IMessageQueue, * * @type {number} */ - private queueLength: number = 0; + private imqLength: number = 0; /** * Template EventEmitter instance used to replicate queue EventEmitters when @@ -130,6 +131,8 @@ export class ClusteredRedisQueue implements IMessageQueue, subscription: null, }; + private initializedClusters: InitializedCluster[] = []; + /** * Class constructor * @@ -166,12 +169,14 @@ export class ClusteredRedisQueue implements IMessageQueue, } if (this.options.clusterManagers?.length) { + this.verbose('Initializing cluster managers...'); + for (const manager of this.options.clusterManagers) { - manager.init({ + this.initializedClusters.push(manager.init({ add: this.addServer.bind(this), remove: this.removeServer.bind(this), find: this.findServer.bind(this), - }); + })); } } } @@ -220,7 +225,7 @@ export class ClusteredRedisQueue implements IMessageQueue, delay?: number, errorHandler?: (err: Error) => void, ): Promise { - if (!this.queueLength) { + if (!this.imqLength) { return await new Promise(resolve => this.clusterEmitter.once( 'initialized', async ({ imq }) => { @@ -234,7 +239,7 @@ export class ClusteredRedisQueue implements IMessageQueue, )); } - if (this.currentQueue >= this.queueLength) { + if (this.currentQueue >= this.imqLength) { this.currentQueue = 0; } @@ -247,7 +252,7 @@ export class ClusteredRedisQueue implements IMessageQueue, } /** - * Safely destroys current queue, unregistered all set event + * Safely destroys the current queue, unregistered all set event * listeners and connections. * Supposed to be an async function. * @@ -258,6 +263,16 @@ export class ClusteredRedisQueue implements IMessageQueue, await this.batch('destroy', 'Destroying clustered redis message queue...'); + + if (!this.options.clusterManagers?.length) { + return; + } + + for await (const manager of this.options.clusterManagers) { + for await (const cluster of this.initializedClusters) { + await manager.remove(cluster); + } + } } // noinspection JSUnusedGlobalSymbols @@ -272,6 +287,26 @@ export class ClusteredRedisQueue implements IMessageQueue, 'Clearing clustered redis message queue...'); } + public async queueLength(): Promise { + const promises = []; + + for (const imq of this.imqs) { + promises.push(imq.queueLength()); + } + + const lengths = await Promise.all(promises); + + return lengths.reduce((total, length) => total + length, 0); + } + + private verbose(message: string): void { + if (this.options.verbose) { + this.logger.info(`[IMQ-CORE][ClusteredRedisQueue][${ + this.name + }]: ${ message }`); + } + } + /** * Batch imq action processing on all registered imqs at once * @@ -281,7 +316,7 @@ export class ClusteredRedisQueue implements IMessageQueue, * @return {Promise} */ private async batch(action: string, message: string): Promise { - this.logger.log(message); + this.logger.info(message); const promises = []; @@ -472,7 +507,9 @@ export class ClusteredRedisQueue implements IMessageQueue, * @param {IServerInput} server * @returns {void} */ - protected addServer(server: IServerInput): void { + protected addServer(server: IServerInput): ClusterServer { + this.verbose(`Adding new server: ${ JSON.stringify(server) }`); + return this.addServerWithQueueInitializing(server, true); } @@ -483,52 +520,62 @@ export class ClusteredRedisQueue implements IMessageQueue, * @returns {void} */ protected removeServer(server: IServerInput): void { + this.verbose(`Removing the server: ${ JSON.stringify(server) }`); + const remove = this.findServer(server); if (!remove) { return; } - if (remove.imq) { - this.imqs = this.imqs.filter(imq => remove.imq !== imq); - remove.imq.destroy().catch(); - } + const imqToRemove = remove.imq; - this.clusterEmitter.emit('remove', { - server: remove, - imq: remove.imq, - }); + if (imqToRemove) { + this.imqs = this.imqs.filter( + imq => imqToRemove.redisKey !== imq.redisKey + ); + imqToRemove.destroy().catch(); + } - this.queueLength = this.imqs.length; + this.imqLength = this.imqs.length; this.servers = this.servers.filter( existing => !ClusteredRedisQueue.matchServers( existing, server, ), ); + this.clusterEmitter.emit('remove', { + server: remove, + imq: imqToRemove, + }); } private addServerWithQueueInitializing( server: ClusterServer, initializeQueue: boolean = true, - ): void { + ): ClusterServer { + const existingServer = this.findServer(server); + + if (existingServer) { + return existingServer; + } + const newServer: ClusterServer = { id: server.id, host: server.host, port: server.port, }; + const opts = { ...this.mqOptions, ...newServer }; const imq = new RedisQueue(this.name, opts); if (initializeQueue) { - this.initializeQueue(imq) - .then(() => { - this.clusterEmitter.emit('initialized', { - server: newServer, - imq, - }); - }) - .catch(); + this.initializeQueue(imq).then(() => { + this.clusterEmitter.emit('initialized', { + server: newServer, + imq, + }); + }); } newServer.imq = imq; @@ -536,7 +583,9 @@ export class ClusteredRedisQueue implements IMessageQueue, this.imqs.push(imq); this.servers.push(newServer); this.clusterEmitter.emit('add', { server: newServer, imq }); - this.queueLength = this.imqs.length; + this.imqLength = this.imqs.length; + + return newServer; } private eventEmitters(): EventEmitter[] { @@ -545,6 +594,9 @@ export class ClusteredRedisQueue implements IMessageQueue, private async initializeQueue(imq: RedisQueue): Promise { copyEventEmitter(this.templateEmitter, imq); + this.verbose(`Initializing queue with state: ${ + JSON.stringify(this.state) + }`); if (this.state.started) { await imq.start(); diff --git a/src/IMessageQueue.ts b/src/IMessageQueue.ts index 2205964..51d714b 100644 --- a/src/IMessageQueue.ts +++ b/src/IMessageQueue.ts @@ -275,6 +275,24 @@ export interface IMQOptions extends Partial { * @type {ClusterManager[]} */ clusterManagers?: ClusterManager[]; + + /** + * Enables/disables verbose logging + * + * @default false + * @type {boolean} + */ + verbose?: boolean; + + /** + * Enables/disables extended verbose logging. The output may contain + * sensitive information, so use it with caution. Does not work if a verbose + * option is disabled. + * + * @default false + * @type {boolean} + */ + verboseExtended?: boolean; } export interface EventMap { @@ -421,4 +439,12 @@ export interface IMessageQueue extends EventEmitter { * @returns {Promise} */ clear(): Promise; + + /** + * Retrieves the current count of messages in the queue. + * Supposed to be an async function. + * + * @returns {Promise} + */ + queueLength(): Promise; } diff --git a/src/RedisQueue.ts b/src/RedisQueue.ts index 1e4b40e..7b54c27 100644 --- a/src/RedisQueue.ts +++ b/src/RedisQueue.ts @@ -34,7 +34,6 @@ import { ILogger, IMQMode, EventMap, - makeRedisSafe, buildOptions, profile, uuid, @@ -109,6 +108,10 @@ export function unpack(data: string): any { type RedisConnectionChannel = 'reader' | 'writer' | 'watcher' | 'subscription'; +const IMQ_REDIS_MAX_LISTENERS_LIMIT = +( + process.env.IMQ_REDIS_MAX_LISTENERS_LIMIT || 10000 +); + /** * Class RedisQueue * Implements simple messaging queue over redis. @@ -180,7 +183,7 @@ export class RedisQueue extends EventEmitter private destroyed: boolean = false; /** - * True if the current instance owns watcher connection, false otherwise + * True if the current instance owns a watcher connection, false otherwise * * @type {boolean} */ @@ -198,10 +201,18 @@ export class RedisQueue extends EventEmitter */ private safeCheckInterval: any; + /** + * Internal per-channel reconnection state + */ + private reconnectTimers: Partial> = {}; + private reconnectAttempts: Partial> + = {}; + private reconnecting: Partial> = {}; + /** * This queue instance unique key (identifier), for internal use */ - private readonly redisKey: string; + public readonly redisKey: string; /** * LUA scripts for redis @@ -262,11 +273,27 @@ export class RedisQueue extends EventEmitter DEFAULT_IMQ_OPTIONS, options, ); + /* tslint:disable */ this.pack = this.options.useGzip ? pack : JSON.stringify; this.unpack = this.options.useGzip ? unpack : JSON.parse; /* tslint:enable */ this.redisKey = `${this.options.host}:${this.options.port}`; + + this.verbose(`Initializing queue on ${ + this.options.host }:${ + this.options.port} with prefix ${ + this.options.prefix } and safeDelivery = ${ + this.options.safeDelivery }, and safeDeliveryTtl = ${ + this.options.safeDeliveryTtl }, and watcherCheckDelay = ${ + this.options.watcherCheckDelay }, and useGzip = ${ + this.options.useGzip }`); + } + + private verbose(message: string): void { + if (this.options.verbose) { + this.logger.info(`[IMQ-CORE][${ this.name }]: ${ message }`); + } } /** @@ -284,7 +311,7 @@ export class RedisQueue extends EventEmitter // istanbul ignore next if (!channel) { throw new TypeError( - `${channel}: No subscription channel name provided!`, + `${ channel }: No subscription channel name provided!`, ); } @@ -292,7 +319,7 @@ export class RedisQueue extends EventEmitter if (this.subscriptionName && this.subscriptionName !== channel) { throw new TypeError( `Invalid channel name provided: expected "${ - this.subscriptionName}", but "${channel}" given instead!`, + this.subscriptionName}", but "${ channel }" given instead!`, ); } else if (!this.subscriptionName) { this.subscriptionName = channel; @@ -306,9 +333,16 @@ export class RedisQueue extends EventEmitter // istanbul ignore next chan.on('message', (ch: string, message: string) => { if (ch === fcn && typeof handler === 'function') { - handler(JSON.parse(message)); + handler(JSON.parse(message) as unknown as JsonObject); } + + this.verbose(`Received message from ${ + ch } channel, data: ${ + JSON.stringify(message) }`, + ); }); + + this.verbose(`Subscribed to ${ channel } channel`); } /** @@ -318,15 +352,24 @@ export class RedisQueue extends EventEmitter */ public async unsubscribe(): Promise { if (this.subscription) { - if (this.subscriptionName) { - this.subscription.unsubscribe( - `${this.options.prefix}:${this.subscriptionName}`, - ); - } + this.verbose('Initialize unsubscribing...'); + + try { + if (this.subscriptionName) { + await this.subscription.unsubscribe( + `${this.options.prefix}:${this.subscriptionName}`, + ); - this.subscription.removeAllListeners(); - this.subscription.disconnect(false); - this.subscription.quit(); + this.verbose(`Unsubscribed from ${ + this.subscriptionName } channel`); + } + + this.subscription.removeAllListeners(); + this.subscription.quit(); + this.subscription.disconnect(false); + } catch (error) { + this.verbose(`Unsubscribe error: ${ error }`); + } } this.subscriptionName = undefined; @@ -349,10 +392,18 @@ export class RedisQueue extends EventEmitter throw new TypeError('Writer is not connected!'); } + const jsonData = JSON.stringify(data); + const name = toName || this.name; + await this.writer.publish( - `${this.options.prefix}:${toName || this.name}`, - JSON.stringify(data), + `${this.options.prefix}:${ name }`, + jsonData, ); + + this.verbose(`Published message to ${ + name } channel, data: ${ + jsonData } + `); } /** @@ -362,35 +413,48 @@ export class RedisQueue extends EventEmitter */ public async start(): Promise { if (!this.name) { - throw new TypeError(`${this.name}: No queue name provided!`); + throw new TypeError(`${ this.name }: No queue name provided!`); } if (this.initialized) { return this; } + this.destroyed = false; + const connPromises = []; // istanbul ignore next if (!this.reader && this.isWorker()) { + this.verbose('Initializing reader...'); connPromises.push(this.connect('reader', this.options)); } if (!this.writer) { + this.verbose('Initializing writer...'); connPromises.push(this.connect('writer', this.options)); } await Promise.all(connPromises); + this.verbose('Connections initialized'); + if (!this.signalsInitialized) { + this.verbose('Setting up OS signal handlers...'); // istanbul ignore next const free = async () => { let exitCode = 0; - setTimeout(() => process.exit(exitCode), IMQ_SHUTDOWN_TIMEOUT); + setTimeout(() => { + this.verbose(`Shutting down after ${ + IMQ_SHUTDOWN_TIMEOUT } timeout`, + ); + process.exit(exitCode); + }, IMQ_SHUTDOWN_TIMEOUT); try { if (this.watchOwner) { + this.verbose('Freeing watcher lock...'); await this.unlock(); } } catch (err) { @@ -404,11 +468,11 @@ export class RedisQueue extends EventEmitter process.on('SIGABRT', free); this.signalsInitialized = true; + this.verbose('OS signal handlers initialized!'); } await this.initWatcher(); this.initialized = true; - this.destroyed = false; return this; } @@ -437,33 +501,55 @@ export class RedisQueue extends EventEmitter await this.start(); } + if (!this.writer) { + throw new TypeError('IMQ: unable to initialize queue!'); + } + const id = uuid(); const data: IMessage = { id, message, from: this.name }; const key = `${this.options.prefix}:${toQueue}`; const packet = this.pack(data); - const cb = (error: any) => { + const cb = (error: any, op: string) => { // istanbul ignore next - if (error && errorHandler) { - errorHandler(error); + if (error) { + this.verbose(`Writer ${ op } error: ${ error }`); + + if (errorHandler) { + errorHandler(error as unknown as Error); + } } }; if (delay) { this.writer.zadd(`${key}:delayed`, Date.now() + delay, packet, - (err) => { + (err: any) => { // istanbul ignore next if (err) { - cb(err); + cb(err, 'ZADD'); return; } this.writer.set(`${key}:${id}:ttl`, '', 'PX', delay, 'NX', - cb, - ); + (err: any) => { + // istanbul ignore next + if (err) { + cb(err, 'SET'); + + return; + } + }, + ).catch((err: any) => cb(err, 'SET')); }); } else { - this.writer.lpush(key, packet, cb); + this.writer.lpush(key, packet, (err: any) => { + // istanbul ignore next + if (err) { + cb(err, 'LPUSH'); + + return; + } + }); } return id; @@ -476,16 +562,19 @@ export class RedisQueue extends EventEmitter */ @profile() public async stop(): Promise { + this.verbose('Stopping queue...'); + if (this.reader) { - this.reader.removeAllListeners(); - this.reader.quit(); - this.reader.disconnect(false); + this.verbose('Destroying reader...'); + this.destroyChannel('reader', this); delete this.reader; } this.initialized = false; + this.verbose('Queue stopped!'); + return this; } @@ -496,6 +585,7 @@ export class RedisQueue extends EventEmitter */ @profile() public async destroy(): Promise { + this.verbose('Destroying queue...'); this.destroyed = true; this.removeAllListeners(); this.cleanSafeCheckInterval(); @@ -504,10 +594,11 @@ export class RedisQueue extends EventEmitter await this.clear(); this.destroyWriter(); await this.unsubscribe(); + this.verbose('Queue destroyed!'); } /** - * Clears queue data in redis; + * Clears queue data in redis * * @returns {Promise} */ @@ -518,16 +609,20 @@ export class RedisQueue extends EventEmitter } try { + this.verbose('Clearing expired queue keys...'); + await Promise.all([ this.writer.del(this.key), this.writer.del(`${ this.key }:delayed`), ]); + + this.verbose('Expired queue keys cleared!'); } catch (err) { // istanbul ignore next if (this.initialized) { this.logger.error( - `${context.name}: error clearing the redis queue host ${ - this.redisKey} on writer, pid ${process.pid}:`, + `${ context.name }: error clearing the redis queue host ${ + this.redisKey } on writer, pid ${ process.pid }:`, err, ); } @@ -536,6 +631,20 @@ export class RedisQueue extends EventEmitter return this; } + /** + * Retrieves the current count of messages in the queue + * + * @returns {Promise} + */ + @profile() + public async queueLength(): Promise { + if (!this.writer) { + return 0; + } + + return this.writer.llen(this.key); + } + /** * Returns true if publisher mode is enabled on this queue, false otherwise. * @@ -586,7 +695,7 @@ export class RedisQueue extends EventEmitter // noinspection JSUnusedLocalSymbols /** - * Watcher setter, sets the watcher connection property for this + * Watcher setter sets the watcher connection property for this * queue instance * * @param {IRedisClient} conn @@ -612,7 +721,7 @@ export class RedisQueue extends EventEmitter * @returns {string} */ private get lockKey(): string { - return `${this.options.prefix}:watch:lock`; + return `${ this.options.prefix }:watch:lock`; } /** @@ -631,12 +740,12 @@ export class RedisQueue extends EventEmitter * @access private */ @profile() - private destroyWatcher() { + private destroyWatcher(): void { if (this.watcher) { - this.watcher.removeAllListeners(); - this.watcher.quit(); - this.watcher.disconnect(false); + this.verbose('Destroying watcher...'); + this.destroyChannel('watcher', this); delete RedisQueue.watchers[this.redisKey]; + this.verbose('Watcher destroyed!'); } } @@ -646,13 +755,38 @@ export class RedisQueue extends EventEmitter * @access private */ @profile() - private destroyWriter() { + private destroyWriter(): void { if (this.writer) { - this.writer.removeAllListeners(); - this.writer.quit(); - this.writer.disconnect(false); - + this.verbose('Destroying writer...'); + this.destroyChannel('writer', this); delete RedisQueue.writers[this.redisKey]; + this.verbose('Writer destroyed!'); + } + } + + /** + * Destroys any channel + * + * @access private + */ + @profile() + private destroyChannel( + channel: RedisConnectionChannel, + context: RedisQueue = this, + ): void { + const client = context[channel]; + + if (client) { + try { + client.removeAllListeners(); + client.quit().then(() => { + client.disconnect(false); + }).catch(e => { + this.verbose(`Error quitting ${ channel }: ${ e }`); + }); + } catch (error) { + this.verbose(`Error destroying ${ channel }: ${ error }`); + } } } @@ -668,96 +802,159 @@ export class RedisQueue extends EventEmitter private async connect( channel: RedisConnectionChannel, options: IMQOptions, - context: any = this, + context: RedisQueue = this, ): Promise { + this.verbose(`Connecting to ${ channel } channel...`); + // istanbul ignore next if (context[channel]) { return context[channel]; } - return new Promise((resolve, reject) => { - const redis = new Redis({ - // istanbul ignore next - port: options.port || 6379, - // istanbul ignore next - host: options.host || 'localhost', - // istanbul ignore next - username: options.username, - // istanbul ignore next - password: options.password, - connectionName: this.getChannelName( - context.name, - options.prefix || '', - channel, - ), - retryStrategy: this.retryStrategy(context), - autoResubscribe: true, + const redis = new Redis({ + // istanbul ignore next + port: options.port || 6379, + // istanbul ignore next + host: options.host || 'localhost', + // istanbul ignore next + username: options.username, + // istanbul ignore next + password: options.password, + connectionName: this.getChannelName( + context.name + '', + options.prefix || '', + channel, + ), + retryStrategy: this.retryStrategy(), + autoResubscribe: true, + enableOfflineQueue: true, + autoResendUnfulfilledCommands: true, + offlineQueue: true, + maxRetriesPerRequest: null, + enableReadyCheck: channel !== 'subscription', + lazyConnect: true, + }); + + context[channel] = redis; + context[channel].__imq = true; + + for (const event of [ + 'wait', + 'reconnecting', + 'connecting', + 'connect', + 'close', + ]) { + redis.on(event, () => { + context.verbose(`Redis Event fired: ${ event }`); }); + } - context[channel] = makeRedisSafe(redis); - context[channel].__imq = true; + redis.setMaxListeners(IMQ_REDIS_MAX_LISTENERS_LIMIT); + redis.on('error', this.onErrorHandler(context, channel)); + redis.on('end', this.onCloseHandler(context, channel)); - redis.setMaxListeners(10000); - redis.on('ready', - this.onReadyHandler(context, channel, resolve), - ); - redis.on('error', - this.onErrorHandler(context, channel, reject), - ); - redis.on('end', - this.onCloseHandler(context, channel), - ); - }); + await redis.connect(); + + this.logger.info( + '%s: %s channel connected, host %s, pid %s', + context.name, channel, this.redisKey, process.pid, + ); + + switch (channel) { + case 'reader': this.read(); break; + case 'writer': await this.processDelayed(this.key); break; + case 'watcher': await this.initWatcher(); break; + } + + return context[channel]; } // istanbul ignore next /** * Builds and returns redis reconnection strategy * - * @param {RedisQueue} context * @returns {() => (number | void | null)} * @private */ - private retryStrategy( - context: RedisQueue, - ): () => number | void | null { + private retryStrategy(): () => null { return () => { - if (context.destroyed) { - return null; - } - - return 200; + return null; }; } /** - * Builds and returns connection ready state handler + * Schedules custom reconnection for a given channel with capped + * exponential backoff * - * @access private - * @param {RedisQueue} context * @param {RedisConnectionChannel} channel - * @param {(...args: any[]) => void} resolve - * @return {() => Promise} + * @private */ - private onReadyHandler( - context: RedisQueue, - channel: RedisConnectionChannel, - resolve: (...args: any[]) => void, - ): () => Promise { - return (async () => { - this.logger.info( - '%s: %s channel connected, host %s, pid %s', - context.name, channel, this.redisKey, process.pid, - ); + private scheduleReconnect(channel: RedisConnectionChannel): void { + if (this.destroyed) { + return; + } + + if (this.reconnecting[channel]) { + return; + } - switch (channel) { - case 'reader': this.read(); break; - case 'writer': await this.processDelayed(this.key); break; - case 'watcher': await this.initWatcher(); break; + const attempts = (this.reconnectAttempts[channel] || 0) + 1; + const delay = Math.min(30000, 1000 * Math.pow(2, attempts - 1)); + + this.reconnecting[channel] = true; + this.reconnectAttempts[channel] = attempts; + + this.verbose(`Scheduling ${ channel } reconnect in ${ + delay } ms (attempt ${ attempts })`); + + if (this.reconnectTimers[channel]) { + clearTimeout(this.reconnectTimers[channel] as any); + } + + this.reconnectTimers[channel] = setTimeout(async () => { + if (this.destroyed) { + this.reconnecting[channel] = false; + + return; } - resolve(context[channel]); - }); + try { + switch (channel) { + case 'watcher': + this.destroyWatcher(); + break; + case 'writer': + this.destroyWriter(); + break; + case 'reader': + this.destroyChannel(channel, this); + this.reader = undefined; + + break; + case 'subscription': + this.destroyChannel(channel, this); + this.subscription = undefined; + + break; + } + + await this.connect(channel, this.options); + this.reconnectAttempts[channel] = 0; + this.reconnecting[channel] = false; + + if (this.reconnectTimers[channel]) { + clearTimeout(this.reconnectTimers[channel] as any); + this.reconnectTimers[channel] = undefined; + } + + this.verbose(`Reconnected ${ channel } channel`); + } catch (err) { + this.reconnecting[channel] = false; + this.verbose(`Reconnect ${ channel } failed: ${ err }`); + this.scheduleReconnect(channel); + } + }, delay); } // noinspection JSMethodCanBeStatic @@ -774,9 +971,9 @@ export class RedisQueue extends EventEmitter prefix: string, name: RedisConnectionChannel, ): string { - const uniqueSuffix = `pid:${process.pid}:host:${ os.hostname()}`; + const uniqueSuffix = `pid:${ process.pid }:host:${ os.hostname() }`; - return`${prefix}:${contextName}:${name}:${uniqueSuffix}`; + return`${ prefix }:${ contextName }:${ name }:${ uniqueSuffix }`; } /** @@ -785,16 +982,16 @@ export class RedisQueue extends EventEmitter * @access private * @param {RedisQueue} context * @param {RedisConnectionChannel} channel - * @param {(...args: any[]) => void} reject * @return {(err: Error) => void} */ private onErrorHandler( context: RedisQueue, channel: RedisConnectionChannel, - reject: (...args: any[]) => void, - ): (err: Error) => void { + ): (error: Error) => void { // istanbul ignore next - return ((err: Error & { code: string }) => { + return ((error: Error & { code: string }) => { + this.verbose(`Redis Error: ${ error }`); + if (this.destroyed) { return; } @@ -803,12 +1000,15 @@ export class RedisQueue extends EventEmitter `${context.name}: error connecting redis host ${ this.redisKey} on ${ channel}, pid ${process.pid}:`, - err, + error, ); - if (!this.initialized) { - this.initialized = false; - reject(err); + if ( + error.code === 'ECONNREFUSED' || + error.code === 'ETIMEDOUT' || + context[channel]?.status !== 'ready' + ) { + this.scheduleReconnect(channel); } }); } @@ -825,13 +1025,20 @@ export class RedisQueue extends EventEmitter context: RedisQueue, channel: RedisConnectionChannel, ): (...args: any[]) => any { + this.verbose(`Redis ${ channel } is closing...`); + // istanbul ignore next return (() => { this.initialized = false; + this.logger.warn( '%s: redis connection %s closed on host %s, pid %s!', context.name, channel, this.redisKey, process.pid, ); + + if (!this.destroyed) { + this.scheduleReconnect(channel); + } }); } @@ -843,6 +1050,7 @@ export class RedisQueue extends EventEmitter * @returns {RedisQueue} */ private process(msg: [any, any]): RedisQueue { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const [queue, data] = msg; // istanbul ignore next @@ -851,12 +1059,16 @@ export class RedisQueue extends EventEmitter } try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-argument const { id, message, from } = this.unpack(data); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument this.emit('message', message, id, from); } catch (err) { // istanbul ignore next - this.emitError('OnMessage', 'process error - message is invalid', - err, + this.emitError( + 'OnMessage', + 'process error - message is invalid', + err as unknown as Error, ); } @@ -864,26 +1076,31 @@ export class RedisQueue extends EventEmitter } /** - * Returns number of established watcher connections on redis + * Returns the number of established watcher connections on redis * * @access private * @returns {Promise} */ // istanbul ignore next private async watcherCount(): Promise { + if (!this.writer) { + return 0; + } + const rx = new RegExp( `\\bname=${this.options.prefix}:[\\S]+?:watcher:`, ); - const list = await this.writer.client('LIST') as string; + const list = await this.writer.client('LIST'); - return (list || '') - .split(/\r?\n/) - .filter(client => rx.test(client)) - .length; + if (!list || !list.split) { + return 0; + } + + return list.split(/\r?\n/).filter(client => rx.test(client)).length; } /** - * Processes delayed message by its given redis key + * Processes delayed a message by its given redis key * * @access private * @param {string} key @@ -894,12 +1111,15 @@ export class RedisQueue extends EventEmitter if (this.scripts.moveDelayed.checksum) { await this.writer.evalsha( this.scripts.moveDelayed.checksum, - 2, `${key}:delayed`, key, Date.now(), + 2, `${ key }:delayed`, key, Date.now(), ); } } catch (err) { - this.emitError('OnProcessDelayed', 'error processing delayed queue', - err); + this.emitError( + 'OnProcessDelayed', + 'error processing delayed queue', + err as unknown as Error, + ); } } @@ -919,7 +1139,7 @@ export class RedisQueue extends EventEmitter const data = await this.writer.scan( cursor, 'MATCH', - `${this.options.prefix}:*:worker:*`, + `${ this.options.prefix }:*:worker:*`, 'COUNT', '1000', ); @@ -931,11 +1151,14 @@ export class RedisQueue extends EventEmitter await this.processKeys(keys, now); if (cursor === '0') { - return ; + return; } } catch (err) { - this.emitError('OnSafeDelivery', - 'safe queue message delivery problem', err); + this.emitError( + 'OnSafeDelivery', + 'safe queue message delivery problem', + err as unknown as Error, + ); this.cleanSafeCheckInterval(); return; @@ -954,9 +1177,13 @@ export class RedisQueue extends EventEmitter */ private async processKeys(keys: string[], now: number): Promise { if (!keys.length) { - return ; + return; } + this.verbose(`Watching ${ keys.length } keys: ${ + keys.map(key => `"${ key }"`).join(', ') + }`); + for (const key of keys) { const kp: string[] = key.split(':'); @@ -964,7 +1191,7 @@ export class RedisQueue extends EventEmitter continue; } - await this.writer.rpoplpush(key, `${kp.shift()}:${kp.shift()}`); + await this.writer.rpoplpush(key, `${ kp.shift() }:${ kp.shift() }`); } } @@ -988,7 +1215,11 @@ export class RedisQueue extends EventEmitter await this.processDelayed(key.join(':')); } catch (err) { - this.emitError('OnWatch', 'watch error', err); + this.emitError( + 'OnWatch', + 'watch error', + err as unknown as Error, + ); } } @@ -1000,7 +1231,7 @@ export class RedisQueue extends EventEmitter */ private cleanSafeCheckInterval(): void { if (this.safeCheckInterval) { - clearInterval(this.safeCheckInterval); + clearInterval(this.safeCheckInterval as number); delete this.safeCheckInterval; } } @@ -1013,38 +1244,58 @@ export class RedisQueue extends EventEmitter */ // istanbul ignore next private watch(): RedisQueue { - if (!this.watcher || this.watcher.__ready__) { + if (!this.writer || !this.watcher || this.watcher.__ready__) { return this; } try { - this.writer.config('SET', 'notify-keyspace-events', 'Ex'); + this.writer.config( + 'SET', + 'notify-keyspace-events', + 'Ex', + ).catch(err => { + this.emitError( + 'OnConfig', + 'events config error', + err as unknown as Error, + ); + }); } catch (err) { - this.emitError('OnConfig', 'events config error', err); + this.emitError( + 'OnConfig', + 'events config error', + err as unknown as Error, + ); } - this.watcher.on('pmessage', this.onWatchMessage.bind(this)); - this.watcher.psubscribe('__keyevent@0__:expired', - `${this.options.prefix}:delayed:*`, + this.watcher.on( + 'pmessage', + this.onWatchMessage.bind(this) as unknown as () => void, ); + this.watcher.psubscribe( + '__keyevent@0__:expired', + `${ this.options.prefix }:delayed:*`, + ).catch(err => { + this.verbose(`Error subscribing to watcher channel: ${ err }`); + }); // watch for expired unhandled safe queues if (!this.safeCheckInterval) { - // tslint:disable-next-line:triple-equals no-null-keyword if (this.options.safeDeliveryTtl != null) { - this.safeCheckInterval = setInterval(async () => { - if (!this.writer) { - this.cleanSafeCheckInterval(); + this.safeCheckInterval = setInterval( + (async (): Promise => { + if (!this.writer) { + this.cleanSafeCheckInterval(); - return; - } + return ; + } - if (this.options.safeDelivery) { - await this.processWatch(); - } + if (this.options.safeDelivery) { + await this.processWatch(); + } - await this.processCleanup(); - }, this.options.safeDeliveryTtl); + await this.processCleanup(); + }) as unknown as () => void, this.options.safeDeliveryTtl); } } @@ -1060,6 +1311,8 @@ export class RedisQueue extends EventEmitter * @returns {Promise} */ private async processCleanup(): Promise { + this.verbose('Cleaning up orphaned keys...'); + try { if (!this.options.cleanup) { return; @@ -1070,7 +1323,12 @@ export class RedisQueue extends EventEmitter (this.options.cleanupFilter || '*').replace(/\*/g, '.*'), 'i', ); - const clients = await this.writer.client('LIST') as string; + + this.verbose(`Cleaning up keys matching ${ filter }`); + + const clients: string = (await this.writer.client( + 'LIST', + ) as string).toString() || ''; const connectedKeys = (clients.match(RX_CLIENT_NAME) || []) .filter((name: string) => RX_CLIENT_TEST.test(name) && filter.test(name), @@ -1085,6 +1343,10 @@ export class RedisQueue extends EventEmitter const keysToRemove: string[] = []; let cursor = '0'; + this.verbose(`Found connected keys: ${ + connectedKeys.map(k => `"${ k }"`).join(', ') + }`); + while (true) { const data = await this.writer.scan( cursor, @@ -1118,6 +1380,10 @@ export class RedisQueue extends EventEmitter if (keysToRemove.length) { await this.writer.del(...keysToRemove); + + this.verbose(`Keys ${ + keysToRemove.map(k => `"${ k }"`).join(', ') + } were successfully removed!`); } } catch (err) { this.logger.warn('Clean-up error occurred:', err); @@ -1130,7 +1396,7 @@ export class RedisQueue extends EventEmitter /** * Unreliable but fast way of message handling by the queue */ - private async readUnsafe() { + private async readUnsafe(): Promise { try { const key = this.key; @@ -1147,6 +1413,7 @@ export class RedisQueue extends EventEmitter } } catch (err) { // istanbul ignore next + // eslint-disable-next-line @typescript-eslint/no-unsafe-call if (err.message.match(/Stream connection ended/)) { break; } @@ -1158,7 +1425,11 @@ export class RedisQueue extends EventEmitter } } catch (err) { // istanbul ignore next - this.emitError('OnReadUnsafe', 'unsafe reader failed', err); + this.emitError( + 'OnReadUnsafe', + 'unsafe reader failed', + err as unknown as Error, + ); } } @@ -1166,7 +1437,7 @@ export class RedisQueue extends EventEmitter /** * Reliable but slow method of message handling by message queue */ - private async readSafe() { + private async readSafe(): Promise { try { const key = this.key; @@ -1181,7 +1452,8 @@ export class RedisQueue extends EventEmitter try { await this.reader.brpoplpush(this.key, workerKey, 0); - } catch (err) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_) { // istanbul ignore next break; } @@ -1189,18 +1461,26 @@ export class RedisQueue extends EventEmitter const msgArr: any = await this.writer.lrange( workerKey, -1, 1, ); - if (msgArr.length !== 1) { + + if (!msgArr || msgArr?.length !== 1) { // noinspection ExceptionCaughtLocallyJS throw new Error('Wrong messages count'); } - const msg = msgArr[0]; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const [msg] = msgArr as any[]; this.process([key, msg]); - this.writer.del(workerKey); + this.writer.del(workerKey).catch(e => + this.logger.warn('OnReadSafe: del error', e)); } } catch (err) { // istanbul ignore next - this.emitError('OnReadSafe', 'safe reader failed', err); + this.emitError( + 'OnReadSafe', + 'safe reader failed', + err as unknown as Error, + ); } } @@ -1224,6 +1504,7 @@ export class RedisQueue extends EventEmitter ? 'readSafe' : 'readUnsafe'; + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument process.nextTick(this[readMethod].bind(this)); return this; @@ -1280,13 +1561,16 @@ export class RedisQueue extends EventEmitter * @param {string} message * @param {Error} err */ - private emitError(eventName: string, message: string, err: Error) { + private emitError(eventName: string, message: string, err: Error): void { this.emit('error', err, eventName); this.logger.error( - `${this.name}: ${message}, pid ${ - process.pid} on redis host ${this.redisKey}:`, + `${this.name}: ${ message }, pid ${ + process.pid } on redis host ${ this.redisKey }:`, err, ); + this.verbose(`Error in event ${ + eventName }: ${ message }, pid ${ + process.pid } on redis host ${ this.redisKey }: ${ err }`); } /** @@ -1299,6 +1583,8 @@ export class RedisQueue extends EventEmitter const owned = await this.lock(); if (owned) { + this.verbose('Watcher connection lock acquired!'); + for (const script of Object.keys(this.scripts)) { try { const checksum = sha1(this.scripts[script].code); @@ -1318,7 +1604,11 @@ export class RedisQueue extends EventEmitter ); } } catch (err) { - this.emitError('OnScriptLoad', 'script load error', err); + this.emitError( + 'OnScriptLoad', + 'script load error', + err as unknown as Error, + ); } } @@ -1330,7 +1620,7 @@ export class RedisQueue extends EventEmitter // istanbul ignore next /** - * This method returns watcher lock resolver function + * This method returns a watcher lock resolver function * * @access private * @param {(...args: any[]) => void} resolve @@ -1365,33 +1655,45 @@ export class RedisQueue extends EventEmitter */ // istanbul ignore next private async initWatcher(): Promise { - return new Promise(async (resolve, reject) => { - try { - if (!await this.watcherCount()) { - await this.ownWatch(); - - if (this.watchOwner && this.watcher) { - resolve(); + return new Promise( + (async ( + resolve: (...args: any[]) => void, + reject: (...args: any[]) => void, + ): Promise => { + try { + if (!await this.watcherCount()) { + this.verbose('Initializing watcher...'); + + await this.ownWatch(); + + if (this.watchOwner && this.watcher) { + resolve(); + } else { + // check for possible deadlock to resolve + setTimeout( + this.watchLockResolver( + resolve, + reject, + ) as unknown as () => void, + intrand(1, 50), + ); + } } else { - // check for possible deadlock to resolve - setTimeout( - this.watchLockResolver(resolve, reject), - intrand(1, 50), - ); + resolve(); } - } else { - resolve(); - } - } catch (err) { - this.logger.error( - `${this.name}: error initializing watcher, pid ${ - process.pid} on redis host ${this.redisKey}`, - err, - ); + } catch (err) { + this.logger.error( + `${ this.name }: error initializing watcher, pid ${ + process.pid } on redis host ${ this.redisKey }`, + err, + ); - reject(err); - } - }); + reject(err); + } + }) as unknown as ( + resolve: (...args: any[]) => void, + reject: (...args: any[]) => void, + ) => void, + ); } - } diff --git a/src/UDPClusterManager.ts b/src/UDPClusterManager.ts index 8f8b181..73bb159 100644 --- a/src/UDPClusterManager.ts +++ b/src/UDPClusterManager.ts @@ -21,392 +21,204 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { - IMessageQueueConnection, -} from './IMessageQueue'; -import { ICluster, ClusterManager } from './ClusterManager'; -import { Socket, createSocket } from 'dgram'; -import { networkInterfaces } from 'os'; - -enum BroadcastedMessageType { - Up = 'up', - Down = 'down', -} - -interface BroadcastedMessage { - name: string; - id: string; - type: BroadcastedMessageType; - host: string; - port: number; - timeout: number; -} - -interface ClusterServer extends IMessageQueueConnection { - timeout?: number; - timestamp?: number; - timer?: NodeJS.Timeout; -} +import { ClusterManager, ICluster } from './ClusterManager'; +import { Worker } from 'worker_threads'; +import * as path from 'path'; -export const DEFAULT_UDP_BROADCAST_CLUSTER_MANAGER_OPTIONS = { - broadcastPort: 63000, - broadcastAddress: '255.255.255.255', - aliveTimeoutCorrection: 1000, -}; +process.setMaxListeners(10000); export interface UDPClusterManagerOptions { - /** - * Represents the cluster operations that are responsible for managing - * clusters. This includes operations such as adding, removing, or checking - * if a cluster server exists. - * - * @type {ICluster} - */ - cluster?: ICluster; - /** * Message queue broadcast port * * @default 63000 * @type {number} */ - broadcastPort?: number; + port: number; /** * Message queue broadcast address * - * @default limitedBroadcastAddress * @type {number} */ - broadcastAddress?: string; + address: string; /** * Message queue limited broadcast address * - * @default 255.255.255.255 + * @default "255.255.255.255" * @type {string} */ - limitedBroadcastAddress?: string; + limitedAddress?: string; /** * Message queue alive timeout correction. Used to correct waiting time to * check if the server is alive * - * @default 1000 + * @default 5000 * @type {number} */ - aliveTimeoutCorrection?: number; + aliveTimeoutCorrection: number; /** - * Skip messages that are broadcast by specified addresses or set to - * "localhost" if you want to skip messages from "127.0.0.1" or "::1" + * Message queue alive-server check flag. If set to false, the server will + * not be checked for liveness on each broadcast message with a timeout. + * Can be specified by the environment variable if the given option is not + * bypassed: IMQ_UDP_CLUSTER_MANAGER_ALIVE_CHECK * - * @type {"local" | string[]} + * @default true + * @type {boolean} */ - excludeHosts?: 'localhost' | string[]; - - /** - * Allow messages that are broadcast only by specified addresses or set to - * "localhost" if you want to allow messages only from "127.0.0.1" or "::1" - * - * @type {"local" | string[]} - */ - includeHosts?: 'localhost' | string[]; + useAliveCheck: boolean; } -const LOCALHOST_ADDRESSES = [ - 'localhost', - '127.0.0.1', - '::1', -]; +const IMQ_UDP_CLUSTER_MANAGER_ALIVE_CHECK = !!+( + process.env.IMQ_UDP_CLUSTER_MANAGER_ALIVE_CHECK || 1 +); + +export const DEFAULT_UDP_CLUSTER_MANAGER_OPTIONS: UDPClusterManagerOptions = { + port: 63000, + address: '255.255.255.255', + aliveTimeoutCorrection: 5000, + useAliveCheck: IMQ_UDP_CLUSTER_MANAGER_ALIVE_CHECK, +}; -/** - * UDP broadcast-based cluster management implementation - * - * @example - * ~~~typescript - * const queue = new ClusteredRedisQueue('ClusteredQueue', { - * clusterManagers: [new UDPBroadcastClusterManager()], - * }); - * ~~~ - */ export class UDPClusterManager extends ClusterManager { - private static sockets: Record = {}; + private static workers: Record = {}; + public static sockets: Record = {}; private readonly options: UDPClusterManagerOptions; + private workerKey: string; + private worker: Worker; - constructor(options?: UDPClusterManagerOptions) { + constructor(options?: Partial) { super(); this.options = { - ...DEFAULT_UDP_BROADCAST_CLUSTER_MANAGER_OPTIONS, + ...DEFAULT_UDP_CLUSTER_MANAGER_OPTIONS, ...options || {}, }; - this.startListening(this.options); - } - - private listenBroadcastedMessages( - listener: (message: BroadcastedMessage) => void, - options: UDPClusterManagerOptions, - ): void { - const address = UDPClusterManager.selectNetworkInterface( - options, - ); - const key = `${ address }:${ options.broadcastPort }`; - - if (!UDPClusterManager.sockets[key]) { - const socket = createSocket({ type: 'udp4', reuseAddr: true }); - socket.bind(options.broadcastPort, address); - UDPClusterManager.sockets[key] = socket; - } - - UDPClusterManager.sockets[key].on( - 'message', - message => listener( - UDPClusterManager.parseBroadcastedMessage(message), - ), - ); - } + this.startWorkerListener(); - private startListening( - options: UDPClusterManagerOptions = {}, - ): void { - this.listenBroadcastedMessages( - UDPClusterManager.processBroadcastedMessage(this), - options, - ); + process.on('SIGTERM', UDPClusterManager.free); + process.on('SIGINT', UDPClusterManager.free); + process.on('SIGABRT', UDPClusterManager.free); } - private static verifyHosts( - host: string, - hosts: string[] | 'localhost', - ): boolean { - const normalizedHosts = hosts === 'localhost' - ? LOCALHOST_ADDRESSES - : hosts - ; + private static async free(): Promise { + const workerKeys = Object.keys(UDPClusterManager.workers); - return normalizedHosts.includes(host); + await Promise.all(workerKeys.map( + workerKey => UDPClusterManager.destroyWorker( + workerKey, + UDPClusterManager.workers[workerKey], + )), + ); } - private static processMessageOnCluster( - cluster: ICluster, - message: BroadcastedMessage, - aliveTimeoutCorrection?: number, - ): void { - const server = cluster.find(message); + private startWorkerListener(): void { + this.workerKey = `${ this.options.address }:${ this.options.port }`; - if (server && message.type === BroadcastedMessageType.Down) { - clearTimeout(server.timer); + if (UDPClusterManager.workers[this.workerKey]) { + this.worker = UDPClusterManager.workers[this.workerKey]; - return cluster.remove(message); + return; } - if (!server && message.type === BroadcastedMessageType.Up) { - cluster.add(message); + this.worker = new Worker(path.join(__dirname, './UDPWorker.js'), { + workerData: this.options, + }); + this.worker.on('message', message => { + const [className, method] = message.type?.split(':'); - const added = cluster.find(message); - - if (added) { - UDPClusterManager.serverAliveWait( - cluster, - added, - aliveTimeoutCorrection, - ); + if (className !== 'cluster') { + return; } - return; - } + return this.anyCluster(cluster => { + if (method === 'add') { + try { + const existing = typeof (cluster as any).find === 'function' + ? (cluster as any).find(message.server, true) + : undefined; + if (existing) { + return; + } + } catch {/* ignore */} + } - if (server && message.type === BroadcastedMessageType.Up) { - return UDPClusterManager.serverAliveWait( - cluster, - server, - aliveTimeoutCorrection, - message, - ); - } - } + const clusterMethod = (cluster as any)[method as keyof ICluster]; - private static processBroadcastedMessage( - context: UDPClusterManager, - ): (message: BroadcastedMessage) => void { - return message => { - if ( - context.options.excludeHosts - && UDPClusterManager.verifyHosts( - message.host, - context.options.excludeHosts, - ) - ) { - return ; - } + if (!clusterMethod) { + return; + } - if ( - context.options.includeHosts - && !UDPClusterManager.verifyHosts( - message.host, - context.options.includeHosts, - ) - ) { - return ; - } + clusterMethod(message.server); + }); + }); - for (const cluster of context.clusters) { - UDPClusterManager.processMessageOnCluster( - cluster, - message, - context.options.aliveTimeoutCorrection, - ); - } - }; + UDPClusterManager.workers[this.workerKey] = this.worker; } - private static parseBroadcastedMessage( - input: Buffer, - ): BroadcastedMessage { - const [ - name, - id, - type, - address = '', - timeout = '0', - ] = input.toString().split('\t'); - const [host, port] = address.split(':'); - - return { - id, - name, - type: type.toLowerCase() as BroadcastedMessageType, - host, - port: parseInt(port), - timeout: parseFloat(timeout) * 1000, - }; + public async destroy(): Promise { + await UDPClusterManager.destroyWorker(this.workerKey, this.worker); } - private static serverAliveWait( - cluster: ICluster, - server: ClusterServer, - aliveTimeoutCorrection?: number, - message?: BroadcastedMessage, - ): void { - clearTimeout(server.timer); - server.timestamp = Date.now(); - - if (message) { - server.timeout = message.timeout; + public static async destroySocket(key: string, socket?: any): Promise { + if (!socket) { + return; } - const correction = aliveTimeoutCorrection || 0; - const timeout = (server.timeout || 0) + correction; - - server.timer = setTimeout(() => { - const existing = cluster.find(server); - - if (!existing) { - return; - } - - const now = Date.now(); - const delta = now - (existing.timestamp || now); - const currentTimeout = (existing.timeout || 0) + correction; - - if (delta >= currentTimeout) { - clearTimeout(server.timer); - - cluster.remove(server); - } - }, timeout); - } - - /** - * Destroys the UDPClusterManager by closing all opened network connections - * and safely destroying all blocking sockets - * - * @returns {Promise} - * @throws {Error} - */ - public async destroy(): Promise { - // Close all UDP sockets and clean up connections - const socketKeys = Object.keys(UDPClusterManager.sockets); - const closePromises: Promise[] = []; - - for (const key of socketKeys) { - const socket = UDPClusterManager.sockets[key]; - - if (socket) { - closePromises.push(new Promise(((socketKey: string): any => - ((resolve: any, reject: any): any => { - try { - // Check if socket has close method and is not already closed - if (typeof socket.close === 'function') { - // Remove all event listeners to prevent memory leaks - socket.removeAllListeners(); - - // Close the socket - socket.close(() => { - socket.unref(); - delete UDPClusterManager.sockets[socketKey]; - resolve(); - }); - } else { - resolve(); - } - } catch (error) { - // Handle any errors during socket closure gracefully - reject(error as Error); - } - }) as any)(key))); + try { + if (typeof socket.removeAllListeners === 'function') { + socket.removeAllListeners(); } + } catch (e) { + throw e; } - // Wait for all sockets to close - await Promise.all(closePromises); + if (typeof socket.close !== 'function') { + return; + } - // Clear the static sockets record - UDPClusterManager.sockets = {}; + await new Promise((resolve) => { + socket.close(() => { + if (typeof socket.unref === 'function') { + socket.unref(); + } + if (UDPClusterManager.sockets && key in UDPClusterManager.sockets) { + delete UDPClusterManager.sockets[key]; + } + resolve(); + }); + }); } - private static selectNetworkInterface( - options: Pick< - UDPClusterManagerOptions, - 'broadcastAddress' - | 'limitedBroadcastAddress' - >, - ): string { - const interfaces = networkInterfaces(); - const limitedBroadcastAddress = options.limitedBroadcastAddress; - const broadcastAddress = options.broadcastAddress - || limitedBroadcastAddress; - const defaultAddress = '0.0.0.0'; - - if (!broadcastAddress) { - return defaultAddress; + private static async destroyWorker( + workerKey: string, + worker?: Worker, + ): Promise { + if (!worker) { + return; } - const equalAddresses = broadcastAddress === limitedBroadcastAddress; + return new Promise(resolve => { + const timeout = setTimeout(() => { + worker.terminate(); + resolve(); + }, 5000); - if (equalAddresses) { - return defaultAddress; - } + worker.postMessage({ type: 'stop' }); + worker.once('message', (message) => { + if (message.type === 'stopped') { + clearTimeout(timeout); + worker.terminate(); - for (const key in interfaces) { - if (!interfaces[key]) { - continue; - } + delete UDPClusterManager.workers[workerKey]; - for (const net of interfaces[key]) { - const shouldBeSelected = net.family === 'IPv4' - && net.address.startsWith( - broadcastAddress.replace(/\.255/g, ''), - ); - - if (shouldBeSelected) { - return net.address; + resolve(); } - } - } - - return defaultAddress; + }); + }); } } diff --git a/src/UDPWorker.ts b/src/UDPWorker.ts new file mode 100644 index 0000000..da83b06 --- /dev/null +++ b/src/UDPWorker.ts @@ -0,0 +1,230 @@ +/*! + * UDP message listener for cluster managing: Worker for processing + * messages + * + * 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 { + isMainThread, + parentPort, + workerData, + MessagePort, +} from 'worker_threads'; +import { createSocket, Socket } from 'dgram'; +import { networkInterfaces } from 'os'; +import { UDPClusterManagerOptions } from './UDPClusterManager'; +import { uuid } from './uuid'; + +process.setMaxListeners(10000); + +enum MessageType { + Up = 'up', + Down = 'down', +} + +interface Message { + id: string; + name: string; + type: MessageType; + host: string; + port: number; + timeout: number; +} + +class UDPWorker { + private readonly socket: Socket; + private readonly servers = new Map(); + + constructor( + private readonly options: UDPClusterManagerOptions, + private readonly messagePort: MessagePort, + ) { + this.setupMessageHandlers(); + this.setupProcessHandlers(); + this.socket = createSocket({ + type: 'udp4', + reuseAddr: true, + reusePort: true, + }).bind(this.options.port, this.selectNetworkInterface()); + this.socket.on( + 'message', + message => this.processMessage(this.parseMessage(message)), + ); + } + + private static getServerKey(message: Message): string { + return message.id; + } + + private setupMessageHandlers(): void { + this.messagePort.on('message', message => { + if (message.type === 'stop') { + this.stop(); + } + }); + } + + private setupProcessHandlers(): void { + process.on('SIGTERM', this.cleanup); + process.on('SIGINT', this.cleanup); + process.on('SIGABRT', this.cleanup); + } + + private addServer(message: Message): void { + this.messagePort.postMessage({ + type: 'cluster:add', + server: UDPWorker.mapMessage(message), + }); + + if (this.options.useAliveCheck) { + this.serverAliveWait(message); + } + } + + private removeServer(message: Message): void { + this.servers.delete(UDPWorker.getServerKey(message)); + this.messagePort.postMessage({ + type: 'cluster:remove', + server: UDPWorker.mapMessage(message), + }); + } + + private static mapMessage(message: Message): Message { + return { + id: message.id, + name: message.name, + type: message.type, + host: message.host, + port: message.port, + timeout: message.timeout, + }; + } + + private serverAliveWait(message: Message): void { + const stamp = uuid(); + const correction = this.options.aliveTimeoutCorrection ?? 0; + const effectiveTimeout = message.timeout + correction + 1; + const key = UDPWorker.getServerKey(message); + + this.servers.set(key, stamp); + + const t: any = setTimeout(() => setImmediate(() => { + if (this.servers.get(key) === stamp) { + this.removeServer(message); + } + }), effectiveTimeout); + // Avoid keeping the event loop alive due to pending timers + try { + if (t && typeof t.unref === 'function') { + t.unref(); + } + } catch {/* ignore */} + } + + private processMessage(message: Message): void { + if (message.type === MessageType.Down) { + return this.removeServer(message); + } + + if (message.type === MessageType.Up) { + return this.addServer(message); + } + } + + private selectNetworkInterface(): string { + const interfaces = networkInterfaces(); + const broadcastAddress = this.options.address + || this.options.limitedAddress; + const defaultAddress = '0.0.0.0'; + + if ( + !broadcastAddress + || broadcastAddress === this.options.limitedAddress + ) { + return defaultAddress; + } + + for (const key in interfaces) { + if (!interfaces[key]) { + continue; + } + + for (const net of interfaces[key]) { + const shouldBeSelected = net.family === 'IPv4' + && net.address.startsWith( + broadcastAddress.replace(/\.255/g, ''), + ); + + if (shouldBeSelected) { + return net.address; + } + } + } + + return defaultAddress; + } + + private parseMessage(input: Buffer): Message { + const [ + name, + id, + type, + address = '', + timeout = '0', + ] = input.toString().split('\t'); + const [host, port] = address.split(':'); + + return { + id, + name, + type: type.toLowerCase() as MessageType, + host, + port: parseInt(port), + timeout: parseFloat(timeout) * 1000, + }; + } + + private stop(): void { + this.cleanup(); + + if (this.socket) { + this.socket.close(() => { + this.messagePort.postMessage({ type: 'stopped' }); + }); + + return; + } + + this.messagePort.postMessage({ type: 'stopped' }); + } + + private cleanup(): void { + this.servers.clear(); + + if (this.socket) { + this.socket.removeAllListeners(); + } + } +} + +if (!isMainThread && parentPort) { + new UDPWorker(workerData, parentPort); +} diff --git a/src/redis.ts b/src/redis.ts index e82d292..f7c47cf 100644 --- a/src/redis.ts +++ b/src/redis.ts @@ -34,30 +34,5 @@ export interface IRedisClient extends Redis { __imq?: boolean; } -// istanbul ignore next -export function makeRedisSafe(redis: IRedisClient): IRedisClient { - return new Proxy(redis, { - get(target, property, receiver) { - const original = Reflect.get(target, property, receiver); - - if (typeof original === 'function') { - return async (...args: unknown[]) => { - try { - if (target.status !== 'ready') { - return null; - } - - return await original.apply(target, args); - } catch (err: unknown) { - return null; - } - }; - } - - return original; - }, - }); -} - export { Redis }; export default Redis; diff --git a/test/ClusterManager.spec.ts b/test/ClusterManager.spec.ts new file mode 100644 index 0000000..42b86df --- /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: () => ({} as any), + 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: () => ({} as any), + 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..e6e229c --- /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).imqLength).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..df4d6c7 --- /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.imqLength).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..47e9287 --- /dev/null +++ b/test/ClusteredRedisQueue.matchServers.spec.ts @@ -0,0 +1,23 @@ +/*! + * 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 match servers if id provided', () => { + expect(match({ id: 'a', host: 'h', port: 1 }, { id: 'a', host: 'h', port: 2 })).to.be.true; + expect(match({ id: 'a', host: 'h', port: 1 }, { id: 'b', host: 'h', port: 1 })).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..70fff96 --- /dev/null +++ b/test/UDPClusterManager.destroySocket.spec.ts @@ -0,0 +1,36 @@ +/*! + * UDPClusterManager.destroyWorker() behavior tests aligned with implementation + */ +import './mocks'; +import { expect } from 'chai'; +import { UDPClusterManager } from '../src'; + +describe('UDPClusterManager.destroyWorker()', () => { + it('should resolve when worker is undefined (no-op)', async () => { + const destroy = (UDPClusterManager as any).destroyWorker as Function; + await destroy('0.0.0.0:63000', undefined); + }); + + it('should terminate worker and remove it from the workers map', async () => { + const destroy = (UDPClusterManager as any).destroyWorker as Function; + const workers = (UDPClusterManager as any).workers as Record; + const key = '1.2.3.4:65000'; + + let terminated = false; + const fakeWorker: any = { + postMessage: () => {}, + once: (event: string, cb: Function) => { + if (event === 'message') { + setImmediate(() => cb({ type: 'stopped' })); + } + }, + terminate: () => { terminated = true; }, + }; + + workers[key] = fakeWorker; + await destroy(key, fakeWorker); + + expect(terminated).to.equal(true); + expect(workers[key]).to.be.undefined; + }); +}); diff --git a/test/UDPClusterManager.missing.branches.spec.ts b/test/UDPClusterManager.missing.branches.spec.ts new file mode 100644 index 0000000..5d4e892 --- /dev/null +++ b/test/UDPClusterManager.missing.branches.spec.ts @@ -0,0 +1,39 @@ +/*! + * 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('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.ts b/test/UDPClusterManager.ts index b1f76b2..98268a1 100644 --- a/test/UDPClusterManager.ts +++ b/test/UDPClusterManager.ts @@ -21,36 +21,47 @@ * 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'; +const testMessageUp = { + name: 'IMQUnitTest', + id: '1234567890', + type: 'up', + address: '127.0.0.1:6379', + timeout: 50, +}; + +const testMessageDown = { + name: 'IMQUnitTest', + id: '1234567890', + type: 'down', + address: '127.0.0.1:6379', + timeout: 50, +}; -const getSocket = (classObject: typeof UDPClusterManager) => { - return Object.values((classObject as any).sockets)[0] as Socket; +const getSocket = (classObject: any) => { + return classObject.worker; }; -const emitMessage = (message: string) => { - getSocket(UDPClusterManager).emit('message', Buffer.from(message)); +const emitMessage = ( + instanceClass: any, + type: 'cluster:add' | 'cluster:remove', +) => { + getSocket(instanceClass).emit('message', { + type, + server: type === 'cluster:add' ? testMessageUp : testMessageDown, + }); }; describe('UDPBroadcastClusterManager', function() { + this.timeout(5000); it('should be a class', () => { expect(typeof UDPClusterManager).to.equal('function'); }); - it('should initialize socket if socket does not exists', async () => { - const manager = new UDPClusterManager(); - expect( - Object.values((UDPClusterManager as any).sockets), - ).not.to.be.length(0); - await manager.destroy(); - }); - it('should call add on cluster', async () => { const cluster: any = { add: () => {}, @@ -63,7 +74,7 @@ describe('UDPBroadcastClusterManager', function() { manager.init(cluster); - emitMessage(testMessageUp); + emitMessage(manager, 'cluster:add'); expect(cluster.add.called).to.be.true; await manager.destroy(); }); @@ -82,7 +93,7 @@ describe('UDPBroadcastClusterManager', function() { manager.init(cluster); - emitMessage(testMessageUp); + emitMessage(manager, 'cluster:add'); expect(cluster.add.called).to.be.false; await manager.destroy(); }); @@ -101,68 +112,11 @@ describe('UDPBroadcastClusterManager', function() { manager.init(cluster); - emitMessage(testMessageDown); + emitMessage(manager, 'cluster:remove'); expect(cluster.remove.called).to.be.true; await manager.destroy(); }); - it('should add server if localhost included', async () => { - const cluster: any = { - add: () => {}, - remove: () => {}, - find: () => {}, - }; - const manager: any = new UDPClusterManager({ - includeHosts: 'localhost', - }); - - sinon.spy(cluster, 'add'); - - manager.init(cluster); - - emitMessage('name\tid\tup\t127.0.0.1:6379\ttimeout'); - expect(cluster.add.called).to.be.true; - await manager.destroy(); - }); - - it('should not add server if localhost excluded', async () => { - const cluster: any = { - add: () => {}, - remove: () => {}, - find: () => {}, - }; - const manager: any = new UDPClusterManager({ - excludeHosts: 'localhost', - }); - - sinon.spy(cluster, 'add'); - - manager.init(cluster); - - emitMessage('name\tid\tup\t127.0.0.1:6379\ttimeout'); - expect(cluster.add.called).to.be.false; - await manager.destroy(); - }); - - it('should not add server if not in includeHosts', async () => { - const cluster: any = { - add: () => {}, - remove: () => {}, - find: () => {}, - }; - const manager: any = new UDPClusterManager({ - includeHosts: ['example.com'], - }); - - sinon.spy(cluster, 'add'); - - manager.init(cluster); - - emitMessage('name\tid\tup\t127.0.0.1:6379\ttimeout'); - expect(cluster.add.called).to.be.false; - await manager.destroy(); - }); - it('should handle server timeout and removal', (done) => { let addedServer: any = null; const cluster: any = { @@ -170,7 +124,6 @@ describe('UDPBroadcastClusterManager', function() { remove: async (server: any) => { expect(server).to.equal(addedServer); await manager.destroy(); - done(); }, find: (message: any) => { if (!addedServer) { @@ -190,11 +143,12 @@ describe('UDPBroadcastClusterManager', function() { manager.init(cluster); // Send up message to add server with short timeout - emitMessage('name\tid\tup\t127.0.0.1:6379\t0.05'); + emitMessage(manager, 'cluster:add'); // Wait for timeout to trigger removal setTimeout(async () => { await manager.destroy(); + done(); }, 1000); }); @@ -222,17 +176,12 @@ describe('UDPBroadcastClusterManager', function() { manager.init(cluster); // This should trigger the timeout handler that returns early (line 307) - emitMessage('name\tid\tup\t127.0.0.1:6379\t0.05'); + emitMessage(manager, 'cluster:add'); await manager.destroy(); }); describe('destroy()', () => { it('should handle empty sockets gracefully', async () => { - const cluster: any = { - add: () => {}, - remove: () => {}, - find: () => {} - }; const manager: any = new UDPClusterManager(); // Clear any existing sockets @@ -243,33 +192,5 @@ describe('UDPBroadcastClusterManager', function() { expect(Object.keys((UDPClusterManager as any).sockets)).to.have.length(0); }); - - it('should close multiple sockets', async () => { - const cluster: any = { - add: () => {}, - remove: () => {}, - find: () => {} - }; - const manager1: any = new UDPClusterManager({ broadcastPort: 8001 }); - const manager2: any = new UDPClusterManager({ broadcastPort: 8002 }); - - manager1.init(cluster); - manager2.init(cluster); - - // Trigger socket creation for both managers - emitMessage('name\tid\tup\t127.0.0.1:6379\t1000'); - - // Verify multiple sockets exist - const sockets = (UDPClusterManager as any).sockets; - const socketKeys = Object.keys(sockets); - expect(socketKeys.length).to.be.greaterThan(0); - - // Call destroy on both managers - await manager1.destroy(); - await manager2.destroy(); - - // Verify all sockets are cleared (since it's a static property) - expect(Object.keys((UDPClusterManager as any).sockets)).to.have.length(0); - }); }); }); 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/mocks/redis.ts b/test/mocks/redis.ts index 1f229d9..d652392 100644 --- a/test/mocks/redis.ts +++ b/test/mocks/redis.ts @@ -60,14 +60,20 @@ export class RedisClientMock extends EventEmitter { // noinspection JSUnusedGlobalSymbols public end() {} // noinspection JSUnusedGlobalSymbols - public quit() {} + public quit() { + return new Promise(resolve => resolve(undefined)); + } + + public connect() { + return new Promise(resolve => resolve(undefined)); + } // noinspection JSMethodCanBeStatic - public set(...args: any[]): number { + public set(...args: any[]): Promise { const [key, val] = args; RedisClientMock.__keys[key] = val; this.cbExecute(args.pop(), null, 1); - return 1; + return new Promise(resolve => resolve(1)); } // noinspection JSUnusedGlobalSymbols,JSMethodCanBeStatic @@ -230,14 +236,14 @@ export class RedisClientMock extends EventEmitter { } // noinspection JSUnusedGlobalSymbols,JSMethodCanBeStatic - public psubscribe(...args: any[]): number { + public psubscribe(...args: any[]): Promise { this.cbExecute(args.pop(), null, 1); - return 1; + return new Promise(resolve => resolve(1)); } - public punsubscribe(...args: any[]): number { + public punsubscribe(...args: any[]): Promise { this.cbExecute(args.pop(), null, 1); - return 1; + return new Promise(resolve => resolve(1)); } // noinspection JSUnusedGlobalSymbols,JSMethodCanBeStatic @@ -247,7 +253,7 @@ export class RedisClientMock extends EventEmitter { } // noinspection JSUnusedGlobalSymbols,JSMethodCanBeStatic - public del(...args: any[]): number { + public del(...args: any[]): Promise { const self = RedisClientMock; let count = 0; for (let key of args) { @@ -261,7 +267,7 @@ export class RedisClientMock extends EventEmitter { } } this.cbExecute(args.pop(), count); - return count; + return new Promise(resolve => resolve(count)); } // noinspection JSUnusedGlobalSymbols @@ -303,8 +309,8 @@ export class RedisClientMock extends EventEmitter { } // noinspection JSUnusedGlobalSymbols,JSMethodCanBeStatic - public config(): boolean { - return true; + public config(): Promise { + return new Promise(resolve => resolve(true)); } private cbExecute(cb: any, ...args: any[]): void { 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 '..';