diff --git a/components/log-viewer-webui/client/package-lock.json b/components/log-viewer-webui/client/package-lock.json index 75e55b0d49..6a8470ec9e 100644 --- a/components/log-viewer-webui/client/package-lock.json +++ b/components/log-viewer-webui/client/package-lock.json @@ -16,13 +16,21 @@ "antd": "^5.24.5", "axios": "^1.7.9", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "socket.io-client": "^4.8.1" }, "devDependencies": { + "@eslint/js": "^9.19.0", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", + "@types/socket.io-client": "^3.0.0", "@vitejs/plugin-react": "^4.3.4", - "eslint-config-yscope": "*", + "eslint": "^9.19.0", + "eslint-config-yscope": "latest", + "eslint-plugin-react": "^7.37.4", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.18", + "globals": "^15.14.0", "typescript": "~5.6.2", "vite": "^6.2.5" } @@ -1060,7 +1068,6 @@ "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "eslint-visitor-keys": "^3.4.3" }, @@ -1080,7 +1087,6 @@ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -1094,7 +1100,6 @@ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -1105,7 +1110,6 @@ "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", @@ -1121,7 +1125,6 @@ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1133,7 +1136,6 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -1147,7 +1149,6 @@ "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -1161,7 +1162,6 @@ "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -1186,7 +1186,6 @@ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1198,7 +1197,6 @@ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1212,7 +1210,6 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -1226,7 +1223,6 @@ "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -1237,7 +1233,6 @@ "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -1248,7 +1243,6 @@ "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@eslint/core": "^0.10.0", "levn": "^0.4.1" @@ -1263,7 +1257,6 @@ "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -1315,7 +1308,6 @@ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=18.18.0" } @@ -1326,7 +1318,6 @@ "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" @@ -1341,7 +1332,6 @@ "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=18.18" }, @@ -1356,7 +1346,6 @@ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=12.22" }, @@ -1371,7 +1360,6 @@ "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=18.18" }, @@ -2160,6 +2148,12 @@ "integrity": "sha512-gu+tdy9WZIRulrR4CAcGXZAAixwakKszkUXudMJ4EhtNflBEify5Pm5vnVEVqdmMkxnT4tcdfJps5XYqaNeF9Q==", "license": "MIT" }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@stylistic/eslint-plugin": { "version": "2.13.0", "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.13.0.tgz", @@ -2238,8 +2232,7 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/json5": { "version": "0.0.29", @@ -2281,6 +2274,17 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/socket.io-client": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-3.0.0.tgz", + "integrity": "sha512-s+IPvFoEIjKA3RdJz/Z2dGR4gLgysKi8owcnrVwNjgvc01Lk68LJDDsG2GRqegFITcxmvCMYM7bhMpwEMlHmDg==", + "deprecated": "This is a stub types definition. socket.io-client provides its own type definitions, so you do not need this installed.", + "dev": true, + "license": "MIT", + "dependencies": { + "socket.io-client": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.0.tgz", @@ -2830,7 +2834,6 @@ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2844,7 +2847,6 @@ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -2855,7 +2857,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2873,7 +2874,6 @@ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -2965,8 +2965,7 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, - "license": "Python-2.0", - "peer": true + "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", @@ -2974,7 +2973,6 @@ "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" @@ -2992,7 +2990,6 @@ "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -3014,7 +3011,6 @@ "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -3058,7 +3054,6 @@ "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -3078,7 +3073,6 @@ "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -3098,7 +3092,6 @@ "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -3116,7 +3109,6 @@ "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", @@ -3139,7 +3131,6 @@ "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" } @@ -3156,7 +3147,6 @@ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -3198,8 +3188,7 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/brace-expansion": { "version": "2.0.1", @@ -3265,7 +3254,6 @@ "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", @@ -3298,7 +3286,6 @@ "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" @@ -3346,7 +3333,6 @@ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3379,7 +3365,6 @@ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -3392,8 +3377,7 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", @@ -3429,8 +3413,7 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/convert-source-map": { "version": "1.9.0", @@ -3478,7 +3461,6 @@ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -3500,7 +3482,6 @@ "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -3519,7 +3500,6 @@ "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -3538,7 +3518,6 @@ "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -3579,8 +3558,7 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/define-data-property": { "version": "1.1.4", @@ -3588,7 +3566,6 @@ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -3607,7 +3584,6 @@ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -3635,7 +3611,6 @@ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "esutils": "^2.0.2" }, @@ -3664,6 +3639,45 @@ "dev": true, "license": "ISC" }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", @@ -3694,7 +3708,6 @@ "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", @@ -3779,7 +3792,6 @@ "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -3843,7 +3855,6 @@ "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "hasown": "^2.0.2" }, @@ -3857,7 +3868,6 @@ "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", @@ -3939,7 +3949,6 @@ "integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -4256,7 +4265,6 @@ "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -4290,7 +4298,6 @@ "integrity": "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -4298,13 +4305,22 @@ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.19.tgz", + "integrity": "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, "node_modules/eslint-plugin-react/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4316,7 +4332,6 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -4330,7 +4345,6 @@ "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -4349,7 +4363,6 @@ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", - "peer": true, "bin": { "semver": "bin/semver.js" } @@ -4382,7 +4395,6 @@ "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -4400,7 +4412,6 @@ "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -4414,7 +4425,6 @@ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4426,7 +4436,6 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -4440,7 +4449,6 @@ "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", @@ -4459,7 +4467,6 @@ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -4473,7 +4480,6 @@ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -4487,7 +4493,6 @@ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -4498,7 +4503,6 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -4508,8 +4512,7 @@ "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", - "peer": true + "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.3", @@ -4548,16 +4551,14 @@ "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", - "peer": true + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/fastq": { "version": "1.19.0", @@ -4592,7 +4593,6 @@ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "flat-cache": "^4.0.0" }, @@ -4626,7 +4626,6 @@ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -4644,7 +4643,6 @@ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" @@ -4658,8 +4656,7 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/follow-redirects": { "version": "1.15.9", @@ -4687,7 +4684,6 @@ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "is-callable": "^1.2.7" }, @@ -4743,7 +4739,6 @@ "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -4765,7 +4760,6 @@ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4823,7 +4817,6 @@ "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -4856,7 +4849,6 @@ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -4870,7 +4862,6 @@ "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -4884,7 +4875,6 @@ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" @@ -4930,7 +4920,6 @@ "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" }, @@ -4944,7 +4933,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -4955,7 +4943,6 @@ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -4969,7 +4956,6 @@ "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "dunder-proto": "^1.0.0" }, @@ -5040,7 +5026,6 @@ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 4" } @@ -5067,7 +5052,6 @@ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.8.19" } @@ -5078,7 +5062,6 @@ "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", @@ -5094,7 +5077,6 @@ "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -5119,7 +5101,6 @@ "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", @@ -5140,7 +5121,6 @@ "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "has-bigints": "^1.0.2" }, @@ -5157,7 +5137,6 @@ "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -5186,7 +5165,6 @@ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" }, @@ -5215,7 +5193,6 @@ "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", @@ -5234,7 +5211,6 @@ "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" @@ -5252,7 +5228,6 @@ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5263,7 +5238,6 @@ "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.3" }, @@ -5280,7 +5254,6 @@ "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.3", "get-proto": "^1.0.0", @@ -5300,7 +5273,6 @@ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -5314,7 +5286,6 @@ "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" }, @@ -5339,7 +5310,6 @@ "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -5357,7 +5327,6 @@ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", @@ -5377,7 +5346,6 @@ "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" }, @@ -5391,7 +5359,6 @@ "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.3" }, @@ -5408,7 +5375,6 @@ "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -5426,7 +5392,6 @@ "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", @@ -5445,7 +5410,6 @@ "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "which-typed-array": "^1.1.16" }, @@ -5462,7 +5426,6 @@ "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" }, @@ -5476,7 +5439,6 @@ "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.3" }, @@ -5493,7 +5455,6 @@ "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" @@ -5510,16 +5471,14 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/iterator.prototype": { "version": "1.1.5", @@ -5527,7 +5486,6 @@ "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", @@ -5552,7 +5510,6 @@ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "argparse": "^2.0.1" }, @@ -5588,8 +5545,7 @@ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -5602,16 +5558,14 @@ "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", - "peer": true + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/json2mq": { "version": "0.2.0", @@ -5641,7 +5595,6 @@ "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", @@ -5658,7 +5611,6 @@ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "json-buffer": "3.0.1" } @@ -5669,7 +5621,6 @@ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -5690,7 +5641,6 @@ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -5706,8 +5656,7 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/loose-envify": { "version": "1.4.0", @@ -5859,8 +5808,7 @@ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/node-releases": { "version": "2.0.19", @@ -5884,7 +5832,6 @@ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" }, @@ -5898,7 +5845,6 @@ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" } @@ -5909,7 +5855,6 @@ "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -5931,7 +5876,6 @@ "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -5947,7 +5891,6 @@ "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -5983,7 +5926,6 @@ "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -6003,7 +5945,6 @@ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -6022,7 +5963,6 @@ "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", @@ -6041,7 +5981,6 @@ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -6058,7 +5997,6 @@ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -6120,7 +6058,6 @@ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -6131,7 +6068,6 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -6177,7 +6113,6 @@ "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" } @@ -6217,7 +6152,6 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8.0" } @@ -6251,7 +6185,6 @@ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -6933,7 +6866,6 @@ "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -6963,7 +6895,6 @@ "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -7107,7 +7038,6 @@ "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", @@ -7128,7 +7058,6 @@ "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" @@ -7146,7 +7075,6 @@ "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -7194,7 +7122,6 @@ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -7213,7 +7140,6 @@ "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -7230,7 +7156,6 @@ "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", @@ -7246,7 +7171,6 @@ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -7260,7 +7184,6 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -7271,7 +7194,6 @@ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", @@ -7292,7 +7214,6 @@ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" @@ -7310,7 +7231,6 @@ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -7330,7 +7250,6 @@ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -7353,6 +7272,68 @@ "license": "ISC", "peer": true }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -7420,7 +7401,6 @@ "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -7449,7 +7429,6 @@ "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" @@ -7461,7 +7440,6 @@ "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", @@ -7484,7 +7462,6 @@ "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", @@ -7504,7 +7481,6 @@ "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -7534,7 +7510,6 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" }, @@ -7554,7 +7529,6 @@ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -7706,7 +7680,6 @@ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -7720,7 +7693,6 @@ "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -7736,7 +7708,6 @@ "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", @@ -7757,7 +7728,6 @@ "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", @@ -7780,7 +7750,6 @@ "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", @@ -7960,7 +7929,6 @@ "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", @@ -8011,7 +7979,6 @@ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "punycode": "^2.1.0" } @@ -8094,7 +8061,6 @@ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "isexe": "^2.0.0" }, @@ -8111,7 +8077,6 @@ "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", @@ -8132,7 +8097,6 @@ "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", @@ -8161,7 +8125,6 @@ "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", @@ -8181,7 +8144,6 @@ "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", @@ -8203,11 +8165,39 @@ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -8236,7 +8226,6 @@ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, diff --git a/components/log-viewer-webui/client/package.json b/components/log-viewer-webui/client/package.json index 403ce6c7a4..1ccd0839ed 100644 --- a/components/log-viewer-webui/client/package.json +++ b/components/log-viewer-webui/client/package.json @@ -21,13 +21,21 @@ "antd": "^5.24.5", "axios": "^1.7.9", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "socket.io-client": "^4.8.1" }, "devDependencies": { + "@eslint/js": "^9.19.0", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", + "@types/socket.io-client": "^3.0.0", "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.19.0", "eslint-config-yscope": "latest", + "eslint-plugin-react": "^7.37.4", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.18", + "globals": "^15.14.0", "typescript": "~5.6.2", "vite": "^6.2.5" } diff --git a/components/log-viewer-webui/client/src/App.tsx b/components/log-viewer-webui/client/src/App.tsx index b54609e539..0e2dbfa1cb 100644 --- a/components/log-viewer-webui/client/src/App.tsx +++ b/components/log-viewer-webui/client/src/App.tsx @@ -1,19 +1,68 @@ -import {CssVarsProvider} from "@mui/joy"; +/* eslint-disable react/jsx-key */ -import {LOCAL_STORAGE_KEY} from "./typings/config"; -import QueryStatus from "./ui/QueryStatus"; +// import { CssVarsProvider } from "@mui/joy"; +// import { LOCAL_STORAGE_KEY } from "./typings/config"; +// import QueryStatus from "./ui/QueryStatus"; +import MongoCollection from "./mongoCDCLib/MongoCollection"; +import {useTracker} from "./mongoCDCLib/useTracker"; +const collection = new MongoCollection("compression-jobs"); + +// const collection2 = new MongoReplicaCollection("compression-jobs"); + /** * Renders the main application. * * @return */ const App = () => { + const results = useTracker( + () => collection.find({ + _id: { + $gte: 1, + $lte: 10, + }, + }, {sort: {start_time: -1}}), + [] + ); + + const results2 = useTracker( + () => collection.find({ + _id: { + $gte: 1, + $lte: 5, + }, + }, {sort: {start_time: -1}}), + [] + ); + + /* for (let i = 0; i < results.length; i++) { + console.log(results[i]); + } + + for (let i = 0; i < results2.length; i++) { + console.log(results2[i]); + } */ + return ( - - - +
+ {results.map((r) => ( +
+ {JSON.stringify(r)} +
+ ))} + {results2.map((r) => ( +
+ {JSON.stringify(r)} +
+ ))} + +
+ + // + // + // ); }; diff --git a/components/log-viewer-webui/client/src/mongoCDCLib/MongoCollection.ts b/components/log-viewer-webui/client/src/mongoCDCLib/MongoCollection.ts new file mode 100644 index 0000000000..07ec7755dd --- /dev/null +++ b/components/log-viewer-webui/client/src/mongoCDCLib/MongoCollection.ts @@ -0,0 +1,76 @@ +import { + io, + Socket, +} from "socket.io-client"; + +import { + MongoCollectionReactiveCursor, + type ServerError, +} from "./MongoCollectionReactiveCursor.js"; + + +let sharedSocket: Socket | null = null; + +/** + * Instantiate a shared socket connection to the server. + * + * @return The shared socket connection. + */ +const getSharedSocket = (): Socket => { + if (!sharedSocket) { + // You can pass a URL here if needed (e.g., from environment vars) + sharedSocket = io(); + } + + return sharedSocket; +}; + +/** + * Represents a MongoDB collection that can be queried over a socket connection. + * + * @class MongoCollection + */ +class MongoCollection { + private socket: Socket; + + /** + * Creates an instance of MongoCollection. + * + * @param collectionName The name of the collection to interact with. + */ + constructor (collectionName: string) { + // eslint-disable-next-line no-warning-comments + // TODO: use the server URL from the environment / constructor args + this.socket = getSharedSocket(); + + this.socket.emit("collection::init", { + collectionName: collectionName, + }, (response: ServerError) => { + if ("error" in response && "collectionName" in response) { + if (collectionName === response.collectionName) { + console.error("Error initializing collection:", response.error); + } + } + }); + } + + // eslint-disable-next-line no-warning-comments + // TODO: add support for non-reactive cursors + /** + * Finds documents in the collection based on the provided query and options. + * + * @param query The query object to filter results. + * @param options The options for the query (e.g., sort, limit). + * @return An instance of MongoCollectionReactiveCursor for reactive querying. + */ + find (query: object, options: object) { + return new MongoCollectionReactiveCursor({ + options: options, + query: query, + socket: this.socket, + }); + } +} + + +export default MongoCollection; diff --git a/components/log-viewer-webui/client/src/mongoCDCLib/MongoCollectionCursor.ts b/components/log-viewer-webui/client/src/mongoCDCLib/MongoCollectionCursor.ts new file mode 100644 index 0000000000..2fb1386283 --- /dev/null +++ b/components/log-viewer-webui/client/src/mongoCDCLib/MongoCollectionCursor.ts @@ -0,0 +1,106 @@ +import {Socket} from "socket.io-client"; + + +interface CursorConstructorArgs { + socket: Socket; + query: object; + options: object; +} + +/** + * Represents a cursor for querying a MongoDB collection over a socket connection. + * + * @class MongoCollectionCursor + */ +class MongoCollectionCursor { + socket: Socket; + + findQuery: object; + + findOptions: object; + + /** + * Creates an instance of MongoCollectionCursor. + * + * @param args The constructor arguments. + * @param args.socket The socket connection to the server. + * @param args.query The query object to filter results. + * @param args.options The options for the query (e.g., sort, limit). + */ + constructor ({socket, query, options}: CursorConstructorArgs) { + this.socket = socket; + + this.findQuery = query; + this.findOptions = options; + } + + /** + * Adds a sort option to the query. + * + * @param sort The sort criteria. + * @return The current instance for method chaining. + */ + sort (sort: object): this { + this.findOptions = { + ...this.findOptions, + sort: sort, + }; + + return this; + } + + /** + * Sets a limit on the number of results returned. + * + * @param number The maximum number of results to return. + * @return The current instance for method chaining. + */ + limit (number: number): this { + this.findOptions = { + ...this.findOptions, + limit: number, + }; + + return this; + } + + /** + * Skips a specified number of results. + * + * @param offset The number of results to skip. + * @return The current instance for method chaining. + */ + skip (offset:number): this { + this.findOptions = { + ...this.findOptions, + skip: offset, + }; + + return this; + } + + /** + * Executes the query and returns the results as an array. + * + * @return A promise that resolves with the array of documents or rejects with an error. + */ + toArray () { + return new Promise((resolve, reject) => { + this.socket.emit("collection::find::toArray", { + query: this.findQuery, + options: this.findOptions, + }, (response: {error?: Error; data?: Document[]}) => { + if (response.error) { + reject(response.error); + + return; + } + + resolve(response.data); + }); + }); + } +} + + +export default MongoCollectionCursor; diff --git a/components/log-viewer-webui/client/src/mongoCDCLib/MongoCollectionReactiveCursor.ts b/components/log-viewer-webui/client/src/mongoCDCLib/MongoCollectionReactiveCursor.ts new file mode 100644 index 0000000000..969cffb153 --- /dev/null +++ b/components/log-viewer-webui/client/src/mongoCDCLib/MongoCollectionReactiveCursor.ts @@ -0,0 +1,110 @@ +import {Socket} from "socket.io-client"; + +import MongoCollectionCursor from "./MongoCollectionCursor.js"; + + +interface ServerError { + collectionName?: string; + error: string; + queryId?: number; +} + +interface Success { + data: T; +} + +type Response = ServerError | Success; + +interface Listener { + onData: (data: Document[]) => void; + onError: (error: Error) => void; +} + +interface ReactiveArrayCallback { + onData: (data: Document[]) => void; + onError: (error: Error) => void; +} + +/** + * Represents a reactive cursor for querying a MongoDB-like collection over a socket connection. + * This class extends MongoCollectionCursor to provide real-time updates. + * + * @class MongoCollectionReactiveCursor + * @augments MongoCollectionCursor + */ +class MongoCollectionReactiveCursor extends MongoCollectionCursor { + // The listener for data and error events. + private listener: Listener | null = null; + + // The unique identifier for the query. + private queryId: number | null = null; + + /** + * Creates an instance of MongoCollectionReactiveCursor. + * + * @param props The constructor properties. + * @param props.socket The socket connection to the server. + * @param props.query The query object to filter results. + * @param props.options The options for the query (e.g., sort, limit). + */ + constructor (props: {socket: Socket; query: object; options: object}) { + super(props); + + this.socket.on( + "collection::find::update", + (response: {error?: Error; data?: Document[]; queryId: number}) => { + if (this.queryId === response.queryId) { + if (response.error) { + return this.listener?.onError(response.error); + } + + return this.listener?.onData(response.data ?? []); + } + + return null; + } + ); + } + + /** + * Subscribe to the collection for real-time updates. + * + * @param callback + * @return The cleanup function. + */ + toReactiveArray (callback: ReactiveArrayCallback): () => void { + const unsubscribedInfo : {queryId: number | null} = {queryId: null}; + + this.socket.emit("collection::find::toReactiveArray", { + query: this.findQuery, + options: this.findOptions, + }, (response: Response<{queryId: number}>) => { + if ("error" in response) { + callback.onError(new Error(response.error)); + + return; + } + + if ("undefined" !== typeof response.data.queryId) { + unsubscribedInfo.queryId = response.data.queryId; + this.queryId = response.data.queryId; + } + + this.listener = callback; + }); + + return () => { + if (null === unsubscribedInfo.queryId) { + return; + } + this.socket.emit("collection::find::unsubscribe", { + queryId: unsubscribedInfo.queryId, + }); + + this.socket.off("collection::find::update"); + }; + } +} + +export {MongoCollectionReactiveCursor}; +export {type ServerError}; diff --git a/components/log-viewer-webui/client/src/mongoCDCLib/useTracker.tsx b/components/log-viewer-webui/client/src/mongoCDCLib/useTracker.tsx new file mode 100644 index 0000000000..a77d187935 --- /dev/null +++ b/components/log-viewer-webui/client/src/mongoCDCLib/useTracker.tsx @@ -0,0 +1,45 @@ +import { + useEffect, + useState, +} from "react"; + +import {MongoCollectionReactiveCursor} from "./MongoCollectionReactiveCursor.js"; + + +/** + * Custom hook to track a MongoDB cursor and return its data as an array. + * + * @param getCursor Function that returns a MongoReplicaCollectionReactiveCursor. + * @param dependencies Array of dependencies for the effect. + * @return The data from the cursor as an array. + */ +const useTracker = ( + getCursor: () => MongoCollectionReactiveCursor, + dependencies: object[] = [] +): unknown[] => { + const [data, setData] = useState([]); + + useEffect(() => { + const cursor = getCursor(); + + const handleData = (newData: unknown[]) => { + setData(newData); + }; + + const unsubscribe = cursor.toReactiveArray({ + onData: handleData, + onError: (error: unknown) => { + console.error("Error fetching data:", error); + }, + }); + + return () => { + unsubscribe(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, dependencies); + + return data; +}; + +export {useTracker}; diff --git a/components/log-viewer-webui/client/vite.config.ts b/components/log-viewer-webui/client/vite.config.ts index 2106c317a5..13be2d5fe8 100644 --- a/components/log-viewer-webui/client/vite.config.ts +++ b/components/log-viewer-webui/client/vite.config.ts @@ -6,7 +6,8 @@ import {defineConfig} from "vite"; export default defineConfig({ plugins: [react()], server: { - port: 8080, + host: "0.0.0.0", + port: 8081, proxy: { "/query/": { // Below target should match the server's configuration in @@ -14,6 +15,11 @@ export default defineConfig({ target: "http://localhost:3000/", changeOrigin: true, }, + "/socket.io/": { + target: "ws://localhost:3000/", + changeOrigin: true, + ws: true, + }, }, }, publicDir: "public", diff --git a/components/log-viewer-webui/server/package-lock.json b/components/log-viewer-webui/server/package-lock.json index 2c940e588c..d45623167a 100644 --- a/components/log-viewer-webui/server/package-lock.json +++ b/components/log-viewer-webui/server/package-lock.json @@ -23,6 +23,7 @@ "fastify-plugin": "^5.0.1", "http-status-codes": "^2.3.0", "pino-pretty": "^13.0.0", + "socket.io": "^4.8.1", "typescript": "~5.7.3" }, "devDependencies": { @@ -2813,6 +2814,12 @@ "node": ">=18.0.0" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@stylistic/eslint-plugin": { "version": "2.13.0", "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.13.0.tgz", @@ -3466,6 +3473,15 @@ "node": "^16.14.0 || >=18.0.0" } }, + "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==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -4060,6 +4076,28 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, + "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==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -4534,6 +4572,15 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "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==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -5166,6 +5213,19 @@ "node": ">=18" } }, + "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==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5421,6 +5481,82 @@ "once": "^1.4.0" } }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/enhanced-resolve": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", @@ -8366,6 +8502,27 @@ "node": ">=10.0.0" } }, + "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==", + "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==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -8622,7 +8779,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/mysql2": { @@ -9009,9 +9165,7 @@ "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, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -10480,6 +10634,119 @@ "npm": ">= 3.0.0" } }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "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.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/socks": { "version": "2.8.4", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", @@ -11830,6 +12097,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/walk-up-path": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", diff --git a/components/log-viewer-webui/server/package.json b/components/log-viewer-webui/server/package.json index d34c7c9572..3058711a51 100644 --- a/components/log-viewer-webui/server/package.json +++ b/components/log-viewer-webui/server/package.json @@ -30,6 +30,7 @@ "fastify-plugin": "^5.0.1", "http-status-codes": "^2.3.0", "pino-pretty": "^13.0.0", + "socket.io": "^4.8.1", "typescript": "~5.7.3" }, "devDependencies": { diff --git a/components/log-viewer-webui/server/src/app.ts b/components/log-viewer-webui/server/src/app.ts index 35cdcdeb9a..063b6435ef 100644 --- a/components/log-viewer-webui/server/src/app.ts +++ b/components/log-viewer-webui/server/src/app.ts @@ -8,6 +8,7 @@ import { import settings from "../settings.json" with {type: "json"}; import DbManager from "./plugins/DbManager.js"; +import MongoSocketIoServer from "./plugins/MongoSocketIoServer/index.js"; import S3Manager from "./plugins/S3Manager.js"; import exampleRoutes from "./routes/example.js"; import queryRoutes from "./routes/query.js"; @@ -54,6 +55,11 @@ const app = async ({ }, }); await server.register(S3Manager, {region: settings.StreamFilesS3Region}); + await server.register(MongoSocketIoServer, { + host: settings.MongoDbHost, + port: settings.MongoDbPort, + database: settings.MongoDbName, + }); } await server.register(staticRoutes); diff --git a/components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/MongoWatcherCollection.ts b/components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/MongoWatcherCollection.ts new file mode 100644 index 0000000000..3b179b3bac --- /dev/null +++ b/components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/MongoWatcherCollection.ts @@ -0,0 +1,208 @@ +import { + Collection, + Db, +} from "mongodb"; +import {Server} from "socket.io"; + +import { + CLIENT_UPDATE_TIMEOUT_MS, + MongoCustomSocket, + QueryId, + QueryParameters, + Watcher, +} from "./typings.js"; +import {convertQueryToChangeStreamFormat} from "./utils.js"; + + +/** + * Provides watchers for MongoDB queries to a specific collection. + */ +class MongoWatcherCollection { + private collection: Collection; + + private io: Server; + + // Active watchers + private queryIdtoWatcherMap: Map = new Map(); + + /** + * @param collectionName + * @param io + * @param mongoDb + */ + constructor (collectionName: string, io: Server, mongoDb: Db) { + this.collection = mongoDb.collection(collectionName); + this.io = io; + } + + /** + * Checks if the collection is currently being referenced by any clients. + * + * @return True if the collection is referenced, false otherwise. + */ + isReferenced (): boolean { + return 0 !== this.queryIdtoWatcherMap.size; + } + + /** + * Gets the watcher for a specific query. If the watcher does not exist, it creates a new one. + * + * @param queryParameters + * @param queryId + * @param socket + */ + async getWatcher ( + queryParameters: QueryParameters, + queryId: QueryId, + socket: MongoCustomSocket, + ) { + let watcher = this.queryIdtoWatcherMap.get(queryId); + + if (watcher) { + await this.#subscribeToWatcher(watcher, queryParameters, queryId, socket); + + return; + } + + watcher = this.#createNewWatcher(queryParameters, queryId, socket); + this.queryIdtoWatcherMap.set(queryId, watcher); + } + + /** + * Unsubscribes a connection from a watcher. If the watcher has no more subscribers, it closes + * the change stream. + * + * @param queryId + * @param connectionId + * @return True if connection is last subcriber, false otherwise. + */ + unsubscribeFromWatcher (queryId: number, connectionId: string): boolean { + const watcher = this.queryIdtoWatcherMap.get(queryId); + + if ("undefined" === typeof watcher) { + console.warn(`No watcher found for queryId ${queryId}`); + + return false; + } + + if (1 < watcher.subscribers.length) { + // Remove the connection ID from the subscribers list + watcher.subscribers = watcher.subscribers.filter((id) => id !== connectionId); + + return false; + } + + watcher.changeStream.close().catch((err: unknown) => { + console.error(`Error closing watcher for queryId ${queryId}:`, err); + }); + this.queryIdtoWatcherMap.delete(queryId); + + return true; + } + + /** + * Adds connection to an existing watcher and sends the current data. + * + * @param watcher + * @param queryParameters + * @param queryId + * @param socket + */ + async #subscribeToWatcher ( + watcher: Watcher, + queryParameters: QueryParameters, + queryId: QueryId, + socket: MongoCustomSocket + ) { + const {query, options} = queryParameters; + watcher.subscribers.push(socket.id); + const data = await this.collection.find(query, options).toArray(); + socket.emit("collection::find::update", {queryId, data}); + } + + /** + * Creates a new watcher for the given query and queryId. + * + * @param queryParams + * @param queryId + * @param socket + * @return The newly created watcher. + */ + #createNewWatcher ( + queryParams: QueryParameters, + queryId: QueryId, + socket: MongoCustomSocket + ): Watcher { + const watcherQuery = convertQueryToChangeStreamFormat(queryParams.query); + const mongoWatcher = this.collection.watch( + [{$match: watcherQuery}], + {fullDocument: "updateLookup"} + ); + + const watcher: Watcher = {changeStream: mongoWatcher, subscribers: [socket.id]}; + this.#setupWatcherListener(watcher, queryParams, queryId); + + return watcher; + } + + /** + * Sets up listener for the watcher to handle change events and emit updates to clients. + * + * @param watcher + * @param queryParameters + * @param queryId + */ + #setupWatcherListener ( + watcher: Watcher, + queryParameters: QueryParameters, + queryId: QueryId + ) { + let lastEmitTime = 0; + let emitTimeout: NodeJS.Timeout | null = null; + + const emitUpdate = async () => { + const currentTime = Date.now(); + if (CLIENT_UPDATE_TIMEOUT_MS <= currentTime - lastEmitTime) { + lastEmitTime = currentTime; + await this.#emitDataUpdate(queryParameters, queryId); + } else if (!emitTimeout) { + const delay = CLIENT_UPDATE_TIMEOUT_MS - (currentTime - lastEmitTime); + emitTimeout = setTimeout(() => { + emitTimeout = null; + + this.#emitDataUpdate(queryParameters, queryId).catch((error: unknown) => { + console.error("Error emitting data update after delay:", error); + }); + lastEmitTime = Date.now(); + }, delay); + } + }; + + watcher.changeStream.on("change", () => { + emitUpdate().catch((error: unknown) => { + console.error("Error in emitUpdate:", error); + }); + }); + } + + /** + * Emits updated data to clients subscribed to the given queryId. + * + * @param queryParameters + * @param queryId + */ + async #emitDataUpdate ( + queryParameters: QueryParameters, + queryId: QueryId + ) { + const {query, options} = queryParameters; + try { + const data = await this.collection.find(query, options).toArray(); + this.io.to(`${queryId}`).emit("collection::find::update", {queryId, data}); + } catch (error) { + console.error("Error fetching data for update:", error); + } + } +} + +export default MongoWatcherCollection; diff --git a/components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts b/components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts new file mode 100644 index 0000000000..ae2a627d0c --- /dev/null +++ b/components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts @@ -0,0 +1,360 @@ +/* eslint-disable max-lines */ +/* eslint-disable no-warning-comments */ +// TODO: Move listeners to a seperate file to reduce lines +// Reference: https://github.com/socketio/socket.io/blob/main/examples/basic-crud-application/server/lib/todo-management/todo.handlers.ts + +import { + FastifyInstance, + FastifyPluginAsync, +} from "fastify"; +import fastifyPlugin from "fastify-plugin"; +import { + Db, + Document, + type Filter, + type FindOptions, +} from "mongodb"; +import {Server} from "socket.io"; + +import MongoWatcherCollection from "./MongoWatcherCollection.js"; +import { + ClientToServerEvents, + ConnectionId, + DbOptions, + InterServerEvents, + MongoCustomSocket, + QueryId, + QueryParameters, + Response, + ServerToClientEvents, + SocketData, +} from "./typings.js"; +import { + getQuery, + getQueryHash, + initializeMongoClient, +} from "./utils.js"; + + +/** + * Manages client interactions with MongoDB. + * + * TODO: In current implementation, all queries in a collection are sent using the same event. A + * potential improvement would be to use different event names per query. + */ +class MongoSocketIoServer { + #fastify: FastifyInstance; + + #io: Server; + + // Collections with active queries + #collections: Map = new Map(); + + // Mapping of active queries to their hash + #queryIdtoQueryHashMap: Map = new Map(); + + // List of subscribed query IDs for each connection + #subscribedQueryIdsMap: Map = new Map(); + + readonly #mongoDb: Db; + + /** + * Private constructor for MongoSocketIoServer. This is not intended to be invoked publicly. + * Instead, use MongoSocketIoServer.create() to create a new instance of the class. + * + * @param fastify + * @param mongoDb + */ + constructor (fastify: FastifyInstance, mongoDb: Db) { + this.#fastify = fastify; + this.#mongoDb = mongoDb; + this.#io = new Server< + ClientToServerEvents, + ServerToClientEvents, + InterServerEvents, + SocketData + >(fastify.server); + this.#registerEventListeners(); + } + + /** + * Creates a new MongoSocketIoServer. + * + * @param fastify + * @param options + * @return + */ + static async create ( + fastify: FastifyInstance, + options: DbOptions + ): Promise { + const mongoDb = await initializeMongoClient(options); + return new MongoSocketIoServer(fastify, mongoDb); + } + + /** + * Registers event listeners on socket connection. + */ + #registerEventListeners () { + this.#io.on("connection", (socket) => { + this.#fastify.log.info(`Socket connected: ${socket.id}`); + socket.on("disconnect", this.#disconnectListener.bind(this, socket)); + socket.on("collection::init", this.#collectionInitListener.bind(this, socket)); + socket.on( + "collection::find::toReactiveArray", + this.#collectionFindToReactiveArrayListener.bind(this, socket) + ); + socket.on( + "collection::find::unsubscribe", + this.#collectionFindUnsubscribeListener.bind(this, socket) + ); + }); + } + + /** + * Listener for socket disconnection events. + * + * @param socket + */ + async #disconnectListener (socket: MongoCustomSocket) { + this.#fastify.log.info(`Socket disconnected: ${socket.id}`); + const subscribedQueryIds = this.#subscribedQueryIdsMap.get(socket.id); + + if ("undefined" === typeof subscribedQueryIds) { + return; + } + + for (const queryId of subscribedQueryIds) { + this.#unsubscribe(socket, queryId); + } + + this.#subscribedQueryIdsMap.delete(socket.id); + } + + /** + * Checks if a collection exists in the MongoDB database. + * + * @param collectionName + * @return Whether the collection exists. + */ + async #collectionExists (collectionName: string): Promise { + const collections = await this.#mongoDb.listCollections().toArray(); + return collections.some((collection) => collection.name === collectionName); + } + + /** + * Listener for initializing a connection to a collection. + * + * @param socket + * @param requestArgs + * @param requestArgs.collectionName + * @param callback + */ + async #collectionInitListener ( + socket: MongoCustomSocket, + requestArgs: {collectionName: string}, + callback:(res: Response) => void + ): Promise { + const {collectionName} = requestArgs; + this.#fastify.log.info( + `Socket ${socket.id} requested init of collection ${collectionName}` + ); + + const exists = await this.#collectionExists(collectionName); + if (false === exists) { + this.#fastify.log.error(`Collection ${collectionName} does not exist in MongoDB`); + callback({ + error: `Collection ${collectionName} does not exist in MongoDB`, + }); + + return; + } + + + if (socket.disconnected) { + return; + } + socket.data.collectionName = collectionName; + } + + + /** + * Adds the query ID to the connection's subscribed query IDs. + * + * @param queryId + * @param socketId + */ + #subscribeToQueryId (queryId: QueryId, socketId: ConnectionId) { + const subscribedQueryIds = this.#subscribedQueryIdsMap.get(socketId); + if ("undefined" === typeof subscribedQueryIds) { + this.#subscribedQueryIdsMap.set(socketId, [queryId]); + + return; + } + + if (false === subscribedQueryIds.includes(queryId)) { + subscribedQueryIds.push(queryId); + } + } + + /** + * Gets query ID based on query parameters. If not found, creates a new ID. + * + * @param queryParams + * @return the query ID. + */ + #getQueryId (queryParams: QueryParameters): number { + const queryHash = getQueryHash(queryParams); + for (const [queryId, hash] of this.#queryIdtoQueryHashMap.entries()) { + if (hash === queryHash) { + return queryId; + } + } + + let queryId = 0; + if (0 === this.#queryIdtoQueryHashMap.size) { + this.#queryIdtoQueryHashMap.set(queryId, queryHash); + } else { + const maxKey = Math.max(...Array.from(this.#queryIdtoQueryHashMap.keys())); + queryId = maxKey + 1; + this.#queryIdtoQueryHashMap.set(queryId, queryHash); + } + + return queryId; + } + + /** + * Listener for subscribing to a reactive find query. The client will receive updates whenever + * the query results change. + * + * @param socket + * @param requestArgs + * @param requestArgs.query + * @param requestArgs.options + * @param callback + */ + async #collectionFindToReactiveArrayListener ( + socket: MongoCustomSocket, + requestArgs: {query: Filter; options: FindOptions}, + callback: (res: Response<{queryId: number}>) => void + ): Promise { + const {query, options} = requestArgs; + const {collectionName} = socket.data; + if (!collectionName) { + this.#fastify.log.error("Collection name is undefined"); + + return; + } + + this.#fastify.log.info( + `Socket ${socket.id} requested query with options to collection ${collectionName}` + ); + + let collection = this.#collections.get(collectionName); + if ("undefined" === typeof collection) { + collection = new MongoWatcherCollection(collectionName, this.#io, this.#mongoDb); + this.#collections.set(collectionName, collection); + } + + const queryParameters: QueryParameters = { + collectionName, + query, + options, + }; + const queryId = this.#getQueryId(queryParameters); + await collection.getWatcher(queryParameters, queryId, socket); + callback({data: {queryId}}); + + await socket.join(queryId.toString()); + this.#subscribeToQueryId(queryId, socket.id); + } + + /** + * Unsubscribes from a query. + * + * @param socket + * @param queryId + */ + #unsubscribe (socket: MongoCustomSocket, queryId: number) { + const queryHash: string | undefined = this.#queryIdtoQueryHashMap.get(queryId); + if ("undefined" === typeof queryHash) { + this.#fastify.log.error(`QueryId ${queryId} not found in query map`); + + return; + } + + const queryParams: QueryParameters = getQuery(queryHash); + + const collection = this.#collections.get(queryParams.collectionName); + if ("undefined" === typeof collection) { + this.#fastify.log.error(`${queryParams.collectionName} is missing from server`); + + return; + } + + const lastSubscriber = collection.unsubscribeFromWatcher(queryId, socket.id); + this.#fastify.log.info(`Socket ${socket.id} unsubscribed from query ${queryId}`); + + if (lastSubscriber) { + this.#fastify.log.info(`Query ${queryId} removed from map`); + this.#queryIdtoQueryHashMap.delete(queryId); + } + + if (false === collection.isReferenced()) { + this.#fastify.log.info(`Collection ${queryParams.collectionName} removed`); + this.#collections.delete(queryParams.collectionName); + } + } + + /** + * Listener for unsubscribing from a find query. + * + * @param socket + * @param requestArgs + * @param requestArgs.queryId + */ + async #collectionFindUnsubscribeListener ( + socket: MongoCustomSocket, + requestArgs: {queryId: number} + ): Promise { + const {queryId} = requestArgs; + this.#fastify.log.info( + `Socket ${socket.id} requested unsubscription to QueryId ${queryId}` + ); + + const subscribedQueryIds = this.#subscribedQueryIdsMap.get(socket.id); + if ("undefined" === typeof subscribedQueryIds || + false === subscribedQueryIds.includes(queryId) + ) { + this.#fastify.log.error(`Socket ${socket.id} is not subscribed to ${queryId}`); + + return; + } + + this.#unsubscribe(socket, queryId); + await socket.leave(queryId.toString()); + + this.#subscribedQueryIdsMap.set( + socket.id, + subscribedQueryIds.filter((id) => id !== queryId) + ); + } +} + +/** + * A Fastify plugin callback for setting up the `MongoSocketIoServer`. + * + * @param app + * @param options + * @param options.database + * @param options.host + * @param options.port + */ +const MongoServerPlugin: FastifyPluginAsync = async ( + app: FastifyInstance, + options: DbOptions +) => { + await MongoSocketIoServer.create(app, options); +}; + +export default fastifyPlugin(MongoServerPlugin); diff --git a/components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/typings.ts b/components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/typings.ts new file mode 100644 index 0000000000..230edaf931 --- /dev/null +++ b/components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/typings.ts @@ -0,0 +1,151 @@ +import { + ChangeStream, + Document, + Filter, + FindOptions, +} from "mongodb"; +import {Socket} from "socket.io"; + + +/** + * Error response to event. + */ +interface Err { + error: string; + queryId?: number; +} + +/** + * Success response to event. + */ +interface Success { + data: T; +} + +/** + * Event response. + */ +type Response = Err | Success; + +/** + * Events that the client can emit to the server. + */ +type ClientToServerEvents = { + "disconnect": () => void; + "collection::init": ( + requestArgs: { + collectionName: string; + }, + callback: (res: Response) => void + ) => void; + "collection::find::toArray": ( + requestArgs: { + query: Filter; + options: FindOptions; + }, + callback: (res: Response<{data: Document[]}>) => void) => void; + "collection::find::toReactiveArray": ( + requestArgs: { + query: Filter; + options: FindOptions; + }, + callback: (res: Response<{queryId: number}>) => void) => void; + "collection::find::unsubscribe": ( + requestArgs: { + queryId: number; + } + ) => Promise; +}; + +/** + * Events that the server can emit to the client. + */ +interface ServerToClientEvents { + "collection::find::update": (respArgs: { + queryId: number; + data: Document[]; + }) => void; +} + +/** + * Collection associated with each socket connection. + */ +interface SocketData { + collectionName: string; +} + +/** + * Empty but required by Socket IO. + */ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +interface InterServerEvents { +} + +/** + * Custom socket type for Mongo Socket IO server. + */ +type MongoCustomSocket = Socket< + ClientToServerEvents, + ServerToClientEvents, + InterServerEvents, + SocketData +>; + +/** + * Unique ID for each active unique query. Multiple clients can subscribe to the same ID if the + * queries are identical. The ID is also used to represent the socket room, and Mongo change stream. + */ +type QueryId = number; + +/** + * Unique ID to repsent each socket connection. + */ +type ConnectionId = string; + + +/** + * Parameters for MongoDB queries. + */ +interface QueryParameters { + collectionName: string; + query: Filter; + options: FindOptions; +} + +/** + * Options to connect to MongoDb database. + */ +interface DbOptions { + // Name of database. + database: string; + host: string; + port: number; +} + +/** + * Timeout for emitting updates to the client. + */ +const CLIENT_UPDATE_TIMEOUT_MS = 500; + +/** + * MongoDb change stream for a query, and a list of subscribed connections. + */ +interface Watcher { + changeStream: ChangeStream; + subscribers: ConnectionId[]; +} + +export { + CLIENT_UPDATE_TIMEOUT_MS, + ClientToServerEvents, + ConnectionId, + DbOptions, + InterServerEvents, + MongoCustomSocket, + QueryId, + QueryParameters, + Response, + ServerToClientEvents, + SocketData, + Watcher, +}; diff --git a/components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/utils.ts b/components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/utils.ts new file mode 100644 index 0000000000..a85017214c --- /dev/null +++ b/components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/utils.ts @@ -0,0 +1,86 @@ +import { + Db, + Document, + Filter, + MongoClient, +} from "mongodb"; + +import { + DbOptions, + QueryParameters, +} from "./typings.js"; + + +/** + * Modifies query so that it can be used to filter change stream update events. Update events in + * MongoDB return the document in the "fullDocument" field, so function prepends "fullDocument" to + * each key in the query. + * + * Reference: https://www.mongodb.com/docs/manual/reference/change-events/update/#mongodb-data-update + * + * @param query + * @return Modified query. + */ +const convertQueryToChangeStreamFormat = (query: Filter): Filter => { + const changeStreamQuery: Filter = {}; + for (const key in query) { + if (Object.hasOwn(query, key)) { + changeStreamQuery[`fullDocument.${key}`] = query[key] as unknown; + } + } + + return changeStreamQuery; +}; + +/** + * Generates a unique hash for a given query parameters. + * + * @param queryParams + * @return Unique hash for query parameters. + */ +const getQueryHash = function ( + queryParams: QueryParameters, +): string { + return JSON.stringify(queryParams); +}; + +/** + * Recovers query parameters from the hash. + * + * @param queryHash + * @return + */ +const getQuery = function ( + queryHash: string +): QueryParameters { + const parsedValue: unknown = JSON.parse(queryHash); + return parsedValue as QueryParameters; +}; + +/** + * Initializes a MongoDB client. + * + * @param options + * @return + * @throws {Error} If there is a MongoDB connection error. + */ +const initializeMongoClient = async function ( + options: DbOptions +): Promise { + const mongoUri = `mongodb://${options.host}:${options.port}`; + const mongoClient = new MongoClient(mongoUri); + try { + await mongoClient.connect(); + + return mongoClient.db(options.database); + } catch (e) { + throw new Error("MongoDB connection error", {cause: e as Error}); + } +}; + +export { + convertQueryToChangeStreamFormat, + getQuery, + getQueryHash, + initializeMongoClient, +};