diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..f1f11a1 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#example-of-a-codeowners-file +# These owners will be the default owners for everything in the repo, Unless a later match takes precedence +* @hero101 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 08a7a95..13190a4 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,7 +2,7 @@ name: Bug report about: Create a report to help us improve title: "BUG: " -labels: "bug, excalidraw, whiteboard, collaboration" +labels: "bug, excalidraw, whiteboard, collaboration, Atlas Team" assignees: "" --- diff --git a/.github/ISSUE_TEMPLATE/epic.md b/.github/ISSUE_TEMPLATE/epic.md index 2052822..ddb803d 100644 --- a/.github/ISSUE_TEMPLATE/epic.md +++ b/.github/ISSUE_TEMPLATE/epic.md @@ -2,7 +2,7 @@ name: Epic about: A theme of work that contain sub-tasks required to complete the larger goal / larger user-story title: "" -labels: "epic, excalidraw, whiteboard, collaboration" +labels: "epic, excalidraw, whiteboard, collaboration, Atlas Team" assignees: "" --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 14e705b..ae67f40 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -2,7 +2,7 @@ name: Feature request about: Suggest an idea for this project title: "" -labels: "enhancement, excalidraw, whiteboard, collaboration" +labels: "enhancement, excalidraw, whiteboard, collaboration, Atlas Team" assignees: "" --- diff --git a/.github/ISSUE_TEMPLATE/user_story.md b/.github/ISSUE_TEMPLATE/user_story.md index 87a5e92..807e51e 100644 --- a/.github/ISSUE_TEMPLATE/user_story.md +++ b/.github/ISSUE_TEMPLATE/user_story.md @@ -2,7 +2,7 @@ name: User Story about: A valuable increment of functionality, testable by the users title: "" -labels: "user story, excalidraw, whiteboard, collaboration" +labels: "user story, excalidraw, whiteboard, collaboration, Atlas Team" assignees: "" --- diff --git a/config.yml b/config.yml index 50eb880..fe92e14 100644 --- a/config.yml +++ b/config.yml @@ -49,15 +49,23 @@ monitoring: settings: # application level settings application: + # On which port the server will be running + port: ${PORT}:4002 # queue queue: ${QUEUE}:alkemio-whiteboards # MILLISECONDS wait time for a response after a request on the message queue - queue_response_timeout: ${QUEUE_RESPONSE_TIMEOUT}:10000 + queue_response_timeout: ${QUEUE_RESPONSE_TIMEOUT}:5000 + # How many times the requests to retry before failing + queue_request_retries: ${QUEUE_REQUEST_RETRIES}:3 + # How many ms without a pong packet to consider the connection closed + ping_timeout: ${PING_TIMEOUT}:40000 + # How many ms before sending a new ping packet + ping_interval: ${PING_INTERVAL}:30000 + # How many bytes or characters a message can be, before closing the session (to avoid DoS). + max_http_buffer_size: ${MAX_HTTP_BUFFER_SIZE}:4e6 # the collaboration experience collaboration: enabled: ${ENABLED}:true - # - port: ${COLLABORATION_PORT}:4002 # the window in which contributions are accepted to be counted towards a single contribution event; # time is in SECONDS contribution_window: ${CONTRIBUTION_WINDOW}:600 diff --git a/package-lock.json b/package-lock.json index 19b9328..b699851 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "whiteboard-collaboration-service", - "version": "0.5.1", + "version": "0.6.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "whiteboard-collaboration-service", - "version": "0.5.1", + "version": "0.6.0", "license": "EUPL-1.2", "dependencies": { "@elastic/elasticsearch": "8.12.2", @@ -16,14 +16,12 @@ "@nestjs/microservices": "^10.3.8", "@nestjs/platform-express": "^10.0.0", "@socket.io/redis-adapter": "^8.3.0", - "amqp-connection-manager": "^4.1.14", - "amqplib": "^0.10.4", "lodash": "^4.17.21", "nest-winston": "^1.9.6", "redis": "^4.6.13", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", - "socket.io": "^4.7.5", + "socket.io": "^4.8.1", "socket.io-adapter": "^2.5.5", "yaml": "^2.4.2" }, @@ -60,6 +58,8 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/@acuminous/bitsyntax/-/bitsyntax-0.1.2.tgz", "integrity": "sha512-29lUK80d1muEQqiUsSo+3A0yP6CdspgC95EnKBMi22Xlwt79i/En4Vr67+cXhU+cZjbti3TgGGC5wy1stIywVQ==", + "optional": true, + "peer": true, "dependencies": { "buffer-more-ints": "~1.0.0", "debug": "^4.3.4", @@ -72,7 +72,9 @@ "node_modules/@acuminous/bitsyntax/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "optional": true, + "peer": true }, "node_modules/@ampproject/remapping": { "version": "2.3.0", @@ -2021,16 +2023,16 @@ "license": "0BSD" }, "node_modules/@nestjs/platform-express": { - "version": "10.4.6", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.6.tgz", - "integrity": "sha512-HcyCpAKccAasrLSGRTGWv5BKRs0rwTIFOSsk6laNyqfqvgvYcJQAedarnm4jmaemtmSJ0PFI9PmtEZADd2ahCg==", + "version": "10.4.15", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.15.tgz", + "integrity": "sha512-63ZZPkXHjoDyO7ahGOVcybZCRa7/Scp6mObQKjcX/fTEq1YJeU75ELvMsuQgc8U2opMGOBD7GVuc4DV0oeDHoA==", "license": "MIT", "dependencies": { "body-parser": "1.20.3", "cors": "2.8.5", - "express": "4.21.1", + "express": "4.21.2", "multer": "1.4.4-lts.1", - "tslib": "2.7.0" + "tslib": "2.8.1" }, "funding": { "type": "opencollective", @@ -2042,9 +2044,9 @@ } }, "node_modules/@nestjs/platform-express/node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/@nestjs/schematics": { @@ -3058,6 +3060,8 @@ "version": "4.1.14", "resolved": "https://registry.npmjs.org/amqp-connection-manager/-/amqp-connection-manager-4.1.14.tgz", "integrity": "sha512-1km47dIvEr0HhMUazqovSvNwIlSvDX2APdUpULaINtHpiki1O+cLRaTeXb/jav4OLtH+k6GBXx5gsKOT9kcGKQ==", + "optional": true, + "peer": true, "dependencies": { "promise-breaker": "^6.0.0" }, @@ -3073,6 +3077,8 @@ "version": "0.10.4", "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.4.tgz", "integrity": "sha512-DMZ4eCEjAVdX1II2TfIUpJhfKAuoCeDIo/YyETbfAqehHTXxxs7WOOd+N1Xxr4cKhx12y23zk8/os98FxlZHrw==", + "optional": true, + "peer": true, "dependencies": { "@acuminous/bitsyntax": "^0.1.2", "buffer-more-ints": "~1.0.0", @@ -3086,12 +3092,16 @@ "node_modules/amqplib/node_modules/isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "optional": true, + "peer": true }, "node_modules/amqplib/node_modules/readable-stream": { "version": "1.1.14", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "optional": true, + "peer": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", @@ -3102,7 +3112,9 @@ "node_modules/amqplib/node_modules/string_decoder": { "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "optional": true, + "peer": true }, "node_modules/ansi-colors": { "version": "4.1.3", @@ -3578,7 +3590,9 @@ "node_modules/buffer-more-ints": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", - "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==" + "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==", + "optional": true, + "peer": true }, "node_modules/busboy": { "version": "1.6.0", @@ -4105,10 +4119,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -4791,9 +4806,9 @@ } }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -4815,7 +4830,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -4830,6 +4845,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/debug": { @@ -4848,9 +4867,9 @@ "license": "MIT" }, "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, "node_modules/external-editor": { @@ -7605,7 +7624,9 @@ "node_modules/promise-breaker": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/promise-breaker/-/promise-breaker-6.0.0.tgz", - "integrity": "sha512-BthzO9yTPswGf7etOBiHCVuugs2N01/Q/94dIPls48z2zCmrnDptUUZzfIb+41xq0MnYZ/BzmOd6ikDR4ibNZA==" + "integrity": "sha512-BthzO9yTPswGf7etOBiHCVuugs2N01/Q/94dIPls48z2zCmrnDptUUZzfIb+41xq0MnYZ/BzmOd6ikDR4ibNZA==", + "optional": true, + "peer": true }, "node_modules/prompts": { "version": "2.4.2", @@ -7676,7 +7697,9 @@ "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "optional": true, + "peer": true }, "node_modules/queue-microtask": { "version": "1.2.3", @@ -7832,7 +7855,9 @@ "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "optional": true, + "peer": true }, "node_modules/resolve": { "version": "1.22.8", @@ -8241,9 +8266,9 @@ } }, "node_modules/socket.io": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", - "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", + "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", @@ -9054,9 +9079,9 @@ } }, "node_modules/undici": { - "version": "6.20.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.20.1.tgz", - "integrity": "sha512-AjQF1QsmqfJys+LXfGTNum+qw4S88CojRInG/6t31W/1fk6G59s92bnAvGz5Cmur+kQv2SURXEvvudLmbrE8QA==", + "version": "6.21.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", + "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==", "license": "MIT", "engines": { "node": ">=18.17" @@ -9129,6 +9154,8 @@ "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "optional": true, + "peer": true, "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" @@ -9583,6 +9610,8 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/@acuminous/bitsyntax/-/bitsyntax-0.1.2.tgz", "integrity": "sha512-29lUK80d1muEQqiUsSo+3A0yP6CdspgC95EnKBMi22Xlwt79i/En4Vr67+cXhU+cZjbti3TgGGC5wy1stIywVQ==", + "optional": true, + "peer": true, "requires": { "buffer-more-ints": "~1.0.0", "debug": "^4.3.4", @@ -9592,7 +9621,9 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "optional": true, + "peer": true } } }, @@ -10977,21 +11008,21 @@ } }, "@nestjs/platform-express": { - "version": "10.4.6", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.6.tgz", - "integrity": "sha512-HcyCpAKccAasrLSGRTGWv5BKRs0rwTIFOSsk6laNyqfqvgvYcJQAedarnm4jmaemtmSJ0PFI9PmtEZADd2ahCg==", + "version": "10.4.15", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.15.tgz", + "integrity": "sha512-63ZZPkXHjoDyO7ahGOVcybZCRa7/Scp6mObQKjcX/fTEq1YJeU75ELvMsuQgc8U2opMGOBD7GVuc4DV0oeDHoA==", "requires": { "body-parser": "1.20.3", "cors": "2.8.5", - "express": "4.21.1", + "express": "4.21.2", "multer": "1.4.4-lts.1", - "tslib": "2.7.0" + "tslib": "2.8.1" }, "dependencies": { "tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" } } }, @@ -11811,6 +11842,8 @@ "version": "4.1.14", "resolved": "https://registry.npmjs.org/amqp-connection-manager/-/amqp-connection-manager-4.1.14.tgz", "integrity": "sha512-1km47dIvEr0HhMUazqovSvNwIlSvDX2APdUpULaINtHpiki1O+cLRaTeXb/jav4OLtH+k6GBXx5gsKOT9kcGKQ==", + "optional": true, + "peer": true, "requires": { "promise-breaker": "^6.0.0" } @@ -11819,6 +11852,8 @@ "version": "0.10.4", "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.4.tgz", "integrity": "sha512-DMZ4eCEjAVdX1II2TfIUpJhfKAuoCeDIo/YyETbfAqehHTXxxs7WOOd+N1Xxr4cKhx12y23zk8/os98FxlZHrw==", + "optional": true, + "peer": true, "requires": { "@acuminous/bitsyntax": "^0.1.2", "buffer-more-ints": "~1.0.0", @@ -11829,12 +11864,16 @@ "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "optional": true, + "peer": true }, "readable-stream": { "version": "1.1.14", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "optional": true, + "peer": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", @@ -11845,7 +11884,9 @@ "string_decoder": { "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "optional": true, + "peer": true } } }, @@ -12194,7 +12235,9 @@ "buffer-more-ints": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", - "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==" + "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==", + "optional": true, + "peer": true }, "busboy": { "version": "1.6.0", @@ -12571,9 +12614,9 @@ "dev": true }, "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -13053,9 +13096,9 @@ } }, "express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -13076,7 +13119,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -13104,9 +13147,9 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" } } }, @@ -15157,7 +15200,9 @@ "promise-breaker": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/promise-breaker/-/promise-breaker-6.0.0.tgz", - "integrity": "sha512-BthzO9yTPswGf7etOBiHCVuugs2N01/Q/94dIPls48z2zCmrnDptUUZzfIb+41xq0MnYZ/BzmOd6ikDR4ibNZA==" + "integrity": "sha512-BthzO9yTPswGf7etOBiHCVuugs2N01/Q/94dIPls48z2zCmrnDptUUZzfIb+41xq0MnYZ/BzmOd6ikDR4ibNZA==", + "optional": true, + "peer": true }, "prompts": { "version": "2.4.2", @@ -15201,7 +15246,9 @@ "querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "optional": true, + "peer": true }, "queue-microtask": { "version": "1.2.3", @@ -15317,7 +15364,9 @@ "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "optional": true, + "peer": true }, "resolve": { "version": "1.22.8", @@ -15618,9 +15667,9 @@ "dev": true }, "socket.io": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", - "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", "requires": { "accepts": "~1.3.4", "base64id": "~2.0.0", @@ -16185,9 +16234,9 @@ "integrity": "sha512-+I6aJUv63YAcY9n4mQreLUt0d4lvwkkopDNmpomkAUz0fAkEMV9pRWxN0EjhW1YfRhcuyHg2v3mwddCDW1+LFQ==" }, "undici": { - "version": "6.20.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.20.1.tgz", - "integrity": "sha512-AjQF1QsmqfJys+LXfGTNum+qw4S88CojRInG/6t31W/1fk6G59s92bnAvGz5Cmur+kQv2SURXEvvudLmbrE8QA==" + "version": "6.21.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", + "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==" }, "undici-types": { "version": "6.19.8", @@ -16228,6 +16277,8 @@ "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "optional": true, + "peer": true, "requires": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" diff --git a/package.json b/package.json index c1aee2b..76461dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "whiteboard-collaboration-service", - "version": "0.5.1", + "version": "0.6.0", "description": "Alkemio Whiteboard Collaboration Service for Excalidraw backend", "author": "Alkemio Foundation", "private": false, @@ -23,14 +23,12 @@ "@nestjs/microservices": "^10.3.8", "@nestjs/platform-express": "^10.0.0", "@socket.io/redis-adapter": "^8.3.0", - "amqp-connection-manager": "^4.1.14", - "amqplib": "^0.10.4", "lodash": "^4.17.21", "nest-winston": "^1.9.6", "redis": "^4.6.13", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", - "socket.io": "^4.7.5", + "socket.io": "^4.8.1", "socket.io-adapter": "^2.5.5", "yaml": "^2.4.2" }, diff --git a/src/config/config.type.ts b/src/config/config.type.ts index a2f3628..93da0cd 100644 --- a/src/config/config.type.ts +++ b/src/config/config.type.ts @@ -37,12 +37,16 @@ export interface ConfigType { }; settings: { application: { + port: number; queue: string; queue_response_timeout: number; + queue_request_retries: number; + ping_timeout: number; + ping_interval: number; + max_http_buffer_size: number; }; collaboration: { enabled: boolean; - port: number; contribution_window: number; save_interval: number; collaborator_inactivity: number; diff --git a/src/config/configuration.ts b/src/config/configuration.ts index fb3f6f2..fa35a45 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -37,6 +37,7 @@ function buildYamlNodeValue(nodeValue: any, envConfig: any) { if (updatedNodeValue.toLowerCase() === 'true') return true; if (updatedNodeValue.toLowerCase() === 'false') return false; + if (!isNaN(updatedNodeValue)) return Number(updatedNodeValue); } return updatedNodeValue; diff --git a/src/excalidraw-backend/get.excalidraw.base.server.ts b/src/excalidraw-backend/get.excalidraw.base.server.ts index f73bf30..951a9e8 100644 --- a/src/excalidraw-backend/get.excalidraw.base.server.ts +++ b/src/excalidraw-backend/get.excalidraw.base.server.ts @@ -5,17 +5,23 @@ import { Adapter } from 'socket.io-adapter'; import { SocketIoServer } from './types'; export const getExcalidrawBaseServerOrFail = ( - port: number, - logger: LoggerService, + options: { + port: number; + pingTimeout: number; + pingInterval: number; + maxHttpBufferSize: number; + }, adapterFactory?: typeof Adapter | ((nsp: Namespace) => Adapter), ): SocketIoServer => { + const { port, pingTimeout, pingInterval, maxHttpBufferSize } = options; const httpServer = http.createServer(); - httpServer.listen(port, () => { - logger.verbose?.(`Collaboration endpoint Listening on port ${port}`); - }); + httpServer.listen(port); return new SocketIO(httpServer, { transports: ['websocket'], adapter: adapterFactory, + pingTimeout, // default 20000 + pingInterval, // default 25000 + maxHttpBufferSize, // default 1e6 - 1MB }); }; diff --git a/src/excalidraw-backend/middlewares/attach.user.info.or.fail.middleware.ts b/src/excalidraw-backend/middlewares/attach.user.info.or.fail.middleware.ts index 3709b91..3538245 100644 --- a/src/excalidraw-backend/middlewares/attach.user.info.or.fail.middleware.ts +++ b/src/excalidraw-backend/middlewares/attach.user.info.or.fail.middleware.ts @@ -1,7 +1,6 @@ -import { UnauthorizedException } from '@nestjs/common'; -import { WrappedMiddlewareHandler } from './middleware.handler.type'; import { UserInfo } from '../../services/whiteboard-integration/user.info'; -import { SocketIoSocket } from '../types'; +import { SocketIoSocket, ERROR_EVENTS } from '../types'; +import { WrappedMiddlewareHandler } from './middleware.handler.type'; export const attachUserInfoOrFailMiddleware: WrappedMiddlewareHandler = (getter: (socket: SocketIoSocket) => Promise) => @@ -9,11 +8,10 @@ export const attachUserInfoOrFailMiddleware: WrappedMiddlewareHandler = try { socket.data.userInfo = await getter(socket); } catch (e: any) { - next( - new UnauthorizedException( - `Error while trying to get user info: ${e.message}`, - ), - ); + // emit an error message before the connection is closed + socket.emit('error', ERROR_EVENTS.USER_INFO_NO_VERIFY); + // no handlers for the exception below - it is handled by socket.io + next(new Error('Disconnected with error')); } next(); diff --git a/src/excalidraw-backend/server.ts b/src/excalidraw-backend/server.ts index 35b09de..53db33a 100644 --- a/src/excalidraw-backend/server.ts +++ b/src/excalidraw-backend/server.ts @@ -1,11 +1,6 @@ import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { setInterval, setTimeout } from 'node:timers/promises'; -import { - Inject, - Injectable, - LoggerService, - UnauthorizedException, -} from '@nestjs/common'; +import { Inject, Injectable, LoggerService } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { debounce, DebouncedFunc, throttle } from 'lodash'; import { @@ -16,12 +11,16 @@ import { defaultContributionInterval, defaultSaveInterval, DISCONNECT, + DisconnectDescription, DISCONNECTING, + ERROR, + ERROR_EVENTS, IDLE_STATE, INIT_ROOM, InMemorySnapshot, JOIN_ROOM, resetCollaboratorModeDebounceWait, + ROOM_NOT_SAVED, ROOM_SAVED, SCENE_INIT, SERVER_BROADCAST, @@ -39,8 +38,8 @@ import { import { UserInfo } from '../services/whiteboard-integration/user.info'; import { UtilService } from '../services/util/util.service'; import { - authorizeWithRoomAndJoinHandler, - closeConnection, + authorizeWithRoomOrFailAndJoinHandler, + closeConnectionWithError, DeepReadonly, disconnectEventHandler, disconnectingEventHandler, @@ -58,6 +57,8 @@ import { tryDecodeIncoming } from './utils/decode.incoming'; import { SceneInitPayload, ServerBroadcastPayload } from './types/events'; import { ExcalidrawElement, ExcalidrawFileStore } from '../excalidraw/types'; import { isSaveErrorData } from '../services/whiteboard-integration/outputs'; +import { UnauthorizedReadAccess } from './types/exceptions'; +import { DisconnectReason } from 'socket.io/dist/socket-types'; type RoomTrackers = Map; type SocketTrackers = Map; @@ -87,11 +88,15 @@ export class Server { private readonly utilService: UtilService, private readonly configService: ConfigService, ) { - const port = this.configService.get('settings.collaboration.port', { + const serverOptions = this.configService.get('settings.application', { infer: true, }); - // this.wsServer = getExcalidrawBaseServerOrFail(redisAdapterFactory); - this.wsServer = getExcalidrawBaseServerOrFail(port, logger); + this.wsServer = getExcalidrawBaseServerOrFail({ + port: +serverOptions.port, + pingTimeout: +serverOptions.ping_timeout, + pingInterval: +serverOptions.ping_interval, + maxHttpBufferSize: +serverOptions.max_http_buffer_size, + }); // don't block the constructor this.init() .then(() => @@ -164,8 +169,10 @@ export class Server { return; } // send an event that the room is actually deleted everywhere, - // because this was the last one + // because this was the last one (only if there are more than one server) + // if ((await adapter.serverCount()) > 1) { this.wsServer.serverSideEmit(SERVER_SIDE_ROOM_DELETED, APP_ID, roomId); + // } this.logger.verbose?.( `Room '${roomId}' deleted locally and everywhere else - this was the final instance`, @@ -188,6 +195,8 @@ export class Server { this.adapterInit(); // middlewares this.wsServer.use(initUserDataMiddleware); + // may get executed after the "connection" event + // as a result the socket will establish connection and then get disconnected immediately this.wsServer.use( attachUserInfoOrFailMiddleware(this.getUserInfo.bind(this)), ); @@ -198,83 +207,90 @@ export class Server { ); this.wsServer.to(socket.id).emit(INIT_ROOM); - // attach error handlers - socket.on('error', (err) => { + // attach error handler + socket.on(ERROR, (err) => { if (!err) { return; } this.logger.error(err); - - if (err && err instanceof UnauthorizedException) { - closeConnection(socket, err.message); - this.deleteCollaboratorInactivityTrackerForSocket(socket); - } }); socket.on(JOIN_ROOM, async (roomID) => { - await this.loadSnapshot(roomID); - + // authorize and join // this logic could be provided by an entitlement (license) service - await authorizeWithRoomAndJoinHandler( - roomID, - socket, - this.wsServer, - this.logger, - (roomId, userId) => - this.utilService.getUserInfoForRoom(userId, roomId), + try { + await authorizeWithRoomOrFailAndJoinHandler( + roomID, + socket, + this.wsServer, + this.logger, + (roomId, userId) => + this.utilService.getUserInfoForRoom(userId, roomId), + ); + } catch (e: any) { + if (e instanceof UnauthorizedReadAccess) { + this.logger.warn?.( + `User '${socket.data.userInfo.email}' insufficient read access to Whiteboard: '${roomID}'`, + ); + closeConnectionWithError(socket, ERROR_EVENTS.ROOM_NO_READ_ACCESS); + return; + } + this.logger.error( + `Error while trying to authorize User '${socket.data.userInfo.email}' with Whiteboard: '${roomID}'`, + e.stack, + ); + closeConnectionWithError(socket, ERROR_EVENTS.GENERIC_ERROR); + return; + } + // log results + this.logger.verbose?.( + `User '${socket.data.userInfo.email}' read=${socket.data.viewer}, update=${socket.data.collaborator}`, ); - await this.initSceneForSocket(socket, roomID); + // load snapshot + await this.loadSnapshot(roomID); + // send scene init to the socket + await this.initSceneForSocket(socket, roomID); + // attach collaborator handler if applicable if (socket.data.collaborator) { this.startCollaboratorInactivityTrackerForSocket(socket); // user can broadcast content change events - socket.on( - SERVER_BROADCAST, - async (roomID: string, data: ArrayBuffer) => { - serverBroadcastEventHandler(roomID, data, socket, (roomId) => - this.utilService.contentModified( - socket.data.userInfo.id, - roomId, - ), - ); - this.resetCollaboratorInactivityTrackerForSocket(socket); - // todo extract to a function - let eventData: - | SocketEventData - | undefined; - try { - eventData = tryDecodeIncoming(data); - } catch (e) { - this.logger.error({ - message: e?.message ?? JSON.stringify(e), - }); - } - if (!eventData) { - return; - } - - this.utilService.reportChanges( - roomID, - socket.data.userInfo.email, - (this.snapshots.get(roomID)?.content.elements ?? - []) as ExcalidrawElement[], - eventData.payload.elements as ExcalidrawElement[], - ); - - this.createAndStoreLatestSnapshot( - roomID, - eventData.payload.elements, - eventData.payload.files, - ); - - this.queueSave(roomID); - }, - ); + socket.on(SERVER_BROADCAST, (roomID: string, data: ArrayBuffer) => { + serverBroadcastEventHandler(roomID, data, socket, (roomId) => + this.utilService.contentModified(socket.data.userInfo.id, roomId), + ); + this.resetCollaboratorInactivityTrackerForSocket(socket); + // todo extract to a function + let eventData: SocketEventData | undefined; + try { + eventData = tryDecodeIncoming(data); + } catch (e) { + this.logger.error({ + message: e?.message ?? JSON.stringify(e), + }); + } + if (!eventData) { + return; + } + + this.utilService.reportChanges( + roomID, + socket.data.userInfo.email, + (this.snapshots.get(roomID)?.content.elements ?? + []) as ExcalidrawElement[], + eventData.payload.elements as ExcalidrawElement[], + ); + + this.createAndStoreLatestSnapshot( + roomID, + eventData.payload.elements, + eventData.payload.files, + ); + + this.queueSave(roomID); + }); } - this.logger.verbose?.( - `User '${socket.data.userInfo.email}' read=${socket.data.viewer}, update=${socket.data.collaborator}`, - ); }); socket.on( @@ -289,18 +305,35 @@ export class Server { idleStateEventHandler(roomID, data, socket, this.logger), ); - socket.on( - DISCONNECTING, - async () => - await disconnectingEventHandler(this.wsServer, socket, this.logger), - ); - socket.on(DISCONNECT, () => { + socket.on(DISCONNECTING, async (reason, description) => { + await disconnectingEventHandler(this.wsServer, socket, this.logger); + this.logDisconnectReason(DISCONNECTING, reason, description); + }); + socket.on(DISCONNECT, (reason, description: DisconnectDescription) => { disconnectEventHandler(socket); this.deleteCollaboratorInactivityTrackerForSocket(socket); + this.logDisconnectReason(DISCONNECT, reason, description); }); }); } + private logDisconnectReason( + event: typeof DISCONNECT | typeof DISCONNECTING, + reason: DisconnectReason, + description?: DisconnectDescription, + ) { + const message = `[${event}] - ${reason} - ${description}`; + if ( + reason === 'transport error' || + reason === 'ping timeout' || + reason === 'parse error' + ) { + this.logger.error?.(message, undefined, reason); + } else { + this.logger.verbose?.(message); + } + } + private startContributionTrackerForRoom(roomId: string) { const ac = new AbortController(); @@ -513,11 +546,8 @@ export class Server { ): ThrottledSaveFunction { const throttledSave = throttle( async (roomId: string) => { - const hasSaved = await this.saveRoom(roomId); - - if (hasSaved) { - this.notifyRoomSaved(roomId); - } + const { hasSaved, error } = await this.saveRoom(roomId); + this.notifyRoomSaveResult(roomId, hasSaved, error); }, wait, { leading: false, trailing: true }, @@ -558,8 +588,12 @@ export class Server { await throttledSaveFn.flush(); } - private notifyRoomSaved(roomId: string) { - this.wsServer.in(roomId).emit(ROOM_SAVED); + private notifyRoomSaveResult(roomId: string, isSaved: boolean, error = '') { + if (isSaved) { + this.wsServer.in(roomId).emit(ROOM_SAVED); + } else { + this.wsServer.in(roomId).emit(ROOM_NOT_SAVED, { error }); + } } /** * Loads the snapshot in the in-memory Map and returns it. @@ -583,23 +617,25 @@ export class Server { return snapshotContent; } - private async saveRoom(roomId: string): Promise { + private async saveRoom( + roomId: string, + ): Promise<{ hasSaved: boolean; error?: string }> { const snapshot = this.snapshots.get(roomId); if (!snapshot) { this.logger.error( `No snapshot found for room '${roomId}' in the local storage!`, ); - return false; + return { hasSaved: false, error: 'No snapshot found' }; } const cleanContent = prepareContentForSave(snapshot); const { data } = await this.utilService.save(roomId, cleanContent); if (isSaveErrorData(data)) { this.logger.error(`Failed to save room '${roomId}': ${data.error}`); - return false; + return { hasSaved: false, error: 'Server failed to save' }; } else { this.logger.verbose?.(`Room '${roomId}' saved successfully`); - return true; + return { hasSaved: true }; } } } diff --git a/src/excalidraw-backend/types/disconnect.description.ts b/src/excalidraw-backend/types/disconnect.description.ts new file mode 100644 index 0000000..a3e724f --- /dev/null +++ b/src/excalidraw-backend/types/disconnect.description.ts @@ -0,0 +1,6 @@ +export declare type DisconnectDescription = + | Error + | { + description: string; + context?: unknown; + }; diff --git a/src/excalidraw-backend/types/error.events.ts b/src/excalidraw-backend/types/error.events.ts new file mode 100644 index 0000000..1bd0adc --- /dev/null +++ b/src/excalidraw-backend/types/error.events.ts @@ -0,0 +1,14 @@ +export const ERROR_EVENTS = { + GENERIC_ERROR: { + code: 0, + description: 'Error with unknown origin', + }, + USER_INFO_NO_VERIFY: { + code: 1, + description: 'Could not verify user info', + }, + ROOM_NO_READ_ACCESS: { + code: 2, + description: 'No read access to the requested room', + }, +} as const; diff --git a/src/excalidraw-backend/types/event.names.ts b/src/excalidraw-backend/types/event.names.ts index dea6902..4426285 100644 --- a/src/excalidraw-backend/types/event.names.ts +++ b/src/excalidraw-backend/types/event.names.ts @@ -1,5 +1,6 @@ export const SCENE_INIT = 'scene-init'; export const ROOM_SAVED = 'room-saved'; +export const ROOM_NOT_SAVED = 'room-not-saved'; export const SERVER_BROADCAST = 'server-broadcast'; export const SERVER_VOLATILE_BROADCAST = 'server-volatile-broadcast'; @@ -11,6 +12,8 @@ export const CLIENT_BROADCAST = 'client-broadcast'; export const CONNECTION_CLOSED = 'connection-closed'; export const SERVER_SIDE_ROOM_DELETED = 'server-side-room-deleted'; + +export const ERROR = 'error'; // messages export const CONNECTION = 'connection'; export const COLLABORATOR_MODE = 'collaborator-mode'; diff --git a/src/excalidraw-backend/types/exceptions/base.exception.ts b/src/excalidraw-backend/types/exceptions/base.exception.ts new file mode 100644 index 0000000..7a492f5 --- /dev/null +++ b/src/excalidraw-backend/types/exceptions/base.exception.ts @@ -0,0 +1,25 @@ +import { randomUUID } from 'crypto'; + +export type ExceptionDetails = ExceptionExtraDetails & Record; + +export type ExceptionExtraDetails = { + message?: string; + /** + * A probable cause added manually by the developer + */ + cause?: string; + originalException?: Error | unknown; +}; + +export class BaseException extends Error { + public readonly errorId: string; + + constructor( + public readonly message: string, + public readonly details?: ExceptionDetails, + ) { + super(message); + this.name = this.constructor.name; + this.errorId = randomUUID(); + } +} diff --git a/src/excalidraw-backend/types/exceptions/index.ts b/src/excalidraw-backend/types/exceptions/index.ts new file mode 100644 index 0000000..4827c52 --- /dev/null +++ b/src/excalidraw-backend/types/exceptions/index.ts @@ -0,0 +1,3 @@ +export * from './base.exception'; + +export * from './unauthorized.read.access'; diff --git a/src/excalidraw-backend/types/exceptions/unauthorized.read.access.ts b/src/excalidraw-backend/types/exceptions/unauthorized.read.access.ts new file mode 100644 index 0000000..03561a4 --- /dev/null +++ b/src/excalidraw-backend/types/exceptions/unauthorized.read.access.ts @@ -0,0 +1,10 @@ +import { BaseException, ExceptionDetails } from '../../types/exceptions'; + +export class UnauthorizedReadAccess extends BaseException { + constructor( + message = 'Unauthorized read access', + details?: ExceptionDetails, + ) { + super(message, details); + } +} diff --git a/src/excalidraw-backend/types/index.ts b/src/excalidraw-backend/types/index.ts index 3ff2c73..2917e11 100644 --- a/src/excalidraw-backend/types/index.ts +++ b/src/excalidraw-backend/types/index.ts @@ -14,3 +14,7 @@ export * from './user.info.for.room'; export * from './user.idle.state'; export * from './in.memory.snapshot'; + +export * from './error.events'; + +export * from './disconnect.description'; diff --git a/src/excalidraw-backend/types/socket.io.server.ts b/src/excalidraw-backend/types/socket.io.server.ts index 2296830..2239501 100644 --- a/src/excalidraw-backend/types/socket.io.server.ts +++ b/src/excalidraw-backend/types/socket.io.server.ts @@ -12,6 +12,7 @@ import { SERVER_BROADCAST, SERVER_SIDE_ROOM_DELETED, SERVER_VOLATILE_BROADCAST, + ROOM_NOT_SAVED, } from './event.names'; import { SocketIoSocket } from './socket.io.socket'; import { CollaboratorModeReasons } from './collaboration.mode.reasons'; @@ -29,6 +30,7 @@ type EmitEvents = { [ROOM_USER_CHANGE]: (socketIDs: Array) => void; [SCENE_INIT]: (scene: ArrayBuffer) => void; [ROOM_SAVED]: () => void; + [ROOM_NOT_SAVED]: ({ error }: { error: string }) => void; [COLLABORATOR_MODE]: (data: { mode: 'read' | 'write'; reason?: CollaboratorModeReasons; diff --git a/src/excalidraw-backend/types/socket.io.socket.ts b/src/excalidraw-backend/types/socket.io.socket.ts index 9c57aa0..e2c41ab 100644 --- a/src/excalidraw-backend/types/socket.io.socket.ts +++ b/src/excalidraw-backend/types/socket.io.socket.ts @@ -1,9 +1,12 @@ import { RemoteSocket, Socket } from 'socket.io'; -import { DefaultEventsMap } from 'socket.io/dist/typed-events'; +import { + DefaultEventsMap, + EventNames, + ReservedOrUserListener, +} from 'socket.io/dist/typed-events'; import { SocketData } from './socket.data'; import { CLIENT_BROADCAST, - CONNECTION_CLOSED, DISCONNECT, DISCONNECTING, IDLE_STATE, @@ -12,6 +15,8 @@ import { ROOM_USER_CHANGE, SERVER_BROADCAST, SERVER_VOLATILE_BROADCAST, + ERROR, + CONNECTION_CLOSED, } from './event.names'; type ListenEvents = { @@ -20,15 +25,20 @@ type ListenEvents = { [SERVER_BROADCAST]: (roomId: string, data: ArrayBuffer) => void; [SERVER_VOLATILE_BROADCAST]: (roomId: string, data: ArrayBuffer) => void; [IDLE_STATE]: (roomId: string, data: ArrayBuffer) => void; - [DISCONNECTING]: () => void; - [DISCONNECT]: () => void; - error: (err: Error) => void; }; + type EmitEvents = { [CLIENT_BROADCAST]: (data: ArrayBuffer) => void; [IDLE_STATE]: (data: ArrayBuffer) => void; [ROOM_USER_CHANGE]: (socketIds: Array) => void; [CONNECTION_CLOSED]: (message?: string) => void; + [ERROR]: ({ + code, + description, + }: { + code: number; + description: string; + }) => void; }; type ServerSideEvents = DefaultEventsMap; @@ -39,4 +49,10 @@ export type SocketIoSocket = Socket< SocketData >; +export type SocketHandlers = ReservedOrUserListener< + Record, + ListenEvents, + EventNames +>; + export type RemoteSocketIoSocket = RemoteSocket; diff --git a/src/excalidraw-backend/utils/handlers.ts b/src/excalidraw-backend/utils/handlers.ts index 8639fea..2fe0a80 100644 --- a/src/excalidraw-backend/utils/handlers.ts +++ b/src/excalidraw-backend/utils/handlers.ts @@ -1,4 +1,4 @@ -import { LoggerService } from '@nestjs/common'; +import { LoggerService, UnauthorizedException } from '@nestjs/common'; import { SocketIoSocket, SocketIoServer, @@ -15,6 +15,7 @@ import { minCollaboratorsInRoom } from '../types'; import { IdleStatePayload } from '../types/events'; import { closeConnection } from './util'; import { tryDecodeBinary, tryDecodeIncoming } from './decode.incoming'; +import { UnauthorizedReadAccess } from '../types/exceptions'; const fetchSocketsSafe = async ( wsServer: SocketIoServer, @@ -28,8 +29,17 @@ const fetchSocketsSafe = async ( return []; } }; +/** + * + * @param roomID + * @param socket + * @param wsServer + * @param logger + * @param getRoomInfo + * @throws UnauthorizedException if the user is not authorized to read the room + */ // todo: split this into multiple functions and combine in one handler -export const authorizeWithRoomAndJoinHandler = async ( +export const authorizeWithRoomOrFailAndJoinHandler = async ( roomID: string, socket: SocketIoSocket, wsServer: SocketIoServer, @@ -44,11 +54,7 @@ export const authorizeWithRoomAndJoinHandler = async ( } = await getRoomInfo(roomID, userInfo.id); if (!canRead) { - logger.error( - `Unable to authorize User '${userInfo.id}' with Whiteboard: '${roomID}'`, - ); - closeConnection(socket, 'Unauthorized read access'); - return; + throw new UnauthorizedReadAccess(); } // the amount must be defined at this point const maxCollaboratorsForThisRoom = maxCollaborators!; diff --git a/src/excalidraw-backend/utils/util.ts b/src/excalidraw-backend/utils/util.ts index 6e62050..0f52549 100644 --- a/src/excalidraw-backend/utils/util.ts +++ b/src/excalidraw-backend/utils/util.ts @@ -1,5 +1,20 @@ -import { SocketIoSocket, CONNECTION_CLOSED } from '../types'; +import { + SocketIoSocket, + ERROR, + ERROR_EVENTS, + CONNECTION_CLOSED, +} from '../types'; +// closes the connection for this socket +// and sends an error message before disconnecting +export const closeConnectionWithError = ( + socket: SocketIoSocket, + error: (typeof ERROR_EVENTS)[keyof typeof ERROR_EVENTS], +) => { + socket.emit(ERROR, error); + socket.removeAllListeners(); + socket.disconnect(true); +}; // closes the connection for this socket // and sends an optional message before disconnecting export const closeConnection = (socket: SocketIoSocket, message?: string) => { diff --git a/src/services/util/util.service.ts b/src/services/util/util.service.ts index b0b8acb..07060f1 100644 --- a/src/services/util/util.service.ts +++ b/src/services/util/util.service.ts @@ -46,7 +46,7 @@ export class UtilService { authorization?: string; }): Promise { const { cookie, authorization } = opts; - + // we want to choose the authorization with priority if (authorization) { return this.integrationService.who(new WhoInputData({ authorization })); } @@ -55,7 +55,7 @@ export class UtilService { return this.integrationService.who(new WhoInputData({ cookie })); } - throw new Error('No cookie or authorization header provided'); + return { id: '', email: '' }; } public getUserInfoForRoom(userId: string, roomId: string) { diff --git a/src/services/whiteboard-integration/types/index.ts b/src/services/whiteboard-integration/types/index.ts index 6a9a0da..b8c648c 100644 --- a/src/services/whiteboard-integration/types/index.ts +++ b/src/services/whiteboard-integration/types/index.ts @@ -1,2 +1,3 @@ export * from './rmq.connection.error'; -export * from './our.timeout.error'; +export * from './timeout.exception'; +export * from './retry.exception'; diff --git a/src/services/whiteboard-integration/types/our.timeout.error.ts b/src/services/whiteboard-integration/types/our.timeout.error.ts deleted file mode 100644 index bdea9d3..0000000 --- a/src/services/whiteboard-integration/types/our.timeout.error.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class OurTimeoutError extends Error { - constructor() { - super('Timeout'); - this.name = this.constructor.name; - } -} diff --git a/src/services/whiteboard-integration/types/retry.exception.ts b/src/services/whiteboard-integration/types/retry.exception.ts new file mode 100644 index 0000000..8c10ce8 --- /dev/null +++ b/src/services/whiteboard-integration/types/retry.exception.ts @@ -0,0 +1,10 @@ +import { + BaseException, + ExceptionDetails, +} from '../../../excalidraw-backend/types/exceptions'; + +export class RetryException extends BaseException { + constructor(public readonly details?: ExceptionDetails) { + super('Retries exceeded', details); + } +} diff --git a/src/services/whiteboard-integration/types/timeout.exception.ts b/src/services/whiteboard-integration/types/timeout.exception.ts new file mode 100644 index 0000000..da3f740 --- /dev/null +++ b/src/services/whiteboard-integration/types/timeout.exception.ts @@ -0,0 +1,10 @@ +import { + BaseException, + ExceptionDetails, +} from '../../../excalidraw-backend/types/exceptions'; + +export class TimeoutException extends BaseException { + constructor(public readonly details?: ExceptionDetails) { + super('Timeout', details); + } +} diff --git a/src/services/whiteboard-integration/whiteboard.integration.adapter.service.ts b/src/services/whiteboard-integration/whiteboard.integration.adapter.service.ts index 68bfde3..59bf64f 100644 --- a/src/services/whiteboard-integration/whiteboard.integration.adapter.service.ts +++ b/src/services/whiteboard-integration/whiteboard.integration.adapter.service.ts @@ -1,6 +1,14 @@ import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { Inject, Injectable, LoggerService } from '@nestjs/common'; -import { firstValueFrom, map, throwError, timeInterval, timeout } from 'rxjs'; +import { + catchError, + firstValueFrom, + map, + retry, + timeInterval, + timeout, + timer, +} from 'rxjs'; import { ClientProxy, ClientProxyFactory, @@ -28,12 +36,13 @@ import { WhiteboardIntegrationEventPattern } from './event.pattern.enum'; import { ConfigService } from '@nestjs/config'; import { ConfigType } from '../../config'; import { RmqOptions } from '@nestjs/microservices/interfaces/microservice-configuration.interface'; -import { RMQConnectionError, OurTimeoutError } from './types'; +import { RetryException, RMQConnectionError, TimeoutException } from './types'; @Injectable() export class WhiteboardIntegrationAdapterService { private readonly client: ClientProxy | undefined; private readonly timeoutMs: number; + private readonly retries: number; constructor( @Inject(WINSTON_MODULE_NEST_PROVIDER) private logger: LoggerService, @@ -76,6 +85,10 @@ export class WhiteboardIntegrationAdapterService { 'settings.application.queue_response_timeout', { infer: true }, ); + this.retries = this.configService.get( + 'settings.application.queue_request_retries', + { infer: true }, + ); } public async who(data: WhoInputData) { @@ -162,7 +175,6 @@ export class WhiteboardIntegrationAdapterService { .catch(() => false); } - // todo: work on exception handling: logging here vs at consumer /** * Sends a message to the queue and waits for a response. * Each consumer needs to manually handle failures, returning the proper type. @@ -173,106 +185,149 @@ export class WhiteboardIntegrationAdapterService { private sendWithResponse = async ( pattern: WhiteboardIntegrationMessagePattern, data: TInput, - options?: { timeoutMs?: number }, + options?: { timeoutMs?: number; retries?: number }, ): Promise => { if (!this.client) { throw new Error(`Connection was not established. Send failed.`); } const timeoutMs = options?.timeoutMs ?? this.timeoutMs; + const retries = options?.retries ?? this.retries; const result$ = this.client.send(pattern, data).pipe( timeInterval(), - map((x) => { - this.logger.debug?.({ - method: `sendWithResponse response took ${x.interval}ms`, - pattern, - data, - value: x.value, - }); - return x.value; - }), timeout({ each: timeoutMs, - with: () => throwError(() => new OurTimeoutError()), - }), - ); - - return firstValueFrom(result$).catch( - ( - error: - | RMQConnectionError - | OurTimeoutError - | Error - | Record - | undefined - | null, - ) => { - // null or undefined - if (error == undefined) { - this.logger.error({ - message: `'${error}' error caught while processing integration request.`, + with: () => { + throw new TimeoutException({ + timeout: this.timeoutMs, pattern, - timeout: timeoutMs, + data, }); + }, + }), + retry({ + count: retries, + delay: (error, retryCount) => { + if (retryCount === this.retries) { + throw new RetryException({ + retries: this.retries, + data, + originalError: error, + cause: `Max retries (${this.retries}) reached`, + }); + } - throw new Error( - `'${error}' error caught while processing integration request.`, + this.logger.warn?.( + `Retrying request to collaboration service [${++retryCount}/${this.retries}]`, ); - } - if (error instanceof OurTimeoutError) { - this.logger.error( - { - message: `Timeout was reached while waiting for response`, + return timer(0); + }, + }), + catchError( + ( + error: + | RMQConnectionError + | TimeoutException + | RetryException + | Error + | Record + | undefined + | null, + ) => { + // null or undefined + if (error == undefined) { + this.logger.error({ + message: `'${error}' error caught while processing integration request.`, pattern, timeout: timeoutMs, - }, - error.stack, - ); + }); - throw new Error('Timeout while processing integration request.'); - } else if (error instanceof RMQConnectionError) { - this.logger.error( - { - message: `Error was received while waiting for response: ${error?.err?.message}`, - pattern, - timeout: timeoutMs, - }, - error?.err?.stack, - ); + throw new Error( + `'${error}' error caught while processing integration request.`, + ); + } - throw new Error( - 'RMQ connection error while processing integration request.', - ); - } else if (error instanceof Error) { - this.logger.error( - { - message: `Error was received while waiting for response: ${error.message}`, + if (error instanceof RetryException) { + this.logger.error( + { + message: `Max retries reached (${this.retries}) while waiting for response`, + pattern, + timeout: timeoutMs, + }, + error.stack, + ); + + throw new Error( + 'Max retries reached while processing integration request.', + ); + } + + if (error instanceof TimeoutException) { + this.logger.error( + { + message: `Timeout was reached while waiting for response`, + pattern, + timeout: timeoutMs, + }, + error.stack, + ); + + throw new Error('Timeout while processing integration request.'); + } else if (error instanceof RMQConnectionError) { + this.logger.error( + { + message: `RMQ connection error was received while waiting for response: ${error?.err?.message}`, + pattern, + timeout: timeoutMs, + }, + error?.err?.stack, + ); + + throw new Error( + 'RMQ connection error while processing integration request.', + ); + } else if (error instanceof Error) { + this.logger.error( + { + message: `Error was received while waiting for response: ${error.message}`, + pattern, + timeout: timeoutMs, + }, + error.stack, + ); + + throw new Error( + `${error.name} error while processing integration request.`, + ); + } else { + this.logger.error({ + message: `Unknown error was received while waiting for response: ${JSON.stringify(error, null, 2)}`, pattern, timeout: timeoutMs, - }, - error.stack, - ); + }); - throw new Error( - `${error.name} error while processing integration request.`, - ); - } else { - this.logger.error({ - message: `Unknown error was received while waiting for response: ${JSON.stringify(error, null, 2)}`, - pattern, - timeout: timeoutMs, - }); - - throw new Error( - `Unknown error while processing integration request.`, - ); - } - }, + throw new Error( + `Unknown error while processing integration request.`, + ); + } + }, + ), + map((x) => { + this.logger.debug?.({ + method: `sendWithResponse response took ${x.interval}ms`, + pattern, + data, + value: x.value, + }); + return x.value; + }), ); + + return firstValueFrom(result$); }; - // todo: work on exception handling: logging here vs at consumer + /** * Sends a message to the queue without waiting for a response. * Each consumer needs to manually handle failures, returning the proper type.