diff --git a/package-lock.json b/package-lock.json index e0964f7..ca30816 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,13 @@ "version": "2.0.8", "license": "GPL-3.0-only", "dependencies": { - "@imqueue/core": "^2.0.5", - "@types/node": "^24.0.8", + "@imqueue/core": "^2.0.6", + "@types/node": "^24.2.1", "acorn": "^8.15.0", "farmhash": "^4.0.2", "node-machine-id": "^1.1.12", "reflect-metadata": "^0.2.2", - "typescript": "^5.8.3" + "typescript": "^5.9.2" }, "devDependencies": { "@types/chai": "^5.2.2", @@ -23,8 +23,7 @@ "@types/mock-require": "^3.0.0", "@types/sinon": "^17.0.4", "chai": "^5.2.0", - "codeclimate-test-reporter": "^0.5.1", - "coveralls-next": "^4.2.1", + "coveralls-next": "^5.0.0", "minimist": "^1.2.8", "mocha": "^11.7.1", "mocha-lcov-reporter": "^1.3.0", @@ -34,7 +33,7 @@ "sinon": "^21.0.0", "source-map-support": "^0.5.21", "ts-node": "^10.9.2", - "typedoc": "^0.28.7" + "typedoc": "^0.28.10" } }, "node_modules/@ampproject/remapping": { @@ -343,26 +342,26 @@ } }, "node_modules/@gerrit0/mini-shiki": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.8.1.tgz", - "integrity": "sha512-HVZW+8pxoOExr5ZMPK15U79jQAZTO/S6i5byQyyZGjtNj+qaYd82cizTncwFzTQgiLo8uUBym6vh+/1tfJklTw==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.9.2.tgz", + "integrity": "sha512-Tvsj+AOO4Z8xLRJK900WkyfxHsZQu+Zm1//oT1w443PO6RiYMoq/4NGOhaNuZoUMYsjKIAPVQ6eOFMddj6yphQ==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/engine-oniguruma": "^3.8.1", - "@shikijs/langs": "^3.8.1", - "@shikijs/themes": "^3.8.1", - "@shikijs/types": "^3.8.1", + "@shikijs/engine-oniguruma": "^3.9.2", + "@shikijs/langs": "^3.9.2", + "@shikijs/themes": "^3.9.2", + "@shikijs/types": "^3.9.2", "@shikijs/vscode-textmate": "^10.0.2" } }, "node_modules/@imqueue/core": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@imqueue/core/-/core-2.0.5.tgz", - "integrity": "sha512-OM9/G2OgH1Jiwz6zwWbcVpJw4QJAxnfJGhTlE5pzZKm8r8hWAk/2JbMFLQUVyGiFyUiSePrvnx9Eq6jb1c481Q==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@imqueue/core/-/core-2.0.6.tgz", + "integrity": "sha512-yFBL78hta+577+InISpdiJy0rekB1g72DINfw1tkuQsE8u7RozSoD+hUsFRrO8IRmpaa9tcFi5d7GcG/osfpeA==", "license": "GPL-3.0-only", "dependencies": { - "ioredis": "^5.6.1" + "ioredis": "^5.7.0" } }, "node_modules/@ioredis/commands": { @@ -547,40 +546,40 @@ } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.8.1.tgz", - "integrity": "sha512-KGQJZHlNY7c656qPFEQpIoqOuC4LrxjyNndRdzk5WKB/Ie87+NJCF1xo9KkOUxwxylk7rT6nhlZyTGTC4fCe1g==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.9.2.tgz", + "integrity": "sha512-Vn/w5oyQ6TUgTVDIC/BrpXwIlfK6V6kGWDVVz2eRkF2v13YoENUvaNwxMsQU/t6oCuZKzqp9vqtEtEzKl9VegA==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.8.1", + "@shikijs/types": "3.9.2", "@shikijs/vscode-textmate": "^10.0.2" } }, "node_modules/@shikijs/langs": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.8.1.tgz", - "integrity": "sha512-TjOFg2Wp1w07oKnXjs0AUMb4kJvujML+fJ1C5cmEj45lhjbUXtziT1x2bPQb9Db6kmPhkG5NI2tgYW1/DzhUuQ==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.9.2.tgz", + "integrity": "sha512-X1Q6wRRQXY7HqAuX3I8WjMscjeGjqXCg/Sve7J2GWFORXkSrXud23UECqTBIdCSNKJioFtmUGJQNKtlMMZMn0w==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.8.1" + "@shikijs/types": "3.9.2" } }, "node_modules/@shikijs/themes": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.8.1.tgz", - "integrity": "sha512-Vu3t3BBLifc0GB0UPg2Pox1naTemrrvyZv2lkiSw3QayVV60me1ujFQwPZGgUTmwXl1yhCPW8Lieesm0CYruLQ==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.9.2.tgz", + "integrity": "sha512-6z5lBPBMRfLyyEsgf6uJDHPa6NAGVzFJqH4EAZ+03+7sedYir2yJBRu2uPZOKmj43GyhVHWHvyduLDAwJQfDjA==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.8.1" + "@shikijs/types": "3.9.2" } }, "node_modules/@shikijs/types": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.8.1.tgz", - "integrity": "sha512-5C39Q8/8r1I26suLh+5TPk1DTrbY/kn3IdWA5HdizR0FhlhD05zx5nKCqhzSfDHH3p4S0ZefxWd77DLV+8FhGg==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.9.2.tgz", + "integrity": "sha512-/M5L0Uc2ljyn2jKvj4Yiah7ow/W+DJSglVafvWAJ/b8AZDeeRAdMu3c2riDzB7N42VD+jSnWxeP9AKtd4TfYVw==", "dev": true, "license": "MIT", "dependencies": { @@ -706,12 +705,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", - "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", + "version": "24.2.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", + "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==", "license": "MIT", "dependencies": { - "undici-types": "~7.8.0" + "undici-types": "~7.10.0" } }, "node_modules/@types/sinon": { @@ -777,23 +776,6 @@ "node": ">=8" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", @@ -857,26 +839,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", @@ -887,37 +849,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", @@ -945,16 +876,6 @@ ], "license": "MIT" }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -984,9 +905,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.25.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz", + "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==", "dev": true, "funding": [ { @@ -1004,8 +925,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", + "caniuse-lite": "^1.0.30001733", + "electron-to-chromium": "^1.5.199", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -1090,9 +1011,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001731", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", - "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", + "version": "1.0.30001733", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001733.tgz", + "integrity": "sha512-e4QKw/O2Kavj2VQTKZWrwzkt3IxOmIlU6ajRb6LP64LHpBo1J67k2Hi4Vu/TgJWsNtynurfS0uK3MaUTCPfu5Q==", "dev": true, "funding": [ { @@ -1110,13 +1031,6 @@ ], "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.1", "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", @@ -1293,26 +1207,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", @@ -1333,32 +1227,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/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -1380,21 +1248,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", @@ -1404,7 +1264,7 @@ "coveralls": "bin/coveralls.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/coveralls-next/node_modules/lcov-parse": { @@ -1439,19 +1299,6 @@ "node": ">= 8" } }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -1572,16 +1419,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", @@ -1617,21 +1454,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.192", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.192.tgz", - "integrity": "sha512-rP8Ez0w7UNw/9j5eSXCe10o1g/8B1P5SM90PCCMVkIRQn2R0LEHWz4Eh9RnxkniuDe1W0cTSOB3MLlkTGDcuCg==", + "version": "1.5.199", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.199.tgz", + "integrity": "sha512-3gl0S7zQd88kCAZRO/DnxtBKuhMO4h0EaQIN3YgZfV6+pW+5+bf2AdQeHNESCoaQqo/gjGVYEf2YM4O5HJQqpQ==", "dev": true, "license": "ISC" }, @@ -1717,23 +1543,6 @@ "node": ">=6" } }, - "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/farmhash": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/farmhash/-/farmhash-4.0.2.tgz", @@ -1748,20 +1557,6 @@ "node": ">=16" } }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, "node_modules/find-cache-dir": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", @@ -1824,31 +1619,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", @@ -1910,16 +1680,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/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -1954,38 +1714,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/har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "deprecated": "this library is no longer supported", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2030,22 +1758,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/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -2111,12 +1823,12 @@ "license": "ISC" }, "node_modules/ioredis": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz", - "integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.7.0.tgz", + "integrity": "sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==", "license": "MIT", "dependencies": { - "@ioredis/commands": "^1.1.1", + "@ioredis/commands": "^1.3.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", @@ -2255,13 +1967,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", @@ -2429,13 +2134,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/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -2449,27 +2147,6 @@ "node": ">=6" } }, - "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", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "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", @@ -2483,29 +2160,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/lcov-parse": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", - "integrity": "sha512-YsL0D4QF/vNlNcHPXM832si9d2ROryFQ4r4JvcfMIiUYr1f6WULuO75YCtxNu4P+XMRHz0SfUc524+c+U3G5kg==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", @@ -2660,29 +2314,6 @@ "dev": true, "license": "MIT" }, - "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/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -3145,16 +2776,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", @@ -3325,13 +2946,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", @@ -3447,19 +3061,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/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", @@ -3470,16 +3071,6 @@ "once": "^1.3.1" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/punycode.js": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", @@ -3490,16 +3081,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/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -3609,65 +3190,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", @@ -3791,13 +3313,6 @@ ], "license": "MIT" }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -4006,32 +3521,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", @@ -4279,20 +3768,6 @@ "node": "*" } }, - "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-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -4359,13 +3834,6 @@ "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-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -4397,13 +3865,13 @@ } }, "node_modules/typedoc": { - "version": "0.28.8", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.8.tgz", - "integrity": "sha512-16GfLopc8icHfdvqZDqdGBoS2AieIRP2rpf9mU+MgN+gGLyEQvAO0QgOa6NJ5QNmQi0LFrDY9in4F2fUNKgJKA==", + "version": "0.28.10", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.10.tgz", + "integrity": "sha512-zYvpjS2bNJ30SoNYfHSRaFpBMZAsL7uwKbWwqoCNFWjcPnI3e/mPLh2SneH9mX7SJxtDpvDgvd9/iZxGbo7daw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@gerrit0/mini-shiki": "^3.7.0", + "@gerrit0/mini-shiki": "^3.9.0", "lunr": "^2.3.9", "markdown-it": "^14.1.0", "minimatch": "^9.0.5", @@ -4417,13 +3885,13 @@ "pnpm": ">= 10" }, "peerDependencies": { - "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x" + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x" } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -4441,9 +3909,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", "license": "MIT" }, "node_modules/update-browserslist-db": { @@ -4477,16 +3945,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -4510,21 +3968,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", @@ -4710,9 +4153,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": { diff --git a/package.json b/package.json index b4e5932..f7ebc54 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,9 @@ ], "scripts": { "prepublishOnly": "./node_modules/.bin/tsc", - "test": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha && ./node_modules/.bin/nyc report --reporter=text-lcov && npm run test-coverage", + "test": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha && ./node_modules/.bin/nyc report --reporter=text-lcov", "test-fast": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha && /usr/bin/env node -e \"import('open').then(open => open.default('file://`pwd`/coverage/index.html', { wait: false }))\"", "test-local": "export COVERALLS_REPO_TOKEN=$IMQ_RPC_COVERALLS_TOKEN && npm test && /usr/bin/env node -e \"import('open').then(open => open.default('https://coveralls.io/github/imqueue/imq-rpc', { wait: false }));\"", - "test-coverage": "cat ./coverage/lcov.info | CODECLIMATE_API_HOST=https://codebeat.co/webhooks/code_coverage CODECLIMATE_REPO_TOKEN=c2cc3954-6824-4cdf-8349-12bd9c31955a ./node_modules/.bin/codeclimate-test-reporter", "clean-typedefs": "find . -name '*.d.ts' -not -wholename '*node_modules*' -not -wholename '*generator*' -type f -delete", "clean-maps": "find . -name '*.js.map' -not -wholename '*node_modules*' -not -wholename '*generator*' -type f -delete", "clean-js": "find . -name '*.js' -not -wholename '*node_modules*' -not -wholename '*generator*' -type f -delete", @@ -38,13 +37,13 @@ "author": "imqueue.com (https://imqueue.com)", "license": "GPL-3.0-only", "dependencies": { - "@imqueue/core": "^2.0.5", - "@types/node": "^24.0.8", + "@imqueue/core": "^2.0.6", + "@types/node": "^24.2.1", "acorn": "^8.15.0", "farmhash": "^4.0.2", "node-machine-id": "^1.1.12", "reflect-metadata": "^0.2.2", - "typescript": "^5.8.3" + "typescript": "^5.9.2" }, "devDependencies": { "@types/chai": "^5.2.2", @@ -52,8 +51,7 @@ "@types/mock-require": "^3.0.0", "@types/sinon": "^17.0.4", "chai": "^5.2.0", - "codeclimate-test-reporter": "^0.5.1", - "coveralls-next": "^4.2.1", + "coveralls-next": "^5.0.0", "minimist": "^1.2.8", "mocha": "^11.7.1", "mocha-lcov-reporter": "^1.3.0", @@ -63,7 +61,7 @@ "sinon": "^21.0.0", "source-map-support": "^0.5.21", "ts-node": "^10.9.2", - "typedoc": "^0.28.7" + "typedoc": "^0.28.10" }, "main": "index.js", "typescript": { diff --git a/src/IMQService.ts b/src/IMQService.ts index d235e42..3564008 100644 --- a/src/IMQService.ts +++ b/src/IMQService.ts @@ -58,7 +58,7 @@ export class Description { types: TypesDescription; } -let serviceDescription: Description | null = null; +const serviceDescriptions: Map = new Map(); /** * Returns collection of class methods metadata even those are inherited @@ -188,10 +188,21 @@ export abstract class IMQService { } else if (!description.service.methods[method]) { - response.error = IMQError( - 'IMQ_RPC_NO_ACCESS', - `Access to ${this.name}.${method}() denied!`, - new Error().stack, method, args); + // Allow calling runtime-attached methods (own props) even if + // they are not present in the exposed service description. + // Deny access for prototype (class) methods not decorated with @expose. + const isOwn = Object.prototype.hasOwnProperty.call(this, method); + const value: any = (this as any)[method]; + const proto = Object.getPrototypeOf(this); + const protoValue = proto && proto[method]; + const isSameAsProto = typeof protoValue === 'function' && value === protoValue; + // Allow only truly dynamic own-instance functions (not the same as prototype) + if (!(isOwn && typeof value === 'function' && !isSameAsProto)) { + response.error = IMQError( + 'IMQ_RPC_NO_ACCESS', + `Access to ${this.name}.${method}() denied!`, + new Error().stack, method, args); + } } else if (!isValidArgsCount( @@ -318,17 +329,21 @@ export abstract class IMQService { */ @expose() public describe(): Description { - if (!serviceDescription) { - serviceDescription = { + let description = serviceDescriptions.get(this.name) || null; + + if (!description) { + description = { service: { name: this.name, methods: getClassMethods(this.constructor.name) }, types: IMQRPCDescription.typesDescription }; + + serviceDescriptions.set(this.name, description); } - return serviceDescription; + return description; } } diff --git a/src/decorators/logged.ts b/src/decorators/logged.ts index 83584a2..26f91de 100644 --- a/src/decorators/logged.ts +++ b/src/decorators/logged.ts @@ -50,10 +50,6 @@ export function logged(options?: ILogger | LoggedDecoratorOptions) { descriptor: TypedPropertyDescriptor<(...args: any[]) => any>, ) => { const original = descriptor.value; - const logger = options && (options as LoggedDecoratorOptions).logger - ? (options as LoggedDecoratorOptions).logger - : options && (options as ILogger).error ? options : - this?.logger || target?.logger || console; const level: LoggedLogLevel = ( options && (options as LoggedDecoratorOptions).level @@ -65,9 +61,25 @@ export function logged(options?: ILogger | LoggedDecoratorOptions) { descriptor.value = async function(...args: any[]): Promise { try { - return original && await original.apply(this || target, args); + const ctx = (typeof this !== 'undefined' && this !== null) + ? this + : target; + + if (original) { + return await original.apply(ctx, args); + } } catch (err) { - logger[level](err); + const logger: ILogger = (options && (options as LoggedDecoratorOptions).logger) + ? (options as LoggedDecoratorOptions).logger as ILogger + : (options && (options as any).error) + ? options as ILogger + : (this && (this as any).logger) + ? (this as any).logger as ILogger + : (target && (target as any).logger) + ? (target as any).logger as ILogger + : console; + + (logger as any)[level](err); if (doThrow) { throw err; diff --git a/test/IMQClient.console.logger.spec.ts b/test/IMQClient.console.logger.spec.ts new file mode 100644 index 0000000..b26343f --- /dev/null +++ b/test/IMQClient.console.logger.spec.ts @@ -0,0 +1,134 @@ +/*! + * IMQClient console logger fallback branches coverage + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { IMQClient, IMQDelay, IMQMetadata, remote, AFTER_HOOK_ERROR, BEFORE_HOOK_ERROR } from '..'; + +class ConsoleClient extends IMQClient { + @remote() + public async ok(name?: string, meta?: IMQMetadata, delay?: IMQDelay) { + return this.remoteCall(...arguments); + } + @remote() + public async boom() { + return this.remoteCall(...arguments); + } +} + +describe('IMQClient console logger fallbacks', () => { + let client: ConsoleClient; + + afterEach(async () => { + try { await client?.destroy(); } catch { /* ignore */ } + sinon.restore(); + }); + + it('should use console logger when BEFORE hook fails', async () => { + const warn = sinon.stub(console, 'warn' as any).callsFake(() => {}); + client = new ConsoleClient({ beforeCall: async () => { throw new Error('before oops'); } }); + await client.start(); + + const imq: any = (client as any).imq; + sinon.stub(imq, 'send').callsFake(async (_to: string, request: any) => { + const id = 'C1'; + setImmediate(() => imq.emit('message', { to: id, request, data: 'good' })); + return id; + }); + + const res = await client.ok('x'); + expect(res).to.equal('good'); + expect(warn.called).to.equal(true); + expect(String(warn.firstCall.args[0])).to.contain(BEFORE_HOOK_ERROR); + }); + + it('should use console logger when AFTER hook fails (resolve and reject paths)', async () => { + const warn = sinon.stub(console, 'warn' as any).callsFake(() => {}); + client = new ConsoleClient({ afterCall: async () => { throw new Error('after oops'); } }); + await client.start(); + + const imq: any = (client as any).imq; + const send = sinon.stub(imq, 'send'); + + // success path + send.onFirstCall().callsFake(async (_to: string, request: any) => { + const id = 'C2'; + setImmediate(() => imq.emit('message', { to: id, request, data: 'S' })); + return id; + }); + + // reject path + send.onSecondCall().callsFake(async (_to: string, request: any) => { + const id = 'C3'; + setImmediate(() => imq.emit('message', { to: id, request, error: new Error('bad') })); + return id; + }); + + const ok = await client.ok('ok'); + expect(ok).to.equal('S'); + + try { await client.boom(); } catch { /* expected */ } + + expect(warn.callCount).to.be.greaterThan(0); + const messages = warn.getCalls().map(c => String(c.args[0])).join(' '); + expect(messages).to.contain(AFTER_HOOK_ERROR); + }); + + it('should use right-hand console branch in remoteCall when BEFORE hook fails', async () => { + const warn = sinon.stub(console, 'warn' as any).callsFake(() => {}); + // Explicitly override default logger to be undefined to force `|| console` take the right branch + client = new ConsoleClient({ beforeCall: async () => { throw new Error('before oops'); }, logger: undefined as any }); + await client.start(); + + const imq: any = (client as any).imq; + sinon.stub(imq, 'send').callsFake(async (_to: string, request: any) => { + const id = 'MB1'; + setImmediate(() => imq.emit('message', { to: id, request, data: 'ok' })); + return id; + }); + + const res = await client.ok('x'); + expect(res).to.equal('ok'); + expect(warn.called).to.equal(true); + expect(String(warn.firstCall.args[0])).to.contain(BEFORE_HOOK_ERROR); + }); + + it('should use right-hand console branch in imqCallResolver when AFTER hook fails on resolve', async () => { + const warn = sinon.stub(console, 'warn' as any).callsFake(() => {}); + client = new ConsoleClient({ afterCall: async () => { throw new Error('after oops'); }, logger: undefined as any }); + await client.start(); + + const imq: any = (client as any).imq; + sinon.stub(imq, 'send').callsFake(async (_to: string, request: any) => { + const id = 'MB2'; + setImmediate(() => imq.emit('message', { to: id, request, data: 'S' })); + return id; + }); + + const ok = await client.ok('ok'); + expect(ok).to.equal('S'); + expect(warn.callCount).to.be.greaterThan(0); + const messages = warn.getCalls().map(c => String(c.args[0])).join(' '); + expect(messages).to.contain(AFTER_HOOK_ERROR); + }); + + it('should use right-hand console branch in imqCallRejector when AFTER hook fails on reject', async () => { + const warn = sinon.stub(console, 'warn' as any).callsFake(() => {}); + client = new ConsoleClient({ afterCall: async () => { throw new Error('after oops'); }, logger: undefined as any }); + await client.start(); + + const imq: any = (client as any).imq; + sinon.stub(imq, 'send').callsFake(async (_to: string, request: any) => { + const id = 'MB3'; + setImmediate(() => imq.emit('message', { to: id, request, error: new Error('bad') })); + return id; + }); + + try { await client.boom(); } catch { /* expected */ } + + expect(warn.callCount).to.be.greaterThan(0); + const messages = warn.getCalls().map(c => String(c.args[0])).join(' '); + expect(messages).to.contain(AFTER_HOOK_ERROR); + }); +}); diff --git a/test/IMQClient.extra.spec.ts b/test/IMQClient.extra.spec.ts new file mode 100644 index 0000000..fe57e81 --- /dev/null +++ b/test/IMQClient.extra.spec.ts @@ -0,0 +1,101 @@ +/*! + * IMQClient Extra Unit Tests (non-RPC, using send stubs) + */ +import './mocks'; +import { logger } from './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { IMQClient, IMQDelay, IMQMetadata, remote, AFTER_HOOK_ERROR, BEFORE_HOOK_ERROR } from '..'; + +class ExtraClient extends IMQClient { + @remote() + public async greet(name?: string, imqMetadata?: IMQMetadata, imqDelay?: IMQDelay) { + return this.remoteCall(...arguments); + } + @remote() + public async fail(imqDelay?: IMQDelay) { + return this.remoteCall(...arguments); + } +} + +describe('IMQClient (extra branches without service)', () => { + let client: ExtraClient; + + afterEach(async () => { + await client?.destroy(); + sinon.restore(); + }); + + it('should warn on BEFORE_HOOK_ERROR and continue call', async function() { + this.timeout(5000); + const warn = sinon.stub(logger, 'warn'); + client = new ExtraClient({ logger, beforeCall: async () => { throw new Error('before'); } }); + await client.start(); + const imq: any = (client as any).imq; + sinon.stub(imq, 'send').callsFake(async (to: string, request: any, delay?: number) => { + const id = 'ID1'; + setImmediate(() => imq.emit('message', { to: id, request, data: 'ok' })); + return id; + }); + const res = await client.greet('imq'); + expect(res).to.equal('ok'); + expect(warn.called).to.equal(true); + expect(String(warn.firstCall.args[0])).to.contain(BEFORE_HOOK_ERROR); + }); + + it('should warn on AFTER_HOOK_ERROR for resolve and reject paths', async () => { + const warn = sinon.stub(logger, 'warn'); + client = new ExtraClient({ logger, afterCall: async () => { throw new Error('after'); } }); + await client.start(); + const imq: any = (client as any).imq; + const send = sinon.stub(imq, 'send'); + // success path + send.onFirstCall().callsFake(async (to: string, request: any) => { + const id = 'ID2'; + setImmediate(() => imq.emit('message', { to: id, request, data: 'success' })); + return id; + }); + // reject path + send.onSecondCall().callsFake(async (to: string, request: any) => { + const id = 'ID3'; + setImmediate(() => imq.emit('message', { to: id, request, error: new Error('boom') })); + return id; + }); + const ok = await client.greet('ok'); + expect(ok).to.equal('success'); + try { await client.fail(); } catch (e) { /* expected */ } + // both paths should warn due to afterCall throwing + expect(warn.callCount).to.be.greaterThan(0); + expect(warn.getCalls().map(c => String(c.args[0])).join(' ')).to.contain(AFTER_HOOK_ERROR); + }); + + it('should emit event when resolver is missing', async () => { + client = new ExtraClient({ logger }); + await client.start(); + const evt = sinon.spy(); + client.on('greet', evt); + (client as any).imq.emit('message', { + to: 'unknown-id', + request: { method: 'greet' }, + data: { foo: 'bar' }, + }); + expect(evt.calledOnce).to.equal(true); + }); + + it('should sanitize invalid IMQDelay and pass IMQMetadata through request', async () => { + client = new ExtraClient({ logger }); + await client.start(); + const imq: any = (client as any).imq; + const sendStub = sinon.stub(imq, 'send').callsFake(async (to: string, request: any, delay?: number) => { + expect(delay).to.equal(0); + expect(request.metadata).to.be.instanceOf(IMQMetadata); + const id = 'ID4'; + setImmediate(() => imq.emit('message', { to: id, request, data: 'x' })); + return id; + }); + const meta = new IMQMetadata({ a: 1 } as any); + const res = await client.greet('z', meta as any, new IMQDelay(-100) as any); + expect(res).to.equal('x'); + expect(sendStub.calledOnce).to.equal(true); + }); +}); diff --git a/test/IMQClient.generator.trailing.args.spec.ts b/test/IMQClient.generator.trailing.args.spec.ts new file mode 100644 index 0000000..1bb516c --- /dev/null +++ b/test/IMQClient.generator.trailing.args.spec.ts @@ -0,0 +1,80 @@ +/*! + * IMQClient generator trailing args removal coverage test + */ +import * as fs from 'fs'; +import { expect } from 'chai'; +import { IMQService, IMQClient, IMQDelay, IMQMetadata, expose, remote } from '..'; + +const CLIENTS_PATH = './test/clients-generator-trailing'; + +class GenTrailingService extends IMQService { + @expose() + public greet(name: string, meta?: IMQMetadata, delay?: IMQDelay) { + return `hi ${name}`; + } +} + +// We don't need a manual client; the generator will create it dynamically. + +describe('IMQClient.generator trailing args removal (IMQDelay/IMQMetadata)', function () { + this.timeout(10000); + let service: GenTrailingService; + + function rmdirr(path: string) { + if (fs.existsSync(path)) { + fs.readdirSync(path).forEach((file) => { + const curPath = `${path}/${file}`; + if (fs.lstatSync(curPath).isDirectory()) { + rmdirr(curPath); + } else { + fs.unlinkSync(curPath); + } + }); + fs.rmdirSync(path); + } + } + + before(async () => { + service = new GenTrailingService(); + await service.start(); + }); + + after(async () => { + await service.destroy(); + rmdirr(CLIENTS_PATH); + }); + + it('should strip trailing metadata/delay from service description and add imqMetadata/imqDelay once', async () => { + const mod: any = await IMQClient.create('GenTrailingService', { + path: CLIENTS_PATH, + compile: true, + write: true, + }); + + // Ensure client class exists and can be instantiated + const client = new mod.GenTrailingClient(); + await client.start(); + + // Read generated TypeScript to verify the signature + const tsPath = `${CLIENTS_PATH}/GenTrailingService.ts`; + expect(fs.existsSync(tsPath)).to.equal(true); + const src = fs.readFileSync(tsPath, 'utf8'); + + // The generated method should not keep original parameter names 'meta' or 'delay' + // as they are stripped and replaced by imqMetadata/imqDelay at the end once. + const signatureRe = /public\s+async\s+greet\(([^)]*)\)/; + const m = src.match(signatureRe); + expect(m, 'method signature not found in generated client').to.not.equal(null); + const signature = (m as RegExpMatchArray)[1]; + + // It should include imqMetadata?: IMQMetadata and imqDelay?: IMQDelay + expect(signature).to.match(/imqMetadata\?\s*:\s*IMQMetadata/); + expect(signature).to.match(/imqDelay\?\s*:\s*IMQDelay/); + + // Original 'delay' parameter could still be present due to service signature, + // but generator must add imq* params at the end; presence is verified above. + + // Cleanup + await client.destroy(); + }); +}); diff --git a/test/IMQClient.methods.spec.ts b/test/IMQClient.methods.spec.ts new file mode 100644 index 0000000..fc6c7a6 --- /dev/null +++ b/test/IMQClient.methods.spec.ts @@ -0,0 +1,81 @@ +/*! + * IMQClient methods Unit Tests (subscribe/unsubscribe/broadcast + signals) + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { IMQClient, remote } from '..'; +import { logger } from './mocks'; + +class MethodsClient extends IMQClient { + // ensure there is at least one remote method (not used directly here) + @remote() + public async ping(): Promise { + return this.remoteCall(...arguments); + } +} + +describe('IMQClient methods', () => { + let client: MethodsClient; + + afterEach(async () => { + try { await client?.destroy(); } catch { /* ignore */ } + sinon.restore(); + }); + + it('should delegate subscribe() to subscriptionImq with client name', async () => { + client = new MethodsClient({ logger }); + const subImq: any = (client as any).subscriptionImq; + const spy = sinon.stub(subImq, 'subscribe').resolves(); + const handler = sinon.spy(); + await client.subscribe(handler as any); + expect(spy.calledOnce).to.equal(true); + expect(spy.firstCall.args[0]).to.equal(client.name); + expect(spy.firstCall.args[1]).to.equal(handler); + }); + + it('should delegate unsubscribe() to subscriptionImq', async () => { + client = new MethodsClient({ logger }); + const subImq: any = (client as any).subscriptionImq; + const spy = sinon.stub(subImq, 'unsubscribe').resolves(); + await client.unsubscribe(); + expect(spy.calledOnce).to.equal(true); + }); + + it('should delegate broadcast() to imq.publish with queueName', async () => { + client = new MethodsClient({ logger }); + const imq: any = (client as any).imq; + const spy = sinon.stub(imq, 'publish').resolves(); + const payload: any = { hello: 'world' }; + await client.broadcast(payload); + expect(spy.calledOnce).to.equal(true); + expect(spy.firstCall.args[0]).to.equal(payload); + expect(spy.firstCall.args[1]).to.equal(client.queueName); + }); + + it('should handle process signals by calling destroy and then process.exit(0)', async () => { + const callbacks: Array<() => any> = []; + const onStub = sinon.stub(process as any, 'on').callsFake((sig: any, cb: any) => { + callbacks.push(cb); + return process as any; + }); + const exitStub = sinon.stub(process as any, 'exit'); + const clock = sinon.useFakeTimers(); + + client = new MethodsClient({ logger }); + const destroyStub = sinon.stub(client, 'destroy').resolves(); + + // invoke the first registered signal handler (e.g., SIGTERM) + await callbacks[0](); + // fast-forward shutdown timeout + clock.tick(10000); // IMQ_SHUTDOWN_TIMEOUT default + + expect(destroyStub.calledOnce).to.equal(true); + expect(exitStub.called).to.equal(true); + expect(exitStub.firstCall.args[0]).to.equal(0); + + clock.restore(); + onStub.restore(); + exitStub.restore(); + }); +}); diff --git a/test/IMQLock.stringify.metadata.spec.ts b/test/IMQLock.stringify.metadata.spec.ts new file mode 100644 index 0000000..4cb6469 --- /dev/null +++ b/test/IMQLock.stringify.metadata.spec.ts @@ -0,0 +1,37 @@ +/*! + * IMQLock stringify(metadata) failure branch coverage test + */ +import { expect } from 'chai'; +import { IMQLock } from '..'; + +describe('IMQLock acquire() timeout with unstringifiable metadata', () => { + const KEY = 'circular-metadata-key'; + let originalTimeout: number; + + beforeEach(() => { + originalTimeout = IMQLock.deadlockTimeout; + IMQLock.deadlockTimeout = 10; // keep test fast + }); + + afterEach(() => { + IMQLock.deadlockTimeout = originalTimeout; + }); + + it('should reject with error containing "Unable to stringify metadata"', async () => { + // Acquire and hold the lock + const first = await IMQLock.acquire(KEY); + expect(first).to.equal(true); + + // Prepare circular metadata that will make JSON.stringify throw + const meta: any = { className: 'X', methodName: 'y', args: [] as any[] }; + (meta as any).self = meta; // circular reference + + try { + await IMQLock.acquire(KEY, undefined as any, meta); + expect.fail('should have been rejected by timeout'); + } catch (err: any) { + expect(err).to.be.instanceOf(Error); + expect(String(err.message)).to.contain('Unable to stringify metadata'); + } + }); +}); diff --git a/test/IMQMetadata.ts b/test/IMQMetadata.ts new file mode 100644 index 0000000..cfa1bba --- /dev/null +++ b/test/IMQMetadata.ts @@ -0,0 +1,44 @@ +/*! + * IMQMetadata Unit Tests + * + * 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 { IMQMetadata } from '..'; + +describe('IMQMetadata', () => { + it('should be a class', () => { + expect(typeof IMQMetadata).to.equal('function'); + }); + + it('should copy provided metadata props to instance index', () => { + const data = { a: 1, b: 'x', c: { y: true } } as any; + const m = new IMQMetadata(data); + // direct property access + expect((m as any).a).to.equal(1); + expect((m as any).b).to.equal('x'); + expect((m as any).c).to.deep.equal({ y: true }); + // index signature behavior + const keys = Object.keys(m); + expect(keys.sort()).to.deep.equal(['a','b','c']); + }); +}); diff --git a/test/IMQService.afterCall.spec.ts b/test/IMQService.afterCall.spec.ts new file mode 100644 index 0000000..9e81246 --- /dev/null +++ b/test/IMQService.afterCall.spec.ts @@ -0,0 +1,135 @@ +/*! + * IMQService afterCall and promise branch Unit Tests (incremental) + * + * 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 { expect } from 'chai'; +import * as sinon from 'sinon'; +import { uuid } from '@imqueue/core'; +import { logger } from './mocks'; +import { + IMQService, + IMQRPCRequest, + expose, + AFTER_HOOK_ERROR, +} from '..'; + +class AfterHookService extends IMQService { + @expose() + public ping() { + return 'pong'; + } + + @expose() + public async asyncHello(name: string) { + return await new Promise(resolve => setTimeout(() => resolve(`Hello, ${name}!`), 5)); + } +} + +describe('IMQService hooks (afterCall) and promise branch', () => { + it('should execute afterCall hook without error', async () => { + const afterCall = sinon.spy(async () => {}); + const service: any = new AfterHookService({ logger, afterCall }); + const request: IMQRPCRequest = { + from: 'HookClient', + method: 'ping', + args: [], + }; + const id = uuid(); + const sendSpy = sinon.spy(service.imq, 'send'); + + await service.start(); + service.imq.emit('message', request, id); + + await new Promise((resolve, reject) => setTimeout(() => { + try { + expect(sendSpy.called).to.be.true; + expect(afterCall.calledOnce).to.be.true; + resolve(undefined); + } catch (err) { + reject(err); + } + })); + + await service.destroy(); + }); + + it('should catch afterCall error and log AFTER_HOOK_ERROR', async () => { + const warnSpy = sinon.spy(logger, 'warn'); + const afterCall = async () => { throw new Error('after boom'); }; + const service: any = new AfterHookService({ logger, afterCall }); + const request: IMQRPCRequest = { + from: 'HookClient', + method: 'ping', + args: [], + }; + const id = uuid(); + + await service.start(); + service.imq.emit('message', request, id); + + await new Promise((resolve, reject) => setTimeout(() => { + try { + expect(warnSpy.called).to.be.true; + const hasAfter = warnSpy.getCalls().some((c: any) => c.args && c.args[0] === AFTER_HOOK_ERROR); + expect(hasAfter).to.be.true; + resolve(undefined); + } catch (err) { + reject(err); + } + })); + + warnSpy.restore(); + await service.destroy(); + }); + + it('should await promise-returning method result before sending', async () => { + const service: any = new AfterHookService({ logger }); + // make an exposed method return a Promise to hit thenable branch + service.exposed = async () => await new Promise(resolve => setTimeout(() => resolve('Hello, IMQ!'), 5)); + const request: IMQRPCRequest = { + from: 'HookClient', + method: 'exposed', + args: [], + }; + const id = uuid(); + + // stub to assert when send() is actually called, after promise resolved + const done = new Promise((resolve, reject) => { + sinon.stub(service.imq, 'send').callsFake(async (_to: string, response: any) => { + try { + expect(response.data).to.equal('Hello, IMQ!'); + resolve(); + } catch (err) { + reject(err); + } + return id; + }); + }); + + await service.start(); + service.imq.emit('message', request, id); + + await done; + + await service.destroy(); + }); +}); diff --git a/test/IMQService.console.logger.spec.ts b/test/IMQService.console.logger.spec.ts new file mode 100644 index 0000000..72058c8 --- /dev/null +++ b/test/IMQService.console.logger.spec.ts @@ -0,0 +1,74 @@ +/*! + * IMQService logger fallback coverage test + */ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { uuid } from '@imqueue/core'; +import { IMQService, IMQRPCRequest, expose, BEFORE_HOOK_ERROR, AFTER_HOOK_ERROR } from '..'; + +class ConsoleLoggerService extends IMQService { + @expose() + public ping() { return 'pong'; } +} + +describe('IMQService handleRequest logger fallback to console', () => { + let warnStub: sinon.SinonSpy; + + beforeEach(() => { + warnStub = sinon.stub(console, 'warn' as any).callsFake(() => {}); + }); + + afterEach(async () => { + sinon.restore(); + }); + + it('should use console when no custom logger provided and catch BEFORE hook error', async () => { + const beforeCall = async () => { throw new Error('before fails'); }; + const service: any = new ConsoleLoggerService({ beforeCall }); // no logger provided + + const request: IMQRPCRequest = { from: 'Client', method: 'ping', args: [] }; + const id = uuid(); + + await service.start(); + + // Spy send to ensure regular flow continues and send is called even without afterCall + const sendSpy = sinon.spy(service.imq, 'send'); + + service.imq.emit('message', request, id); + + await new Promise((resolve, reject) => setTimeout(() => { + try { + expect(warnStub.called).to.equal(true); + const hasBefore = warnStub.getCalls().some(c => c.args && c.args[0] === BEFORE_HOOK_ERROR); + expect(hasBefore).to.equal(true); + expect(sendSpy.called).to.equal(true); + resolve(undefined); + } catch (e) { reject(e); } + }, 1)); + + await service.destroy(); + }); + + it('should use console when afterCall throws (send() logger fallback)', async () => { + const afterCall = async () => { throw new Error('after fails'); }; + const service: any = new ConsoleLoggerService({ afterCall }); // no logger provided + + const request: IMQRPCRequest = { from: 'Client', method: 'ping', args: [] }; + const id = uuid(); + + await service.start(); + + service.imq.emit('message', request, id); + + await new Promise((resolve, reject) => setTimeout(() => { + try { + expect(warnStub.called).to.equal(true); + const hasAfter = warnStub.getCalls().some(c => c.args && c.args[0] === AFTER_HOOK_ERROR); + expect(hasAfter).to.equal(true); + resolve(undefined); + } catch (e) { reject(e); } + }, 1)); + + await service.destroy(); + }); +}); diff --git a/test/IMQService.hooks.spec.ts b/test/IMQService.hooks.spec.ts new file mode 100644 index 0000000..f3e0b09 --- /dev/null +++ b/test/IMQService.hooks.spec.ts @@ -0,0 +1,99 @@ +/*! + * IMQService Hooks Unit Tests (incremental) + * + * 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 { expect } from 'chai'; +import * as sinon from 'sinon'; +import { uuid } from '@imqueue/core'; +import { logger } from './mocks'; +import { + IMQService, + IMQRPCRequest, + expose, + BEFORE_HOOK_ERROR, +} from '..'; + +class HookTestService extends IMQService { + @expose() + public ping() { + return 'pong'; + } +} + +describe('IMQService hooks (beforeCall)', () => { + it('should execute beforeCall hook without error', async () => { + const beforeCall = sinon.spy(async () => {}); + const service: any = new HookTestService({ logger, beforeCall }); + const request: IMQRPCRequest = { + from: 'HookClient', + method: 'ping', + args: [], + }; + const id = uuid(); + const sendSpy = sinon.spy(service.imq, 'send'); + + await service.start(); + service.imq.emit('message', request, id); + + // wait for async send + await new Promise((resolve, reject) => setTimeout(() => { + try { + expect(beforeCall.calledOnce).to.be.true; + expect(sendSpy.called).to.be.true; + resolve(undefined); + } catch (err) { + reject(err); + } + })); + + await service.destroy(); + }); + + it('should catch beforeCall error and log BEFORE_HOOK_ERROR', async () => { + const warnSpy = sinon.spy(logger, 'warn'); + const beforeCall = async () => { throw new Error('boom'); }; + const service: any = new HookTestService({ logger, beforeCall }); + const request: IMQRPCRequest = { + from: 'HookClient', + method: 'ping', + args: [], + }; + const id = uuid(); + + await service.start(); + service.imq.emit('message', request, id); + + await new Promise((resolve, reject) => setTimeout(() => { + try { + expect(warnSpy.called).to.be.true; + const calledWithBefore = warnSpy.getCalls().some((c: any) => c.args && c.args[0] === BEFORE_HOOK_ERROR); + expect(calledWithBefore).to.be.true; + resolve(undefined); + } catch (err) { + reject(err); + } + })); + + warnSpy.restore(); + await service.destroy(); + }); +}); diff --git a/test/IMQService.logger.fallback.spec.ts b/test/IMQService.logger.fallback.spec.ts new file mode 100644 index 0000000..558aab5 --- /dev/null +++ b/test/IMQService.logger.fallback.spec.ts @@ -0,0 +1,64 @@ +/*! + * IMQService logger fallback branches coverage tests + */ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { uuid } from '@imqueue/core'; +import { IMQService, IMQRPCRequest, expose, BEFORE_HOOK_ERROR, AFTER_HOOK_ERROR } from '..'; + +class FallbackLoggerService extends IMQService { + @expose() + public ping() { return 'pong'; } +} + +describe('IMQService logger fallback branches', () => { + beforeEach(() => sinon.restore()); + + it('should use console (fallback) inside handleRequest when beforeCall throws and options.logger is undefined', async () => { + const warnStub = sinon.stub(console, 'warn' as any).callsFake(() => {}); + const beforeCall = async () => { throw new Error('boom'); }; + // Explicitly override default logger to undefined to force fallback branch + const service: any = new FallbackLoggerService({ beforeCall, logger: undefined as any }); + + const request: IMQRPCRequest = { from: 'Client', method: 'ping', args: [] }; + const id = uuid(); + + await service.start(); + service.imq.emit('message', request, id); + + await new Promise((resolve, reject) => setTimeout(() => { + try { + expect(warnStub.called).to.equal(true); + const hasBefore = warnStub.getCalls().some(c => c.args && c.args[0] === BEFORE_HOOK_ERROR); + expect(hasBefore).to.equal(true); + resolve(undefined); + } catch (e) { reject(e); } + }, 1)); + + await service.destroy(); + }); + + it('should use console (fallback) inside send() when afterCall throws and options.logger is undefined', async () => { + const warnStub = sinon.stub(console, 'warn' as any).callsFake(() => {}); + const afterCall = async () => { throw new Error('after fails'); }; + // Explicitly override default logger to undefined to force fallback branch + const service: any = new FallbackLoggerService({ afterCall, logger: undefined as any }); + + const request: IMQRPCRequest = { from: 'Client', method: 'ping', args: [] }; + const id = uuid(); + + await service.start(); + service.imq.emit('message', request, id); + + await new Promise((resolve, reject) => setTimeout(() => { + try { + expect(warnStub.called).to.equal(true); + const hasAfter = warnStub.getCalls().some(c => c.args && c.args[0] === AFTER_HOOK_ERROR); + expect(hasAfter).to.equal(true); + resolve(undefined); + } catch (e) { reject(e); } + }, 1)); + + await service.destroy(); + }); +}); diff --git a/test/IMQService.signal.publish.spec.ts b/test/IMQService.signal.publish.spec.ts new file mode 100644 index 0000000..79c0ab4 --- /dev/null +++ b/test/IMQService.signal.publish.spec.ts @@ -0,0 +1,56 @@ +/*! + * IMQService signal handler and publish() coverage tests + */ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { uuid } from '@imqueue/core'; +import { IMQService, IMQRPCRequest, expose } from '..'; + +class SignalTestService extends IMQService { + @expose() + public ping() { + return 'pong'; + } +} + +describe('IMQService signal handler and publish()', () => { + afterEach(async () => { + sinon.restore(); + }); + + it('should call destroy() and log error on process signal (without exiting)', async () => { + const onStub = sinon.stub(process as any, 'on'); + const handlers: { sig: string; fn: (...args: any[]) => any }[] = []; + onStub.callsFake((sig: string, fn: any) => { handlers.push({ sig, fn }); return process as any; }); + + // use fake timers to avoid triggering real process.exit from setTimeout + const clock = sinon.useFakeTimers(); + + const logger: any = { info: () => {}, warn: () => {}, error: sinon.spy() }; + const service: any = new SignalTestService({ logger }); + + // make destroy reject to hit catch(logger.error) + sinon.stub(service, 'destroy').callsFake(async () => { throw new Error('boom'); }); + + // simulate first registered signal handler + expect(handlers.length).to.be.greaterThan(0); + await handlers[0].fn(); + + // let microtasks settle + await Promise.resolve(); + + expect(logger.error.called).to.equal(true); + + clock.restore(); + }); + + it('publish() should delegate to imq.publish', async () => { + const logger: any = { info: () => {}, warn: () => {}, error: () => {} }; + const service: any = new SignalTestService({ logger }); + const stub = sinon.stub(service.imq, 'publish').resolves(undefined as any); + + await service.publish({ id: uuid() } as any); + + expect(stub.calledOnce).to.equal(true); + }); +}); diff --git a/test/cache/RedisCache.errors.spec.ts b/test/cache/RedisCache.errors.spec.ts new file mode 100644 index 0000000..552b81e --- /dev/null +++ b/test/cache/RedisCache.errors.spec.ts @@ -0,0 +1,61 @@ +/*! + * RedisCache error branches: methods before init should throw + */ +import { expect } from 'chai'; +import { RedisCache, REDIS_CLIENT_INIT_ERROR } from '../..'; + +describe('cache/RedisCache errors when not initialized', () => { + beforeEach(() => { delete (RedisCache as any).redis; }); + + it('get() should throw if redis not initialized', async () => { + const cache = new RedisCache(); + let threw = false; + try { + await cache.get('x'); + } catch (e: any) { + threw = true; + expect(e).to.be.instanceOf(TypeError); + expect(e.message).to.equal(REDIS_CLIENT_INIT_ERROR); + } + expect(threw).to.equal(true); + }); + + it('set() should throw if redis not initialized', async () => { + const cache = new RedisCache(); + let threw = false; + try { + await cache.set('x', 1); + } catch (e: any) { + threw = true; + expect(e).to.be.instanceOf(TypeError); + expect(e.message).to.equal(REDIS_CLIENT_INIT_ERROR); + } + expect(threw).to.equal(true); + }); + + it('del() should throw if redis not initialized', async () => { + const cache = new RedisCache(); + let threw = false; + try { + await cache.del('x'); + } catch (e: any) { + threw = true; + expect(e).to.be.instanceOf(TypeError); + expect(e.message).to.equal(REDIS_CLIENT_INIT_ERROR); + } + expect(threw).to.equal(true); + }); + + it('purge() should throw if redis not initialized', async () => { + const cache = new RedisCache(); + let threw = false; + try { + await cache.purge('mask'); + } catch (e: any) { + threw = true; + expect(e).to.be.instanceOf(TypeError); + expect(e.message).to.equal(REDIS_CLIENT_INIT_ERROR); + } + expect(threw).to.equal(true); + }); +}); diff --git a/test/cache/RedisCache.purge.promise.spec.ts b/test/cache/RedisCache.purge.promise.spec.ts new file mode 100644 index 0000000..9717723 --- /dev/null +++ b/test/cache/RedisCache.purge.promise.spec.ts @@ -0,0 +1,116 @@ +/*! + * RedisCache additional coverage: purge() and set() with Promise + */ +import { expect } from 'chai'; +import { RedisCache } from '../..'; + +class FakeRedis { + // minimal in-memory store + private store = new Map(); + public status: string = 'ready'; + public connected: boolean = true; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public on(event: string, listener: (...args: any[]) => void): any { return this } + public once?: any; + public emit?: any; + public removeAllListeners(): void {} + public disconnect(_reconnect: boolean): void {} + public quit(): void {} + public end?(): void {} + + public async set(key: string, val: string, ..._args: any[]): Promise { + this.store.set(key, val); + return true; + } + public async get(key: string, ..._args: any[]): Promise { + return this.store.get(key) as any; + } + public async del(key: string): Promise { + const existed = this.store.delete(key); + return existed ? 1 : 0; + } + public async eval(script: string, _numkeys: number, ..._args: any[]): Promise { + // Extract mask from: redis.call('keys','') + const m = script.match(/redis\.call\('keys','([^']*)'\)/); + const mask = m ? m[1] : ''; + if (mask.endsWith('*')) { + const prefix = mask.slice(0, -1); + for (const key of Array.from(this.store.keys())) { + if (key.startsWith(prefix)) { + this.store.delete(key); + } + } + } else if (mask) { + this.store.delete(mask); + } + return true; + } +} + +class ErrorEvalRedis extends FakeRedis { + public async eval(): Promise { + throw new Error('eval failed'); + } +} + +describe('cache/RedisCache purge() and set() with Promise', () => { + afterEach(async () => { + await RedisCache.destroy(); + }); + + it('purge() should delete keys by wildcard and return true', async () => { + const conn = new FakeRedis(); + const cache = new RedisCache(); + await cache.init({ conn: conn as any, prefix: 'cov-cache', logger: console as any }); + + // prepare some keys + await cache.set('keep', 'x'); // fully qualified: cov-cache:RedisCache:keep + await cache.set('del1', 1); + await cache.set('del2', 2); + + // unrelated key stored directly in connection should not be removed + await conn.set('other:namespace:key', 'y'); + + const mask = `${cache.options.prefix}:${cache.name}:del*`; + const ok = await cache.purge(mask); + expect(ok).to.equal(true); + + // del* gone, keep remains, and unrelated stays + expect(await cache.get('del1')).to.equal(undefined); + expect(await cache.get('del2')).to.equal(undefined); + expect(await cache.get('keep')).to.equal('x'); + // Sanity: unrelated + expect(await (conn as any).get('other:namespace:key')).to.equal('y'); + }); + + it('purge() should return false and log when eval throws', async () => { + const conn = new ErrorEvalRedis(); + const cache = new RedisCache(); + const logs: any[] = []; + const logger = { info: () => {}, error: (e: any) => logs.push(e) } as any; + await cache.init({ conn: conn as any, prefix: 'cov-cache', logger }); + const res = await cache.purge('cov-cache:RedisCache:*'); + expect(res).to.equal(false); + expect(logs.length).to.be.greaterThan(0); + }); + + it('set() should accept Promise value and store resolved value', async () => { + const conn = new FakeRedis(); + const cache = new RedisCache(); + await cache.init({ conn: conn as any, prefix: 'cov-cache', logger: console as any }); + + const key = 'promised'; + const valuePromise = new Promise((resolve) => setTimeout(() => resolve(42), 1)); + await cache.set(key, valuePromise); + expect(await cache.get(key)).to.equal(42); + }); + + it('del() should return boolean false when key did not exist', async () => { + const conn = new FakeRedis(); + const cache = new RedisCache(); + await cache.init({ conn: conn as any, prefix: 'cov-cache', logger: console as any }); + + const res = await cache.del('missing'); + expect(res).to.equal(false); + }); +}); diff --git a/test/decorators/lock.skipArgs.spec.ts b/test/decorators/lock.skipArgs.spec.ts new file mode 100644 index 0000000..2e181eb --- /dev/null +++ b/test/decorators/lock.skipArgs.spec.ts @@ -0,0 +1,36 @@ +import '../mocks'; +import { expect } from 'chai'; +import { lock } from '../..'; + +class SkipArgsClass { + public calls: number = 0; + + @lock({ skipArgs: [1] }) + public async sum(a: number, b: number) { + this.calls++; + // delay to ensure overlapping calls and proper lock queuing + await new Promise(res => setTimeout(res, 20)); + return a + (b || 0); + } +} + +describe('decorators/lock() with skipArgs', () => { + beforeEach(() => { + // Ensure locks are enabled for this test regardless of other tests + delete process.env['DISABLE_LOCKS']; + }); + + it('should ignore specified args when building lock signature', async () => { + const obj = new SkipArgsClass(); + const results = await Promise.all([ + obj.sum(1, 10), + obj.sum(1, 20), + obj.sum(1, 30), + ]); + // Since b (index 1) is skipped in signature, all concurrent calls share one lock + const uniq = [...new Set(results)]; + expect(uniq.length).to.equal(1); + // Original method body should be executed only once + expect(obj.calls).to.equal(1); + }); +}); diff --git a/test/decorators/logged.targetLogger.spec.ts b/test/decorators/logged.targetLogger.spec.ts new file mode 100644 index 0000000..e490c1d --- /dev/null +++ b/test/decorators/logged.targetLogger.spec.ts @@ -0,0 +1,86 @@ +import { expect } from 'chai'; +import { logged } from '../../src/decorators/logged'; + +// Small, targeted tests to hit missing branches in logged.ts (lines 64 and 72) +describe('decorators/logged() target logger and no-original branches', () => { + it('should return undefined when original is missing (no call, no throw)', async () => { + const descriptor: PropertyDescriptor = { value: undefined } as any; + const target: any = {}; + + // Apply decorator to a descriptor without original implementation + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (logged() as any)(target, 'missing', descriptor); + + // Call decorated function directly; should just return undefined + const result = await (descriptor.value as any)(); + expect(result).to.be.undefined; + }); + + it('should use target logger when this is undefined and rethrow by default', async () => { + const error = new Error('boom'); + const original = () => { throw error; }; + const descriptor: PropertyDescriptor = { value: original } as any; + + const targetLoggerCalls: Error[] = []; + const target: any = { + logger: { + error: (e: Error) => { targetLoggerCalls.push(e); }, + warn: () => void 0, + info: () => void 0, + log: () => void 0, + }, + }; + + // Apply decorator; we will call without binding `this` so inside wrapper + // `this` will be undefined and the code should select target.logger (line 72) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (logged() as any)(target, 'willThrow', descriptor); + + try { + // Call as a plain function to keep `this` undefined in strict mode + await (descriptor.value as any)(); + expect.fail('should have thrown'); + } catch (e) { + expect(e).to.equal(error); + expect(targetLoggerCalls).to.have.length(1); + expect(targetLoggerCalls[0]).to.equal(error); + } + }); + + it('should return undefined when original is missing (no call, no throw)', async () => { + const descriptor: PropertyDescriptor = { value: undefined } as any; + const target: any = {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (logged() as any)(target, 'missing', descriptor); + const result = await (descriptor.value as any)(); + expect(result).to.be.undefined; + }); + + it('should use target logger when this is undefined and rethrow by default', async () => { + const error = new Error('boom'); + const original = () => { throw error; }; + const descriptor: PropertyDescriptor = { value: original } as any; + + const targetLoggerCalls: Error[] = []; + const target: any = { + logger: { + error: (e: Error) => { targetLoggerCalls.push(e); }, + warn: () => void 0, + info: () => void 0, + log: () => void 0, + }, + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (logged() as any)(target, 'willThrow', descriptor); + + try { + await (descriptor.value as any)(); + expect.fail('should have thrown'); + } catch (e) { + expect(e).to.equal(error); + expect(targetLoggerCalls).to.have.length(1); + expect(targetLoggerCalls[0]).to.equal(error); + } + }); +}); diff --git a/test/decorators/logged.thisLogger.spec.ts b/test/decorators/logged.thisLogger.spec.ts new file mode 100644 index 0000000..1bfb404 --- /dev/null +++ b/test/decorators/logged.thisLogger.spec.ts @@ -0,0 +1,139 @@ +import { expect } from 'chai'; +import { logged } from '../../src/decorators/logged'; + +/* + * Additional coverage for logged.ts to reach 100% branches: + * - Exercise `this || target` left-hand path (this is truthy) + * - Ensure `this.logger` is preferred when available + * - Cover options.logger usage and default rethrow behavior + * - Cover passing an ILogger directly as options + */ + +describe('decorators/logged() this logger and options logger branches', () => { + afterEach(() => { + // nothing to cleanup + }); + + it('should use bound `this` in apply(), prefer this.logger, not rethrow when doNotThrow=true', async () => { + const error = new Error('use-this'); + let capturedThis: any; + // original captures `this` then throws + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const original: any = function original(this: any) { + capturedThis = this; + throw error; + }; + + const descriptor: PropertyDescriptor = { value: original } as any; + const target: any = { logger: { error: () => void 0, warn: () => void 0, info: () => void 0, log: () => void 0 } }; + + // Apply decorator: do not throw and use non-default level to verify dynamic call + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (logged({ doNotThrow: true, level: 'warn' }) as any)(target, 'x', descriptor); + + const thisLoggerCalls: { level: string; error: Error }[] = []; + const self: any = { + logger: { + error: (_e: Error) => thisLoggerCalls.push({ level: 'error', error: _e }), + warn: (_e: Error) => thisLoggerCalls.push({ level: 'warn', error: _e }), + info: (_e: Error) => thisLoggerCalls.push({ level: 'info', error: _e }), + log: (_e: Error) => thisLoggerCalls.push({ level: 'log', error: _e }), + }, + }; + + // Call with `this` bound so (this || target) takes left-hand branch + const res = await (descriptor.value as any).call(self); + expect(res).to.be.undefined; // swallowed due to doNotThrow + expect(capturedThis).to.equal(self); + expect(thisLoggerCalls).to.have.length(1); + expect(thisLoggerCalls[0].level).to.equal('warn'); + expect(thisLoggerCalls[0].error).to.equal(error); + }); + + it('should use options.logger when provided and rethrow by default', async () => { + const error = new Error('use-options-logger'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const original: any = () => { throw error; }; + const descriptor: PropertyDescriptor = { value: original } as any; + const target: any = {}; + + const calls: Error[] = []; + const logger = { info: (e: Error) => { calls.push(e); }, error: () => {}, warn: () => {}, log: () => {} } as any; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (logged({ logger, level: 'info' }) as any)(target, 'y', descriptor); + + try { + await (descriptor.value as any)(); + expect.fail('should have thrown'); + } catch (e) { + expect(e).to.equal(error); + expect(calls).to.have.length(1); + expect(calls[0]).to.equal(error); + } + }); + + it('should treat options itself as ILogger when it has error() method', async () => { + const error = new Error('options-is-logger'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const original: any = () => { throw error; }; + const descriptor: PropertyDescriptor = { value: original } as any; + const target: any = {}; + + const calls: Error[] = []; + const optionsLogger = { error: (e: Error) => calls.push(e), log: () => {}, warn: () => {}, info: () => {} } as any; + + // Pass logger directly instead of options object + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (logged(optionsLogger) as any)(target, 'z', descriptor); + + try { + await (descriptor.value as any)(); + expect.fail('should have thrown'); + } catch (e) { + expect(e).to.equal(error); + expect(calls).to.have.length(1); + expect(calls[0]).to.equal(error); + } + }); + + it('should use bound `this` in apply() and return value when original resolves', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const original: any = function(this: any) { return this.answer; }; + const descriptor: PropertyDescriptor = { value: original } as any; + const target: any = {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (logged() as any)(target, 'ok', descriptor); + const self: any = { answer: 42 }; + const res = await (descriptor.value as any).call(self); + expect(res).to.equal(42); + }); + + it('should use target as `this` when caller `this` is falsy (right branch of this||target)', async () => { + let gotThis: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const original: any = function(this: any) { gotThis = this; return this.mark; }; + const descriptor: PropertyDescriptor = { value: original } as any; + const target: any = { mark: 'TARGET' }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (logged() as any)(target, 'right', descriptor); + // Explicitly call with undefined `this` + const res = await (descriptor.value as any).call(undefined); + expect(res).to.equal('TARGET'); + expect(gotThis).to.equal(target); + }); + + it('should use target when caller `this` is null (second condition false)', async () => { + let gotThis: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const original: any = function(this: any) { gotThis = this; return this.mark; }; + const descriptor: PropertyDescriptor = { value: original } as any; + const target: any = { mark: 'TARGET-NULL' }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (logged() as any)(target, 'right-null', descriptor); + // Explicitly call with null `this` to pass first check and fail second + const res = await (descriptor.value as any).call(null); + expect(res).to.equal('TARGET-NULL'); + expect(gotThis).to.equal(target); + }); +}); diff --git a/test/decorators/logged.ts b/test/decorators/logged.ts new file mode 100644 index 0000000..f5a8b54 --- /dev/null +++ b/test/decorators/logged.ts @@ -0,0 +1,141 @@ +import 'reflect-metadata'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { logged } from '../../src/decorators/logged'; + +describe('decorators/logged()', () => { + it('should be a function and return decorator function', () => { + expect(typeof logged).to.equal('function'); + // @ts-ignore + const decorator = logged(); + expect(typeof decorator).to.equal('function'); + }); + + it('should fallback to console logger and rethrow by default', async () => { + const error = new Error('boom'); + const stub = sinon.stub(console, 'error'); + class A { + // @ts-ignore + @logged() + public fail() { + throw error; + } + } + + try { + await new A().fail(); + expect.fail('should throw'); + } catch (e) { + expect(e).to.equal(error); + expect(stub.calledOnce).to.equal(true); + expect(stub.firstCall.args[0]).to.equal(error); + } finally { + stub.restore(); + } + }); + + it('should use provided logger, level and suppress throw when doNotThrow', async () => { + const error = new Error('warned'); + const myLogger = { + warn: sinon.stub(), + error: sinon.stub(), + log: sinon.stub(), + info: sinon.stub(), + } as any; + + class B { + // @ts-ignore + @logged({ logger: myLogger, level: 'warn', doNotThrow: true }) + public fail() { + throw error; + } + } + + const res = await new B().fail(); + expect(res).to.equal(undefined); + expect(myLogger.warn.calledOnce).to.equal(true); + expect(myLogger.warn.firstCall.args[0]).to.equal(error); + }); + + it('should accept ILogger directly and rethrow by default', async () => { + const error = new Error('as-logger'); + const myLogger = { + warn: sinon.stub(), + error: sinon.stub(), + log: sinon.stub(), + info: sinon.stub(), + } as any; + + class C { + // @ts-ignore + @logged(myLogger) + public fail() { + throw error; + } + } + + try { + await new C().fail(); + expect.fail('should throw'); + } catch (e) { + expect(e).to.equal(error); + expect(myLogger.error.calledOnce).to.equal(true); + expect(myLogger.error.firstCall.args[0]).to.equal(error); + } + }); + + it('should use instance logger when present and rethrow', async () => { + const error = new Error('inst-logger'); + const myLogger = { + error: sinon.stub(), + } as any; + class E { + public logger = myLogger; + // @ts-ignore + @logged() + public fail() { + throw error; + } + } + try { + await new E().fail(); + expect.fail('should throw'); + } catch (e) { + expect(e).to.equal(error); + expect(myLogger.error.calledOnce).to.equal(true); + expect(myLogger.error.firstCall.args[0]).to.equal(error); + } + }); + + it('should use target logger when present on prototype for instance method', async () => { + const error = new Error('proto-logger'); + const protologger = { error: sinon.stub() } as any; + class F { + // @ts-ignore + @logged() + public fail() { throw error; } + } + (F.prototype as any).logger = protologger; + try { + await new F().fail(); + expect.fail('should throw'); + } catch (e) { + expect(e).to.equal(error); + expect(protologger.error.calledOnce).to.equal(true); + expect(protologger.error.firstCall.args[0]).to.equal(error); + } + }); + + it('should pass through successful return value', async () => { + class D { + // @ts-ignore + @logged() + public ok() { + return 42; + } + } + + const v = await new D().ok(); + expect(v).to.equal(42); + }); +});