diff --git a/package-lock.json b/package-lock.json index 469dc98f..f28ff55c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,10 +21,7 @@ "chalk": "^4.1.2", "ci-info": "^3.3.0", "content-type": "^1.0.4", - "find-up": "^5.0.0", "form-data": "^4.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.4", "reflect-metadata": "^0.1.13", "semver": "^7.5.2", "socket.io-client": "^4.7.5", @@ -72,6 +69,7 @@ "nx": "14.5.6", "prettier": "2.7.1", "semantic-release": "~19.0.3", + "socket.io": "^4.7.5", "ts-jest": "27.1.4", "ts-mockito": "^2.6.1", "typescript": "4.7.4" @@ -2481,6 +2479,21 @@ "integrity": "sha512-dgMN+syt1xb7Hk8LU6AODOfPlvz5z1CbXpPuJE5ZrX9STfBOIXF09pEB8N7a97WT9dbngt3ksDCm6GW6yMrxfQ==", "dev": true }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "8.4.5", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.5.tgz", @@ -3030,6 +3043,19 @@ "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", "dev": true }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.8.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", @@ -3527,6 +3553,15 @@ } ] }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/before-after-hook": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz", @@ -4233,6 +4268,15 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/copy-webpack-plugin": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-10.2.4.tgz", @@ -4318,6 +4362,19 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cosmiconfig": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", @@ -4805,6 +4862,27 @@ "once": "^1.4.0" } }, + "node_modules/engine.io": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", + "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", + "dev": true, + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, "node_modules/engine.io-client": { "version": "6.5.3", "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", @@ -5776,6 +5854,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -6429,52 +6508,6 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/http-proxy-agent/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -8420,6 +8453,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -8890,6 +8924,15 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -11672,6 +11715,15 @@ "node": ">=12" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", @@ -11831,6 +11883,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -11845,6 +11898,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -13338,6 +13392,34 @@ "node": ">=6" } }, + "node_modules/socket.io": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz", + "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==", + "dev": true, + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.11.0" + } + }, "node_modules/socket.io-client": { "version": "4.7.5", "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", @@ -14605,6 +14687,15 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -15050,6 +15141,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, "engines": { "node": ">=10" }, @@ -17059,6 +17151,21 @@ "integrity": "sha512-dgMN+syt1xb7Hk8LU6AODOfPlvz5z1CbXpPuJE5ZrX9STfBOIXF09pEB8N7a97WT9dbngt3ksDCm6GW6yMrxfQ==", "dev": true }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true + }, + "@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/eslint": { "version": "8.4.5", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.5.tgz", @@ -17518,6 +17625,16 @@ "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", "dev": true }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, "acorn": { "version": "8.8.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", @@ -17873,6 +17990,12 @@ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true + }, "before-after-hook": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz", @@ -18405,6 +18528,12 @@ } } }, + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "dev": true + }, "copy-webpack-plugin": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-10.2.4.tgz", @@ -18461,6 +18590,16 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "cosmiconfig": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", @@ -18846,6 +18985,24 @@ "once": "^1.4.0" } }, + "engine.io": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", + "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", + "dev": true, + "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0" + } + }, "engine.io-client": { "version": "6.5.3", "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", @@ -19586,6 +19743,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, "requires": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -20070,44 +20228,6 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, - "http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "requires": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "dependencies": { - "agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "requires": { - "debug": "^4.3.4" - } - } - } - }, - "https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", - "requires": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "dependencies": { - "agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "requires": { - "debug": "^4.3.4" - } - } - } - }, "human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -21516,6 +21636,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, "requires": { "p-locate": "^5.0.0" } @@ -21865,6 +21986,12 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true + }, "neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -23837,6 +23964,12 @@ } } }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true + }, "object-inspect": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", @@ -23950,6 +24083,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "requires": { "yocto-queue": "^0.1.0" } @@ -23958,6 +24092,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, "requires": { "p-limit": "^3.0.2" } @@ -25061,6 +25196,31 @@ } } }, + "socket.io": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + } + }, + "socket.io-adapter": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz", + "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==", + "dev": true, + "requires": { + "debug": "~4.3.4", + "ws": "~8.11.0" + } + }, "socket.io-client": { "version": "4.7.5", "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", @@ -26045,6 +26205,12 @@ "spdx-expression-parse": "^3.0.0" } }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true + }, "w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -26384,7 +26550,8 @@ "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true } } } diff --git a/package.json b/package.json index 4f16f37e..63e5ca5d 100644 --- a/package.json +++ b/package.json @@ -84,10 +84,7 @@ "chalk": "^4.1.2", "ci-info": "^3.3.0", "content-type": "^1.0.4", - "find-up": "^5.0.0", "form-data": "^4.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.4", "reflect-metadata": "^0.1.13", "semver": "^7.5.2", "socket.io-client": "^4.7.5", @@ -135,6 +132,7 @@ "nx": "14.5.6", "prettier": "2.7.1", "semantic-release": "~19.0.3", + "socket.io": "^4.7.5", "ts-jest": "27.1.4", "ts-mockito": "^2.6.1", "typescript": "4.7.4" diff --git a/packages/repeater/src/bus/DefaultRepeaterBus.spec.ts b/packages/repeater/src/bus/DefaultRepeaterBus.spec.ts new file mode 100644 index 00000000..203dc2f4 --- /dev/null +++ b/packages/repeater/src/bus/DefaultRepeaterBus.spec.ts @@ -0,0 +1,229 @@ +import { DefaultRepeaterBus } from './DefaultRepeaterBus'; +import { Protocol } from '../models/Protocol'; +import { Request, Response } from '../request-runner'; +import { RepeaterApplicationEvents } from './RepeaterApplicationEvents'; +import { + RepeaterErrorCodes, + RepeaterServer, + RepeaterServerEvents, + RepeaterServerRequestEvent +} from './RepeaterServer'; +import { RepeaterCommandHub } from './RepeaterCommandHub'; +import { delay, Logger } from '@sectester/core'; +import { + anything, + instance, + mock, + objectContaining, + reset, + verify, + when +} from 'ts-mockito'; + +describe('DefaultRepeaterBus', () => { + const RepeaterId = 'fooId'; + + let events!: RepeaterApplicationEvents; + let sut!: DefaultRepeaterBus; + + const mockedRepeaterServer = mock(); + const mockedRepeaterCommandHub = mock(); + const mockedLogger = mock(); + + beforeEach(() => { + events = new RepeaterApplicationEvents(); + + when(mockedRepeaterServer.on(anything(), anything())).thenCall( + (event, handler) => events.on(event, handler) + ); + when(mockedRepeaterServer.off(anything(), anything())).thenCall( + (event, handler) => events.off(event, handler) + ); + + sut = new DefaultRepeaterBus( + RepeaterId, + instance(mockedLogger), + instance(mockedRepeaterServer), + instance(mockedRepeaterCommandHub) + ); + }); + + afterEach(() => + reset( + mockedRepeaterServer, + mockedRepeaterCommandHub, + mockedLogger + ) + ); + + describe('connect', () => { + it('should connect', async () => { + // act + await sut.connect(); + + // assert + verify(mockedRepeaterServer.connect(RepeaterId)).once(); + verify( + mockedRepeaterServer.deploy( + objectContaining({ repeaterId: RepeaterId }) + ) + ).once(); + }); + + it('should throw when underlying connect throws', async () => { + // arrange + when(mockedRepeaterServer.connect(RepeaterId)).thenReject( + new Error('foo') + ); + + // act + const act = () => sut.connect(); + + // assert + await expect(act).rejects.toThrowError('foo'); + }); + + it('should throw when underlying deploy throws', async () => { + // arrange + when(mockedRepeaterServer.deploy(anything())).thenReject( + new Error('foo') + ); + + // act + const act = () => sut.connect(); + + // assert + await expect(act).rejects.toThrowError('foo'); + }); + }); + + describe('close', () => { + it('should close', async () => { + // act + await sut.close(); + + // assert + verify(mockedRepeaterServer.disconnect()).once(); + }); + }); + + describe('events', () => { + it(`should subscribe to ${RepeaterServerEvents.UPDATE_AVAILABLE}`, async () => { + // arrange + await sut.connect(); + + // act + events.emit(RepeaterServerEvents.UPDATE_AVAILABLE, { version: '1.0.0' }); + + // assert + verify( + mockedLogger.warn( + '%s: A new Repeater version (%s) is available, for update instruction visit https://docs.brightsec.com/docs/installation-options', + anything(), + '1.0.0' + ) + ).once(); + }); + + it(`should subscribe to ${RepeaterServerEvents.REQUEST}`, async () => { + // arrange + const requestEvent: RepeaterServerRequestEvent = { + protocol: Protocol.HTTP, + url: 'http://foo.bar', + method: 'GET' + }; + + const request = new Request(requestEvent); + + when(mockedRepeaterCommandHub.sendRequest(anything())).thenResolve( + new Response({ + protocol: Protocol.HTTP, + statusCode: 200 + }) + ); + + await sut.connect(); + + // act + events.emit(RepeaterServerEvents.REQUEST, requestEvent); + + // assert + await delay(200); + verify( + mockedRepeaterCommandHub.sendRequest(objectContaining(request)) + ).once(); + }); + + it(`should subscribe to ${RepeaterServerEvents.RECONNECT_ATTEMPT}`, async () => { + // arrange + await sut.connect(); + + // act + events.emit(RepeaterServerEvents.RECONNECT_ATTEMPT, { + attempt: 1, + maxAttempts: 3 + }); + + // assert + verify( + mockedLogger.warn( + 'Failed to connect to Bright cloud (attempt %d/%d)', + anything(), + anything() + ) + ).once(); + }); + + it(`should subscribe to ${RepeaterServerEvents.ERROR} and proceed on error`, async () => { + // arrange + await sut.connect(); + + // act + events.emit(RepeaterServerEvents.ERROR, { + code: RepeaterErrorCodes.UNKNOWN_ERROR, + message: 'error' + }); + + // assert + verify(mockedLogger.error('error')).once(); + }); + + it(`should subscribe to ${RepeaterServerEvents.ERROR} and proceed on critical error`, async () => { + // arrange + await sut.connect(); + + // act + events.emit(RepeaterServerEvents.ERROR, { + code: RepeaterErrorCodes.UNEXPECTED_ERROR, + message: 'unexpected error', + remediation: 'remediation' + }); + + // assert + verify( + mockedLogger.error( + '%s: %s. %s', + anything(), + 'unexpected error', + 'remediation' + ) + ).once(); + verify(mockedRepeaterServer.disconnect()).once(); + }); + + it(`should subscribe to ${RepeaterServerEvents.RECONNECTION_FAILED}`, async () => { + // arrange + const error = new Error('test error'); + await sut.connect(); + + // act + events.emit(RepeaterServerEvents.RECONNECTION_FAILED, { + error + }); + + // assert + verify(mockedLogger.error(error)).once(); + verify(mockedRepeaterServer.disconnect()).once(); + }); + }); +}); diff --git a/packages/repeater/src/bus/DefaultRepeaterBus.ts b/packages/repeater/src/bus/DefaultRepeaterBus.ts index f17b1209..a3bf6318 100644 --- a/packages/repeater/src/bus/DefaultRepeaterBus.ts +++ b/packages/repeater/src/bus/DefaultRepeaterBus.ts @@ -45,13 +45,22 @@ export class DefaultRepeaterBus implements RepeaterBus { this.logger.log('Deploying the Repeater (%s)...', this.repeaterId); - await this.repeaterServer.deploy({ - repeaterId: this.repeaterId - }); + await this.deploy(); this.logger.log('The Repeater (%s) started', this.repeaterId); } + private async deploy() { + await this.deployRepeater(); + this.repeaterServer.on(RepeaterServerEvents.CONNECTED, this.deployRepeater); + } + + private deployRepeater = async () => { + await this.repeaterServer.deploy({ + repeaterId: this.repeaterId + }); + }; + private subscribeToEvents() { this.repeaterServer.on(RepeaterServerEvents.ERROR, this.handleError); this.repeaterServer.on( @@ -118,7 +127,6 @@ export class DefaultRepeaterBus implements RepeaterBus { remediation ); this.close().catch(this.logger.error); - process.exitCode = 1; } private reconnectionFailed = ({ @@ -126,7 +134,6 @@ export class DefaultRepeaterBus implements RepeaterBus { }: RepeaterServerReconnectionFailedEvent) => { this.logger.error(error); this.close().catch(this.logger.error); - process.exitCode = 1; }; private requestReceived = async (event: RepeaterServerRequestEvent) => { diff --git a/packages/repeater/src/bus/DefaultRepeaterBusFactory.spec.ts b/packages/repeater/src/bus/DefaultRepeaterBusFactory.spec.ts new file mode 100644 index 00000000..b7bce6aa --- /dev/null +++ b/packages/repeater/src/bus/DefaultRepeaterBusFactory.spec.ts @@ -0,0 +1,47 @@ +import { DefaultRepeaterBusFactory } from './DefaultRepeaterBusFactory'; +import { RepeaterServer } from './RepeaterServer'; +import { RepeaterCommandHub } from './RepeaterCommandHub'; +import { DefaultRepeaterBus } from './DefaultRepeaterBus'; +import { Configuration, Logger } from '@sectester/core'; +import { instance, mock, reset } from 'ts-mockito'; + +describe('DefaultRepeaterBusFactory', () => { + const repeaterId = 'fooId'; + + const mockedLogger = mock(); + const mockedConfiguration = mock(); + const mockedRepeaterServer = mock(); + const mockedRepeaterCommandHub = mock(); + + const configuration = instance(mockedConfiguration); + + let sut!: DefaultRepeaterBusFactory; + + beforeEach(() => { + sut = new DefaultRepeaterBusFactory( + instance(mockedLogger), + configuration, + instance(mockedRepeaterServer), + instance(mockedRepeaterCommandHub) + ); + }); + + afterEach(() => { + reset( + mockedLogger, + mockedConfiguration, + mockedRepeaterServer, + mockedRepeaterCommandHub + ); + }); + + describe('create', () => { + it('should create', () => { + // act + const res = sut.create(repeaterId); + + // assert + expect(res).toBeInstanceOf(DefaultRepeaterBus); + }); + }); +}); diff --git a/packages/repeater/src/bus/DefaultRepeaterCommandHub.spec.ts b/packages/repeater/src/bus/DefaultRepeaterCommandHub.spec.ts new file mode 100644 index 00000000..9ddfda88 --- /dev/null +++ b/packages/repeater/src/bus/DefaultRepeaterCommandHub.spec.ts @@ -0,0 +1,62 @@ +import { DefaultRepeaterCommandHub } from './DefaultRepeaterCommandHub'; +import { Protocol } from '../models/Protocol'; +import { RequestRunner, Request, Response } from '../request-runner'; +import { instance, mock, reset, when } from 'ts-mockito'; + +describe('DefaultRepeaterCommandHub', () => { + let sut!: DefaultRepeaterCommandHub; + + const mockedRequestRunner = mock(); + + beforeEach(() => { + sut = new DefaultRepeaterCommandHub([instance(mockedRequestRunner)]); + }); + + afterEach(() => reset(mockedRequestRunner)); + + describe('sendRequest', () => { + it('should send', async () => { + // arrange + const request = new Request({ + protocol: Protocol.HTTP, + url: 'http://foo.bar', + method: 'GET' + }); + + const response = new Response({ + protocol: Protocol.HTTP, + statusCode: 200 + }); + + when(mockedRequestRunner.protocol).thenReturn(Protocol.HTTP); + when(mockedRequestRunner.run(request)).thenResolve(response); + + // act + const result = await sut.sendRequest(request); + + // assert + expect(result).toEqual(response); + }); + + it('should throw when there are no suitable protocol handler', async () => { + // arrange + const request = new Request({ + protocol: Protocol.HTTP, + url: 'http://foo.bar', + method: 'GET' + }); + + when(mockedRequestRunner.protocol).thenReturn( + 'someOtherProtocol' as Protocol + ); + + // act + const act = () => sut.sendRequest(request); + + // assert + await expect(act).rejects.toThrow( + `Unsupported protocol "${Protocol.HTTP}"` + ); + }); + }); +}); diff --git a/packages/repeater/src/bus/DefaultRepeaterCommandHub.ts b/packages/repeater/src/bus/DefaultRepeaterCommandHub.ts index 5f147207..46c7e8a4 100644 --- a/packages/repeater/src/bus/DefaultRepeaterCommandHub.ts +++ b/packages/repeater/src/bus/DefaultRepeaterCommandHub.ts @@ -9,7 +9,7 @@ export class DefaultRepeaterCommandHub implements RepeaterCommandHub { private readonly requestRunners: RequestRunner[] ) {} - public sendRequest(request: Request): Promise { + public async sendRequest(request: Request): Promise { const { protocol } = request; const requestRunner = this.requestRunners.find( diff --git a/packages/repeater/src/bus/DefaultRepeaterServer.spec.ts b/packages/repeater/src/bus/DefaultRepeaterServer.spec.ts new file mode 100644 index 00000000..6825c777 --- /dev/null +++ b/packages/repeater/src/bus/DefaultRepeaterServer.spec.ts @@ -0,0 +1,221 @@ +import { + DefaultRepeaterServer, + DefaultRepeaterServerOptions, + SocketEvents +} from './DefaultRepeaterServer'; +import { + RepeaterErrorCodes, + RepeaterServerEventHandler, + RepeaterServerEvents +} from './RepeaterServer'; +import { Protocol } from '../models/Protocol'; +import { RepeaterApplicationEvents } from './RepeaterApplicationEvents'; +import { RepeaterCommandHub } from './RepeaterCommandHub'; +import { delay, Logger } from '@sectester/core'; +import { anything, instance, mock, reset, verify, when } from 'ts-mockito'; +import { Server } from 'socket.io'; +import msgpack from 'socket.io-msgpack-parser'; +import { createServer, Server as HttpServer } from 'http'; + +class MockSocketServer { + private readonly httpServer: HttpServer; + private readonly io: Server; + + get address() { + const address = this.httpServer.address(); + if (typeof address === 'string') { + return address; + } + + return `http://localhost:${address?.port}`; + } + + constructor() { + this.httpServer = createServer(); + + this.httpServer.listen(0); + + this.io = new Server(this.httpServer, { + path: '/api/ws/v1', + parser: msgpack + }); + } + + public onConnection(callback: (socket: any) => void) { + this.io.on('connection', callback); + } + + public close() { + this.io.close(); + } + + public emit(event: string, data: any) { + this.io.sockets.emit(event, data); + } +} + +describe('DefaultRepeaterServer', () => { + const RepeaterId = 'fooId'; + + let events!: RepeaterApplicationEvents; + let sut!: DefaultRepeaterServer; + let mockServer!: MockSocketServer; + + const mockedLogger = mock(); + const mockedDefaultRepeaterServerOptions = + mock(); + + beforeEach(() => { + mockServer = new MockSocketServer(); + + events = new RepeaterApplicationEvents(); + + sut = new DefaultRepeaterServer( + instance(mockedLogger), + events, + instance(mockedDefaultRepeaterServerOptions) + ); + + const address = mockServer.address; + + when(mockedDefaultRepeaterServerOptions.uri).thenReturn(address); + when(mockedDefaultRepeaterServerOptions.token).thenReturn('token'); + when(mockedDefaultRepeaterServerOptions.connectTimeout).thenReturn(10_00); + }); + + afterEach(() => { + sut.disconnect(); + + mockServer.close(); + + reset( + mockedLogger, + mockedDefaultRepeaterServerOptions + ); + }); + + describe('connect', () => { + it('should connect', async () => { + // act + await sut.connect(RepeaterId); + + // assert + verify(mockedLogger.debug('Repeater connected to %s', anything())).once(); + }); + }); + + describe('deploy', () => { + it('should deploy', async () => { + // arrange + const event = { repeaterId: RepeaterId }; + + mockServer.onConnection(socket => { + socket.on('deploy', () => { + socket.emit('deployed', event); + }); + }); + + const handler: RepeaterServerEventHandler = jest.fn(); + + sut.on(RepeaterServerEvents.DEPLOY, handler); + + await sut.connect(RepeaterId); + + // act + await sut.deploy({ repeaterId: RepeaterId }); + + // assert + expect(handler).toHaveBeenCalledWith(event); + }); + }); + + describe('disconnect', () => { + it('should disconnect', async () => { + // arrange + const handler: RepeaterServerEventHandler = jest.fn(); + + sut.on(RepeaterServerEvents.DEPLOY, handler); + + await sut.connect(RepeaterId); + + // act + const act = () => sut.disconnect(); + + // assert + expect(act).not.toThrow(); + }); + }); + + describe('on', () => { + it.each([ + { + input: { + event: SocketEvents.UPDATE_AVAILABLE, + data: { version: '1.0.0' } + }, + expected: { + event: RepeaterServerEvents.UPDATE_AVAILABLE, + data: [{ version: '1.0.0' }] + } + }, + { + input: { + event: SocketEvents.ERROR, + data: { code: RepeaterErrorCodes.UNKNOWN_ERROR, message: 'msg' } + }, + expected: { + event: RepeaterServerEvents.ERROR, + data: [{ code: RepeaterErrorCodes.UNKNOWN_ERROR, message: 'msg' }] + } + }, + { + input: { + event: SocketEvents.REQUEST, + data: { protocol: Protocol.HTTP, url: 'https://foo.com' } + }, + expected: { + event: RepeaterServerEvents.REQUEST, + data: [{ protocol: Protocol.HTTP, url: 'https://foo.com' }, undefined] + } + } + ])( + 'should propagate $input.event data to $expected.event', + async ({ input, expected }) => { + // arrange + const handler: RepeaterServerEventHandler = jest.fn(); + + sut.on(expected.event, handler); + + await sut.connect(RepeaterId); + + // act + mockServer.emit(input.event, input.data); + + // assert + await delay(200); + expect(handler).toHaveBeenCalledWith(...expected.data); + } + ); + }); + + describe('off', () => { + it('should not invoke handler when it switched off', async () => { + // arrange + const event = { code: RepeaterErrorCodes.UNKNOWN_ERROR, message: 'msg' }; + + const handler: RepeaterServerEventHandler = jest.fn(); + + sut.on(RepeaterServerEvents.ERROR, handler); + + await sut.connect(RepeaterId); + + sut.off(RepeaterServerEvents.ERROR, handler); + + // act + mockServer.emit(SocketEvents.ERROR, event); + + // assert + expect(handler).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/repeater/src/bus/DefaultRepeaterServer.ts b/packages/repeater/src/bus/DefaultRepeaterServer.ts index a62accb6..2748312d 100644 --- a/packages/repeater/src/bus/DefaultRepeaterServer.ts +++ b/packages/repeater/src/bus/DefaultRepeaterServer.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line max-classes-per-file import { DeployCommandOptions, DeploymentRuntime, @@ -14,12 +15,16 @@ import { RepeaterServerRequestResponse, RepeaterUpgradeAvailableEvent } from './RepeaterServer'; +import { + CallbackFunction, + RepeaterApplicationEvents +} from './RepeaterApplicationEvents'; import { Logger } from '@sectester/core'; import { inject, injectable } from 'tsyringe'; import io, { Socket } from 'socket.io-client'; import parser from 'socket.io-msgpack-parser'; import { ErrorEvent } from 'ws'; -import { EventEmitter, once } from 'events'; +import { once } from 'events'; import Timer = NodeJS.Timer; export interface DefaultRepeaterServerOptions { @@ -34,10 +39,7 @@ export const DefaultRepeaterServerOptions: unique symbol = Symbol( 'DefaultRepeaterServerOptions' ); -type CallbackFunction = (arg: T) => unknown; -type HandlerFunction = (args: unknown[]) => unknown; - -const enum SocketEvents { +export const enum SocketEvents { DEPLOYED = 'deployed', DEPLOY = 'deploy', UNDEPLOY = 'undeploy', @@ -77,11 +79,7 @@ export class DefaultRepeaterServer implements RepeaterServer { private readonly MAX_RECONNECTION_ATTEMPTS = 20; private readonly MIN_RECONNECTION_DELAY = 1000; private readonly MAX_RECONNECTION_DELAY = 86_400_000; - private readonly events = new EventEmitter(); - private readonly handlerMap = new WeakMap< - RepeaterServerEventHandler, - HandlerFunction - >(); + private latestReconnectionError?: Error; private pingTimer?: Timer; private connectionTimer?: Timer; @@ -100,12 +98,29 @@ export class DefaultRepeaterServer implements RepeaterServer { constructor( private readonly logger: Logger, + private readonly applicationEvents: RepeaterApplicationEvents, @inject(DefaultRepeaterServerOptions) private readonly options: DefaultRepeaterServerOptions - ) {} + ) { + this.applicationEvents.onError = this.handleEventError; + } + + public on( + event: K, + handler: RepeaterServerEventHandler + ): void { + this.applicationEvents.on(event, handler); + } + + public off( + event: K, + handler: RepeaterServerEventHandler + ): void { + this.applicationEvents.off(event, handler); + } public disconnect() { - this.events.removeAllListeners(); + this.applicationEvents.removeAllListeners(); this.clearPingTimer(); this.clearConnectionTimer(); @@ -135,7 +150,7 @@ export class DefaultRepeaterServer implements RepeaterServer { return result; } - public async connect(hostname: string) { + public async connect(repeaterId: string) { this._socket = io(this.options.uri, { parser, path: '/api/ws/v1', @@ -147,7 +162,7 @@ export class DefaultRepeaterServer implements RepeaterServer { reconnectionAttempts: this.MAX_RECONNECTION_ATTEMPTS, auth: { token: this.options.token, - domain: hostname + domain: repeaterId } }); @@ -159,39 +174,18 @@ export class DefaultRepeaterServer implements RepeaterServer { this.logger.debug('Repeater connected to %s', this.options.uri); } - public off( - event: K, - handler: RepeaterServerEventHandler - ): void { - const wrappedHandler = this.handlerMap.get(handler); - if (wrappedHandler) { - this.events.off(event, wrappedHandler); - this.handlerMap.delete(handler); - } - } - - public on( - event: K, - handler: RepeaterServerEventHandler - ): void { - const wrappedHandler = (...args: unknown[]) => - this.wrapEventListener(event, handler, ...args); - this.handlerMap.set(handler, wrappedHandler); - this.events.on(event, wrappedHandler); - } - private listenToApplicationEvents() { this.socket.on(SocketEvents.DEPLOYED, event => - this.events.emit(RepeaterServerEvents.DEPLOY, event) + this.applicationEvents.emit(RepeaterServerEvents.DEPLOY, event) ); this.socket.on(SocketEvents.REQUEST, (event, callback) => - this.events.emit(RepeaterServerEvents.REQUEST, event, callback) + this.applicationEvents.emit(RepeaterServerEvents.REQUEST, event, callback) ); this.socket.on(SocketEvents.ERROR, event => { - this.events.emit(RepeaterServerEvents.ERROR, event); + this.applicationEvents.emit(RepeaterServerEvents.ERROR, event); }); this.socket.on(SocketEvents.UPDATE_AVAILABLE, event => - this.events.emit(RepeaterServerEvents.UPDATE_AVAILABLE, event) + this.applicationEvents.emit(RepeaterServerEvents.UPDATE_AVAILABLE, event) ); } @@ -207,18 +201,18 @@ export class DefaultRepeaterServer implements RepeaterServer { error => (this.latestReconnectionError = error) ); this.socket.io.on('reconnect_failed', () => - this.events.emit(RepeaterServerEvents.RECONNECTION_FAILED, { + this.applicationEvents.emit(RepeaterServerEvents.RECONNECTION_FAILED, { error: this.latestReconnectionError } as RepeaterServerReconnectionFailedEvent) ); this.socket.io.on('reconnect_attempt', attempt => - this.events.emit(RepeaterServerEvents.RECONNECT_ATTEMPT, { + this.applicationEvents.emit(RepeaterServerEvents.RECONNECT_ATTEMPT, { attempt, maxAttempts: this.MAX_RECONNECTION_ATTEMPTS } as RepeaterServerReconnectionAttemptedEvent) ); this.socket.io.on('reconnect', () => - this.events.emit(RepeaterServerEvents.RECONNECTION_SUCCEEDED) + this.applicationEvents.emit(RepeaterServerEvents.RECONNECTION_SUCCEEDED) ); } @@ -235,7 +229,7 @@ export class DefaultRepeaterServer implements RepeaterServer { } if (this.suppressConnectionError(data)) { - this.events.emit(RepeaterServerEvents.ERROR, { + this.applicationEvents.emit(RepeaterServerEvents.ERROR, { ...data, message: err.message }); @@ -244,7 +238,7 @@ export class DefaultRepeaterServer implements RepeaterServer { } if (this.connectionAttempts >= this.MAX_RECONNECTION_ATTEMPTS) { - this.events.emit(RepeaterServerEvents.RECONNECTION_FAILED, { + this.applicationEvents.emit(RepeaterServerEvents.RECONNECTION_FAILED, { error: err } as RepeaterServerReconnectionFailedEvent); @@ -274,7 +268,7 @@ export class DefaultRepeaterServer implements RepeaterServer { this.connectionAttempts++; - this.events.emit(RepeaterServerEvents.RECONNECT_ATTEMPT, { + this.applicationEvents.emit(RepeaterServerEvents.RECONNECT_ATTEMPT, { attempt: this.connectionAttempts, maxAttempts: this.MAX_RECONNECTION_ATTEMPTS } as RepeaterServerReconnectionAttemptedEvent); @@ -298,35 +292,6 @@ export class DefaultRepeaterServer implements RepeaterServer { } } - private async wrapEventListener( - event: string, - handler: (...payload: TArgs) => unknown, - ...args: unknown[] - ) { - try { - const callback = this.extractLastArgument(args); - - // eslint-disable-next-line @typescript-eslint/return-await - const response = await handler(...(args as TArgs)); - - callback?.(response); - } catch (err) { - this.handleEventError(err, event, args); - } - } - - private extractLastArgument(args: unknown[]): CallbackFunction | undefined { - const lastArg = args.pop(); - if (typeof lastArg === 'function') { - return lastArg as CallbackFunction; - } else { - // If the last argument is not a function, add it back to the args array - args.push(lastArg); - - return undefined; - } - } - private clearConnectionTimer() { if (this.connectionTimer) { clearTimeout(this.connectionTimer); @@ -336,14 +301,14 @@ export class DefaultRepeaterServer implements RepeaterServer { private handleConnect = () => { this.connectionAttempts = 0; this.clearConnectionTimer(); - this.events.emit(RepeaterServerEvents.CONNECTED); + this.applicationEvents.emit(RepeaterServerEvents.CONNECTED); }; private handleDisconnect = (reason: string): void => { this.clearPingTimer(); if (reason !== 'io client disconnect') { - this.events.emit(RepeaterServerEvents.DISCONNECTED); + this.applicationEvents.emit(RepeaterServerEvents.DISCONNECTED); } // the disconnection was initiated by the server, you need to reconnect manually @@ -352,22 +317,25 @@ export class DefaultRepeaterServer implements RepeaterServer { } }; - private handleEventError(error: Error, event: string, args: unknown[]): void { + private handleEventError = ( + error: Error, + event: string, + args: unknown[] + ): void => { this.logger.debug( 'An error occurred while processing the %s event with the following payload: %j', event, args ); this.logger.error(error); - } + }; private createPingTimer() { this.clearPingTimer(); - this.pingTimer = setInterval( - () => this.socket.volatile.emit(SocketEvents.PING), - this.MAX_PING_INTERVAL - ).unref(); + this.pingTimer = setInterval(() => { + this.socket.volatile.emit(SocketEvents.PING); + }, this.MAX_PING_INTERVAL).unref(); } private clearPingTimer() { diff --git a/packages/repeater/src/bus/RepeaterApplicationEvents.spec.ts b/packages/repeater/src/bus/RepeaterApplicationEvents.spec.ts new file mode 100644 index 00000000..c7c98362 --- /dev/null +++ b/packages/repeater/src/bus/RepeaterApplicationEvents.spec.ts @@ -0,0 +1,96 @@ +import { + ErrorHandlerFunction, + RepeaterApplicationEvents +} from './RepeaterApplicationEvents'; +import { + RepeaterServerEventHandler, + RepeaterServerEvents, + RepeaterServerEventsMap +} from './RepeaterServer'; +import { delay } from '@sectester/core'; + +describe('RepeaterApplicationEvents', () => { + let sut: RepeaterApplicationEvents; + + beforeEach(() => { + sut = new RepeaterApplicationEvents(); + }); + + describe('emit', () => { + it('should invoke handler after subscription', () => { + // arrange + const handler: RepeaterServerEventHandler = jest.fn(); + const event = 'testEvent'; + + sut.on(event as keyof RepeaterServerEventsMap, handler); + + // act + sut.emit(event as RepeaterServerEvents, 'arg1', 'arg2'); + + // assert + expect(handler).toHaveBeenCalledWith('arg1', 'arg2'); + }); + + it('should not invoke handler after removal', () => { + // arrange + const handler: RepeaterServerEventHandler = jest.fn(); + const event = 'testEvent'; + + sut.on(event as keyof RepeaterServerEventsMap, handler); + sut.off(event as keyof RepeaterServerEventsMap, handler); + + // act + sut.emit(event as RepeaterServerEvents, 'arg1', 'arg2'); + + // Capture the arguments passed to the handler + + expect(handler).not.toHaveBeenCalled(); + }); + + it('should call the callback with the handler result', async () => { + // arrange + const handler: RepeaterServerEventHandler = jest + .fn() + .mockResolvedValue('result'); + const callback = jest.fn(); + + const event = 'testEvent'; + + sut.on(event as keyof RepeaterServerEventsMap, handler); + + // act + sut.emit(event as RepeaterServerEvents, 'arg1', 'arg2', callback); + + // assert + await delay(200); + expect(callback).toHaveBeenCalledWith('result'); + }); + + it('should handle errors in event handlers', async () => { + // arrange + const error = new Error('test error'); + + const errorHandlerMock: ErrorHandlerFunction = jest.fn(); + sut.onError = errorHandlerMock; + + const handler: RepeaterServerEventHandler = jest + .fn() + .mockRejectedValue(error); + const event = 'testEvent'; + + sut.on(event as keyof RepeaterServerEventsMap, handler); + + // act + sut.emit(event as RepeaterServerEvents, 'arg1', 'arg2'); + + // assert + await delay(200); + + expect(errorHandlerMock).toHaveBeenCalledWith( + new Error('test error'), + event, + ['arg1', 'arg2'] + ); + }); + }); +}); diff --git a/packages/repeater/src/bus/RepeaterApplicationEvents.ts b/packages/repeater/src/bus/RepeaterApplicationEvents.ts new file mode 100644 index 00000000..7674f2cd --- /dev/null +++ b/packages/repeater/src/bus/RepeaterApplicationEvents.ts @@ -0,0 +1,87 @@ +import 'reflect-metadata'; +import { + RepeaterServerEventHandler, + RepeaterServerEvents, + RepeaterServerEventsMap +} from './RepeaterServer'; +import { injectable, Lifecycle, scoped } from 'tsyringe'; +import { EventEmitter } from 'events'; + +export type CallbackFunction = (arg: T) => unknown; +export type HandlerFunction = (args: unknown[]) => unknown; +export type ErrorHandlerFunction = ( + error: Error, + event: string, + args: unknown[] +) => unknown; + +@scoped(Lifecycle.ContainerScoped) +@injectable() +export class RepeaterApplicationEvents { + public onError: ErrorHandlerFunction | undefined; + + protected readonly events = new EventEmitter(); + + private readonly handlerMap = new WeakMap< + RepeaterServerEventHandler, + HandlerFunction + >(); + + public emit(event: RepeaterServerEvents, ...rest: unknown[]) { + this.events.emit(event, ...rest); + } + + public removeAllListeners() { + this.events.removeAllListeners(); + } + + public off( + event: K, + handler: RepeaterServerEventHandler + ): void { + const wrappedHandler = this.handlerMap.get(handler); + if (wrappedHandler) { + this.events.off(event, wrappedHandler); + this.handlerMap.delete(handler); + } + } + + public on( + event: K, + handler: RepeaterServerEventHandler + ): void { + const wrappedHandler = (...args: unknown[]) => + this.wrapEventListener(event, handler, ...args); + this.handlerMap.set(handler, wrappedHandler); + this.events.on(event, wrappedHandler); + } + + private async wrapEventListener( + event: string, + handler: (...payload: TArgs) => unknown, + ...args: unknown[] + ) { + try { + const callback = this.extractLastArgument(args); + + // eslint-disable-next-line @typescript-eslint/return-await + const response = await handler(...(args as TArgs)); + + callback?.(response); + } catch (err) { + this.onError?.(err, event, args); + } + } + + private extractLastArgument(args: unknown[]): CallbackFunction | undefined { + const lastArg = args.pop(); + if (typeof lastArg === 'function') { + return lastArg as CallbackFunction; + } else { + // If the last argument is not a function, add it back to the args array + args.push(lastArg); + + return undefined; + } + } +} diff --git a/packages/repeater/src/bus/index.ts b/packages/repeater/src/bus/index.ts index 7ba307a6..0547d204 100644 --- a/packages/repeater/src/bus/index.ts +++ b/packages/repeater/src/bus/index.ts @@ -5,3 +5,4 @@ export * from './RepeaterBus'; export * from './RepeaterBusFactory'; export * from './RepeaterCommandHub'; export * from './RepeaterServer'; +export * from './RepeaterApplicationEvents';